243 Commits

Author SHA1 Message Date
45eaa252bd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:50 +00:00
45222f6bb7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:49 +00:00
d9715fff07 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:48 +00:00
5227767283 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:46 +00:00
f76d79a606 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:45 +00:00
14fd3e2f1e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:43 +00:00
e3ce2be1f4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:40 +00:00
0329122548 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:39 +00:00
f31284a113 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:38 +00:00
dcccf8b034 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:37 +00:00
9751faf173 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:36 +00:00
0d94303107 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:35 +00:00
fcd0cfb26f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:34 +00:00
48863fc6d5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:33 +00:00
8ba35217bf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:32 +00:00
afe168723d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:30 +00:00
72273b580f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:28 +00:00
095ed48ded Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:27 +00:00
3a18336cc4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:26 +00:00
33c3ae3585 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:24 +00:00
aa7c94b0dc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:22 +00:00
e30a37898b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:19 +00:00
88e1fa7965 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:19 +00:00
9f19b53414 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:18 +00:00
26fa9e2871 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:16 +00:00
825423b3af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:14 +00:00
06a1e15a4f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:12 +00:00
23c6cf64cf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:11 +00:00
ae9617dedd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:10 +00:00
2884546072 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:09 +00:00
d5ae939266 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:08 +00:00
89047fe79c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:06 +00:00
d323267a28 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:05 +00:00
edf915943b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:04 +00:00
d7c9aee436 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:03 +00:00
e497c2b5d0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:00 +00:00
582b9e1b9b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:59 +00:00
2b1c7afdf1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:57 +00:00
882ba9247a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:55 +00:00
0671d0b2e9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:55 +00:00
509006cce4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:53 +00:00
44c55e900a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:51 +00:00
db83188b88 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:50 +00:00
aa4d44164c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:49 +00:00
ea9d3ff61f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:48 +00:00
f63dd818aa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:47 +00:00
27f8b2541e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:46 +00:00
558a815535 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:44 +00:00
6d6add697b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:42 +00:00
bb235faf39 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:40 +00:00
3951faca64 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:39 +00:00
3db314cedb Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:36 +00:00
3e4faf0dc7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:34 +00:00
a70964ad21 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:31 +00:00
d0a79ac5a6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:28 +00:00
04c6e4d523 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:27 +00:00
3d2cbfcb74 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:26 +00:00
320f8e5bbf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:24 +00:00
4d495e5cc5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:21 +00:00
dbb7322fce Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:19 +00:00
ad78baae06 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:17 +00:00
b3f9c9f989 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:16 +00:00
773476029f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:14 +00:00
bdf313b39e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:13 +00:00
0b3fafc478 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:11 +00:00
ebe63f69ab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:08 +00:00
a59bf97a36 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:05 +00:00
a64c72a5d4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:04 +00:00
8b0f9d6361 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:02 +00:00
aa5439325d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:00 +00:00
b1dbab9942 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:59 +00:00
43ae1490f5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:57 +00:00
2a07cab00b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:56 +00:00
08fe99a3dd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:55 +00:00
1dae9452af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:54 +00:00
0c15c42d4f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:54 +00:00
7a0ffe51cd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:53 +00:00
effc02f01b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:51 +00:00
e585583bf5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:49 +00:00
eab73a9964 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:47 +00:00
fe62898189 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:47 +00:00
dd454b6269 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:46 +00:00
3a469c333d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:44 +00:00
3fe3d5944a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:43 +00:00
42ecceac62 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:42 +00:00
f5a379f683 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:40 +00:00
834b292f73 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:39 +00:00
d29c58ac6c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:39 +00:00
1095317b23 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:38 +00:00
80ff9d095b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:36 +00:00
e110187874 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:35 +00:00
0786f81d63 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:34 +00:00
dcb6ca59fc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:32 +00:00
3eb6ee6758 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:30 +00:00
97753affb8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:29 +00:00
e19d374f2b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:28 +00:00
30a33e939f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:26 +00:00
d2e38977c2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:25 +00:00
fa3cb047c8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:24 +00:00
8441d1956d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:23 +00:00
633253de00 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:21 +00:00
9dfa1d4d03 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:20 +00:00
4350577ab7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:20 +00:00
63becde73e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:19 +00:00
65dc01b15e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:17 +00:00
d0e0bb7b0c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:16 +00:00
26ce32efb8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:14 +00:00
aa9004b2ef Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:12 +00:00
cd8b9c7975 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:11 +00:00
500026c640 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:10 +00:00
bd2ee6d072 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:09 +00:00
da58849aae Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:07 +00:00
570140673c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:05 +00:00
0e56aa652c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:04 +00:00
f3b0ba4632 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:02 +00:00
2462220921 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:59 +00:00
17efcd0fbc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:57 +00:00
5d3db75b14 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:55 +00:00
ea03f85024 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:53 +00:00
be600989ba Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:51 +00:00
362fe7126a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:49 +00:00
439ebf4356 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:47 +00:00
9b50ec37a6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:45 +00:00
829a0fe36f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:44 +00:00
6fc36fd5c5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:44 +00:00
cbc31eaf55 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:43 +00:00
d63a402aaf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:42 +00:00
31047a2670 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:41 +00:00
24adc03ab3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:39 +00:00
4e051d6c52 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:38 +00:00
2ebb0a73c2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:36 +00:00
906671a8b5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:35 +00:00
a401dc3abd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:33 +00:00
124377f7c5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:31 +00:00
90836e2f2a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:31 +00:00
90d9a2b202 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:30 +00:00
c62d637f56 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:28 +00:00
ce90945990 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:25 +00:00
bb42154f35 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:24 +00:00
3fd4eb215e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:23 +00:00
543f423825 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:22 +00:00
3d2ad55cfa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:20 +00:00
50cf4b4107 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:17 +00:00
8b6528cb8b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:14 +00:00
a55dafa6e0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:12 +00:00
24c89d97e5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:11 +00:00
a7c36be076 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:10 +00:00
2ef98a3897 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:09 +00:00
e21d4e66a9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:07 +00:00
38c91ef94c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:05 +00:00
454e6936fb Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:03 +00:00
6d49162c09 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:00 +00:00
5bd5c7b906 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:59 +00:00
a4de8697da Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:58 +00:00
ce8f7f8711 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:55 +00:00
ca0ca65f14 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:54 +00:00
30f2cf9b0e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:54 +00:00
f68e2d23e7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:53 +00:00
05c33d06c3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:52 +00:00
46fca55d81 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:50 +00:00
4db469f273 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:49 +00:00
72a652a5c6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:48 +00:00
5523995594 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:46 +00:00
7b7bcf73e6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:45 +00:00
acb3564750 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:43 +00:00
bb3359a309 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:40 +00:00
8b1544647a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:38 +00:00
f727f3ab67 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:38 +00:00
a371ac9bdc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:36 +00:00
f84c891e96 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:35 +00:00
c31947bfab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:33 +00:00
d783d0d08d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:31 +00:00
7a9e3c56fe Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:30 +00:00
11770d4950 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:29 +00:00
0d150fa92e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:28 +00:00
2933122464 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:26 +00:00
5ab0886edf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:25 +00:00
e2045b21e2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:24 +00:00
596bd0a761 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:22 +00:00
2a19267d2f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:19 +00:00
50105c5ba6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:16 +00:00
434b1d46ab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:15 +00:00
d4945d20ee Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:13 +00:00
6e05ee4c57 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:12 +00:00
33ba2de5dd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:11 +00:00
bb075fe036 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:09 +00:00
60687c611a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:08 +00:00
776bcc1f7f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:07 +00:00
61061bdee9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:06 +00:00
77d467de70 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:04 +00:00
e4a63ec0e4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:04 +00:00
77dc1762c0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:03 +00:00
e2bf6c22f9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:01 +00:00
f9b4e1582a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:59 +00:00
53a05d2240 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:58 +00:00
8512a1d196 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:57 +00:00
41a130c1d6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:55 +00:00
13b8322e3c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:54 +00:00
d18265433c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:53 +00:00
16e661f8e8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:51 +00:00
8a40edf6c6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:49 +00:00
831cbe1449 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:48 +00:00
aea1722933 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:48 +00:00
51a39b2e6e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:46 +00:00
21e6a6f3f6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:43 +00:00
dbca7e34e1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:42 +00:00
ac8400512b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:41 +00:00
817b05fe06 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:40 +00:00
cb1c9a1ffa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:39 +00:00
bc7dbb494a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:38 +00:00
7827e2f224 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:36 +00:00
c4edb6b3af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:35 +00:00
bdc66120f3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:34 +00:00
636938581d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:33 +00:00
2c3cfc3978 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:32 +00:00
0236ef0809 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:31 +00:00
de9fd7b71d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:29 +00:00
eaeb978c19 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:27 +00:00
2e55efb312 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:26 +00:00
b786492d5e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:25 +00:00
6086717e66 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:24 +00:00
375466beea Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:23 +00:00
8dfe9e9874 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:21 +00:00
385ef060ed Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:20 +00:00
6c8b3cbd74 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:18 +00:00
90d1e47e03 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:17 +00:00
9700c192d0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:16 +00:00
25596b4149 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:14 +00:00
0885b59bb3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:14 +00:00
fcbe9a17db Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:12 +00:00
860fedb7e0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:10 +00:00
85ada8e9b7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:08 +00:00
6b98bf8be1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:07 +00:00
55fb716490 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:06 +00:00
Tower Deploy
ab214ac9dd Wipe addons/: full reset for clean re-upload 2026-04-27 11:20:55 +03:00
Tower Deploy
304da43eb8 Wipe test artifacts from 18.0 branch (test_correct_encode + web_responsive*); keep odoosky_demo 2026-04-27 10:59:55 +03:00
48b0b7a283 Tower: upload test_correct_encode 18.0.1.0.0 (via marketplace) 2026-04-27 07:38:22 +00:00
40c3e1d471 Tower: upload test_correct_encode 18.0.1.0.0 (via marketplace) 2026-04-27 07:38:22 +00:00
c1ecf1289d Tower: upload web_responsive 18.0.1.0.0 (via marketplace) 2026-04-27 06:42:13 +00:00
0741834b31 Tower: upload web_responsive 18.0.1.0.0 (via marketplace) 2026-04-27 06:42:13 +00:00
7a2debb3d7 Tower: upload web_responsive_test 18.0.0.1.0 (via marketplace) 2026-04-27 06:03:21 +00:00
6ef9d029eb Tower: upload web_responsive_test 18.0.0.1.0 (via marketplace) 2026-04-27 06:03:21 +00:00
Tower Deploy
96edc0c694 Seed addons/odoosky_demo (18.0.1.0.0) — platform smoke-test addon 2026-04-27 00:44:36 +03:00
790 changed files with 64619 additions and 121624 deletions

View File

@@ -0,0 +1,105 @@
from . import models
from . import wizard
from . import controllers
from odoo import Command
import logging
_logger = logging.getLogger(__name__)
def _at_accounting_post_init(env):
country_code = env.company.country_id.code
if country_code:
module_list = []
sepa_zone = env.ref('base.sepa_zone', raise_if_not_found=False)
sepa_zone_country_codes = sepa_zone and sepa_zone.mapped('country_ids.code') or []
if country_code in sepa_zone_country_codes:
module_list.extend(['account_iso20022', 'account_bank_statement_import_camt'])
module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
if module_ids:
module_ids.sudo().button_install()
for company in env['res.company'].search([('chart_template', '!=', False)], order="parent_path"):
ChartTemplate = env['account.chart.template'].with_company(company)
ChartTemplate._load_data({
'res.company': ChartTemplate._get_account_accountant_res_company(company.chart_template),
})
country_code = env.company.country_id.code
if country_code:
module_list = []
if country_code in ('AU', 'CA', 'US'):
module_list.append('account_reports_cash_basis')
module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
if module_ids:
module_ids.sudo().button_install()
for company in env['res.company'].search([]):
company.account_tax_periodicity_journal_id = company._get_default_misc_journal()
company.account_tax_periodicity_journal_id.show_on_dashboard = True
company._initiate_account_onboardings()
def uninstall_hook(env):
group_basic = env.ref('account.group_account_basic')
group_manager = env.ref('account.group_account_manager')
if group_basic:
group_basic.write({
'users': [Command.clear()],
'category_id': env.ref("base.module_category_hidden").id,
})
group_manager.write({
'implied_ids': [Command.unlink(group_basic.id)],
})
try:
group_user = env.ref("account.group_account_user")
group_user.write({
'name': "Show Full Accounting Features",
'implied_ids': [(3, env.ref('account.group_account_invoice').id)],
'category_id': env.ref("base.module_category_hidden").id,
})
group_readonly = env.ref("account.group_account_readonly")
group_readonly.write({
'name': "Show Full Accounting Features - Readonly",
'category_id': env.ref("base.module_category_hidden").id,
})
except ValueError as e:
_logger.warning(e)
try:
group_manager = env.ref("account.group_account_manager")
group_manager.write({'name': "Billing Manager",
'implied_ids': [(4, env.ref("account.group_account_invoice").id),
(3, env.ref("account.group_account_readonly").id),
(3, env.ref("account.group_account_user").id)]})
except ValueError as e:
_logger.warning(e)
# make the account_accountant features disappear (magic)
env.ref("account.group_account_user").write({'users': [(5, False, False)]})
env.ref("account.group_account_readonly").write({'users': [(5, False, False)]})
invoicing_menu = env.ref("account.menu_finance")
menus_to_move = [
"account.menu_finance_receivables",
"account.menu_finance_payables",
"account.menu_finance_entries",
"account.menu_finance_reports",
"account.menu_finance_configuration",
"account.menu_board_journal_1",
]
for menu_xmlids in menus_to_move:
try:
env.ref(menu_xmlids).parent_id = invoicing_menu
except ValueError as e:
_logger.warning(e)

View File

@@ -0,0 +1,178 @@
{
'name': "Odoo 18 Vera Accounting",
'version': "18.0.1.7",
'category': 'Accounting/Accounting',
'sequence': 1,
'summary': "A complete accounting toolkit for Odoo 18 Community with advanced reports, wizards, and workflows.",
'description': """
========================================================
Your Complete Professional Accounting Suite for Odoo 18
========================================================
Transform your Odoo 18 Community into a powerful, professional-grade financial management system. This module provides the advanced tools and deep financial insights that growing businesses need to thrive.
Move beyond standard accounting with a comprehensive toolkit designed to improve accuracy, streamline workflows, and empower you to make smarter, data-driven decisions.
Key Features:
-------------
* **Advanced Reporting Engine:** Generate a full suite of essential financial reports on-demand, including:
* Profit and Loss (P&L)
* Balance Sheet
* Cash Flow Statement
* Aged Receivables & Payables
* General Ledger & Partner Ledger
* Trial Balance
* Comprehensive Tax Reports
* ...and many more.
* **Interactive Financial Dashboards:** Visualize your financial health with intuitive and dynamic dashboard components, bringing your numbers to life.
* **Streamlined Bank Reconciliation:** Utilize an enhanced bank reconciliation widget and powerful auto-reconciliation wizards to manage your statements faster and more accurately.
* **Powerful Accounting Wizards:** Simplify complex processes with guided, user-friendly wizards for tasks like Fiscal Year Closing, Multicurrency Revaluation, and Report Exporting.
* **Guided User Tours:** Onboard your team quickly and ensure they can leverage all the powerful new features with integrated user tours.
* **Professional PDF Exports:** Create clean, professionally formatted PDF documents for all your financial reports, ready for sharing with stakeholders.
Empower Your Finance Team
-------------------------
This module provides your team with the information they need, right where they need it. Reduce manual work, eliminate errors, and give your accountants the tools they need to perform at their best.
""",
'icon': 'static/description/icon.png',
'author': 'Vera Software',
"website": "https://www.erp-tradepro.com/",
'support': 'vera@Software.com',
'maintainer': 'Vera Software',
'depends': ['account','web_tour', 'stock_account', 'base_import'],
'data': [
'data/ir_cron.xml',
'data/digest_data.xml',
'data/at_accounting_tour.xml',
'data/at_accounting_data.xml',
'data/pdf_export_templates.xml',
'data/balance_sheet.xml',
'data/cash_flow_report.xml',
'data/executive_summary.xml',
'data/profit_and_loss.xml',
'data/bank_reconciliation_report.xml',
'data/aged_partner_balance.xml',
'data/general_ledger.xml',
'data/trial_balance.xml',
'data/sales_report.xml',
'data/partner_ledger.xml',
'data/multicurrency_revaluation_report.xml',
'data/deferred_reports.xml',
'data/journal_report.xml',
'data/generic_tax_report.xml',
'views/account_report_view.xml',
'data/account_report_actions.xml',
'data/report_send_cron.xml',
'data/menuitems.xml',
'data/mail_activity_type_data.xml',
'data/mail_templates.xml',
'security/ir.model.access.csv',
'security/at_accounting_security.xml',
'security/accounting_security.xml',
'views/account_account_views.xml',
'views/account_fiscal_year_view.xml',
'views/account_journal_dashboard_views.xml',
'views/account_move_views.xml',
'views/account_payment_views.xml',
'views/account_reconcile_views.xml',
'views/account_reconcile_model_views.xml',
'views/at_accounting_menuitems.xml',
'views/digest_views.xml',
'views/res_config_settings_views.xml',
'views/product_views.xml',
'views/bank_rec_widget_views.xml',
'views/report_invoice.xml',
'views/partner_views.xml',
'views/account_activity.xml',
'views/account_tax_views.xml',
'views/mail_activity_views.xml',
'views/report_template.xml',
'views/res_company_views.xml',
'views/res_partner_views.xml',
'wizard/account_change_lock_date.xml',
'wizard/account_auto_reconcile_wizard.xml',
'wizard/account_reconcile_wizard.xml',
'wizard/reconcile_model_wizard.xml',
'wizard/account_report_send.xml',
'wizard/multicurrency_revaluation.xml',
'wizard/report_export_wizard.xml',
'wizard/account_report_file_download_error_wizard.xml',
'wizard/fiscal_year.xml',
'wizard/mail_activity_schedule_views.xml',
'security/at_account_asset_security.xml',
'security/ir.model.access.csv',
'wizard/asset_modify_views.xml',
# 'views/account_account_views.xml',
'views/account_asset_views.xml',
'views/account_asset_group_views.xml',
# 'views/account_move_views.xml',
'data/assets_reports.xml',
'data/account_report_actions_depr.xml',
'views/account_bank_statement_import_view.xml',
],
'demo': [
'demo/at_accounting_demo.xml',
'demo/partner_bank.xml',
],
'installable': True,
'application': True,
'post_init_hook': '_at_accounting_post_init',
'uninstall_hook': "uninstall_hook",
'license': 'OPL-1',
'assets':{
'web.assets_backend': [
'at_accounting/static/src/js/tours/at_accounting.js',
'at_accounting/static/src/components/**/*',
'at_accounting/static/src/**/*.xml',
'at_accounting/static/src/js/**/*',
'at_accounting/static/src/widgets/**/*',
# Root-level JS files not covered by the patterns above
'at_accounting/static/src/account_bank_statement_import_model.js',
'at_accounting/static/src/bank_statement_csv_import_action.js',
'at_accounting/static/src/bank_statement_csv_import_model.js',
# Bank reconciliation view files (outside /components/)
'at_accounting/static/src/bank_reconciliation/**/*',
# Backend-only SCSS (excludes PDF stylesheets and dark-mode files)
'at_accounting/static/src/scss/account_asset.scss',
],
'web.assets_unit_tests': [
'at_accounting/static/tests/**/*',
('remove', 'at_accounting/static/tests/tours/**/*'),
'at_accounting/static/tests/*.js',
'at_accounting/static/tests/account_report/**/*.js',
],
'web.assets_tests': [
'at_accounting/static/tests/tours/**/*',
],
'at_accounting.assets_pdf_export': [
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
'web/static/fonts/fonts.scss',
'at_accounting/static/src/scss/**/*',
],
'web.report_assets_common': [
'at_accounting/static/src/scss/account_pdf_export_template.scss',
],
'web.assets_web_dark': [
'at_accounting/static/src/scss/*.dark.scss',
],
'web.qunit_suite_tests': [
'at_accounting/static/tests/legacy/*.js',
],
},
'images': ['static/description/banner.png'],
}

View File

@@ -0,0 +1,89 @@
import werkzeug
from werkzeug.exceptions import InternalServerError
from odoo.addons.at_accounting.models.account_report import AccountReportFileDownloadException
from odoo.addons.account.controllers.download_docs import _get_headers
from odoo import http
from odoo.models import check_method_name
from odoo.http import content_disposition, request
from odoo.tools.misc import html_escape
import json
class AccountReportController(http.Controller):
@http.route('/at_accounting', type='http', auth='user', methods=['POST'], csrf=False)
def get_report(self, options, file_generator, **kwargs):
uid = request.uid
options = json.loads(options)
allowed_company_ids = request.env['account.report'].get_report_company_ids(options)
if not allowed_company_ids:
company_str = request.cookies.get('cids', str(request.env.user.company_id.id))
allowed_company_ids = [int(str_id) for str_id in company_str.split('-')]
report = request.env['account.report'].with_user(uid).with_context(allowed_company_ids=allowed_company_ids).browse(options['report_id'])
try:
check_method_name(file_generator)
generated_file_data = report.dispatch_report_action(options, file_generator)
file_content = generated_file_data['file_content']
file_type = generated_file_data['file_type']
response_headers = self._get_response_headers(file_type, generated_file_data['file_name'], file_content)
if file_type == 'xlsx':
response = request.make_response(None, headers=response_headers)
response.stream.write(file_content)
else:
response = request.make_response(file_content, headers=response_headers)
if file_type in ('zip', 'xaf'):
# Adding direct_passthrough to the response and giving it a file
# as content means that we will stream the content of the file to the user
# Which will prevent having the whole file in memory
response.direct_passthrough = True
return response
except AccountReportFileDownloadException as e:
if e.content:
e.content['file_content'] = e.content['file_content'].decode()
data = {
'name': type(e).__name__,
'arguments': [e.errors, e.content],
}
raise InternalServerError(response=self._generate_response(data)) from e
except Exception as e: # noqa: BLE001
data = http.serialize_exception(e)
raise InternalServerError(response=self._generate_response(data)) from e
def _generate_response(self, data):
error = {
'code': 200,
'message': 'Odoo Server Error',
'data': data,
}
return request.make_response(html_escape(json.dumps(error)))
def _get_response_headers(self, file_type, file_name, file_content):
headers = [
('Content-Type', request.env['account.report'].get_export_mime_type(file_type)),
('Content-Disposition', content_disposition(file_name)),
]
if file_type in ('xml', 'txt', 'csv', 'kvr', 'csv'):
headers.append(('Content-Length', len(file_content)))
return headers
@http.route('/at_accounting/download_attachments/<models("ir.attachment"):attachments>', type='http', auth='user')
def download_report_attachments(self, attachments):
attachments.check_access('read')
assert all(attachment.res_id and attachment.res_model == 'res.partner' for attachment in attachments)
if len(attachments) == 1:
headers = _get_headers(attachments.name, attachments.mimetype, attachments.raw)
return request.make_response(attachments.raw, headers)
else:
content = attachments._build_zip_from_attachments()
headers = _get_headers('attachments.zip', 'zip', content)
return request.make_response(content, headers)

View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_account_report_cs" model="ir.actions.client">
<field name="name">Cash Flow Statement</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('at_accounting.cash_flow_report')}"/>
</record>
<record id="action_account_report_bs" model="ir.actions.client">
<field name="name">Balance Sheet</field>
<field name="tag">account_report</field>
<field name="path">balance-sheet</field>
<field name="context" eval="{'report_id': ref('at_accounting.balance_sheet')}"/>
</record>
<record id="action_account_report_exec_summary" model="ir.actions.client">
<field name="name">Executive Summary</field>
<field name="tag">account_report</field>
<field name="path">executive-summary</field>
<field name="context" eval="{'report_id': ref('at_accounting.executive_summary')}"/>
</record>
<record id="action_account_report_pl" model="ir.actions.client">
<field name="name">Profit and Loss</field>
<field name="tag">account_report</field>
<field name="path">profit-and-loss</field>
<field name="context" eval="{'report_id': ref('at_accounting.profit_and_loss')}"/>
</record>
<record id="action_account_report_gt" model="ir.actions.client">
<field name="name">Tax Return</field>
<field name="tag">account_report</field>
<field name="path">tax-report</field>
<field name="context" eval="{'report_id': ref('account.generic_tax_report')}"/>
</record>
<record id="action_account_report_ja" model="ir.actions.client">
<field name="name">Journal Audit</field>
<field name="tag">account_report</field>
<field name="path">journal-report</field>
<field name="context" eval="{'report_id': ref('at_accounting.journal_report')}"/>
</record>
<record id="action_account_report_general_ledger" model="ir.actions.client">
<field name="name">General Ledger</field>
<field name="tag">account_report</field>
<field name="path">general-ledger</field>
<field name="context" eval="{'report_id': ref('at_accounting.general_ledger_report')}"/>
</record>
<record id="action_account_report_multicurrency_revaluation" model="ir.actions.client">
<field name="name">Unrealized Currency Gains/Losses</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('at_accounting.multicurrency_revaluation_report')}"/>
</record>
<record id="action_account_report_ar" model="ir.actions.client">
<field name="name">Aged Receivable</field>
<field name="tag">account_report</field>
<field name="path">aged-receivable</field>
<field name="context" eval="{'report_id': ref('at_accounting.aged_receivable_report')}"/>
</record>
<record id="action_account_report_ap" model="ir.actions.client">
<field name="name">Aged Payable</field>
<field name="tag">account_report</field>
<field name="path">aged-payable</field>
<field name="context" eval="{'report_id': ref('at_accounting.aged_payable_report')}"/>
</record>
<record id="action_account_report_coa" model="ir.actions.client">
<field name="name">Trial Balance</field>
<field name="tag">account_report</field>
<field name="path">trial-balance</field>
<field name="context" eval="{'report_id': ref('at_accounting.trial_balance_report')}"/>
</record>
<record id="action_account_report_partner_ledger" model="ir.actions.client">
<field name="name">Partner Ledger</field>
<field name="tag">account_report</field>
<field name="path">partner-ledger</field>
<field name="context" eval="{'report_id': ref('at_accounting.partner_ledger_report')}"/>
</record>
<record id="action_account_report_sales" model="ir.actions.client">
<field name="name">EC Sales List</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('at_accounting.generic_ec_sales_report')}"/>
</record>
<record id="action_account_report_deferred_expense" model="ir.actions.client">
<field name="name">Deferred Expense</field>
<field name="tag">account_report</field>
<field name="path">deferred-expense</field>
<field name="context" eval="{'report_id': ref('at_accounting.deferred_expense_report')}"/>
</record>
<record id="action_account_report_deferred_revenue" model="ir.actions.client">
<field name="name">Deferred Revenue</field>
<field name="tag">account_report</field>
<field name="path">deferred-revenue</field>
<field name="context" eval="{'report_id': ref('at_accounting.deferred_revenue_report')}"/>
</record>
<record id="account_financial_current_year_earnings0" model="account.report.line">
<field name="action_id" ref="action_account_report_pl"/>
</record>
<record id="account_financial_report_executivesummary_profitability0" model="account.report.line">
<field name="action_id" ref="action_account_report_pl"/>
</record>
<record id="account_financial_report_executivesummary_balancesheet0" model="account.report.line">
<field name="action_id" ref="action_account_report_bs"/>
</record>
<record id="action_create_report_menu" model="ir.actions.server">
<field name="name">Create Menu Item</field>
<field name="model_id" ref="account.model_account_report"/>
<field name="binding_model_id" ref="account.model_account_report"/>
<field name="state">code</field>
<field name="binding_view_types">form</field>
<field name="code">
if records:
action = records._create_menu_item_for_report()
</field>
</record>
<record id="action_account_report_tree" model="ir.actions.act_window">
<field name="name">Accounting Reports</field>
<field name="res_model">account.report</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="account_report_tree"/>
<field name="search_view_id" ref="view_account_report_search"/>
</record>
<record id="action_account_report_horizontal_groups" model="ir.actions.act_window">
<field name="name">Horizontal Groups</field>
<field name="res_model">account.report.horizontal.group</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="account_report_horizontal_group_tree"/>
</record>
<record id="action_account_report_bank_reconciliation" model="ir.actions.client">
<field name="name">Bank Reconciliation</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('at_accounting.bank_reconciliation_report')}"/>
</record>
<record id="action_account_report_budget_tree" model="ir.actions.act_window">
<field name="name">Financial Budgets</field>
<field name="res_model">account.report.budget</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="account_report_budget_tree"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_account_report_assets" model="ir.actions.client">
<field name="name">Depreciation Schedule</field>
<field name="tag">account_report</field>
<field name="context" eval="{'report_id': ref('at_accounting.assets_report')}"/>
</record>
<menuitem id="menu_action_account_report_assets"
name="Depreciation Schedule"
action="action_account_report_assets"
parent="account.account_reports_management_menu"
groups="account.group_account_readonly"/>
</odoo>

View File

@@ -0,0 +1,326 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="aged_receivable_report" model="account.report">
<field name="name">Aged Receivable</field>
<field name="filter_date_range" eval="False"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_partner" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_account_type">receivable</field>
<field name="filter_hierarchy">never</field>
<field name="filter_show_draft" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="default_opening_date_filter">today</field>
<field name="custom_handler_model_id" ref="model_account_aged_receivable_report_handler"/>
<field name="column_ids">
<record id="aged_receivable_report_invoice_date" model="account.report.column">
<field name="name">Invoice Date</field>
<field name="expression_label">invoice_date</field>
<field name="figure_type">date</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_amount_currency" model="account.report.column">
<field name="name">Amount Currency</field>
<field name="expression_label">amount_currency</field>
</record>
<record id="aged_receivable_report_currency" model="account.report.column">
<field name="name">Currency</field>
<field name="expression_label">currency</field>
<field name="figure_type">string</field>
</record>
<record id="aged_receivable_report_account_name" model="account.report.column">
<field name="name">Account</field>
<field name="expression_label">account_name</field>
<field name="figure_type">string</field>
</record>
<record id="aged_receivable_report_period0" model="account.report.column">
<field name="name">At Date</field>
<field name="expression_label">period0</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_period1" model="account.report.column">
<field name="name">Period 1</field>
<field name="expression_label">period1</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_period2" model="account.report.column">
<field name="name">Period 2</field>
<field name="expression_label">period2</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_period3" model="account.report.column">
<field name="name">Period 3</field>
<field name="expression_label">period3</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_period4" model="account.report.column">
<field name="name">Period 4</field>
<field name="expression_label">period4</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_period5" model="account.report.column">
<field name="name">Older</field>
<field name="expression_label">period5</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_receivable_report_total" model="account.report.column">
<field name="name">Total</field>
<field name="expression_label">total</field>
<field name="sortable" eval="True"/>
</record>
</field>
<field name="line_ids">
<record id="aged_receivable_line" model="account.report.line">
<field name="name">Aged Receivable</field>
<field name="groupby">partner_id, id</field>
<field name="expression_ids">
<record id="aged_receivable_line_invoice_date" model="account.report.expression">
<field name="label">invoice_date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">invoice_date</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_receivable_line_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_receivable_line_amount_currency_forced_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">currency_id</field>
</record>
<record id="aged_receivable_line_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_receivable_line_account_name" model="account.report.expression">
<field name="label">account_name</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">account_name</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_receivable_line_period0" model="account.report.expression">
<field name="label">period0</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period0</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_period1" model="account.report.expression">
<field name="label">period1</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period1</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_period2" model="account.report.expression">
<field name="label">period2</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period2</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_period3" model="account.report.expression">
<field name="label">period3</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period3</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_period4" model="account.report.expression">
<field name="label">period4</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period4</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_period5" model="account.report.expression">
<field name="label">period5</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">period5</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_receivable_line_total" model="account.report.expression">
<field name="label">total</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_receivable</field>
<field name="subformula">total</field>
<field name="auditable" eval="True"/>
</record>
</field>
</record>
</field>
</record>
<record id="aged_payable_report" model="account.report">
<field name="name">Aged Payable</field>
<field name="filter_date_range" eval="False"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_partner" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_account_type">payable</field>
<field name="filter_hierarchy">never</field>
<field name="filter_show_draft" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="default_opening_date_filter">today</field>
<field name="custom_handler_model_id" ref="model_account_aged_payable_report_handler"/>
<field name="column_ids">
<record id="aged_payable_report_invoice_date" model="account.report.column">
<field name="name">Invoice Date</field>
<field name="expression_label">invoice_date</field>
<field name="figure_type">date</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_amount_currency" model="account.report.column">
<field name="name">Amount Currency</field>
<field name="expression_label">amount_currency</field>
</record>
<record id="aged_payable_report_currency" model="account.report.column">
<field name="name">Currency</field>
<field name="expression_label">currency</field>
<field name="figure_type">string</field>
</record>
<record id="aged_payable_report_account_name" model="account.report.column">
<field name="name">Account</field>
<field name="expression_label">account_name</field>
<field name="figure_type">string</field>
</record>
<record id="aged_payable_report_period0" model="account.report.column">
<field name="name">At Date</field>
<field name="expression_label">period0</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_period1" model="account.report.column">
<field name="name">Period 1</field>
<field name="expression_label">period1</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_period2" model="account.report.column">
<field name="name">Period 2</field>
<field name="expression_label">period2</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_period3" model="account.report.column">
<field name="name">Period 3</field>
<field name="expression_label">period3</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_period4" model="account.report.column">
<field name="name">Period 4</field>
<field name="expression_label">period4</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_period5" model="account.report.column">
<field name="name">Older</field>
<field name="expression_label">period5</field>
<field name="sortable" eval="True"/>
</record>
<record id="aged_payable_report_total" model="account.report.column">
<field name="name">Total</field>
<field name="expression_label">total</field>
<field name="sortable" eval="True"/>
</record>
</field>
<field name="line_ids">
<record id="aged_payable_line" model="account.report.line">
<field name="name">Aged Payable</field>
<field name="groupby">partner_id, id</field>
<field name="expression_ids">
<record id="aged_payable_line_invoice_date" model="account.report.expression">
<field name="label">invoice_date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">invoice_date</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_payable_line_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_payable_line_amount_currency_forced_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">currency_id</field>
</record>
<record id="aged_payable_line_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_payable_line_account_name" model="account.report.expression">
<field name="label">account_name</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">account_name</field>
<field name="auditable" eval="False"/>
</record>
<record id="aged_payable_line_period0" model="account.report.expression">
<field name="label">period0</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period0</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_period1" model="account.report.expression">
<field name="label">period1</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period1</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_period2" model="account.report.expression">
<field name="label">period2</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period2</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_period3" model="account.report.expression">
<field name="label">period3</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period3</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_period4" model="account.report.expression">
<field name="label">period4</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period4</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_period5" model="account.report.expression">
<field name="label">period5</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">period5</field>
<field name="auditable" eval="True"/>
</record>
<record id="aged_payable_line_total" model="account.report.expression">
<field name="label">total</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_aged_payable</field>
<field name="subformula">total</field>
<field name="auditable" eval="True"/>
</record>
</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="assets_report" model="account.report">
<field name="name">Depreciation Schedule</field>
<field name="filter_hierarchy">optional</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_journals" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_asset_report_handler"/>
<field name="load_more_limit" eval="80"/>
<field name="column_ids">
<record id="assets_report_acquisition_date" model="account.report.column">
<field name="name">Acquisition Date</field>
<field name="expression_label">acquisition_date</field>
<field name="figure_type">date</field>
</record>
<record id="assets_report_first_depreciation" model="account.report.column">
<field name="name">First Depreciation</field>
<field name="expression_label">first_depreciation</field>
<field name="figure_type">date</field>
</record>
<record id="assets_report_first_method" model="account.report.column">
<field name="name">Method</field>
<field name="expression_label">method</field>
<field name="figure_type">string</field>
</record>
<record id="assets_report_duration_rate" model="account.report.column">
<field name="name">Duration / Rate</field>
<field name="expression_label">duration_rate</field>
<field name="figure_type">string</field>
</record>
<record id="assets_report_date_from" model="account.report.column">
<field name="name">date from</field>
<field name="expression_label">assets_date_from</field>
</record>
<record id="assets_report_assets_plus" model="account.report.column">
<field name="name">+</field>
<field name="expression_label">assets_plus</field>
</record>
<record id="assets_report_assets_minus" model="account.report.column">
<field name="name">-</field>
<field name="expression_label">assets_minus</field>
</record>
<record id="assets_report_assets_date_to" model="account.report.column">
<field name="name">date to</field>
<field name="expression_label">assets_date_to</field>
</record>
<record id="assets_report_depre_date_from" model="account.report.column">
<field name="name">date from</field>
<field name="expression_label">depre_date_from</field>
</record>
<record id="assets_report_depre_plus" model="account.report.column">
<field name="name">+</field>
<field name="expression_label">depre_plus</field>
</record>
<record id="assets_report_depre_minus" model="account.report.column">
<field name="name">-</field>
<field name="expression_label">depre_minus</field>
</record>
<record id="assets_report_depre_date_to" model="account.report.column">
<field name="name">date to</field>
<field name="expression_label">depre_date_to</field>
</record>
<record id="assets_report_balance" model="account.report.column">
<field name="name">book_value</field>
<field name="expression_label">balance</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Switch root menu "Invoicing" to "Accounting" -->
<!-- Top menu item -->
<menuitem name="Accounting"
id="menu_accounting"
groups="account.group_account_readonly,account.group_account_invoice"
web_icon="at_accounting,static/description/icon.png"
sequence="60"/>
<!-- move existing submenus to point to the new parent -->
<record id="account.menu_finance_receivables" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_payables" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_entries" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_reports" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_finance_configuration" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<record id="account.menu_board_journal_1" model="ir.ui.menu">
<field name="parent_id" ref="menu_accounting"/>
</record>
<menuitem id="account.menu_account_config" name="Settings" parent="account.menu_finance_configuration" sequence="0" groups="base.group_system"/>
</odoo>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_accountant_tour" model="web_tour.tour">
<field name="name">account_accountant_tour</field>
<field name="sequence">50</field>
<field name="rainbow_man_message"><![CDATA[
<strong><b>Good job!</b> You went through all steps of this tour.</strong>
<br>See how to manage your customer invoices in the <b>Customers/Invoices</b> menu
]]></field>
</record>
</odoo>

View File

@@ -0,0 +1,285 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="balance_sheet" model="account.report">
<field name="name">Balance Sheet</field>
<field name="filter_date_range" eval="False"/>
<field name="filter_analytic_groupby" eval="True"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_journals" eval="True"/>
<field name="filter_multi_company">selector</field>
<field name="default_opening_date_filter">today</field>
<field name="custom_handler_model_id" ref="model_account_balance_sheet_report_handler"/>
<field name="column_ids">
<record id="balance_sheet_balance" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
<field name="line_ids">
<record id="account_financial_report_total_assets0" model="account.report.line">
<field name="name">ASSETS</field>
<field name="hierarchy_level">0</field>
<field name="code">TA</field>
<field name="horizontal_split_side">left</field>
<field name="aggregation_formula">CA.balance + FA.balance + PNCA.balance</field>
<field name="children_ids">
<record id="account_financial_report_current_assets_view0" model="account.report.line">
<field name="name">Current Assets</field>
<field name="code">CA</field>
<field name="aggregation_formula">BA.balance + REC.balance + CAS.balance + PRE.balance</field>
<field name="children_ids">
<record id="account_financial_report_bank_view0" model="account.report.line">
<field name="name">Bank and Cash Accounts</field>
<field name="code">BA</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_cash')])</field>
</record>
<record id="account_financial_report_receivable0" model="account.report.line">
<field name="name">Receivables</field>
<field name="code">REC</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', False)])</field>
</record>
<record id="account_financial_report_current_assets0" model="account.report.line">
<field name="name">Current Assets</field>
<field name="code">CAS</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum(['|', ('account_id.account_type', '=', 'asset_current'), '&amp;', ('account_id.account_type', '=', 'asset_receivable'), ('account_id.non_trade', '=', True)])</field>
</record>
<record id="account_financial_report_prepayements0" model="account.report.line">
<field name="name">Prepayments</field>
<field name="code">PRE</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_prepayments')])</field>
</record>
</field>
</record>
<record id="account_financial_report_fixed_assets_view0" model="account.report.line">
<field name="name">Plus Fixed Assets</field>
<field name="code">FA</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_fixed')])</field>
</record>
<record id="account_financial_report_non_current_assets_view0" model="account.report.line">
<field name="name">Plus Non-current Assets</field>
<field name="code">PNCA</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="domain_formula">sum([('account_id.account_type', '=', 'asset_non_current')])</field>
</record>
</field>
</record>
<record id="account_financial_report_liabilities_view0" model="account.report.line">
<field name="name">LIABILITIES</field>
<field name="hierarchy_level">0</field>
<field name="code">L</field>
<field name="horizontal_split_side">right</field>
<field name="expression_ids">
<record id="account_financial_report_liabilities_view0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">CL.balance + NL.balance</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
<field name="children_ids">
<record id="account_financial_report_current_liabilities0" model="account.report.line">
<field name="name">Current Liabilities</field>
<field name="code">CL</field>
<field name="expression_ids">
<record id="account_financial_report_current_liabilities0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">CL1.balance + CL2.balance</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
<field name="children_ids">
<record id="account_financial_report_current_liabilities1" model="account.report.line">
<field name="name">Current Liabilities</field>
<field name="code">CL1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_current_liabilities1_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="['|', ('account_id.account_type', 'in', ('liability_current', 'liability_credit_card')), '&amp;', ('account_id.account_type', '=', 'liability_payable'), ('account_id.non_trade', '=', True)]"/>
<field name="subformula">-sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_current_liabilities_payable" model="account.report.line">
<field name="name">Payables</field>
<field name="code">CL2</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_current_liabilities_payable_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'liability_payable'), ('account_id.non_trade', '=', False)]"/>
<field name="subformula">-sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_non_current_liabilities0" model="account.report.line">
<field name="name">Plus Non-current Liabilities</field>
<field name="code">NL</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_non_current_liabilities0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'liability_non_current')]"/>
<field name="subformula">-sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_equity0" model="account.report.line">
<field name="name">EQUITY</field>
<field name="hierarchy_level">0</field>
<field name="code">EQ</field>
<field name="horizontal_split_side">right</field>
<field name="aggregation_formula">UNAFFECTED_EARNINGS.balance + RETAINED_EARNINGS.balance</field>
<field name="children_ids">
<record id="account_financial_unaffected_earnings0" model="account.report.line">
<field name="name">Unallocated Earnings</field>
<field name="code">UNAFFECTED_EARNINGS</field>
<field name="aggregation_formula">CURR_YEAR_EARNINGS.balance + PREV_YEAR_EARNINGS.balance</field>
<field name="children_ids">
<record id="account_financial_current_year_earnings0" model="account.report.line">
<field name="name">Current Year Unallocated Earnings</field>
<field name="code">CURR_YEAR_EARNINGS</field>
<field name="aggregation_formula"/>
<field name="expression_ids">
<record id="account_financial_current_year_earnings_pnl" model="account.report.expression">
<field name="label">pnl</field>
<field name="engine">aggregation</field>
<field name="formula">NEP.balance</field>
<field name="date_scope">from_fiscalyear</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_current_year_earnings_alloc" model="account.report.expression">
<field name="label">alloc</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'equity_unaffected')]"/>
<field name="date_scope">from_fiscalyear</field>
<field name="subformula">-sum</field>
</record>
<record id="account_financial_current_year_earnings_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">CURR_YEAR_EARNINGS.pnl + CURR_YEAR_EARNINGS.alloc</field>
</record>
</field>
</record>
<record id="account_financial_previous_year_earnings0" model="account.report.line">
<field name="name">Previous Years Unallocated Earnings</field>
<field name="code">PREV_YEAR_EARNINGS</field>
<field name="expression_ids">
<record id="account_financial_previous_year_earnings0_allocated_earnings" model="account.report.expression">
<field name="label">allocated_earnings</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'equity_unaffected')]"/>
<field name="subformula">-sum</field>
<field name="date_scope">from_beginning</field>
</record>
<record id="account_financial_previous_year_earnings0_balance_domain" model="account.report.expression">
<field name="label">balance_domain</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', 'in', ('income', 'income_other', 'expense_direct_cost', 'expense', 'expense_depreciation'))]"/>
<field name="subformula">-sum</field>
<field name="date_scope">from_beginning</field>
</record>
<record id="account_financial_previous_year_earnings0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">PREV_YEAR_EARNINGS.balance_domain + PREV_YEAR_EARNINGS.allocated_earnings - CURR_YEAR_EARNINGS.balance</field>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_retained_earnings0" model="account.report.line">
<field name="name">Retained Earnings</field>
<field name="code">RETAINED_EARNINGS</field>
<field name="aggregation_formula">CURR_RETAINED_EARNINGS.balance + PREV_RETAINED_EARNINGS.balance</field>
<field name="groupby" eval="False"/>
<field name="foldable" eval="False"/>
<field name="children_ids">
<record id="account_financial_retained_earnings_line_1" model="account.report.line">
<field name="name">Current Year Retained Earnings</field>
<field name="code">CURR_RETAINED_EARNINGS</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_retained_earnings_current" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'equity')]"/>
<field name="subformula">-sum</field>
<field name="date_scope">from_fiscalyear</field>
</record>
</field>
</record>
<record id="account_financial_retained_earnings_line_2" model="account.report.line">
<field name="name">Previous Years Retained Earnings</field>
<field name="code">PREV_RETAINED_EARNINGS</field>
<field name="expression_ids">
<record id="account_financial_retained_earnings_total" model="account.report.expression">
<field name="label">total</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'equity')]"/>
<field name="subformula">-sum</field>
</record>
<record id="account_financial_retained_earnings_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">PREV_RETAINED_EARNINGS.total - CURR_RETAINED_EARNINGS.balance</field>
</record>
</field>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_liabilities_and_equity_view0" model="account.report.line">
<field name="name">LIABILITIES + EQUITY</field>
<field name="hierarchy_level">0</field>
<field name="code">LE</field>
<field name="horizontal_split_side">right</field>
<field name="expression_ids">
<record id="account_financial_report_liabilities_and_equity_view0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">L.balance + EQ.balance</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_off_sheet" model="account.report.line">
<field name="name">OFF BALANCE SHEET ACCOUNTS</field>
<field name="hierarchy_level">0</field>
<field name="code">OS</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="hide_if_zero" eval="1"/>
<field name="domain_formula">-sum([('account_id.account_type', '=', 'off_balance')])</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,474 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="bank_reconciliation_report" model="account.report">
<field name="name">Bank Reconciliation Report</field>
<field name="filter_show_draft" eval="True"/>
<field name="filter_date_range" eval="False"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_hide_0_lines">by_default</field>
<field name="search_bar" eval="True"/>
<field name="default_opening_date_filter">today</field>
<field name="custom_handler_model_id" ref="model_account_bank_reconciliation_report_handler"/>
<field name="column_ids">
<record id="bank_reconciliation_report_date" model="account.report.column">
<field name="name">Date</field>
<field name="expression_label">date</field>
<field name="figure_type">date</field>
</record>
<record id="bank_reconciliation_report_label" model="account.report.column">
<field name="name">Label</field>
<field name="expression_label">label</field>
<field name="figure_type">string</field>
</record>
<record id="bank_reconciliation_report_amount_currency" model="account.report.column">
<field name="name">Amount Currency</field>
<field name="expression_label">amount_currency</field>
<field name="figure_type">monetary</field>
</record>
<record id="bank_reconciliation_report_currency" model="account.report.column">
<field name="name">Currency</field>
<field name="expression_label">currency</field>
<field name="figure_type">string</field>
</record>
<record id="bank_reconciliation_report_amount" model="account.report.column">
<field name="name">Amount</field>
<field name="expression_label">amount</field>
<field name="figure_type">monetary</field>
</record>
</field>
<field name="line_ids">
<record id="balance_bank" model="account.report.line">
<field name="name">Balance of Bank</field>
<field name="code">balance_bank</field>
<field name="hierarchy_level">0</field>
<field name="expression_ids">
<record id="balance_bank_expr" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">aggregation</field>
<field name="formula">last_statement_balance.amount + transaction_without_statement.amount + misc_operations.amount</field>
<field name="auditable" eval="True"/>
</record>
<record id="balance_bank_expr_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_forced_currency_amount</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
<field name="children_ids">
<record id="last_statement_balance" model="account.report.line">
<field name="name">Last statement balance</field>
<field name="code">last_statement_balance</field>
<field name="expression_ids">
<record id="last_statement_balance_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_last_statement_balance_amount</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="last_statement_balance_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_last_statement_balance_amount</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
<field name="children_ids">
<record id="unreconciled_last_statement_receipts" model="account.report.line">
<field name="name">Including Unreconciled Receipts</field>
<field name="code">last_statement_receipts</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="unreconciled_last_statement_receipts_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_receipts_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_receipts_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_receipts_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="unreconciled_last_statement_receipts_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_receipts_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_receipts_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_receipts</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
<record id="unreconciled_last_statement_payments" model="account.report.line">
<field name="name">Including Unreconciled Payments</field>
<field name="code">last_statement_payments</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="unreconciled_last_statement_payments_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_payments_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_payments_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_payments_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="unreconciled_last_statement_payments_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_payments_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="unreconciled_last_statement_payments_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_last_statement_payments</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
</field>
</record>
<record id="transaction_without_statement" model="account.report.line">
<field name="name">Transactions without statement</field>
<field name="code">transaction_without_statement</field>
<field name="expression_ids">
<record id="transaction_without_statement_expr" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_transaction_without_statement_amount</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="transaction_without_statement_expr_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_transaction_without_statement_amount</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
<field name="children_ids">
<record id="no_statement_unreconciled_receipt" model="account.report.line">
<field name="name">Including Unreconciled Receipts</field>
<field name="code">unreconciled_receipt</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="no_statement_unreconciled_receipt_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_receipt_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_receipt_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_receipt_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="no_statement_unreconciled_receipt_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_receipt_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_receipt_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_receipts</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
<record id="no_statement_unreconciled_payments" model="account.report.line">
<field name="name">Including Unreconciled Payments</field>
<field name="code">unreconciled_payments</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="no_statement_unreconciled_payments_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_payments_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_payments_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_payments_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="no_statement_unreconciled_payments_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_payments_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="no_statement_unreconciled_payments_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_unreconciled_payments</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
</field>
</record>
<record id="misc_operations" model="account.report.line">
<field name="name">Misc. operations</field>
<field name="code">misc_operations</field>
<field name="expression_ids">
<record id="misc_operations_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_misc_operations</field>
<field name="subformula">amount</field>
<field name="auditable" eval="True"/>
</record>
<record id="misc_operations_amount_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_misc_operations</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
</field>
</record>
<record id="outstanding" model="account.report.line">
<field name="name">Outstanding Receipts/Payments</field>
<field name="hierarchy_level">0</field>
<field name="expression_ids">
<record id="outstanding_expr" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">aggregation</field>
<field name="formula">outstanding_receipts.amount + outstanding_payments.amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_expr_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_forced_currency_amount</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
<field name="children_ids">
<record id="outstanding_receipts" model="account.report.line">
<field name="name">(+) Outstanding Receipts</field>
<field name="code">outstanding_receipts</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="outstanding_receipts_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_receipts_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_receipts_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_receipts_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="outstanding_receipts_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_receipts_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_receipts_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_receipts</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
<record id="outstanding_payments" model="account.report.line">
<field name="name">(-) Outstanding Payments</field>
<field name="code">outstanding_payments</field>
<field name="groupby">id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="outstanding_payments_date" model="account.report.expression">
<field name="label">date</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">date</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_payments_label" model="account.report.expression">
<field name="label">label</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">label</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_payments_amount_currency" model="account.report.expression">
<field name="label">amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">amount_currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_payments_forced_currency_amount_currency" model="account.report.expression">
<field name="label">_currency_amount_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">amount_currency_currency_id</field>
</record>
<record id="outstanding_payments_currency" model="account.report.expression">
<field name="label">currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">currency</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_payments_amount" model="account.report.expression">
<field name="label">amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">amount</field>
<field name="auditable" eval="False"/>
</record>
<record id="outstanding_payments_forced_currency_amount" model="account.report.expression">
<field name="label">_currency_amount</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_outstanding_payments</field>
<field name="subformula">amount_currency_id</field>
</record>
</field>
</record>
</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="cash_flow_report" model="account.report">
<field name="name">Cash Flow Statement</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_date_range" eval="True"/>
<field name="filter_journals" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="currency_translation">current</field>
<field name="custom_handler_model_id" ref="model_account_cash_flow_report_handler"/>
<field name="column_ids">
<record id="cash_flow_report_balance" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="deferred_expense_report" model="account.report">
<field name="name">Deferred Expense Report</field>
<field name="filter_journals" eval="True"/>
<field name="filter_analytic" eval="True"/>
<field name="filter_period_comparison" eval="True"/>
<field name="filter_growth_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_hierarchy">by_default</field>
<field name="default_opening_date_filter">previous_month</field>
<field name="search_bar" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_deferred_expense_report_handler"/>
<field name="column_ids">
<record id="deferred_expense_current" model="account.report.column">
<field name="name">Current</field>
<field name="expression_label">current</field>
</record>
</field>
</record>
<record id="deferred_revenue_report" model="account.report">
<field name="name">Deferred Revenue Report</field>
<field name="filter_journals" eval="True"/>
<field name="filter_analytic" eval="True"/>
<field name="filter_period_comparison" eval="True"/>
<field name="filter_growth_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_hierarchy">by_default</field>
<field name="default_opening_date_filter">previous_month</field>
<field name="search_bar" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_deferred_revenue_report_handler"/>
<field name="column_ids">
<record id="deferred_revenue_current" model="account.report.column">
<field name="name">Current</field>
<field name="expression_label">current</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,37 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data noupdate="1">
<record id="digest.digest_digest_default" model="digest.digest">
<field name="kpi_account_bank_cash">True</field>
</record>
</data>
<data noupdate="0">
<record id="digest_tip_account_accountant_0" model="digest.tip">
<field name="name">Tip: Bulk update journal items</field>
<field name="sequence">900</field>
<field name="group_id" ref="account.group_account_user" />
<field name="tip_description" type="html">
<div>
<b class="tip_title">Tip: Bulk update journal items</b>
<p class="tip_content">From any list view, select multiple records and the list becomes editable. If you update a cell, selected records are updated all at once. Use this feature to update multiple journal entries from the General Ledger, or any Journal view.</p>
<img src="https://download.odoocdn.com/digests/at_accounting/static/src/img/milk-accounting-bulk.gif" width="540" class="illustration_border" />
</div>
</field>
</record>
<record id="digest_tip_account_accountant_1" model="digest.tip">
<field name="name">Tip: Find an Accountant or register your Accounting Firm</field>
<field name="sequence">1000</field>
<field name="group_id" ref="account.group_account_user" />
<field name="tip_description" type="html">
<div>
<b class="tip_title">Tip: Find an Accountant or register your Accounting Firm</b>
<p class="tip_content">Click here to find an accountant or if you want to list out your accounting services on Odoo</p>
<p class="mt-3">
<a class="tip_button" href="https://odoo.com/accounting-firms" target="_blank"><span class="tip_button_text">Find an Accountant</span></a>
<a class="tip_button" href="https://odoo.com/accounting-firms/register" target="_blank"><span class="tip_button_text">Register your Accounting Firm</span></a>
</p>
</div>
</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,395 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="executive_summary" model="account.report">
<field name="name">Executive Summary</field>
<field name="filter_multi_company">selector</field>
<field name="default_opening_date_filter">this_year</field>
<field name="column_ids">
<record id="executive_summary_column" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
<field name="line_ids">
<record id="account_financial_report_executivesummary_cash0" model="account.report.line">
<field name="name">Cash</field>
<field name="hierarchy_level">0</field>
<field name="children_ids">
<record id="account_financial_report_executivesummary_cash_received0" model="account.report.line">
<field name="name">Cash received</field>
<field name="code">CR</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_cash_received0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card')), ('debit', '&gt;', 0.0)]"/>
<field name="subformula">sum</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_cash_spent0" model="account.report.line">
<field name="name">Cash spent</field>
<field name="code">CS</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_cash_spent0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card')), ('credit', '&gt;', 0.0)]"/>
<field name="subformula">sum</field>
<field name="green_on_positive" eval="False"/>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_cash_surplus0" model="account.report.line">
<field name="name">Cash surplus</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_cash_surplus0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">CR.balance + CS.balance</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_closing_bank_balance0" model="account.report.line">
<field name="name">Closing bank balance</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_closing_bank_balance0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', 'in', ('asset_cash', 'liability_credit_card'))]"/>
<field name="date_scope">from_beginning</field>
<field name="subformula">sum</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_profitability0" model="account.report.line">
<field name="name">Profitability</field>
<field name="hierarchy_level">0</field>
<field name="children_ids">
<record id="account_financial_report_executivesummary_income0" model="account.report.line">
<field name="name">Revenue</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_income0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_direct_costs0" model="account.report.line">
<field name="name">Cost of Revenue</field>
<field name="code">EXEC_COS</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_direct_costs0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">COS.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
<field name="green_on_positive" eval="False"/>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_gross_profit0" model="account.report.line">
<field name="name">Gross profit</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_gross_profit0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">GRP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_expenses0" model="account.report.line">
<field name="name">Expenses</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_expenses0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">EXP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
<field name="green_on_positive" eval="False"/>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_profit0" model="account.report.line">
<field name="name">Net Profit</field>
<field name="code">EXEC_NEP</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_profit0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">NEP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_balancesheet0" model="account.report.line">
<field name="name">Balance Sheet</field>
<field name="hierarchy_level">0</field>
<field name="children_ids">
<record id="account_financial_report_executivesummary_debtors0" model="account.report.line">
<field name="name">Receivables</field>
<field name="code">DEB</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_debtors0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'asset_receivable')]"/>
<field name="date_scope">from_beginning</field>
<field name="subformula">sum</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_creditors0" model="account.report.line">
<field name="name">Payables</field>
<field name="code">CRE</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_creditors0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'liability_payable')]"/>
<field name="date_scope">from_beginning</field>
<field name="subformula">sum</field>
<field name="green_on_positive" eval="False"/>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_net_assets0" model="account.report.line">
<field name="name">Net assets</field>
<field name="code">EXEC_SUMMARY_NA</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_net_assets0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">TA.balance - L.balance</field>
<field name="date_scope">from_beginning</field>
<field name="subformula">cross_report</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_performance0" model="account.report.line">
<field name="name">Performance</field>
<field name="hierarchy_level">0</field>
<field name="children_ids">
<record id="account_financial_report_executivesummary_gpmargin0" model="account.report.line">
<field name="name">Gross profit margin (gross profit / operating income)</field>
<field name="code">GPMARGIN0</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_gpmargin0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">GPMARGIN0.grp / GPMARGIN0.opinc * 100</field>
<field name="subformula">ignore_zero_division</field>
<field name="figure_type">percentage</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_gpmargin0_grp" model="account.report.expression">
<field name="label">grp</field>
<field name="engine">aggregation</field>
<field name="formula">GRP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_gpmargin0_opinc" model="account.report.expression">
<field name="label">opinc</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_npmargin0" model="account.report.line">
<field name="name">Net profit margin (net profit / income)</field>
<field name="code">NPMARGIN0</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_npmargin0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">NPMARGIN0.nep / NPMARGIN0.inc * 100</field>
<field name="subformula">ignore_zero_division</field>
<field name="figure_type">percentage</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_npmargin0_nep" model="account.report.expression">
<field name="label">nep</field>
<field name="engine">aggregation</field>
<field name="formula">NEP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_npmargin0_inc" model="account.report.expression">
<field name="label">inc</field>
<field name="engine">aggregation</field>
<field name="formula">INC.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_return_investment0" model="account.report.line">
<field name="name">Return on investments (net profit / assets)</field>
<field name="code">ROI</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_return_investment0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">ROI.nep / ROI.ta * 100</field>
<field name="subformula">ignore_zero_division</field>
<field name="figure_type">percentage</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_return_investment0_nep" model="account.report.expression">
<field name="label">nep</field>
<field name="engine">aggregation</field>
<field name="formula">NEP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_return_investment0_ta" model="account.report.expression">
<field name="label">ta</field>
<field name="engine">aggregation</field>
<field name="formula">TA.balance</field>
<field name="date_scope">from_beginning</field>
<field name="subformula">cross_report</field>
</record>
</field>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_position0" model="account.report.line">
<field name="name">Position</field>
<field name="hierarchy_level">0</field>
<field name="children_ids">
<record id="account_financial_report_executivesummary_avdebt0" model="account.report.line">
<field name="name">Average debtors days</field>
<field name="code">AVG_DEBT_DAYS</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_avdebt0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">DEB.balance / AVG_DEBT_DAYS.opinc * AVG_DEBT_DAYS.NDays</field>
<field name="subformula">ignore_zero_division</field>
<field name="green_on_positive" eval="False"/>
<field name="figure_type">float</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_avdebt0_opinc" model="account.report.expression">
<field name="label">opinc</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_avdebt0_ndays" model="account.report.expression">
<field name="label">NDays</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_executive_summary_ndays</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_avgcre0" model="account.report.line">
<field name="name">Average creditors days</field>
<field name="code">AVG_CRED_DAYS</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_avgcre0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">-CRE.balance / (AVG_CRED_DAYS.cos + AVG_CRED_DAYS.exp) * AVG_CRED_DAYS.NDays</field>
<field name="subformula">ignore_zero_division</field>
<field name="green_on_positive" eval="False"/>
<field name="figure_type">float</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_avgcre0_cos" model="account.report.expression">
<field name="label">cos</field>
<field name="engine">aggregation</field>
<field name="formula">COS.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_avgcre0_exp" model="account.report.expression">
<field name="label">exp</field>
<field name="engine">aggregation</field>
<field name="formula">EXP.balance</field>
<field name="date_scope">strict_range</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_avgcre0_ndays" model="account.report.expression">
<field name="label">NDays</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_executive_summary_ndays</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_st_cash_forecast0" model="account.report.line">
<field name="name">Short term cash forecast</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_st_cash_forecast0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">DEB.balance + CRE.balance</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_executivesummary_ca_to_l0" model="account.report.line">
<field name="name">Current assets to liabilities</field>
<field name="code">CATL</field>
<field name="expression_ids">
<record id="account_financial_report_executivesummary_ca_to_l0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">CATL.ca / CATL.cl</field>
<field name="subformula">ignore_zero_division</field>
<field name="figure_type">float</field>
<field name="auditable" eval="False"/>
</record>
<record id="account_financial_report_executivesummary_ca_to_l0_ca" model="account.report.expression">
<field name="label">ca</field>
<field name="engine">aggregation</field>
<field name="formula">CA.balance</field>
<field name="date_scope">from_beginning</field>
<field name="subformula">cross_report</field>
</record>
<record id="account_financial_report_executivesummary_ca_to_l0_cl" model="account.report.expression">
<field name="label">cl</field>
<field name="engine">aggregation</field>
<field name="formula">CL.balance</field>
<field name="date_scope">from_beginning</field>
<field name="subformula">cross_report</field>
</record>
</field>
</record>
</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="general_ledger_report" model="account.report">
<field name="name">General Ledger</field>
<field name="filter_journals" eval="True"/>
<field name="filter_analytic" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_hide_0_lines">never</field>
<field name="default_opening_date_filter">this_month</field>
<field name="search_bar" eval="True"/>
<field name="load_more_limit" eval="80"/>
<field name="custom_handler_model_id" ref="model_account_general_ledger_report_handler"/>
<field name="column_ids">
<record id="general_ledger_report_date" model="account.report.column">
<field name="name">Date</field>
<field name="expression_label">date</field>
<field name="figure_type">date</field>
</record>
<record id="general_ledger_report_communication" model="account.report.column">
<field name="name">Communication</field>
<field name="expression_label">communication</field>
<field name="figure_type">string</field>
</record>
<record id="general_ledger_report_partner_name" model="account.report.column">
<field name="name">Partner</field>
<field name="expression_label">partner_name</field>
<field name="figure_type">string</field>
</record>
<record id="general_ledger_report_amount_currency" model="account.report.column">
<field name="name">Currency</field>
<field name="expression_label">amount_currency</field>
</record>
<record id="general_ledger_report_debit" model="account.report.column">
<field name="name">Debit</field>
<field name="expression_label">debit</field>
</record>
<record id="general_ledger_report_credit" model="account.report.column">
<field name="name">Credit</field>
<field name="expression_label">credit</field>
</record>
<record id="general_ledger_report_balance" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="account.generic_tax_report" model="account.report">
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler"/>
</record>
<record id="account.generic_tax_report_account_tax" model="account.report">
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler_account_tax"/>
</record>
<record id="account.generic_tax_report_tax_account" model="account.report">
<field name="custom_handler_model_id" ref="model_account_generic_tax_report_handler_tax_account"/>
</record>
</odoo>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="auto_reconcile_bank_statement_line" model="ir.cron">
<field name="name">Try to reconcile automatically your statement lines</field>
<field name="model_id" ref="model_account_bank_statement_line"/>
<field name="state">code</field>
<field name="code">model._cron_try_auto_reconcile_statement_lines(batch_size=100)</field>
<field name='interval_number'>1</field>
<field name='interval_type'>days</field>
</record>
</odoo>

View File

@@ -0,0 +1,235 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="journal_report" model="account.report">
<field name="name">Journal Report</field>
<field name="filter_journals" eval="True"/>
<field name="filter_show_draft" eval="True"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_hierarchy">never</field>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_unreconciled" eval="False"/>
<field name="filter_hide_0_lines">never</field>
<field name="default_opening_date_filter">this_year</field>
<field name="custom_handler_model_id" ref="model_account_journal_report_handler"/>
<field name="column_ids">
<record id="journal_report_code" model="account.report.column">
<field name="name">Code</field>
<field name="expression_label">code</field>
<field name="figure_type">string</field>
</record>
<record id="journal_report_debit" model="account.report.column">
<field name="name">Debit</field>
<field name="expression_label">debit</field>
</record>
<record id="journal_report_credit" model="account.report.column">
<field name="name">Credit</field>
<field name="expression_label">credit</field>
</record>
<record id="journal_report_balance" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
<field name="line_ids">
<record id="journal_report_line" model="account.report.line">
<field name="name">Name</field>
<field name="groupby">journal_id, account_id</field>
<field name="hierarchy_level">0</field>
<field name="expression_ids">
<record id="journal_report_line_code" model="account.report.expression">
<field name="label">code</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_journal_report</field>
<field name="subformula">code</field>
</record>
<record id="journal_report_line_debit" model="account.report.expression">
<field name="label">debit</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_journal_report</field>
<field name="subformula">debit</field>
</record>
<record id="journal_report_line_credit" model="account.report.expression">
<field name="label">credit</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_journal_report</field>
<field name="subformula">credit</field>
</record>
<record id="journal_report_line_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_journal_report</field>
<field name="subformula">balance</field>
</record>
</field>
</record>
</field>
</record>
<template id="journal_report_pdf_export_main">
<html>
<head>
<base t-att-href="base_url"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<t t-call-assets="at_accounting.assets_pdf_export" t-js="False"/>
</head>
<body t-att-dir="env['res.lang']._get_data(code=lang or env.user.lang).direction or 'ltr'">
<div t-att-class="options['css_custom_class']">
<header>
<div class="row align-items-center">
<div class="col-4 o_header_font">
<t t-call="at_accounting.company_information"/>
</div>
<div class="col-4">
<div class="o_title">
<t t-if="report.filter_show_draft and options['all_entries']">[Draft]</t>
<t t-out="report.name"/>
</div>
<div class="o_subtitle">
<t t-out="options['date']['date_from']"/> - <t t-out="options['date']['date_to']"/>
</div>
</div>
</div>
</header>
<!-- Journal entries -->
<t t-foreach="document_data['journals_vals']" t-as="journal_vals">
<section style="page-break-after: always;">
<div class="o_section_title">
<t t-out="journal_vals.get('name')"/>
</div>
<div class="d-flex align-items-start">
<t t-call="at_accounting.journal_report_pdf_body_default"/>
</div>
<t t-if="journal_vals.get('tax_summary')">
<t t-call="at_accounting.pdf_journal_report_taxes_summary">
<t t-set="tax_summary" t-value="journal_vals['tax_summary']"/>
</t>
</t>
</section>
</t>
<section t-if="document_data.get('global_tax_summary')">
<div class="o_section_title">
Global Tax Summary
</div>
<t t-call="at_accounting.pdf_journal_report_taxes_summary">
<t t-set="tax_summary" t-value="document_data['global_tax_summary']"/>
</t>
</section>
</div>
</body>
</html>
</template>
<template id="journal_report_pdf_body_default">
<table class="o_table">
<thead>
<tr>
<t t-foreach="journal_vals['columns']" t-as="column">
<th t-att-class="column.get('class', '')">
<t t-out="column.get('name', '')"/>
</th>
</t>
</tr>
</thead>
<tbody>
<t t-foreach="journal_vals['lines']" t-as="line">
<tr t-att-class="line.get('line_class', '')">
<t t-foreach="journal_vals['columns']" t-as="column">
<t t-if="line.get(column['label'])">
<t t-set="cell_style" t-value="line[column['label']].get('class', '')"/>
<t t-set="column_style" t-value="column.get('class', '')"/>
<td t-att-class="cell_style + column_style">
<t t-out="line[column['label']]['data']"/>
</td>
</t>
<t t-else="">
<td/>
</t>
</t>
</tr>
</t>
</tbody>
</table>
</template>
<template id="pdf_journal_report_taxes_summary">
<div class="container tax_summary" style="page-break-inside: avoid;">
<t t-set="taxes" t-value="tax_summary.get('tax_report_lines')"/>
<t t-if="taxes">
<div class="row o_section_subtitle">
<p>Tax Applied</p>
</div>
<div class="row taxes">
<t t-set="extra_columns" t-value="tax_summary.get('extra_columns')"/>
<table class="o_table">
<thead>
<tr>
<th t-if="len(taxes) > 1">Country</th>
<th>Name</th>
<th class="o_right_alignment">Base Amount</th>
<th class="o_right_alignment">Tax Amount</th>
<th t-if="tax_summary.get('tax_non_deductible_column')" class="o_right_alignment">Non-Deductible</th>
<th t-if="tax_summary.get('tax_deductible_column')" class="o_right_alignment">Deductible</th>
<th t-if="tax_summary.get('tax_due_column')" class="o_right_alignment">Due</th>
</tr>
</thead>
<tbody>
<t t-foreach="taxes" t-as="country_name">
<tr t-foreach="taxes[country_name]" t-as="tax">
<t t-if="country_name_size > 1">
<td>
<t t-if="tax_index == 0" t-out="country_name"/>
</td>
</t>
<td t-out="tax['name']"/>
<td class="o_right_alignment" t-out="tax['base_amount']"/>
<td class="o_right_alignment" t-out="tax['tax_amount']"/>
<td t-if="tax_summary.get('tax_non_deductible_column')" class="o_right_alignment" t-out="tax['tax_non_deductible']"/>
<td t-if="tax_summary.get('tax_deductible_column')" class="o_right_alignment" t-out="tax['tax_deductible']"/>
<td t-if="tax_summary.get('tax_due_column')" class="o_right_alignment" t-out="tax['tax_due']"/>
</tr>
</t>
</tbody>
</table>
</div>
</t>
<t t-set="grids" t-value="tax_summary.get('tax_grid_summary_lines')"/>
<t t-if="grids">
<div class="row o_section_subtitle">
<p>Impacted Tax Grids</p>
</div>
<div class="row tax_grid">
<table class="o_table">
<thead>
<tr>
<th t-if="len(grids) > 1">Country</th>
<th>Grid</th>
<th class="o_right_alignment">+</th>
<th class="o_right_alignment">-</th>
<th class="o_right_alignment">Impact On Grid</th>
</tr>
</thead>
<tbody>
<t t-foreach="grids" t-as="country_name">
<tr t-foreach="grids[country_name]" t-as="grid_name">
<t t-if="country_name_size > 1">
<td>
<t t-if="grid_name_index == 0" t-out="country_name"/>
</td>
</t>
<td t-out="grid_name"/>
<td class="o_right_alignment" t-out="grids[country_name][grid_name].get('+', 0)"/>
<td class="o_right_alignment" t-out="grids[country_name][grid_name].get('-', 0)"/>
<td class="o_right_alignment" t-out="grids[country_name][grid_name]['impact']"/>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</div>
</template>
</odoo>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="tax_closing_activity_type" model="mail.activity.type">
<field name="name">Tax Report</field>
<field name="summary">Tax Report</field>
<field name="category">tax_report</field>
<field name="res_model">account.journal</field>
<field name="chaining_type">suggest</field>
</record>
<record id="mail_activity_type_tax_report_to_pay" model="mail.activity.type">
<field name="name">Pay Tax</field>
<field name="summary">Tax is ready to be paid</field>
<field name="category">tax_report</field>
<field name="delay_count">0</field>
<field name="delay_unit">days</field>
<field name="delay_from">previous_activity</field>
<field name="res_model">account.move</field>
<field name="chaining_type">suggest</field>
</record>
<record id="mail_activity_type_tax_report_to_be_sent" model="mail.activity.type">
<field name="name">Tax Report Ready</field>
<field name="summary">Tax report is ready to be sent to the administration</field>
<field name="category">tax_report</field>
<field name="delay_count">0</field>
<field name="delay_unit">days</field>
<field name="delay_from">current_date</field>
<field name="res_model">account.move</field>
<field name="chaining_type">suggest</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,26 @@
<odoo>
<record id="email_template_customer_statement" model="mail.template">
<field name="name">Customer Statement</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="email_from">{{ object._get_followup_responsible().email_formatted }}</field>
<field name="subject">{{ (object.company_id or object._get_followup_responsible().company_id).name }} Statement - {{ object.commercial_company_name }}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px;">
<t t-if="object.id != object.commercial_partner_id.id">Dear <t t-out="object.name or ''"/> (<t t-out="object.commercial_partner_id.name or ''"/>),</t>
<t t-else="">Dear <t t-out="object.name or ''"/>,</t>
<br/>
Please find enclosed the statement of your account.
<br/>
Do not hesitate to contact us if you have any questions.
<br/>
Sincerely,
<br/>
<t t-out="object._get_followup_responsible().name if is_html_empty(object._get_followup_responsible().signature) else object._get_followup_responsible().signature"/>
</p>
</div>
</field>
<field name="lang">{{ object.lang }}</field>
<field name="auto_delete" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_action_account_report_partner_ledger" name="Partner Ledger"
action="action_account_report_partner_ledger" groups="account.group_account_readonly"
parent="account.account_reports_partners_reports_menu"/>
<menuitem id="menu_action_account_report_aged_receivable" name="Aged Receivable" action="action_account_report_ar"
groups="account.group_account_readonly" parent="account.account_reports_partners_reports_menu"/>
<menuitem id="menu_action_account_report_aged_payable" name="Aged Payable" action="action_account_report_ap"
groups="account.group_account_readonly" parent="account.account_reports_partners_reports_menu"/>
<menuitem id="account_reports_audit_reports_menu" name="Audit Reports" parent="account.menu_finance_reports"
sequence="2">
<menuitem id="menu_action_account_report_general_ledger" name="General Ledger"
action="action_account_report_general_ledger" groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_coa" name="Trial Balance" action="action_account_report_coa"
groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_ja" name="Journal Audit" action="action_account_report_ja"
groups="account.group_account_readonly"/>
</menuitem>
<menuitem id="menu_action_account_report_gt" name="Tax Return" action="action_account_report_gt"
parent="account.account_reports_legal_statements_menu" sequence="50"
groups="account.group_account_readonly,account.group_account_basic"/>
<menuitem id="menu_action_account_report_sales" action="action_account_report_sales"
parent="account.account_reports_legal_statements_menu" sequence="60"
groups="account.group_account_readonly" active="False"/>
<menuitem id="menu_action_account_report_multicurrency_revaluation" name="Unrealized Currency Gains/Losses"
action="action_account_report_multicurrency_revaluation" parent="account.account_reports_management_menu"
groups="base.group_multi_currency"/>
<menuitem id="menu_action_account_report_balance_sheet" name="Balance Sheet" action="action_account_report_bs"
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_profit_and_loss" name="Profit and Loss" action="action_account_report_pl"
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_cash_flow" name="Cash Flow Statement" action="action_account_report_cs"
parent="account.account_reports_legal_statements_menu" groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_exec_summary" name="Executive Summary"
action="action_account_report_exec_summary" parent="account.account_reports_legal_statements_menu"
groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_deferred_expense" name="Deferred Expense"
action="action_account_report_deferred_expense" parent="account.account_reports_management_menu"
groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_deferred_revenue" name="Deferred Revenue"
action="action_account_report_deferred_revenue" parent="account.account_reports_management_menu"
groups="account.group_account_readonly"/>
<menuitem id="menu_action_account_report_tree" name="Accounting Reports" sequence="6"
parent="account.account_management_menu" action="action_account_report_tree" groups="base.group_no_one"/>
<menuitem id="menu_action_account_report_horizontal_groups" name="Horizontal Groups"
action="action_account_report_horizontal_groups" parent="account.account_account_menu" sequence="10"
groups="base.group_no_one"/>
<menuitem id="menu_action_account_report_budget_tree" name="Financial Budgets"
action="action_account_report_budget_tree" parent="account.account_account_menu" sequence="11"/>
</odoo>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_action_account_report_assets"
name="Depreciation Schedule"
action="action_account_report_assets"
parent="account.account_reports_management_menu"
groups="account.group_account_readonly"/>
</odoo>

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="multicurrency_revaluation_report" model="account.report">
<field name="name">Unrealized Currency Gains/Losses</field>
<field name="filter_date_range" eval="False"/>
<field name="filter_show_draft" eval="True"/>
<field name="default_opening_date_filter">previous_month</field>
<field name="custom_handler_model_id" ref="model_account_multicurrency_revaluation_report_handler"/>
<field name="column_ids">
<record id="multicurrency_revaluation_report_balance_currency" model="account.report.column">
<field name="name">Balance in Foreign Currency</field>
<field name="expression_label">balance_currency</field>
</record>
<record id="multicurrency_revaluation_report_balance_operation" model="account.report.column">
<field name="name">Balance at Operation Rate</field>
<field name="expression_label">balance_operation</field>
</record>
<record id="multicurrency_revaluation_report_balance_current" model="account.report.column">
<field name="name">Balance at Current Rate</field>
<field name="expression_label">balance_current</field>
</record>
<record id="multicurrency_revaluation_report_adjustment" model="account.report.column">
<field name="name">Adjustment</field>
<field name="expression_label">adjustment</field>
</record>
</field>
<field name="line_ids">
<record id="multicurrency_revaluation_to_adjust" model="account.report.line">
<field name="name">Accounts To Adjust</field>
<field name="code">multicurrency_included</field>
<field name="groupby">currency_id, account_id, id</field>
<field name="expression_ids">
<record id="multicurrency_revaluation_to_adjust_balance_currency" model="account.report.expression">
<field name="label">balance_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
<field name="subformula">balance_currency</field>
</record>
<record id="multicurrency_revaluation_to_adjust_balance_currency_forced_currency" model="account.report.expression">
<field name="label">_currency_balance_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
<field name="subformula">currency_id</field>
</record>
<record id="multicurrency_revaluation_to_adjust_balance_operation" model="account.report.expression">
<field name="label">balance_operation</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
<field name="subformula">balance_operation</field>
<field name="auditable" eval="False"/>
</record>
<record id="multicurrency_revaluation_to_adjust_balance_current" model="account.report.expression">
<field name="label">balance_current</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
<field name="subformula">balance_current</field>
<field name="auditable" eval="False"/>
</record>
<record id="multicurrency_revaluation_to_adjust_adjustment" model="account.report.expression">
<field name="label">adjustment</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_to_adjust</field>
<field name="subformula">adjustment</field>
<field name="auditable" eval="False"/>
</record>
</field>
</record>
<record id="multicurrency_revaluation_excluded" model="account.report.line">
<field name="name">Excluded Accounts</field>
<field name="groupby">currency_id, account_id, id</field>
<field name="expression_ids">
<record id="multicurrency_revaluation_excluded_balance_currency" model="account.report.expression">
<field name="label">balance_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
<field name="subformula">balance_currency</field>
</record>
<record id="multicurrency_revaluation_excluded_balance_currency_forced_currency" model="account.report.expression">
<field name="label">_currency_balance_currency</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
<field name="subformula">currency_id</field>
</record>
<record id="multicurrency_revaluation_excluded_balance_operation" model="account.report.expression">
<field name="label">balance_operation</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
<field name="subformula">balance_operation</field>
</record>
<record id="multicurrency_revaluation_excluded_balance_current" model="account.report.expression">
<field name="label">balance_current</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
<field name="subformula">balance_current</field>
</record>
<record id="multicurrency_revaluation_excluded_adjustment" model="account.report.expression">
<field name="label">adjustment</field>
<field name="engine">custom</field>
<field name="formula">_report_custom_engine_multi_currency_revaluation_excluded</field>
<field name="subformula">adjustment</field>
</record>
</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="partner_ledger_report" model="account.report">
<field name="name">Partner Ledger</field>
<field name="filter_show_draft" eval="True"/>
<field name="filter_account_type">both</field>
<field name="filter_partner" eval="True"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_unreconciled" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="filter_hide_0_lines">never</field>
<field name="default_opening_date_filter">this_year</field>
<field name="search_bar" eval="True"/>
<field name="load_more_limit" eval="80"/>
<field name="custom_handler_model_id" ref="model_account_partner_ledger_report_handler"/>
<field name="column_ids">
<record id="partner_ledger_report_journal_code" model="account.report.column">
<field name="name">Journal</field>
<field name="expression_label">journal_code</field>
<field name="figure_type">string</field>
</record>
<record id="partner_ledger_report_account_code" model="account.report.column">
<field name="name">Account</field>
<field name="expression_label">account_code</field>
<field name="figure_type">string</field>
</record>
<record id="partner_ledger_report_invoicing_date" model="account.report.column">
<field name="name">Invoice Date</field>
<field name="expression_label">invoice_date</field>
<field name="figure_type">date</field>
</record>
<record id="partner_ledger_report_date_maturity" model="account.report.column">
<field name="name">Due Date</field>
<field name="expression_label">date_maturity</field>
<field name="figure_type">date</field>
</record>
<record id="partner_ledger_report_matching_number" model="account.report.column">
<field name="name">Matching</field>
<field name="expression_label">matching_number</field>
<field name="figure_type">string</field>
</record>
<record id="partner_ledger_report_debit" model="account.report.column">
<field name="name">Debit</field>
<field name="expression_label">debit</field>
</record>
<record id="partner_ledger_report_credit" model="account.report.column">
<field name="name">Credit</field>
<field name="expression_label">credit</field>
</record>
<record id="partner_ledger_amount" model="account.report.column">
<field name="name">Amount</field>
<field name="expression_label">amount</field>
</record>
<record id="partner_ledger_report_amount_currency" model="account.report.column">
<field name="name">Amount Currency</field>
<field name="expression_label">amount_currency</field>
</record>
<record id="partner_ledger_report_balance" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,335 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="pdf_export_main">
<html>
<head>
<base t-att-href="base_url"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<t t-call-assets="at_accounting.assets_pdf_export" t-js="False"/>
</head>
<body t-att-dir="env['res.lang']._get_data(code=lang or env.user.lang).direction or 'ltr'">
<div t-att-class="'o_content ' + options['css_custom_class']">
<header>
<div class="o_title">
<t t-if="report.filter_show_draft and options['all_entries']">[Draft]</t>
<t t-out="report_title"/>
</div>
<div class="row o_header_font">
<div class="col-8">
<!-- All company information (name, address, vat, ...) -->
<t t-call="{{custom_templates.get('company_information', 'at_accounting.company_information')}}"/>
</div>
<div class="col-4">
<!-- All filters and options -->
<t t-call="{{custom_templates.get('pdf_export_filters', 'at_accounting.pdf_export_filters')}}"/>
</div>
</div>
</header>
<div class="d-flex align-items-start">
<t t-foreach="options.get('horizontal_split') and ['left', 'right'] or [None]" t-as="split_side">
<table t-attf-class="o_table #{options.get('horizontal_split') and 'horizontal_split_page'}">
<!-- Header -->
<t t-call="{{custom_templates.get('pdf_export_main_table_header', 'at_accounting.pdf_export_main_table_header')}}"/>
<!-- Body -->
<tbody>
<t t-if="lines">
<t t-call="{{custom_templates.get('pdf_export_main_table_body', 'at_accounting.pdf_export_main_table_body')}}">
<t t-set="lines" t-value="filter(lambda x: not split_side or split_side == x.get('horizontal_split_side', 'left'), lines)"/>
</t>
</t>
</tbody>
</table>
</t>
</div>
<!-- Annotations -->
<ol class="o_annotation">
<t t-foreach="annotations" t-as="annotation">
<li>
<t t-out="annotation.get('number')"/>.
<t t-if="annotation.get('date')"><t t-out="annotation['date']"/> -</t>
<t t-out="annotation.get('text')"/>
</li>
</t>
</ol>
</div>
</body>
</html>
</template>
<template id="company_information">
<t t-set="company_names" t-value="[company['name'] for company in options['companies']]"/>
<div class="row">
<div class="col-10" t-out="', '.join(company_names)"/>
</div>
<address class="mb-0 o_text_muted" t-field="env.company.partner_id" t-options='{"widget": "contact", "fields": ["address"], "no_marker": True}'/>
<t t-if="options.get('tax_unit', 'company_only') == 'company_only'">
<t t-if="env.company.account_fiscal_country_id.vat_label" t-out="env.company.account_fiscal_country_id.vat_label+':'"/>
<t t-else="">Tax ID:</t>
<t t-out="env.company.vat"/>
</t>
<t t-else="">
Tax ID: <t t-out="env['account.tax.unit'].browse(options.get('tax_unit')).vat"/>
</t>
</template>
<template id="pdf_export_filters">
<!-- Journals -->
<t t-if="options.get('journals')">
<div class="row" name="filter_info_template_journals">
<t t-set="journal_group_selected" t-value="options.get('selected_journal_groups')"/>
<t t-if="journal_group_selected">
<div class="col-3">Multi-Ledger: </div>
<div class="col-9 o_text_muted" t-out="journal_group_selected['title']"/>
</t>
<t t-else="">
<t t-set="journal_value" t-value="[journal.get('title') for journal in options['journals'] if journal.get('selected')]"/>
<t t-if="journal_value">
<div class="col-3">Journals: </div>
<div class="col-9 o_text_muted" t-out="', '.join(journal_value)"/>
</t>
</t>
</div>
</t>
<!-- Partners -->
<t t-if="options.get('partner_ids') != None">
<div class="row">
<t t-set="partner_value" t-value="[partner for partner in options['selected_partner_ids']]"/>
<t t-if="partner_value">
<div class="col-3">Partners:</div>
<div class="col-9 o_text_muted" t-out="', '.join(partner_value)"/>
</t>
</div>
</t>
<!-- Partners categories -->
<t t-if="options.get('partner_categories') != None">
<div class="row">
<t t-set="partner_category_value" t-value="[partner for partner in options['selected_partner_categories']]"/>
<t t-if="partner_category_value">
<div class="col-3">Partners Categories:</div>
<div class="col-9 o_text_muted" t-out="', '.join(partner_category_value)"/>
</t>
</div>
</t>
<!-- Horizontal -->
<t t-if="options.get('selected_horizontal_group_id')">
<div class="row">
<t t-set="horizontal_group" t-value="[hg['name'] for hg in options['available_horizontal_groups'] if hg['id'] == options.get('selected_horizontal_group_id')]"/>
<t t-if="horizontal_group">
<div class="col-3">Horizontal:</div>
<div class="col-9 o_text_muted" t-out="horizontal_group[0]"/>
</t>
</div>
</t>
<!-- Currency -->
<t t-if="options.get('company_currency')">
<div class="row">
<div class="col-3">Currency:</div>
<div class="col-9 o_text_muted" t-out="options['company_currency']['currency_name']"/>
</div>
</t>
<!-- Filters -->
<t t-if="options.get('aml_ir_filters') and any(opt['selected'] for opt in options['aml_ir_filters'])" name="aml_ir_filters">
<div class="row">
<t t-set="aml_ir_filters" t-value="opt['name'] for opt in options['aml_ir_filters'] if opt['selected']"/>
<t t-if="aml_ir_filters">
<div class="col-3">Filters:</div>
<div class="col-9 o_text_muted" t-out="', '.join(aml_ir_filters)"/>
</t>
</div>
</t>
<!-- Extra options -->
<div class="row" name="pdf_options_header">
<t t-call="{{custom_templates.get('pdf_export_filter_extra_options_template', 'at_accounting.pdf_export_filter_extra_options_template')}}"/>
</div>
</template>
<template id="pdf_export_filter_extra_options_template">
<t t-set="rounding_unit_display_names" t-value="{k: v[1] for k, v in options['rounding_unit_names'].items() if v[1]}"/>
<div class="col-3" t-if="(report.filter_show_draft and options['all_entries']) or
(report.filter_unreconciled and options['unreconciled']) or
options.get('include_analytic_without_aml') or
options['rounding_unit'] in rounding_unit_display_names">
Options:
</div>
<div class="col-9 o_text_muted">
<t t-set="extra_options" t-value="[]"/>
<!-- All entries -->
<t t-if="report.filter_show_draft and options['all_entries']" groups="account.group_account_readonly">
<t t-set="label_draft_entries">With Draft Entries</t>
<t t-set="extra_options" t-value="extra_options + [label_draft_entries]"/>
</t>
<!-- Unreconciled -->
<t t-if="report.filter_unreconciled and options['unreconciled']">
<t t-set="label_unreconciled_entries">Unreconciled Entries</t>
<t t-set="extra_options" t-value="extra_options + [label_unreconciled_entries]"/>
</t>
<!-- Analytic -->
<t t-if="options.get('include_analytic_without_aml')" name="include_analytic">
<t t-set="label_analytic_simulations">Including Analytic Simulations</t>
<t t-set="extra_options" t-value="extra_options + [label_analytic_simulations]"/>
</t>
<!-- Currency Unit Amount Text -->
<t t-if="options['rounding_unit'] in rounding_unit_display_names">
<t t-set="rounding_unit" t-value="options.get('rounding_unit')"/>
<t t-set="extra_options" t-value="extra_options + [rounding_unit_display_names[rounding_unit]]"/>
</t>
<t t-out="', '.join(extra_options)"/>
</div>
</template>
<template id="pdf_export_main_table_header">
<thead id="table_header">
<t t-foreach="options['column_headers']" t-as="column_header">
<tr>
<!-- First empty column -->
<th/>
<!-- Other columns -->
<t t-foreach="column_header * column_headers_render_data['level_repetitions'][column_header_index]" t-as="header">
<th t-att-colspan="header.get('colspan', column_headers_render_data['level_colspan'][column_header_index]) + (1 if options.get('show_horizontal_group_total') and column_header_first else 0)" class="o_overflow_name">
<t t-out="header.get('name')"/>
</th>
</t>
<th t-if="options.get('show_horizontal_group_total') and not column_header_first">
<t t-out="[group['name'] for group in options['available_horizontal_groups'] if group['id'] == options['selected_horizontal_group_id']][0]"/>
</th>
<th t-if="options.get('column_percent_comparison') == 'growth'">%</th>
</tr>
</t>
<!-- Custom subheaders -->
<t t-if="column_headers_render_data['custom_subheaders']">
<tr>
<!-- First empty column -->
<th/>
<!-- Other columns -->
<t t-foreach="column_headers_render_data['custom_subheaders']" t-as="subheader">
<th t-att-colspan="subheader.get('colspan', 1)">
<t t-out="subheader.get('name')"/>
</th>
</t>
</tr>
</t>
<tr>
<!-- First empty column -->
<th/>
<t t-foreach="options['columns']" t-as="subheader">
<th>
<t t-out="subheader.get('name')"/>
</th>
</t>
<th t-if="options.get('show_horizontal_group_total')">
<t t-out="options['columns'][0].get('name')"/>
</th>
<th t-if="options.get('column_percent_comparison') == 'growth'"/>
</tr>
</thead>
</template>
<template id="pdf_export_main_table_body">
<t t-foreach="lines" t-as="line">
<t t-set="o_line_level" t-value="'o_line_level_' + str(line['level'])"/>
<t t-if="line.get('page_break') and not options.get('horizontal_split')">
<!-- End current table -->
<t t-out="table_end"/>
<!-- Append table header -->
<t t-call="{{custom_templates.get('pdf_export_main_table_header', 'at_accounting.pdf_export_main_table_header')}}"/>
<!-- Start new table -->
<t t-out="table_start"/>
</t>
<!-- Adds an empty row above line with level 0 to add some spacing (it is the easiest and cleanest way) -->
<t t-if="line_index != 0 and line['level'] == 0">
<tr>
<td/>
<t t-foreach="line.get('columns')" t-as="cell">
<td/>
</t>
<t t-if="options.get('column_percent_comparison')">
<td/>
</t>
<t t-if="options.get('show_horizontal_group_total')">
<td/>
</t>
</tr>
</t>
<t t-set="o_bold" t-value="(' o_fw_bold' if line.get('unfolded') or 'total' in line.get('id') else '')"/>
<t t-set="o_overflow" t-value="(' o_overflow_name' if len(line.get('name') or '') > 42 else '')"/>
<tr t-att-class="o_line_level + o_bold + o_overflow" name="pdf_export_main_table_body_lines_tr">
<td t-att-colspan="line.get('colspan', '1')" class="o_line_name_level">
<t t-out="line.get('name')"/>
<t t-if="line.get('annotations')">
<t t-foreach="annotations" t-as="annotation">
<t t-if="annotation.get('number') and annotation['number'] in (line.get('annotations') or [])">
<sup t-out="annotation['number']"/>
</t>
</t>
</t>
</td>
<t t-foreach="line.get('columns')" t-as="cell">
<td class="o_cell_td">
<t t-if="not env.company.totals_below_sections or options.get('ignore_totals_below_sections') or not line.get('unfolded')">
<t t-call="{{custom_templates.get('pdf_export_cell', 'at_accounting.pdf_export_cell')}}"/>
</t>
</td>
</t>
<t t-if="options.get('column_percent_comparison')">
<td class="o_column_percent_comparison">
<t t-if="line.get('column_percent_comparison_data')">
<t t-out="line['column_percent_comparison_data'].get('name')"/>
</t>
</td>
</t>
<t t-if="options.get('show_horizontal_group_total')">
<td class="o_cell_td">
<t t-if="line.get('horizontal_group_total_data')">
<t t-set="o_classes" t-value="'o_line_cell_value_number' + (' o_muted' if line['horizontal_group_total_data'].get('no_format') == 0 else '')"/>
<span t-att-class="o_classes" t-out="line['horizontal_group_total_data'].get('name')"/>
</t>
</td>
</t>
</tr>
</t>
</template>
<template id="pdf_export_cell">
<t t-if="cell.get('figure_type', '') in ['float', 'integer', 'monetary', 'percentage']">
<t t-set="o_classes" t-value="'o_line_cell_value_number' + (' o_muted' if cell.get('is_zero') else '')"/>
</t>
<t t-else="">
<t t-set="o_classes" t-value="'o_overflow_value'"/>
</t>
<span t-att-class="o_classes" t-out="cell.get('name')"/>
</template>
</odoo>

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="profit_and_loss" model="account.report">
<field name="name">Profit and Loss</field>
<field name="filter_analytic_groupby" eval="True"/>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_journals" eval="True"/>
<field name="filter_multi_company">selector</field>
<field name="filter_budgets" eval="True"/>
<field name="default_opening_date_filter">this_year</field>
<field name="column_ids">
<record id="profit_and_loss_column" model="account.report.column">
<field name="name">Balance</field>
<field name="expression_label">balance</field>
</record>
</field>
<field name="line_ids">
<record id="account_financial_report_revenue0" model="account.report.line">
<field name="name">Revenue</field>
<field name="code">REV</field>
<field name="hierarchy_level">1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_revenue0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'income')]"/>
<field name="subformula">-sum</field>
</record>
</field>
</record>
<record id="account_financial_report_cost_sales0" model="account.report.line">
<field name="name">Less Costs of Revenue</field>
<field name="code">COS</field>
<field name="hierarchy_level">1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_cost_sales0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'expense_direct_cost')]"/>
<field name="subformula">sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_gross_profit0" model="account.report.line">
<field name="name">Gross Profit</field>
<field name="code">GRP</field>
<field name="hierarchy_level">0</field>
<field name="expression_ids">
<record id="account_financial_report_gross_profit0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance - COS.balance</field>
</record>
</field>
</record>
<record id="account_financial_report_expense0" model="account.report.line">
<field name="name">Less Operating Expenses</field>
<field name="code">EXP</field>
<field name="hierarchy_level">1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_expense0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'expense')]"/>
<field name="subformula">sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_operating_income0" model="account.report.line">
<field name="name">Operating Income (or Loss)</field>
<field name="hierarchy_level">0</field>
<field name="code">INC</field>
<field name="expression_ids">
<record id="account_financial_report_operating_income0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance - COS.balance - EXP.balance</field>
</record>
</field>
</record>
<record id="account_financial_report_other_income0" model="account.report.line">
<field name="name">Plus Other Income</field>
<field name="code">OIN</field>
<field name="hierarchy_level">1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_other_income0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'income_other')]"/>
<field name="subformula">-sum</field>
</record>
</field>
</record>
<record id="account_financial_report_depreciation0" model="account.report.line">
<field name="name">Less Other Expenses</field>
<field name="code">OEXP</field>
<field name="hierarchy_level">1</field>
<field name="groupby">account_id</field>
<field name="foldable" eval="True"/>
<field name="expression_ids">
<record id="account_financial_report_depreciation0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">domain</field>
<field name="formula" eval="[('account_id.account_type', '=', 'expense_depreciation')]"/>
<field name="subformula">sum</field>
<field name="green_on_positive" eval="False"/>
</record>
</field>
</record>
<record id="account_financial_report_net_profit0" model="account.report.line">
<field name="name">Net Profit</field>
<field name="hierarchy_level">0</field>
<field name="code">NEP</field>
<field name="expression_ids">
<record id="account_financial_report_net_profit0_balance" model="account.report.expression">
<field name="label">balance</field>
<field name="engine">aggregation</field>
<field name="formula">REV.balance + OIN.balance - COS.balance - EXP.balance - OEXP.balance</field>
</record>
</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,11 @@
<odoo>
<record id="ir_cron_account_report_send" model="ir.cron">
<field name="name">Send account reports automatically</field>
<field name="model_id" ref="model_account_report"/>
<field name="state">code</field>
<field name="code">model._cron_account_report_send(job_count=20)</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
</record>
</odoo>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="generic_ec_sales_report" model="account.report">
<field name="name">Generic EC Sales List</field>
<field name="filter_show_draft" eval="True"/>
<field name="filter_period_comparison" eval="False"/>
<field name="filter_date_range" eval="True"/>
<field name="filter_journals" eval="True"/>
<field name="filter_show_draft" eval="False"/>
<field name="filter_unreconciled" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="default_opening_date_filter">previous_month</field>
<field name="load_more_limit" eval="80"/>
<field name="search_bar" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_ec_sales_report_handler"/>
<field name="column_ids">
<record id="account_financial_report_ec_sales_country" model="account.report.column">
<field name="name">Country Code</field>
<field name="expression_label">country_code</field>
<field name="figure_type">string</field>
<field name="sortable" eval="True"/>
</record>
<record id="account_financial_report_ec_sales_vat" model="account.report.column">
<field name="name">VAT Number</field>
<field name="expression_label">vat_number</field>
<field name="figure_type">string</field>
<field name="sortable" eval="True"/>
</record>
<record id="account_financial_report_ec_sales_amount" model="account.report.column">
<field name="name">Amount</field>
<field name="expression_label">balance</field>
<field name="sortable" eval="True"/>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="trial_balance_report" model="account.report">
<field name="name">Trial Balance</field>
<field name="filter_journals" eval="True"/>
<field name="filter_analytic" eval="True"/>
<field name="filter_growth_comparison" eval="False"/>
<field name="filter_multi_company">selector</field>
<field name="filter_unfold_all" eval="True"/>
<field name="filter_hierarchy">by_default</field>
<field name="filter_hide_0_lines">never</field>
<field name="default_opening_date_filter">this_month</field>
<field name="search_bar" eval="True"/>
<field name="custom_handler_model_id" ref="model_account_trial_balance_report_handler"/>
<field name="column_ids">
<record id="trial_balance_report_debit" model="account.report.column">
<field name="name">Debit</field>
<field name="expression_label">debit</field>
</record>
<record id="trial_balance_report_credit" model="account.report.column">
<field name="name">Credit</field>
<field name="expression_label">credit</field>
</record>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
</record>
<data noupdate="1">
<record id="account_asset_group_demo" model="account.asset.group">
<field name="name">Odoo Office</field>
</record>
<record id="account_asset_model_demo" model="account.asset">
<field name="name">Asset - 5 Years</field>
<field name="prorata_computation_type">none</field>
<field name="original_value">1000</field>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'general'),
('id', '!=', obj().env.user.company_id.currency_exchange_journal_id.id)]"/>
<field name="account_asset_id" model="account.account" search="[
('account_type', '=', 'asset_fixed'),
('company_ids', '=', ref('base.main_company'))]"/>
<field name="account_depreciation_id" model="account.account" search="[
('account_type', '=', 'asset_fixed'),
('company_ids', '=', ref('base.main_company'))]"/>
<field name="account_depreciation_expense_id" model="account.account" search="[
('account_type', '=', 'expense'),
('tag_ids', 'in', [ref('account.account_tag_operating')]),
('company_ids', '=', ref('base.main_company'))]"/>
<field name="state">open</field>
<field name="asset_group_id" ref="at_accounting.account_asset_group_demo"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="ofx_partner_bank_1" model="res.partner.bank">
<field name="acc_number">BE68539007547034</field>
<field name="partner_id" ref="base.res_partner_2"></field>
<field name="bank_id" ref="base.res_bank_1"/>
</record>
<record id="ofx_partner_bank_2" model="res.partner.bank">
<field name="acc_number">00987654322</field>
<field name="partner_id" ref="base.res_partner_3"></field>
<field name="bank_id" ref="base.res_bank_1"/>
</record>
<record id="qif_partner_bank_1" model="res.partner.bank">
<field name="acc_number">10987654320</field>
<field name="partner_id" ref="base.res_partner_4"></field>
<field name="bank_id" ref="base.res_bank_1"/>
</record>
<record id="qif_partner_bank_2" model="res.partner.bank">
<field name="acc_number">10987654322</field>
<field name="partner_id" ref="base.res_partner_3"></field>
<field name="bank_id" ref="base.res_bank_1"/>
</record>
</data>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
from . import account_account
from . import account_bank_statement
from . import account_chart_template
from . import account_fiscal_year
from . import account_journal_dashboard
from . import account_move
from . import account_payment
from . import account_reconcile_model
from . import account_reconcile_model_line
from . import account_tax
from . import digest
from . import res_config_settings
from . import res_company
from . import bank_rec_widget
from . import bank_rec_widget_line
from . import ir_ui_menu
from . import res_currency
from . import res_partner
from . import account_report
from . import account_analytic_report
from . import bank_reconciliation_report
from . import account_general_ledger
from . import account_generic_tax_report
from . import account_journal_report
from . import account_cash_flow_report
from . import account_deferred_reports
from . import account_multicurrency_revaluation_report
from . import account_move_line
from . import account_trial_balance_report
from . import account_aged_partner_balance
from . import account_partner_ledger
from . import mail_activity
from . import mail_activity_type
from . import chart_template
from . import ir_actions
from . import account_sales_report
from . import executive_summary_report
from . import budget
from . import balance_sheet
from . import account_fiscal_position
from . import account_asset,account_journal
from . import account_journal_csv

View File

@@ -0,0 +1,45 @@
import ast
from odoo import api, fields, models, _
class AccountAccount(models.Model):
_inherit = "account.account"
def action_open_reconcile(self):
self.ensure_one()
# Open reconciliation view for this account
action_values = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled')
domain = ast.literal_eval(action_values['domain'])
domain.append(('account_id', '=', self.id))
action_values['domain'] = domain
return action_values
exclude_provision_currency_ids = fields.Many2many('res.currency', relation='account_account_exclude_res_currency_provision', help="Whether or not we have to make provisions for the selected foreign currencies.")
budget_item_ids = fields.One2many(comodel_name='account.report.budget.item', inverse_name='account_id') # To use it in the domain when adding accounts from the report
asset_model_ids = fields.Many2many(
'account.asset',
domain=[('state', '=', 'model')],
help="An asset wil be created for each asset model when this account is used on a vendor bill or a refund",
tracking=True,
)
create_asset = fields.Selection([('no', 'No'), ('draft', 'Create in draft'), ('validate', 'Create and validate')],
required=True, default='no', tracking=True)
# specify if the account can generate asset depending on it's type. It is used in the account form view
can_create_asset = fields.Boolean(compute="_compute_can_create_asset")
form_view_ref = fields.Char(compute='_compute_can_create_asset')
# decimal quantities are not supported, quantities are rounded to the lower int
multiple_assets_per_line = fields.Boolean(string='Multiple Assets per Line', default=False, tracking=True,
help="Multiple asset items will be generated depending on the bill line quantity instead of 1 global asset.")
@api.depends('account_type')
def _compute_can_create_asset(self):
for record in self:
record.can_create_asset = record.account_type in ('asset_fixed', 'asset_non_current')
record.form_view_ref = 'at_accountingview_account_asset_form'
@api.onchange('create_asset')
def _onchange_multiple_assets_per_line(self):
for record in self:
if record.create_asset == 'no':
record.multiple_assets_per_line = False

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from odoo import models, fields, _
from odoo.tools import SQL
from odoo.tools.misc import format_date
from dateutil.relativedelta import relativedelta
from itertools import chain
class AgedPartnerBalanceCustomHandler(models.AbstractModel):
_name = 'account.aged.partner.balance.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Aged Partner Balance Custom Handler'
def _get_custom_display_config(self):
return {
'css_custom_class': 'aged_partner_balance',
'templates': {
'AccountReportLineName': 'at_accounting.AgedPartnerBalanceLineName',
},
'components': {
'AccountReportFilters': 'at_accounting.AgedPartnerBalanceFilters',
},
}
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
hidden_columns = set()
options['multi_currency'] = report.env.user.has_group('base.group_multi_currency')
options['show_currency'] = options['multi_currency'] and (previous_options or {}).get('show_currency', False)
if not options['show_currency']:
hidden_columns.update(['amount_currency', 'currency'])
options['show_account'] = (previous_options or {}).get('show_account', False)
if not options['show_account']:
hidden_columns.add('account_name')
options['columns'] = [
column for column in options['columns']
if column['expression_label'] not in hidden_columns
]
default_order_column = {
'expression_label': 'invoice_date',
'direction': 'ASC',
}
options['order_column'] = previous_options.get('order_column') or default_order_column
options['aging_based_on'] = previous_options.get('aging_based_on') or 'base_on_maturity_date'
options['aging_interval'] = previous_options.get('aging_interval') or 30
# Set aging column names
interval = options['aging_interval']
for column in options['columns']:
if column['expression_label'].startswith('period'):
period_number = int(column['expression_label'].replace('period', '')) - 1
if 0 <= period_number < 4:
column['name'] = f'{interval * period_number + 1}-{interval * (period_number + 1)}'
def _custom_line_postprocessor(self, report, options, lines):
partner_lines_map = {}
# Sort line dicts by partner
for line in lines:
model, model_id = report._get_model_info_from_id(line['id'])
if model == 'res.partner':
partner_lines_map[model_id] = line
if partner_lines_map:
for partner, line_dict in zip(
self.env['res.partner'].browse(partner_lines_map),
partner_lines_map.values()
):
line_dict['trust'] = partner.with_company(partner.company_id or self.env.company).trust
return lines
def _report_custom_engine_aged_receivable(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._aged_partner_report_custom_engine_common(options, 'asset_receivable', current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_aged_payable(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._aged_partner_report_custom_engine_common(options, 'liability_payable', current_groupby, next_groupby, offset=offset, limit=limit)
def _aged_partner_report_custom_engine_common(self, options, internal_type, current_groupby, next_groupby, offset=0, limit=None):
report = self.env['account.report'].browse(options['report_id'])
report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else []))
def minus_days(date_obj, days):
return fields.Date.to_string(date_obj - relativedelta(days=days))
aging_date_field = SQL.identifier('invoice_date') if options['aging_based_on'] == 'base_on_invoice_date' else SQL.identifier('date_maturity')
date_to = fields.Date.from_string(options['date']['date_to'])
interval = options['aging_interval']
periods = [(False, fields.Date.to_string(date_to))]
# Since we added the first period in the list we have to do one less iteration
nb_periods = len([column for column in options['columns'] if column['expression_label'].startswith('period')]) - 1
for i in range(nb_periods):
start_date = minus_days(date_to, (interval * i) + 1)
# The last element of the list will have False for the end date
end_date = minus_days(date_to, interval * (i + 1)) if i < nb_periods - 1 else False
periods.append((start_date, end_date))
def build_result_dict(report, query_res_lines):
rslt = {f'period{i}': 0 for i in range(len(periods))}
for query_res in query_res_lines:
for i in range(len(periods)):
period_key = f'period{i}'
rslt[period_key] += query_res[period_key]
if current_groupby == 'id':
query_res = query_res_lines[0] # We're grouping by id, so there is only 1 element in query_res_lines anyway
currency = self.env['res.currency'].browse(query_res['currency_id'][0]) if len(query_res['currency_id']) == 1 else None
rslt.update({
'invoice_date': query_res['invoice_date'][0] if len(query_res['invoice_date']) == 1 else None,
'due_date': query_res['due_date'][0] if len(query_res['due_date']) == 1 else None,
'amount_currency': query_res['amount_currency'],
'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None,
'currency': currency.display_name if currency else None,
'account_name': query_res['account_name'][0] if len(query_res['account_name']) == 1 else None,
'total': None,
'has_sublines': query_res['aml_count'] > 0,
# Needed by the custom_unfold_all_batch_data_generator, to speed-up unfold_all
'partner_id': query_res['partner_id'][0] if query_res['partner_id'] else None,
})
else:
rslt.update({
'invoice_date': None,
'due_date': None,
'amount_currency': None,
'currency_id': None,
'currency': None,
'account_name': None,
'total': sum(rslt[f'period{i}'] for i in range(len(periods))),
'has_sublines': False,
})
return rslt
# Build period table
period_table_format = ('(VALUES %s)' % ','.join("(%s, %s, %s)" for period in periods))
params = list(chain.from_iterable(
(period[0] or None, period[1] or None, i)
for i, period in enumerate(periods)
))
period_table = SQL(period_table_format, *params)
# Build query
query = report._get_report_query(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)])
account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
always_present_groupby = SQL("period_table.period_index")
if current_groupby:
select_from_groupby = SQL("%s AS grouping_key,", SQL.identifier("account_move_line", current_groupby))
groupby_clause = SQL("%s, %s", SQL.identifier("account_move_line", current_groupby), always_present_groupby)
else:
select_from_groupby = SQL()
groupby_clause = always_present_groupby
multiplicator = -1 if internal_type == 'liability_payable' else 1
select_period_query = SQL(',').join(
SQL("""
CASE WHEN period_table.period_index = %(period_index)s
THEN %(multiplicator)s * SUM(%(balance_select)s)
ELSE 0 END AS %(column_name)s
""",
period_index=i,
multiplicator=multiplicator,
column_name=SQL.identifier(f"period{i}"),
balance_select=report._currency_table_apply_rate(SQL(
"account_move_line.balance - COALESCE(part_debit.amount, 0) + COALESCE(part_credit.amount, 0)"
)),
)
for i in range(len(periods))
)
tail_query = report._get_engine_query_tail(offset, limit)
query = SQL(
"""
WITH period_table(date_start, date_stop, period_index) AS (%(period_table)s)
SELECT
%(select_from_groupby)s
%(multiplicator)s * (
SUM(account_move_line.amount_currency)
- COALESCE(SUM(part_debit.debit_amount_currency), 0)
+ COALESCE(SUM(part_credit.credit_amount_currency), 0)
) AS amount_currency,
ARRAY_AGG(DISTINCT account_move_line.partner_id) AS partner_id,
ARRAY_AGG(account_move_line.payment_id) AS payment_id,
ARRAY_AGG(DISTINCT move.invoice_date) AS invoice_date,
ARRAY_AGG(DISTINCT COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date)) AS report_date,
ARRAY_AGG(DISTINCT %(account_code)s) AS account_name,
ARRAY_AGG(DISTINCT COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date)) AS due_date,
ARRAY_AGG(DISTINCT account_move_line.currency_id) AS currency_id,
COUNT(account_move_line.id) AS aml_count,
ARRAY_AGG(%(account_code)s) AS account_code,
%(select_period_query)s
FROM %(table_references)s
JOIN account_journal journal ON journal.id = account_move_line.journal_id
JOIN account_move move ON move.id = account_move_line.move_id
%(currency_table_join)s
LEFT JOIN LATERAL (
SELECT
SUM(part.amount) AS amount,
SUM(part.debit_amount_currency) AS debit_amount_currency,
part.debit_move_id
FROM account_partial_reconcile part
WHERE part.max_date <= %(date_to)s AND part.debit_move_id = account_move_line.id
GROUP BY part.debit_move_id
) part_debit ON TRUE
LEFT JOIN LATERAL (
SELECT
SUM(part.amount) AS amount,
SUM(part.credit_amount_currency) AS credit_amount_currency,
part.credit_move_id
FROM account_partial_reconcile part
WHERE part.max_date <= %(date_to)s AND part.credit_move_id = account_move_line.id
GROUP BY part.credit_move_id
) part_credit ON TRUE
JOIN period_table ON
(
period_table.date_start IS NULL
OR COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date) <= DATE(period_table.date_start)
)
AND
(
period_table.date_stop IS NULL
OR COALESCE(account_move_line.%(aging_date_field)s, account_move_line.date) >= DATE(period_table.date_stop)
)
WHERE %(search_condition)s
GROUP BY %(groupby_clause)s
HAVING
ROUND(SUM(%(having_debit)s), %(currency_precision)s) != 0
OR ROUND(SUM(%(having_credit)s), %(currency_precision)s) != 0
ORDER BY %(groupby_clause)s
%(tail_query)s
""",
account_code=account_code,
period_table=period_table,
select_from_groupby=select_from_groupby,
select_period_query=select_period_query,
multiplicator=multiplicator,
aging_date_field=aging_date_field,
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(options),
date_to=date_to,
search_condition=query.where_clause,
groupby_clause=groupby_clause,
having_debit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance > 0 THEN account_move_line.balance else 0 END - COALESCE(part_debit.amount, 0)")),
having_credit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance < 0 THEN -account_move_line.balance else 0 END - COALESCE(part_credit.amount, 0)")),
currency_precision=self.env.company.currency_id.decimal_places,
tail_query=tail_query,
)
self._cr.execute(query)
query_res_lines = self._cr.dictfetchall()
if not current_groupby:
return build_result_dict(report, query_res_lines)
else:
rslt = []
all_res_per_grouping_key = {}
for query_res in query_res_lines:
grouping_key = query_res['grouping_key']
all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res)
for grouping_key, query_res_lines in all_res_per_grouping_key.items():
rslt.append((grouping_key, build_result_dict(report, query_res_lines)))
return rslt
def open_journal_items(self, options, params):
params['view_ref'] = 'account.view_move_line_tree_grouped_partner'
options_for_audit = {**options, 'date': {**options['date'], 'date_from': None}}
report = self.env['account.report'].browse(options['report_id'])
action = report.open_journal_items(options=options_for_audit, params=params)
action.get('context', {}).update({'search_default_group_by_account': 0, 'search_default_group_by_partner': 1})
return action
def open_partner_ledger(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
record_model, record_id = report._get_model_info_from_id(params.get('line_id'))
return self.env[record_model].browse(record_id).open_partner_ledger()
def _common_custom_unfold_all_batch_data_generator(self, internal_type, report, options, lines_to_expand_by_function):
rslt = {} # In the form {full_sub_groupby_key: all_column_group_expression_totals for this groupby computation}
report_periods = 6 # The report has 6 periods
for expand_function_name, lines_to_expand in lines_to_expand_by_function.items():
for line_to_expand in lines_to_expand: # In standard, this loop will execute only once
if expand_function_name == '_report_expand_unfoldable_line_with_groupby':
report_line_id = report._get_res_id_from_line_id(line_to_expand['id'], 'account.report.line')
expressions_to_evaluate = report.line_ids.expression_ids.filtered(lambda x: x.report_line_id.id == report_line_id and x.engine == 'custom')
if not expressions_to_evaluate:
continue
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
# Get all aml results by partner
aml_data_by_partner = {}
for aml_id, aml_result in self._aged_partner_report_custom_engine_common(column_group_options, internal_type, 'id', None):
aml_result['aml_id'] = aml_id
aml_data_by_partner.setdefault(aml_result['partner_id'], []).append(aml_result)
# Iterate on results by partner to generate the content of the column group
partner_expression_totals = rslt.setdefault(f"[{report_line_id}]=>partner_id", {})\
.setdefault(column_group_key, {expression: {'value': []} for expression in expressions_to_evaluate})
for partner_id, aml_data_list in aml_data_by_partner.items():
partner_values = self._prepare_partner_values()
for i in range(report_periods):
partner_values[f'period{i}'] = 0
# Build expression totals under the right key
partner_aml_expression_totals = rslt.setdefault(f"[{report_line_id}]partner_id:{partner_id}=>id", {})\
.setdefault(column_group_key, {expression: {'value': []} for expression in expressions_to_evaluate})
for aml_data in aml_data_list:
for i in range(report_periods):
period_value = aml_data[f'period{i}']
partner_values[f'period{i}'] += period_value
partner_values['total'] += period_value
for expression in expressions_to_evaluate:
partner_aml_expression_totals[expression]['value'].append(
(aml_data['aml_id'], aml_data[expression.subformula])
)
for expression in expressions_to_evaluate:
partner_expression_totals[expression]['value'].append(
(partner_id, partner_values[expression.subformula])
)
return rslt
def _prepare_partner_values(self):
return {
'invoice_date': None,
'due_date': None,
'amount_currency': None,
'currency_id': None,
'currency': None,
'account_name': None,
'total': 0,
}
def aged_partner_balance_audit(self, options, params, journal_type):
""" Open a list of invoices/bills and/or deferral entries for the clicked cell
:param dict options: the report's `options`
:param dict params: a dict containing:
`calling_line_dict_id`: line id containing the optional account of the cell
`expression_label`: the expression label of the cell
"""
report = self.env['account.report'].browse(options['report_id'])
action = self.env['ir.actions.actions']._for_xml_id('account.action_amounts_to_settle')
journal_type_to_exclude = {'purchase': 'sale', 'sale': 'purchase'}
if options:
domain = [
('account_id.reconcile', '=', True),
('journal_id.type', '!=', journal_type_to_exclude.get(journal_type)),
*self._build_domain_from_period(options, params['expression_label']),
*report._get_options_domain(options, 'from_beginning'),
*report._get_audit_line_groupby_domain(params['calling_line_dict_id']),
]
action['domain'] = domain
return action
def _build_domain_from_period(self, options, period):
if period != "total" and period[-1].isdigit():
period_number = int(period[-1])
if period_number == 0:
domain = [('date_maturity', '>=', options['date']['date_to'])]
else:
options_date_to = datetime.datetime.strptime(options['date']['date_to'], '%Y-%m-%d')
period_end = options_date_to - datetime.timedelta(30*(period_number-1)+1)
period_start = options_date_to - datetime.timedelta(30*(period_number))
domain = [('date_maturity', '>=', period_start), ('date_maturity', '<=', period_end)]
if period_number == 5:
domain = [('date_maturity', '<=', period_end)]
else:
domain = []
return domain
class AgedPayableCustomHandler(models.AbstractModel):
_name = 'account.aged.payable.report.handler'
_inherit = 'account.aged.partner.balance.report.handler'
_description = 'Aged Payable Custom Handler'
def open_journal_items(self, options, params):
payable_account_type = {'id': 'trade_payable', 'name': _("Payable"), 'selected': True}
if 'account_type' in options:
options['account_type'].append(payable_account_type)
else:
options['account_type'] = [payable_account_type]
return super().open_journal_items(options, params)
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
# We only optimize the unfold all if the groupby value of the report has not been customized. Else, we'll just run the full computation
if self.env.ref('at_accounting.aged_payable_line').groupby.replace(' ', '') == 'partner_id,id':
return self._common_custom_unfold_all_batch_data_generator('liability_payable', report, options, lines_to_expand_by_function)
return {}
def action_audit_cell(self, options, params):
return super().aged_partner_balance_audit(options, params, 'purchase')
class AgedReceivableCustomHandler(models.AbstractModel):
_name = 'account.aged.receivable.report.handler'
_inherit = 'account.aged.partner.balance.report.handler'
_description = 'Aged Receivable Custom Handler'
def open_journal_items(self, options, params):
receivable_account_type = {'id': 'trade_receivable', 'name': _("Receivable"), 'selected': True}
if 'account_type' in options:
options['account_type'].append(receivable_account_type)
else:
options['account_type'] = [receivable_account_type]
return super().open_journal_items(options, params)
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
# We only optimize the unfold all if the groupby value of the report has not been customized. Else, we'll just run the full computation
if self.env.ref('at_accounting.aged_receivable_line').groupby.replace(' ', '') == 'partner_id,id':
return self._common_custom_unfold_all_batch_data_generator('asset_receivable', report, options, lines_to_expand_by_function)
return {}
def action_audit_cell(self, options, params):
return super().aged_partner_balance_audit(options, params, 'sale')

View File

@@ -0,0 +1,267 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, osv
from odoo.addons.web.controllers.utils import clean_action
from odoo.tools import SQL, Query
class AccountReport(models.AbstractModel):
_inherit = 'account.report'
filter_analytic_groupby = fields.Boolean(
string="Analytic Group By",
compute=lambda x: x._compute_report_option_filter('filter_analytic_groupby'), readonly=False, store=True, depends=['root_report_id'],
)
def _get_options_initializers_forced_sequence_map(self):
""" Force the sequence for the init_options so columns headers are already generated but not the columns
So, between _init_options_column_headers and _init_options_columns"""
sequence_map = super(AccountReport, self)._get_options_initializers_forced_sequence_map()
sequence_map[self._init_options_analytic_groupby] = 995
return sequence_map
def _init_options_analytic_groupby(self, options, previous_options):
if not self.filter_analytic_groupby:
return
enable_analytic_accounts = self.env.user.has_group('analytic.group_analytic_accounting')
if not enable_analytic_accounts:
return
options['display_analytic_groupby'] = True
options['display_analytic_plan_groupby'] = True
options['include_analytic_without_aml'] = previous_options.get('include_analytic_without_aml', False)
previous_analytic_accounts = previous_options.get('analytic_accounts_groupby', [])
analytic_account_ids = [int(x) for x in previous_analytic_accounts]
selected_analytic_accounts = self.env['account.analytic.account'].with_context(active_test=False).search(
[('id', 'in', analytic_account_ids)])
options['analytic_accounts_groupby'] = selected_analytic_accounts.ids
options['selected_analytic_account_groupby_names'] = selected_analytic_accounts.mapped('name')
previous_analytic_plans = previous_options.get('analytic_plans_groupby', [])
analytic_plan_ids = [int(x) for x in previous_analytic_plans]
selected_analytic_plans = self.env['account.analytic.plan'].search([('id', 'in', analytic_plan_ids)])
options['analytic_plans_groupby'] = selected_analytic_plans.ids
options['selected_analytic_plan_groupby_names'] = selected_analytic_plans.mapped('name')
self._create_column_analytic(options)
def _init_options_readonly_query(self, options, previous_options):
super()._init_options_readonly_query(options, previous_options)
options['readonly_query'] = options['readonly_query'] and not options.get('analytic_groupby_option')
def _create_column_analytic(self, options):
""" Creates the analytic columns for each plan or account in the filters.
This will duplicate all previous columns and adding the analytic accounts in the domain of the added columns.
The analytic_groupby_option is used so the table used is the shadowed table.
The domain on analytic_distribution can just use simple comparison as the column of the shadowed
table will simply be filled with analytic_account_ids.
"""
analytic_headers = []
plans = self.env['account.analytic.plan'].browse(options.get('analytic_plans_groupby'))
for plan in plans:
account_list = []
accounts = self.env['account.analytic.account'].search([('plan_id', 'child_of', plan.id)])
for account in accounts:
account_list.append(account.id)
analytic_headers.append({
'name': plan.name,
'forced_options': {
'analytic_groupby_option': True,
'analytic_accounts_list': tuple(account_list), # Analytic accounts used in the domain to filter the lines.
}
})
accounts = self.env['account.analytic.account'].browse(options.get('analytic_accounts_groupby'))
for account in accounts:
analytic_headers.append({
'name': account.name,
'forced_options': {
'analytic_groupby_option': True,
'analytic_accounts_list': (account.id,),
}
})
if analytic_headers:
has_selected_budgets = any([budget for budget in options.get('budgets', []) if budget['selected']])
if has_selected_budgets:
# if budget is selected, then analytic headers are placed on the same header level
options['column_headers'][-1] = analytic_headers + options['column_headers'][-1]
else:
# We add the analytic layer to the column_headers before creating the columns
analytic_headers.append({'name': ''})
options['column_headers'] = [
*options['column_headers'],
analytic_headers,
]
@api.model
def _prepare_lines_for_analytic_groupby(self):
"""Prepare the analytic_temp_account_move_line
This method should be used once before all the SQL queries using the
table account_move_line for the analytic columns for the financial reports.
It will create a new table with the schema of account_move_line table, but with
the data from account_analytic_line.
We inherit the schema of account_move_line, make the correspondence between
account_move_line fields and account_analytic_line fields and put NULL for those
who don't exist in account_analytic_line.
We also drop the NOT NULL constraints for fields who are not required in account_analytic_line.
"""
self.env.cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name='analytic_temp_account_move_line'")
if self.env.cr.fetchone():
return
project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
analytic_cols = SQL(", ").join(SQL('"account_analytic_line".%s', SQL.identifier(n._column_name())) for n in (project_plan + other_plans))
analytic_distribution_equivalent = SQL('to_jsonb(UNNEST(ARRAY[%s]))', analytic_cols)
change_equivalence_dict = {
'id': SQL("account_analytic_line.id"),
'balance': SQL("-amount"),
'display_type': 'product',
'parent_state': 'posted',
'account_id': SQL.identifier("general_account_id"),
'debit': SQL("CASE WHEN (amount < 0) THEN -amount else 0 END"),
'credit': SQL("CASE WHEN (amount > 0) THEN amount else 0 END"),
'analytic_distribution': analytic_distribution_equivalent,
}
all_stored_aml_fields = {
field
for field, attrs in self.env['account.move.line'].fields_get().items()
if attrs['type'] not in ['many2many', 'one2many'] and attrs.get('store')
}
for aml_field in all_stored_aml_fields:
if aml_field not in change_equivalence_dict:
change_equivalence_dict[aml_field] = SQL('"account_move_line".%s', SQL.identifier(aml_field))
stored_aml_fields, fields_to_insert = self.env['account.move.line']._prepare_aml_shadowing_for_report(change_equivalence_dict)
query = SQL("""
-- Create a temporary table, dropping not null constraints because we're not filling those columns
CREATE TEMPORARY TABLE IF NOT EXISTS analytic_temp_account_move_line () inherits (account_move_line) ON COMMIT DROP;
ALTER TABLE analytic_temp_account_move_line NO INHERIT account_move_line;
ALTER TABLE analytic_temp_account_move_line DROP CONSTRAINT IF EXISTS account_move_line_check_amount_currency_balance_sign;
ALTER TABLE analytic_temp_account_move_line ALTER COLUMN move_id DROP NOT NULL;
ALTER TABLE analytic_temp_account_move_line ALTER COLUMN currency_id DROP NOT NULL;
INSERT INTO analytic_temp_account_move_line (%(stored_aml_fields)s)
SELECT %(fields_to_insert)s
FROM account_analytic_line
LEFT JOIN account_move_line
ON account_analytic_line.move_line_id = account_move_line.id
WHERE
account_analytic_line.general_account_id IS NOT NULL;
-- Create a supporting index to avoid seq.scans
CREATE INDEX IF NOT EXISTS analytic_temp_account_move_line__composite_idx ON analytic_temp_account_move_line (analytic_distribution, journal_id, date, company_id);
-- Update statistics for correct planning
ANALYZE analytic_temp_account_move_line
""", stored_aml_fields=stored_aml_fields, fields_to_insert=fields_to_insert)
self.env.cr.execute(query)
def _get_report_query(self, options, date_scope, domain=None) -> Query:
# Override to add the context key which will eventually trigger the shadowing of the table
context_self = self.with_context(account_report_analytic_groupby=options.get('analytic_groupby_option'))
# We add the domain filter for analytic_distribution here, as the search is not available
query = super(AccountReport, context_self)._get_report_query(options, date_scope, domain)
if options.get('analytic_accounts'):
if 'analytic_accounts_list' in options:
# the table will be `analytic_temp_account_move_line` and thus analytic_distribution will be a single ID
analytic_account_ids = tuple(str(account_id) for account_id in options['analytic_accounts'])
query.add_where(SQL("""account_move_line.analytic_distribution IN %s""", analytic_account_ids))
else:
# Real `account_move_line` table so real JSON with percentage
analytic_account_ids = [[str(account_id) for account_id in options['analytic_accounts']]]
query.add_where(SQL('%s && %s', analytic_account_ids, self.env['account.move.line']._query_analytic_accounts()))
return query
def action_audit_cell(self, options, params):
column_group_options = self._get_column_group_options(options, params['column_group_key'])
if not column_group_options.get('analytic_groupby_option'):
return super(AccountReport, self).action_audit_cell(options, params)
else:
# Start by getting the domain from the options. Note that this domain is targeting account.move.line
report_line = self.env['account.report.line'].browse(params['report_line_id'])
expression = report_line.expression_ids.filtered(lambda x: x.label == params['expression_label'])
line_domain = self._get_audit_line_domain(column_group_options, expression, params)
# The line domain is made for move lines, so we need some postprocessing to have it work with analytic lines.
domain = []
AccountAnalyticLine = self.env['account.analytic.line']
for expression in line_domain:
if len(expression) == 1: # For operators such as '&' or '|' we can juste add them again.
domain.append(expression)
continue
field, operator, right_term = expression
# On analytic lines, the account.account field is named general_account_id and not account_id.
if field.split('.')[0] == 'account_id':
field = field.replace('account_id', 'general_account_id')
expression = [(field, operator, right_term)]
# Replace the 'analytic_distribution' by the account_id domain as we expect for analytic lines.
elif field == 'analytic_distribution':
expression = [('auto_account_id', 'in', right_term)]
# For other fields not present in on the analytic line model, map them to get the info from the move_line.
# Or ignore these conditions if there is no move lines.
elif field.split('.')[0] not in AccountAnalyticLine._fields:
expression = [(f'move_line_id.{field}', operator, right_term)]
if options.get('include_analytic_without_aml'):
expression = osv.expression.OR([
[('move_line_id', '=', False)],
expression,
])
else:
expression = [expression] # just for the extend
domain.extend(expression)
action = clean_action(self.env.ref('analytic.account_analytic_line_action_entries')._get_action_dict(), env=self.env)
action['domain'] = domain
return action
@api.model
def _get_options_journals_domain(self, options):
domain = super(AccountReport, self)._get_options_journals_domain(options)
# Add False to the domain in order to select lines without journals for analytics columns.
if options.get('include_analytic_without_aml'):
domain = osv.expression.OR([
domain,
[('journal_id', '=', False)],
])
return domain
def _get_options_domain(self, options, date_scope):
self.ensure_one()
domain = super()._get_options_domain(options, date_scope)
# Get the analytic accounts that we need to filter on from the options and add a domain for them.
if 'analytic_accounts_list' in options:
domain = osv.expression.AND([
domain,
[('analytic_distribution', 'in', options.get('analytic_accounts_list', []))],
])
return domain
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
def _where_calc(self, domain, active_test=True):
""" In case we need an analytic column in an account_report, we shadow the account_move_line table
with a temp table filled with analytic data, that will be used for the analytic columns.
We do it in this function to only create and fill it once for all computations of a report.
The following analytic columns and computations will just query the shadowed table instead of the real one.
"""
query = super()._where_calc(domain, active_test)
if self.env.context.get('account_report_analytic_groupby') and not self.env.context.get('account_report_cash_basis'):
self.env['account.report']._prepare_lines_for_analytic_groupby()
query._tables['account_move_line'] = SQL.identifier('analytic_temp_account_move_line')
return query

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,248 @@
import logging
from odoo import _, api, fields, models
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.exceptions import UserError
from odoo.tools import html2plaintext
from odoo.osv import expression
from dateutil.relativedelta import relativedelta
from itertools import product
from lxml import etree
from markupsafe import Markup
_logger = logging.getLogger(__name__)
class AccountBankStatement(models.Model):
_name = "account.bank.statement"
_inherit = ['mail.thread.main.attachment', 'account.bank.statement']
def action_open_bank_reconcile_widget(self):
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
name=self.name,
default_context={
'search_default_statement_id': self.id,
'search_default_journal_id': self.journal_id.id,
},
extra_domain=[('statement_id', '=', self.id)]
)
def action_generate_attachment(self):
ir_actions_report_sudo = self.env['ir.actions.report'].sudo()
statement_report_action = self.env.ref('account.action_report_account_statement')
for statement in self:
statement_report = statement_report_action.sudo()
content, _content_type = ir_actions_report_sudo._render_qweb_pdf(statement_report, res_ids=statement.ids)
statement.attachment_ids |= self.env['ir.attachment'].create({
'name': _("Bank Statement %s.pdf", statement.name) if statement.name else _("Bank Statement.pdf"),
'type': 'binary',
'mimetype': 'application/pdf',
'raw': content,
'res_model': statement._name,
'res_id': statement.id,
})
return statement_report_action.report_action(docids=self)
class AccountBankStatementLine(models.Model):
_inherit = 'account.bank.statement.line'
cron_last_check = fields.Datetime()
def action_save_close(self):
return {'type': 'ir.actions.act_window_close'}
def action_save_new(self):
action = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_bank_statement_line_form_bank_rec_widget')
action['context'] = {'default_journal_id': self._context['default_journal_id']}
return action
####################################################
# RECONCILIATION PROCESS
####################################################
@api.model
def _action_open_bank_reconciliation_widget(self, extra_domain=None, default_context=None, name=None, kanban_first=True):
action_reference = 'at_accounting.action_bank_statement_line_transactions' + ('_kanban' if kanban_first else '')
action = self.env['ir.actions.act_window']._for_xml_id(action_reference)
action.update({
'name': name or _("Bank Reconciliation"),
'context': default_context or {},
'domain': [('state', '!=', 'cancel')] + (extra_domain or []),
})
return action
def action_open_recon_st_line(self):
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
name=self.name,
default_context={
'default_statement_id': self.statement_id.id,
'default_journal_id': self.journal_id.id,
'default_st_line_id': self.id,
'search_default_id': self.id,
},
)
def _cron_try_auto_reconcile_statement_lines(self, batch_size=None, limit_time=0):
def _compute_st_lines_to_reconcile(configured_company):
# Find the bank statement lines that are not reconciled and try to reconcile them automatically.
# The ones that are never be processed by the CRON before are processed first.
remaining_line_id = None
limit = batch_size + 1 if batch_size else None
domain = [
('is_reconciled', '=', False),
('create_date', '>', start_time.date() - relativedelta(months=3)),
('company_id', 'in', configured_company.ids),
]
st_lines = self.search(domain, limit=limit, order="cron_last_check ASC NULLS FIRST, id")
if batch_size and len(st_lines) > batch_size:
remaining_line_id = st_lines[batch_size].id
st_lines = st_lines[:batch_size]
return st_lines, remaining_line_id
start_time = fields.Datetime.now()
configured_company = children_company = self.env['account.reconcile.model'].search_fetch([
('auto_reconcile', '=', True),
('rule_type', 'in', ('writeoff_suggestion', 'invoice_matching')),
], ['company_id']).company_id
if not configured_company:
return
while children_company := children_company.child_ids:
configured_company += children_company
st_lines, remaining_line_id = (self, None) if self else _compute_st_lines_to_reconcile(configured_company)
nb_auto_reconciled_lines = 0
for index, st_line in enumerate(st_lines):
if limit_time and fields.Datetime.now().timestamp() - start_time.timestamp() > limit_time:
remaining_line_id = st_line.id
st_lines = st_lines[:index]
break
wizard = self.env['bank.rec.widget'].with_context(default_st_line_id=st_line.id).new({})
wizard._action_trigger_matching_rules()
if wizard.state == 'valid' and wizard.matching_rules_allow_auto_reconcile:
try:
wizard._action_validate()
if st_line.is_reconciled:
st_line.move_id.message_post(body=_(
"This bank transaction has been automatically validated using the reconciliation model '%s'.",
', '.join(st_line.move_id.line_ids.reconcile_model_id.mapped('name')),
))
nb_auto_reconciled_lines += 1
except UserError as e:
_logger.info("Failed to auto reconcile statement line %s due to user error: %s",
st_line.id,
str(e)
)
continue
st_lines.write({'cron_last_check': start_time})
if remaining_line_id:
remaining_st_line = self.env['account.bank.statement.line'].browse(remaining_line_id)
if nb_auto_reconciled_lines or not remaining_st_line.cron_last_check:
self.env.ref('at_accounting.auto_reconcile_bank_statement_line')._trigger()
def _retrieve_partner(self):
self.ensure_one()
if self.partner_id:
return self.partner_id
if self.account_number:
account_number_nums = sanitize_account_number(self.account_number)
if account_number_nums:
domain = [('sanitized_acc_number', 'ilike', account_number_nums)]
for extra_domain in ([('company_id', 'parent_of', self.company_id.id)], [('company_id', '=', False)]):
bank_accounts = self.env['res.partner.bank'].search(extra_domain + domain)
if len(bank_accounts.partner_id) == 1:
return bank_accounts.partner_id
else:
# We have several partner with same account, possibly some archived partner
# so try to filter out inactive partner and if one remains, select this one
bank_accounts = bank_accounts.filtered(lambda bacc: bacc.partner_id.active)
if len(bank_accounts) == 1:
return bank_accounts.partner_id
if self.partner_name:
domains = product(
[
('complete_name', '=ilike', self.partner_name),
('complete_name', 'ilike', self.partner_name),
],
[
('company_id', 'parent_of', self.company_id.id),
('company_id', '=', False),
],
)
for domain in domains:
partner = self.env['res.partner'].search(list(domain) + [('parent_id', '=', False)], limit=2)
if len(partner) == 1:
return partner
# Retrieve the partner from the 'reconcile models'.
rec_models = self.env['account.reconcile.model'].search([
*self.env['account.reconcile.model']._check_company_domain(self.company_id),
('rule_type', '!=', 'writeoff_button'),
])
for rec_model in rec_models:
partner = rec_model._get_partner_from_mapping(self)
if partner and rec_model._is_applicable_for(self, partner):
return partner
return self.env['res.partner']
def _get_st_line_strings_for_matching(self, allowed_fields=None):
self.ensure_one()
st_line_text_values = []
if not allowed_fields or 'payment_ref' in allowed_fields:
if self.payment_ref:
st_line_text_values.append(self.payment_ref)
if not allowed_fields or 'narration' in allowed_fields:
value = html2plaintext(self.narration or "")
if value:
st_line_text_values.append(value)
if not allowed_fields or 'ref' in allowed_fields:
if self.ref:
st_line_text_values.append(self.ref)
return st_line_text_values
def _get_default_amls_matching_domain(self):
# EXTENDS account
domain = super()._get_default_amls_matching_domain()
categories = self.env['product.category'].search([
'|',
('property_stock_account_input_categ_id', '!=', False),
('property_stock_account_output_categ_id', '!=', False)
])
accounts = (categories.mapped('property_stock_account_input_categ_id') +
categories.mapped('property_stock_account_output_categ_id'))
if accounts:
return expression.AND([domain, [('account_id', 'not in', tuple(set(accounts.ids)))]])
return domain
# Ensure transactions can be imported only once (if the import format provides unique transaction ids)
unique_import_id = fields.Char(string='Import ID', readonly=True, copy=False)
_sql_constraints = [
('unique_import_id', 'unique (unique_import_id)', 'A bank account transactions can be imported only once!')
]
def _action_open_bank_reconciliation_widget(self, extra_domain=None, default_context=None, name=None,
kanban_first=True):
res = super()._action_open_bank_reconciliation_widget(extra_domain, default_context, name, kanban_first)
res['help'] = Markup("<p class='o_view_nocontent_smiling_face'>{}</p><p>{}<br/>{}</p>").format(
_('Nothing to do here!'),
_('No transactions matching your filters were found.'),
_('Click "New" or upload a %s.',
", ".join(self.env['account.journal']._get_bank_statements_available_import_formats())),
)
return res

View File

@@ -0,0 +1,712 @@
from odoo import models, _
from odoo.tools import SQL, Query
class CashFlowReportCustomHandler(models.AbstractModel):
_name = 'account.cash.flow.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Cash Flow Report Custom Handler'
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
# Compute the cash flow report using the direct method: https://www.investopedia.com/terms/d/direct_method.asp
lines = []
layout_data = self._get_layout_data()
report_data = self._get_report_data(report, options, layout_data)
for layout_line_id, layout_line_data in layout_data.items():
lines.append((0, self._get_layout_line(report, options, layout_line_id, layout_line_data, report_data)))
if layout_line_id in report_data and 'aml_groupby_account' in report_data[layout_line_id]:
aml_data_values = report_data[layout_line_id]['aml_groupby_account'].values()
aml_data_values_with_account_code = []
aml_data_values_without_account_code = []
for aml_data in aml_data_values:
if aml_data['account_code'] is not None:
aml_data_values_with_account_code.append(aml_data)
else:
aml_data_values_without_account_code.append(aml_data)
for aml_data in (sorted(aml_data_values_with_account_code, key=lambda x: x['account_code'])
+ aml_data_values_without_account_code):
lines.append((0, self._get_aml_line(report, options, aml_data)))
unexplained_difference_line = self._get_unexplained_difference_line(report, options, report_data)
if unexplained_difference_line:
lines.append((0, unexplained_difference_line))
return lines
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
report._init_options_journals(options, previous_options=previous_options, additional_journals_domain=[('type', 'in', ('bank', 'cash', 'general'))])
def _get_report_data(self, report, options, layout_data):
report_data = {}
payment_account_ids = self._get_account_ids(report, options)
if not payment_account_ids:
return report_data
# Compute 'Cash and cash equivalents, beginning of period'
for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'to_beginning_of_period'):
self._add_report_data('opening_balance', aml_data, layout_data, report_data)
self._add_report_data('closing_balance', aml_data, layout_data, report_data)
# Compute 'Cash and cash equivalents, closing balance'
for aml_data in self._compute_liquidity_balance(report, options, payment_account_ids, 'strict_range'):
self._add_report_data('closing_balance', aml_data, layout_data, report_data)
tags_ids = self._get_tags_ids()
cashflow_tag_ids = self._get_cashflow_tag_ids()
# Process liquidity moves
for aml_groupby_account in self._get_liquidity_moves(report, options, payment_account_ids, cashflow_tag_ids):
for aml_data in aml_groupby_account.values():
self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data)
# Process reconciled moves
for aml_groupby_account in self._get_reconciled_moves(report, options, payment_account_ids, cashflow_tag_ids):
for aml_data in aml_groupby_account.values():
self._dispatch_aml_data(tags_ids, aml_data, layout_data, report_data)
return report_data
def _add_report_data(self, layout_line_id, aml_data, layout_data, report_data):
"""
Add or update the report_data dictionnary with aml_data.
report_data is a dictionnary where the keys are keys from _cash_flow_report_get_layout_data() (used for mapping)
and the values can contain 2 dictionnaries:
* (required) 'balance' where the key is the column_group_key and the value is the balance of the line
* (optional) 'aml_groupby_account' where the key is an account_id and the values are the aml data
"""
def _report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data):
# Update the balance in report_data of the parent of the layout_line_id recursively (Stops when the line has no parent)
if 'parent_line_id' in layout_data[layout_line_id]:
parent_line_id = layout_data[layout_line_id]['parent_line_id']
report_data.setdefault(parent_line_id, {'balance': {}})
report_data[parent_line_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[parent_line_id]['balance'][aml_column_group_key] += aml_balance
_report_update_parent(parent_line_id, aml_column_group_key, aml_balance, layout_data, report_data)
aml_column_group_key = aml_data['column_group_key']
aml_account_id = aml_data['account_id']
aml_account_code = aml_data['account_code']
aml_account_name = aml_data['account_name']
aml_balance = aml_data['balance']
aml_account_tag = aml_data.get('account_tag_id', None)
if self.env.company.currency_id.is_zero(aml_balance):
return
report_data.setdefault(layout_line_id, {
'balance': {},
'aml_groupby_account': {},
})
report_data[layout_line_id]['aml_groupby_account'].setdefault(aml_account_id, {
'parent_line_id': layout_line_id,
'account_id': aml_account_id,
'account_code': aml_account_code,
'account_name': aml_account_name,
'account_tag_id': aml_account_tag,
'level': layout_data[layout_line_id]['level'] + 1,
'balance': {},
})
report_data[layout_line_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[layout_line_id]['balance'][aml_column_group_key] += aml_balance
report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'].setdefault(aml_column_group_key, 0.0)
report_data[layout_line_id]['aml_groupby_account'][aml_account_id]['balance'][aml_column_group_key] += aml_balance
_report_update_parent(layout_line_id, aml_column_group_key, aml_balance, layout_data, report_data)
def _get_tags_ids(self):
''' Get a dict to pass on to _dispatch_aml_data containing information mapping account tags to report lines. '''
return {
'operating': self.env.ref('account.account_tag_operating').id,
'investing': self.env.ref('account.account_tag_investing').id,
'financing': self.env.ref('account.account_tag_financing').id,
}
def _get_cashflow_tag_ids(self):
''' Get the list of account tags that are relevant for the cash flow report. '''
return self._get_tags_ids().values()
def _dispatch_aml_data(self, tags_ids, aml_data, layout_data, report_data):
# Dispatch the aml_data in the correct layout_line
if aml_data['account_account_type'] == 'asset_receivable':
self._add_report_data('advance_payments_customer', aml_data, layout_data, report_data)
elif aml_data['account_account_type'] == 'liability_payable':
self._add_report_data('advance_payments_suppliers', aml_data, layout_data, report_data)
elif aml_data['balance'] < 0:
if aml_data['account_tag_id'] == tags_ids['operating']:
self._add_report_data('paid_operating_activities', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['investing']:
self._add_report_data('investing_activities_cash_out', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['financing']:
self._add_report_data('financing_activities_cash_out', aml_data, layout_data, report_data)
else:
self._add_report_data('unclassified_activities_cash_out', aml_data, layout_data, report_data)
elif aml_data['balance'] > 0:
if aml_data['account_tag_id'] == tags_ids['operating']:
self._add_report_data('received_operating_activities', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['investing']:
self._add_report_data('investing_activities_cash_in', aml_data, layout_data, report_data)
elif aml_data['account_tag_id'] == tags_ids['financing']:
self._add_report_data('financing_activities_cash_in', aml_data, layout_data, report_data)
else:
self._add_report_data('unclassified_activities_cash_in', aml_data, layout_data, report_data)
# -------------------------------------------------------------------------
# QUERIES
# -------------------------------------------------------------------------
def _get_account_ids(self, report, options):
''' Retrieve all accounts to be part of the cash flow statement and also the accounts making them.
:param options: The report options.
:return: payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
'''
# Fetch liquidity accounts:
# Accounts being used by at least one bank/cash journal.
selected_journal_ids = [j['id'] for j in report._get_options_journals(options)]
where_clause = "account_journal.id IN %s" if selected_journal_ids else "account_journal.type IN ('bank', 'cash', 'general')"
where_params = [tuple(selected_journal_ids)] if selected_journal_ids else []
self._cr.execute(f'''
SELECT
array_remove(ARRAY_AGG(DISTINCT account_account.id), NULL),
array_remove(ARRAY_AGG(DISTINCT account_payment_method_line.payment_account_id), NULL)
FROM account_journal
JOIN res_company
ON account_journal.company_id = res_company.id
LEFT JOIN account_payment_method_line
ON account_journal.id = account_payment_method_line.journal_id
LEFT JOIN account_account
ON account_journal.default_account_id = account_account.id
AND account_account.account_type IN ('asset_cash', 'liability_credit_card')
WHERE {where_clause}
''', where_params)
res = self._cr.fetchall()[0]
payment_account_ids = set((res[0] or []) + (res[1] or []))
if not payment_account_ids:
return ()
return tuple(payment_account_ids)
def _get_move_ids_query(self, report, payment_account_ids, column_group_options) -> SQL:
''' Get all liquidity moves to be part of the cash flow statement.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: query: The SQL query to retrieve the move IDs.
'''
query = report._get_report_query(column_group_options, 'strict_range', [('account_id', 'in', list(payment_account_ids))])
return SQL(
'''
SELECT
array_agg(DISTINCT account_move_line.move_id) AS move_id
FROM %(table_references)s
WHERE %(search_condition)s
''',
table_references=query.from_clause,
search_condition=query.where_clause,
)
def _compute_liquidity_balance(self, report, options, payment_account_ids, date_scope):
''' Compute the balance of all liquidity accounts to populate the following sections:
'Cash and cash equivalents, beginning of period' and 'Cash and cash equivalents, closing balance'.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, balance).
'''
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
query = report._get_report_query(column_group_options, date_scope, domain=[('account_id', 'in', payment_account_ids)])
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.account_id, account_code, account_name
''',
column_group_key=column_group_key,
account_code=account_code,
account_name=account_name,
table_references=query.from_clause,
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
currency_table_join=report._currency_table_aml_join(column_group_options),
search_condition=query.where_clause,
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
return self._cr.dictfetchall()
def _get_liquidity_moves(self, report, options, payment_account_ids, cash_flow_tag_ids):
''' Fetch all information needed to compute lines from liquidity moves.
The difficulty is to represent only the not-reconciled part of balance.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, account_type, amount).
'''
reconciled_aml_groupby_account = {}
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options)
query = Query(self.env, 'account_move_line')
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_type = SQL.identifier(account_alias, 'account_type')
queries.append(SQL(
'''
(WITH payment_move_ids AS (%(move_ids_query)s)
-- Credit amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(partial_amount_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Debit amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
-SUM(%(partial_amount_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = account_move_line.id
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY account_move_line.company_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
UNION ALL
-- Total amount of each account
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.account_id AS account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(aml_balance_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_move_line.account_id NOT IN %(payment_account_ids)s
GROUP BY account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id)
''',
column_group_key=column_group_key,
move_ids_query=move_ids_query,
account_code=account_code,
account_name=account_name,
account_type=account_type,
from_clause=query.from_clause,
currency_table_join=report._currency_table_aml_join(column_group_options),
partial_amount_select=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
aml_balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cash_flow_tag_ids=tuple(cash_flow_tag_ids),
payment_account_ids=payment_account_ids,
date_from=column_group_options['date']['date_from'],
date_to=column_group_options['date']['date_to'],
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
reconciled_aml_groupby_account.setdefault(aml_data['account_id'], {})
reconciled_aml_groupby_account[aml_data['account_id']].setdefault(aml_data['column_group_key'], {
'column_group_key': aml_data['column_group_key'],
'account_id': aml_data['account_id'],
'account_code': aml_data['account_code'],
'account_name': aml_data['account_name'],
'account_account_type': aml_data['account_account_type'],
'account_tag_id': aml_data['account_tag_id'],
'balance': 0.0,
})
reconciled_aml_groupby_account[aml_data['account_id']][aml_data['column_group_key']]['balance'] -= aml_data['balance']
return list(reconciled_aml_groupby_account.values())
def _get_reconciled_moves(self, report, options, payment_account_ids, cash_flow_tag_ids):
''' Retrieve all moves being not a liquidity move to be shown in the cash flow statement.
Each amount must be valued at the percentage of what is actually paid.
E.g. An invoice of 1000 being paid at 50% must be valued at 500.
:param options: The report options.
:param payment_account_ids: A tuple containing all account.account's ids being used in a liquidity journal.
:return: A list of tuple (account_id, account_code, account_name, account_type, amount).
'''
reconciled_account_ids = {column_group_key: set() for column_group_key in options['column_groups']}
reconciled_percentage_per_move = {column_group_key: {} for column_group_key in options['column_groups']}
currency_table = report._get_currency_table(options)
queries = []
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
move_ids_query = self._get_move_ids_query(report, payment_account_ids, column_group_options)
queries.append(SQL(
'''
(WITH payment_move_ids AS (%(move_ids_query)s)
SELECT
%(column_group_key)s AS column_group_key,
debit_line.move_id,
debit_line.account_id,
SUM(%(partial_amount)s) AS balance
FROM account_move_line AS credit_line
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.credit_move_id = credit_line.id
JOIN %(currency_table)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
INNER JOIN account_move_line AS debit_line
ON debit_line.id = account_partial_reconcile.debit_move_id
WHERE credit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND credit_line.account_id NOT IN %(payment_account_ids)s
AND credit_line.credit > 0.0
AND debit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY debit_line.move_id, debit_line.account_id
UNION ALL
SELECT
%(column_group_key)s AS column_group_key,
credit_line.move_id,
credit_line.account_id,
-SUM(%(partial_amount)s) AS balance
FROM account_move_line AS debit_line
LEFT JOIN account_partial_reconcile
ON account_partial_reconcile.debit_move_id = debit_line.id
JOIN %(currency_table)s
ON account_currency_table.company_id = account_partial_reconcile.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
INNER JOIN account_move_line AS credit_line
ON credit_line.id = account_partial_reconcile.credit_move_id
WHERE debit_line.move_id IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND debit_line.account_id NOT IN %(payment_account_ids)s
AND debit_line.debit > 0.0
AND credit_line.move_id NOT IN (SELECT unnest(payment_move_ids.move_id) FROM payment_move_ids)
AND account_partial_reconcile.max_date BETWEEN %(date_from)s AND %(date_to)s
GROUP BY credit_line.move_id, credit_line.account_id)
''',
move_ids_query=move_ids_query,
column_group_key=column_group_key,
payment_account_ids=payment_account_ids,
date_from=column_group_options['date']['date_from'],
date_to=column_group_options['date']['date_to'],
currency_table=currency_table,
partial_amount=report._currency_table_apply_rate(SQL("account_partial_reconcile.amount")),
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
reconciled_percentage_per_move[aml_data['column_group_key']].setdefault(aml_data['move_id'], {})
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']].setdefault(aml_data['account_id'], [0.0, 0.0])
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][0] += aml_data['balance']
reconciled_account_ids[aml_data['column_group_key']].add(aml_data['account_id'])
if not reconciled_percentage_per_move:
return []
queries = []
for column in options['columns']:
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
SUM(%(balance_select)s) AS balance
FROM account_move_line
JOIN %(currency_table)s
ON account_currency_table.company_id = account_move_line.company_id
AND account_currency_table.rate_type = 'current' -- For payable/receivable accounts it'll always be 'current' anyway
WHERE account_move_line.move_id IN %(move_ids)s
AND account_move_line.account_id IN %(account_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id
''',
column_group_key=column['column_group_key'],
currency_table=currency_table,
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,),
account_ids=tuple(reconciled_account_ids[column['column_group_key']]) or (None,)
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
if aml_data['account_id'] in reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']]:
reconciled_percentage_per_move[aml_data['column_group_key']][aml_data['move_id']][aml_data['account_id']][1] += aml_data['balance']
reconciled_aml_per_account = {}
queries = []
query = Query(self.env, 'account_move_line')
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_type = SQL.identifier(account_alias, 'account_type')
for column in options['columns']:
queries.append(SQL(
'''
SELECT
%(column_group_key)s AS column_group_key,
account_move_line.move_id,
account_move_line.account_id,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_account_type,
account_account_account_tag.account_account_tag_id AS account_tag_id,
SUM(%(balance_select)s) AS balance
FROM %(from_clause)s
%(currency_table_join)s
LEFT JOIN account_account_account_tag
ON account_account_account_tag.account_account_id = account_move_line.account_id
AND account_account_account_tag.account_account_tag_id IN %(cash_flow_tag_ids)s
WHERE account_move_line.move_id IN %(move_ids)s
GROUP BY account_move_line.move_id, account_move_line.account_id, account_code, account_name, account_account_type, account_account_account_tag.account_account_tag_id
''',
column_group_key=column['column_group_key'],
account_code=account_code,
account_name=account_name,
account_type=account_type,
from_clause=query.from_clause,
currency_table_join=report._currency_table_aml_join(options),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
cash_flow_tag_ids=tuple(cash_flow_tag_ids),
move_ids=tuple(reconciled_percentage_per_move[column['column_group_key']].keys()) or (None,)
))
self._cr.execute(SQL(' UNION ALL ').join(queries))
for aml_data in self._cr.dictfetchall():
aml_column_group_key = aml_data['column_group_key']
aml_move_id = aml_data['move_id']
aml_account_id = aml_data['account_id']
aml_account_code = aml_data['account_code']
aml_account_name = aml_data['account_name']
aml_account_account_type = aml_data['account_account_type']
aml_account_tag_id = aml_data['account_tag_id']
aml_balance = aml_data['balance']
# Compute the total reconciled for the whole move.
total_reconciled_amount = 0.0
total_amount = 0.0
for reconciled_amount, amount in reconciled_percentage_per_move[aml_column_group_key][aml_move_id].values():
total_reconciled_amount += reconciled_amount
total_amount += amount
# Compute matched percentage for each account.
if total_amount and aml_account_id not in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]:
# Lines being on reconciled moves but not reconciled with any liquidity move must be valued at the
# percentage of what is actually paid.
reconciled_percentage = total_reconciled_amount / total_amount
aml_balance *= reconciled_percentage
elif not total_amount and aml_account_id in reconciled_percentage_per_move[aml_column_group_key][aml_move_id]:
# The total amount to reconcile is 0. In that case, only add entries being on these accounts. Otherwise,
# this special case will lead to an unexplained difference equivalent to the reconciled amount on this
# account.
# E.g:
#
# Liquidity move:
# Account | Debit | Credit
# --------------------------------------
# Bank | | 100
# Receivable | 100 |
#
# Reconciled move: <- reconciled_amount=100, total_amount=0.0
# Account | Debit | Credit
# --------------------------------------
# Receivable | | 200
# Receivable | 200 | <- Only the reconciled part of this entry must be added.
aml_balance = -reconciled_percentage_per_move[aml_column_group_key][aml_move_id][aml_account_id][0]
else:
# Others lines are not considered.
continue
reconciled_aml_per_account.setdefault(aml_account_id, {})
reconciled_aml_per_account[aml_account_id].setdefault(aml_column_group_key, {
'column_group_key': aml_column_group_key,
'account_id': aml_account_id,
'account_code': aml_account_code,
'account_name': aml_account_name,
'account_account_type': aml_account_account_type,
'account_tag_id': aml_account_tag_id,
'balance': 0.0,
})
reconciled_aml_per_account[aml_account_id][aml_column_group_key]['balance'] -= aml_balance
return list(reconciled_aml_per_account.values())
# -------------------------------------------------------------------------
# COLUMNS / LINES
# -------------------------------------------------------------------------
def _get_layout_data(self):
# Indentation of the following dict reflects the structure of the report.
return {
'opening_balance': {'name': _('Cash and cash equivalents, beginning of period'), 'level': 0},
'net_increase': {'name': _('Net increase in cash and cash equivalents'), 'level': 0, 'unfolded': True},
'operating_activities': {'name': _('Cash flows from operating activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True},
'advance_payments_customer': {'name': _('Advance Payments received from customers'), 'level': 4, 'parent_line_id': 'operating_activities'},
'received_operating_activities': {'name': _('Cash received from operating activities'), 'level': 4, 'parent_line_id': 'operating_activities'},
'advance_payments_suppliers': {'name': _('Advance payments made to suppliers'), 'level': 4, 'parent_line_id': 'operating_activities'},
'paid_operating_activities': {'name': _('Cash paid for operating activities'), 'level': 4, 'parent_line_id': 'operating_activities'},
'investing_activities': {'name': _('Cash flows from investing & extraordinary activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True},
'investing_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'investing_activities'},
'investing_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'investing_activities'},
'financing_activities': {'name': _('Cash flows from financing activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True},
'financing_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'financing_activities'},
'financing_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'financing_activities'},
'unclassified_activities': {'name': _('Cash flows from unclassified activities'), 'level': 2, 'parent_line_id': 'net_increase', 'class': 'fw-bold', 'unfolded': True},
'unclassified_activities_cash_in': {'name': _('Cash in'), 'level': 4, 'parent_line_id': 'unclassified_activities'},
'unclassified_activities_cash_out': {'name': _('Cash out'), 'level': 4, 'parent_line_id': 'unclassified_activities'},
'closing_balance': {'name': _('Cash and cash equivalents, closing balance'), 'level': 0},
}
def _get_layout_line(self, report, options, layout_line_id, layout_line_data, report_data):
line_id = report._get_generic_line_id(None, None, markup=layout_line_id)
unfoldable = 'aml_groupby_account' in report_data[layout_line_id] if layout_line_id in report_data else False
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
value = report_data[layout_line_id][expression_label].get(column_group_key, 0.0) if layout_line_id in report_data else 0.0
column_values.append(report._build_column_dict(value, column, options=options))
return {
'id': line_id,
'name': layout_line_data['name'],
'level': layout_line_data['level'],
'class': layout_line_data.get('class', ''),
'columns': column_values,
'unfoldable': unfoldable,
'unfolded': line_id in options['unfolded_lines'] or layout_line_data.get('unfolded') or (options.get('unfold_all') and unfoldable),
}
def _get_aml_line(self, report, options, aml_data):
parent_line_id = report._get_generic_line_id(None, None, aml_data['parent_line_id'])
line_id = report._get_generic_line_id('account.account', aml_data['account_id'], parent_line_id=parent_line_id)
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
value = aml_data[expression_label].get(column_group_key, 0.0)
column_values.append(report._build_column_dict(value, column, options=options))
return {
'id': line_id,
'name': f"{aml_data['account_code']} {aml_data['account_name']}" if aml_data['account_code'] else aml_data['account_name'],
'caret_options': 'account.account',
'level': aml_data['level'],
'parent_id': parent_line_id,
'columns': column_values,
}
def _get_unexplained_difference_line(self, report, options, report_data):
unexplained_difference = False
column_values = []
for column in options['columns']:
expression_label = column['expression_label']
column_group_key = column['column_group_key']
opening_balance = report_data['opening_balance'][expression_label].get(column_group_key, 0.0) if 'opening_balance' in report_data else 0.0
closing_balance = report_data['closing_balance'][expression_label].get(column_group_key, 0.0) if 'closing_balance' in report_data else 0.0
net_increase = report_data['net_increase'][expression_label].get(column_group_key, 0.0) if 'net_increase' in report_data else 0.0
balance = closing_balance - opening_balance - net_increase
if not self.env.company.currency_id.is_zero(balance):
unexplained_difference = True
column_values.append(report._build_column_dict(
balance,
{
'figure_type': 'monetary',
'expression_label': 'balance',
},
options=options,
))
if unexplained_difference:
return {
'id': report._get_generic_line_id(None, None, markup='unexplained_difference'),
'name': 'Unexplained Difference',
'level': 1,
'columns': column_values,
}

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.models.chart_template import template
from odoo import models
class AccountChartTemplate(models.AbstractModel):
_inherit = 'account.chart.template'
def _get_account_accountant_res_company(self, chart_template):
# Called when installing the Accountant module
company = self.env.company
data = self._get_chart_template_data(chart_template)
company_data = data['res.company'].get(company.id, {})
# Pre-reload to ensure the necessary xmlids for the load exist in case they were deleted or not created yet.
required_data = {k: v for k, v in data.items() if k in ['account.journal', 'account.account']}
self._pre_reload_data(company, data['template_data'], required_data)
return {
company.id: {
'deferred_expense_journal_id': company.deferred_expense_journal_id.id or company_data.get('deferred_expense_journal_id'),
'deferred_revenue_journal_id': company.deferred_revenue_journal_id.id or company_data.get('deferred_revenue_journal_id'),
'deferred_expense_account_id': company.deferred_expense_account_id.id or company_data.get('deferred_expense_account_id'),
'deferred_revenue_account_id': company.deferred_revenue_account_id.id or company_data.get('deferred_revenue_account_id'),
}
}
def _get_chart_template_data(self, chart_template):
# OVERRIDE chart template to process the default values for deferred journal and accounts.
data = super()._get_chart_template_data(chart_template)
for _company_id, company_data in data['res.company'].items():
company_data['deferred_expense_journal_id'] = (
company_data.get('deferred_expense_journal_id')
or next((xid for xid, d in data['account.journal'].items() if d['type'] == 'general'), None)
)
company_data['deferred_revenue_journal_id'] = (
company_data.get('deferred_revenue_journal_id')
or next((xid for xid, d in data['account.journal'].items() if d['type'] == 'general'), None)
)
company_data['deferred_expense_account_id'] = (
company_data.get('deferred_expense_account_id')
or next((xid for xid, d in data['account.account'].items() if d['account_type'] == 'asset_current'), None)
)
company_data['deferred_revenue_account_id'] = (
company_data.get('deferred_revenue_account_id')
or next((xid for xid, d in data['account.account'].items() if d['account_type'] == 'liability_current'), None)
)
return data

View File

@@ -0,0 +1,581 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import models, fields, _, api, Command
from odoo.exceptions import UserError
from odoo.tools import groupby, SQL
from odoo.addons.at_accounting.models.account_move import DEFERRED_DATE_MIN, DEFERRED_DATE_MAX
class DeferredReportCustomHandler(models.AbstractModel):
_name = 'account.deferred.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Deferred Expense Report Custom Handler'
def _get_deferred_report_type(self):
raise NotImplementedError("This method is not implemented in the deferred report handler.")
############################################
# DEFERRED COMMON (DISPLAY AND GENERATION) #
############################################
def _get_domain(self, report, options, filter_already_generated=False, filter_not_started=False):
domain = report._get_options_domain(options, "from_beginning")
account_types = ('expense', 'expense_depreciation', 'expense_direct_cost') if self._get_deferred_report_type() == 'expense' else ('income', 'income_other')
domain += [
('account_id.account_type', 'in', account_types),
('deferred_start_date', '!=', False),
('deferred_end_date', '!=', False),
('deferred_end_date', '>=', options['date']['date_from']),
('move_id.date', '<=', options['date']['date_to']),
]
domain += [ # Exclude if entirely inside the period
'!', '&', '&', '&', '&', '&',
('deferred_start_date', '>=', options['date']['date_from']),
('deferred_start_date', '<=', options['date']['date_to']),
('deferred_end_date', '>=', options['date']['date_from']),
('deferred_end_date', '<=', options['date']['date_to']),
('move_id.date', '>=', options['date']['date_from']),
('move_id.date', '<=', options['date']['date_to']),
]
if filter_already_generated:
domain += [
('deferred_end_date', '>=', options['date']['date_from']),
'!',
'&',
('move_id.deferred_move_ids.date', '=', options['date']['date_to']),
('move_id.deferred_move_ids.state', '=', 'posted'),
]
if filter_not_started:
domain += [('deferred_start_date', '>', options['date']['date_to'])]
return domain
@api.model
def _get_select(self):
account_name = self.env['account.account']._field_to_sql('account_move_line__account_id', 'name')
return [
SQL("account_move_line.id AS line_id"),
SQL("account_move_line.account_id AS account_id"),
SQL("account_move_line.partner_id AS partner_id"),
SQL("account_move_line.product_id AS product_id"),
SQL("account_move_line__product_template_id.categ_id AS product_category_id"),
SQL("account_move_line.name AS line_name"),
SQL("account_move_line.deferred_start_date AS deferred_start_date"),
SQL("account_move_line.deferred_end_date AS deferred_end_date"),
SQL("account_move_line.deferred_end_date - account_move_line.deferred_start_date AS diff_days"),
SQL("account_move_line.balance AS balance"),
SQL("account_move_line.analytic_distribution AS analytic_distribution"),
SQL("account_move_line__move_id.id as move_id"),
SQL("account_move_line__move_id.name AS move_name"),
SQL("%s AS account_name", account_name),
]
def _get_lines(self, report, options, filter_already_generated=False):
domain = self._get_domain(report, options, filter_already_generated)
query = report._get_report_query(options, domain=domain, date_scope='from_beginning')
select_clause = SQL(', ').join(self._get_select())
query = SQL(
"""
SELECT %(select_clause)s
FROM %(table_references)s
LEFT JOIN product_product AS account_move_line__product_id ON account_move_line.product_id = account_move_line__product_id.id
LEFT JOIN product_template AS account_move_line__product_template_id ON account_move_line__product_id.product_tmpl_id = account_move_line__product_template_id.id
WHERE %(search_condition)s
ORDER BY account_move_line.deferred_start_date, account_move_line.id
""",
select_clause=select_clause,
table_references=query.from_clause,
search_condition=query.where_clause,
)
self.env.cr.execute(query)
res = self.env.cr.dictfetchall()
return res
@api.model
def _get_grouping_fields_deferred_lines(self, filter_already_generated=False, grouping_field='account_id'):
return (grouping_field,)
@api.model
def _group_by_deferred_fields(self, line, filter_already_generated=False, grouping_field='account_id'):
return tuple(line[k] for k in self._get_grouping_fields_deferred_lines(filter_already_generated, grouping_field))
@api.model
def _get_grouping_fields_deferral_lines(self):
return ()
@api.model
def _group_by_deferral_fields(self, line):
return tuple(line[k] for k in self._get_grouping_fields_deferral_lines())
@api.model
def _group_deferred_amounts_by_grouping_field(self, deferred_amounts_by_line, periods, is_reverse, filter_already_generated=False, grouping_field='account_id'):
"""
Groups the deferred amounts by account and computes the totals for each account for each period.
And the total for all accounts for each period.
E.g. (where period1 = (date1, date2, label1), period2 = (date2, date3, label2), ...)
{
self._get_grouping_keys_deferred_lines(): {
'account_id': account1, 'amount_total': 600, period_1: 200, period_2: 400
},
self._get_grouping_keys_deferred_lines(): {
'account_id': account2, 'amount_total': 700, period_1: 300, period_2: 400
},
}, {'totals_aggregated': 1300, period_1: 500, period_2: 800}
"""
deferred_amounts_by_line = groupby(deferred_amounts_by_line, key=lambda x: self._group_by_deferred_fields(x, filter_already_generated, grouping_field))
totals_per_key = {} # {key: {**self._get_grouping_fields_deferral_lines(), total, before, current, later}}
totals_aggregated_by_period = {period: 0 for period in periods + ['totals_aggregated']}
sign = 1 if is_reverse else -1
for key, lines_per_key in deferred_amounts_by_line:
lines_per_key = list(lines_per_key)
current_key_totals = self._get_current_key_totals_dict(lines_per_key, sign)
totals_aggregated_by_period['totals_aggregated'] += current_key_totals['amount_total']
for period in periods:
current_key_totals[period] = sign * sum(line[period] for line in lines_per_key)
totals_aggregated_by_period[period] += self.env.company.currency_id.round(current_key_totals[period])
totals_per_key[key] = current_key_totals
return totals_per_key, totals_aggregated_by_period
###########################
# DEFERRED REPORT DISPLAY #
###########################
def _get_custom_display_config(self):
return {
'templates': {
'AccountReportFilters': 'at_accounting.DeferredFilters',
},
}
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
options_per_col_group = report._split_options_per_column_group(options)
for column_dict in options['columns']:
column_options = options_per_col_group[column_dict['column_group_key']]
column_dict['name'] = column_options['date']['string']
column_dict['date_from'] = column_options['date']['date_from']
column_dict['date_to'] = column_options['date']['date_to']
options['columns'] = list(reversed(options['columns']))
total_column = [{
**options['columns'][0],
'name': _('Total'),
'expression_label': 'total',
'date_from': DEFERRED_DATE_MIN,
'date_to': DEFERRED_DATE_MAX,
}]
not_started_column = [{
**options['columns'][0],
'name': _('Not Started'),
'expression_label': 'not_started',
'date_from': options['columns'][-1]['date_to'],
'date_to': DEFERRED_DATE_MAX,
}]
before_column = [{
**options['columns'][0],
'name': _('Before'),
'expression_label': 'before',
'date_from': DEFERRED_DATE_MIN,
'date_to': options['columns'][0]['date_from'],
}]
later_column = [{
**options['columns'][0],
'name': _('Later'),
'expression_label': 'later',
'date_from': options['columns'][-1]['date_to'],
'date_to': DEFERRED_DATE_MAX,
}]
options['columns'] = total_column + not_started_column + before_column + options['columns'] + later_column
options['column_headers'] = []
options['deferred_report_type'] = self._get_deferred_report_type()
options['deferred_grouping_field'] = previous_options.get('deferred_grouping_field') or 'account_id'
if (
self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual'
or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual'
):
options['buttons'].append({'name': _('Generate entry'), 'action': 'action_generate_entry', 'sequence': 80, 'always_show': True})
def action_audit_cell(self, options, params):
""" Open a list of invoices/bills and/or deferral entries for the clicked cell in a deferred report.
Specifically, we show the following lines, grouped by their journal entry, filtered by the column date bounds:
- Total: Lines of all invoices/bills being deferred in the current period
- Not Started: Lines of all deferral entries for which the original invoice/bill date is before or in the
current period, but the deferral only starts after the current period, as well as the lines of
their original invoices/bills
- Before: Lines of all deferral entries with a date before the current period, created by invoices/bills also
being deferred in the current period, as well as the lines of their original invoices/bills
- Current: Lines of all deferral entries in the current period, as well as these of their original
invoices/bills
- Later: Lines of all deferral entries with a date after the current period, created by invoices/bills also
being deferred in the current period, as well as the lines of their original invoices/bills
:param dict options: the report's `options`
:param dict params: a dict containing:
`calling_line_dict_id`: line id containing the optional account of the cell
`column_group_id`: the column group id of the cell
`expression_label`: the expression label of the cell
"""
report = self.env['account.report'].browse(options['report_id'])
column_values = next(
(column for column in options['columns'] if (
column['column_group_key'] == params.get('column_group_key')
and column['expression_label'] == params.get('expression_label')
)),
None
)
if not column_values:
return
column_date_from = fields.Date.to_date(column_values['date_from'])
column_date_to = fields.Date.to_date(column_values['date_to'])
report_date_from = fields.Date.to_date(options['date']['date_from'])
report_date_to = fields.Date.to_date(options['date']['date_to'])
# Corrections for comparisons
if column_values['expression_label'] in ('not_started', 'later'):
# Not Started and Later period start one day after `report_date_to`
column_date_from = report_date_to + relativedelta(days=1)
if column_values['expression_label'] == 'before':
# Before period ends one day before `report_date_from`
column_date_to = report_date_from - relativedelta(days=1)
# calling_line_dict_id is of the format `~account.report~15|~account.account~25`
_grouping_model, grouping_record_id = report._get_model_info_from_id(params.get('calling_line_dict_id'))
# Find the original lines to be deferred in the report period
original_move_lines_domain = self._get_domain(
report, options, filter_not_started=column_values['expression_label'] == 'not_started'
)
if grouping_record_id:
# We're auditing a specific account, so we only want moves containing this account
original_move_lines_domain.append((options['deferred_grouping_field'], '=', grouping_record_id))
# We're getting all lines from the concerned moves. They are filtered later for flexibility.
original_move = self.env['account.move.line'].search(original_move_lines_domain).move_id
# For the Total period only show the original move lines
line_ids = original_move.line_ids.ids
# Show both the original move lines and deferral move lines for all other periods
if not column_values['expression_label'] == 'total':
line_ids += original_move.deferred_move_ids.line_ids.ids
return {
'type': 'ir.actions.act_window',
'name': _('Deferred Entries'),
'res_model': 'account.move.line',
'domain': [('id', 'in', line_ids)],
'views': [(self.env.ref('at_accounting.view_deferred_entries_tree').id, 'list')],
# Most filters are set here to allow auditing flexibility to the user
'context': {
'search_default_pl_accounts': True,
f'search_default_{options["deferred_grouping_field"]}': grouping_record_id,
'date_from': column_date_from,
'date_to': column_date_to,
'search_default_date_between': True,
'expand': True,
}
}
def _caret_options_initializer(self):
return {
'deferred_caret': [
{'name': _("Journal Items"), 'action': 'open_journal_items'},
],
}
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
already_generated = (
(
self._get_deferred_report_type() == 'expense' and self.env.company.generate_deferred_expense_entries_method == 'manual'
or self._get_deferred_report_type() == 'revenue' and self.env.company.generate_deferred_revenue_entries_method == 'manual'
)
and self.env['account.move'].search_count(
report._get_generated_deferral_entries_domain(options)
)
)
if already_generated:
warnings['at_accounting.deferred_report_warning_already_posted'] = {'alert_type': 'warning'}
def open_journal_items(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
record_model, record_id = report._get_model_info_from_id(params.get('line_id'))
domain = self._get_domain(report, options)
if record_model == 'account.account' and record_id:
domain += [('account_id', '=', record_id)]
elif record_model == 'product.product' and record_id:
domain += [('product_id', '=', record_id)]
elif record_model == 'product.category' and record_id:
domain += [('product_category_id', '=', record_id)]
return {
'type': 'ir.actions.act_window',
'name': _("Deferred Entries"),
'res_model': 'account.move.line',
'domain': domain,
'views': [(self.env.ref('at_accounting.view_deferred_entries_tree').id, 'list')],
'context': {
'search_default_group_by_move': True,
'expand': True,
}
}
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
def get_columns(totals):
return [
{
**report._build_column_dict(
totals[(
fields.Date.to_date(column['date_from']),
fields.Date.to_date(column['date_to']),
column['expression_label']
)],
column,
options=options,
currency=self.env.company.currency_id,
),
'auditable': True,
}
for column in options['columns']
]
lines = self._get_lines(report, options)
periods = [
(
fields.Date.from_string(column['date_from']),
fields.Date.from_string(column['date_to']),
column['expression_label'],
)
for column in options['columns']
]
deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, periods, self._get_deferred_report_type())
totals_per_grouping_field, totals_all_grouping_field = self._group_deferred_amounts_by_grouping_field(
deferred_amounts_by_line=deferred_amounts_by_line,
periods=periods,
is_reverse=self._get_deferred_report_type() == 'expense',
filter_already_generated=False,
grouping_field=options['deferred_grouping_field'],
)
report_lines = []
grouping_model = self.env['account.move.line'][options['deferred_grouping_field']]._name
for totals_grouping_field in totals_per_grouping_field.values():
grouping_record = self.env[grouping_model].browse(totals_grouping_field[options['deferred_grouping_field']])
grouping_field_description = self.env['account.move.line'][options['deferred_grouping_field']]._description
if options['deferred_grouping_field'] == 'product_id':
grouping_field_description = _("Product")
grouping_name = grouping_record.display_name or _("(No %s)", grouping_field_description)
report_lines.append((0, {
'id': report._get_generic_line_id(grouping_model, grouping_record.id),
'name': grouping_name,
'caret_options': 'deferred_caret',
'level': 1,
'columns': get_columns(totals_grouping_field),
}))
if totals_per_grouping_field:
report_lines.append((0, {
'id': report._get_generic_line_id(None, None, markup='total'),
'name': 'Total',
'level': 1,
'columns': get_columns(totals_all_grouping_field),
}))
return report_lines
#######################
# DEFERRED GENERATION #
#######################
def action_generate_entry(self, options):
new_deferred_moves = self._generate_deferral_entry(options)
return {
'name': _('Deferred Entries'),
'type': 'ir.actions.act_window',
'views': [(False, "list"), (False, "form")],
'domain': [('id', 'in', new_deferred_moves.ids)],
'res_model': 'account.move',
'context': {
'search_default_group_by_move': True,
'expand': True,
},
'target': 'current',
}
def _generate_deferral_entry(self, options):
journal = self.env.company.deferred_expense_journal_id if self._get_deferred_report_type() == "expense" else self.env.company.deferred_revenue_journal_id
if not journal:
raise UserError(_("Please set the deferred journal in the accounting settings."))
date_from = fields.Date.to_date(DEFERRED_DATE_MIN)
date_to = fields.Date.from_string(options['date']['date_to'])
if date_to.day != calendar.monthrange(date_to.year, date_to.month)[1]:
raise UserError(_("You cannot generate entries for a period that does not end at the end of the month."))
if self.env.company._get_violated_lock_dates(date_to, False, journal):
raise UserError(_("You cannot generate entries for a period that is locked."))
options['all_entries'] = False # We only want to create deferrals for posted moves
report = self.env["account.report"].browse(options["report_id"])
self.env['account.move.line'].flush_model()
lines = self._get_lines(report, options, filter_already_generated=True)
deferral_entry_period = self.env['account.report']._get_dates_period(date_from, date_to, 'range', period_type='month')
ref = _("Grouped Deferral Entry of %s", deferral_entry_period['string'])
ref_rev = _("Reversal of Grouped Deferral Entry of %s", deferral_entry_period['string'])
deferred_account = self.env.company.deferred_expense_account_id if self._get_deferred_report_type() == 'expense' else self.env.company.deferred_revenue_account_id
move_lines, original_move_ids = self._get_deferred_lines(lines, deferred_account, (date_from, date_to, 'current'), self._get_deferred_report_type() == 'expense', ref)
if not move_lines:
raise UserError(_("No entry to generate."))
deferred_move = self.env['account.move'].with_context(skip_account_deprecation_check=True).create({
'move_type': 'entry',
'deferred_original_move_ids': [Command.set(original_move_ids)],
'journal_id': journal.id,
'date': date_to,
'auto_post': 'at_date',
'ref': ref,
})
# We write the lines after creation, to make sure the `deferred_original_move_ids` is set.
# This way we can avoid adding taxes for deferred moves.
deferred_move.write({'line_ids': move_lines})
reverse_move = deferred_move._reverse_moves()
reverse_move.write({
'date': deferred_move.date + relativedelta(days=1),
'ref': ref_rev,
})
reverse_move.line_ids.name = ref_rev
new_deferred_moves = deferred_move + reverse_move
# We create the relation (original deferred move, deferral entry)
# using SQL. This avoids a MemoryError using the ORM which will
# load huge amounts of moves in memory for nothing
self.env.cr.execute_values("""
INSERT INTO account_move_deferred_rel(original_move_id, deferred_move_id)
VALUES %s
ON CONFLICT DO NOTHING
""", [
(original_move_id, deferral_move.id)
for original_move_id in original_move_ids
for deferral_move in new_deferred_moves
])
(deferred_move + reverse_move)._post(soft=True)
return new_deferred_moves
@api.model
def _get_current_key_totals_dict(self, lines_per_key, sign):
return {
'account_id': lines_per_key[0]['account_id'],
'product_id': lines_per_key[0]['product_id'],
'product_category_id': lines_per_key[0]['product_category_id'],
'amount_total': sign * sum(line['balance'] for line in lines_per_key),
'move_ids': {line['move_id'] for line in lines_per_key},
}
@api.model
def _get_deferred_lines(self, lines, deferred_account, period, is_reverse, ref):
"""
Returns a list of Command objects to create the deferred lines of a single given period.
And the move_ids of the original lines that created these deferred
(to keep track of the original invoice in the deferred entries).
"""
if not deferred_account:
raise UserError(_("Please set the deferred accounts in the accounting settings."))
deferred_amounts_by_line = self.env['account.move']._get_deferred_amounts_by_line(lines, [period], is_reverse)
deferred_amounts_by_key, deferred_amounts_totals = self._group_deferred_amounts_by_grouping_field(deferred_amounts_by_line, [period], is_reverse, filter_already_generated=True)
if deferred_amounts_totals['totals_aggregated'] == deferred_amounts_totals[period]:
return [], set()
# compute analytic distribution to populate on deferred lines
# structure: {self._get_grouping_keys_deferred_lines(): [analytic distribution]}
# dict of keys: self._get_grouping_keys_deferred_lines()
# values: dict of keys: "account.analytic.account.id" (string)
# values: float
anal_dist_by_key = defaultdict(lambda: defaultdict(float))
# using another var for the analytic distribution of the deferral account
deferred_anal_dist = defaultdict(lambda: defaultdict(float))
for line in lines:
if not line['analytic_distribution']:
continue
# Analytic distribution should be computed from the lines with the same _get_grouping_keys_deferred_lines(), except for
# the deferred line with the deferral account which will use _get_grouping_fields_deferral_lines()
full_ratio = (line['balance'] / deferred_amounts_totals['totals_aggregated']) if deferred_amounts_totals['totals_aggregated'] else 0
key_amount = deferred_amounts_by_key.get(self._group_by_deferred_fields(line, True))
key_ratio = (line['balance'] / key_amount['amount_total']) if key_amount and key_amount['amount_total'] else 0
for account_id, distribution in line['analytic_distribution'].items():
anal_dist_by_key[self._group_by_deferred_fields(line, True)][account_id] += distribution * key_ratio
deferred_anal_dist[self._group_by_deferral_fields(line)][account_id] += distribution * full_ratio
remaining_balance = 0
deferred_lines = []
original_move_ids = set()
for key, line in deferred_amounts_by_key.items():
for balance in (-line['amount_total'], line[period]):
if balance != 0 and line[period] != line['amount_total']:
original_move_ids |= line['move_ids']
deferred_balance = self.env.company.currency_id.round((1 if is_reverse else -1) * balance)
deferred_lines.append(
Command.create(
self.env['account.move.line']._get_deferred_lines_values(
account_id=line['account_id'],
balance=deferred_balance,
ref=ref,
analytic_distribution=anal_dist_by_key[key] or False,
line=line,
)
)
)
remaining_balance += deferred_balance
grouped_by_key = {
key: list(value)
for key, value in groupby(
deferred_amounts_by_key.values(),
key=self._group_by_deferral_fields,
)
}
deferral_lines = []
for key, lines_per_key in grouped_by_key.items():
balance = 0
for line in lines_per_key:
if line[period] != line['amount_total']:
balance += self.env.company.currency_id.round((1 if is_reverse else -1) * (line['amount_total'] - line[period]))
deferral_lines.append(
Command.create(
self.env['account.move.line']._get_deferred_lines_values(
account_id=deferred_account.id,
balance=balance,
ref=ref,
analytic_distribution=deferred_anal_dist[key] or False,
line=lines_per_key[0],
)
)
)
remaining_balance += balance
if not self.env.company.currency_id.is_zero(remaining_balance):
deferral_lines.append(
Command.create({
'account_id': deferred_account.id,
'balance': -remaining_balance,
'name': ref,
})
)
return deferred_lines + deferral_lines, original_move_ids
class DeferredExpenseCustomHandler(models.AbstractModel):
_name = 'account.deferred.expense.report.handler'
_inherit = 'account.deferred.report.handler'
_description = 'Deferred Expense Custom Handler'
def _get_deferred_report_type(self):
return 'expense'
class DeferredRevenueCustomHandler(models.AbstractModel):
_name = 'account.deferred.revenue.report.handler'
_inherit = 'account.deferred.report.handler'
_description = 'Deferred Revenue Custom Handler'
def _get_deferred_report_type(self):
return 'revenue'

View File

@@ -0,0 +1,19 @@
from odoo import models
class AccountFiscalPosition(models.Model):
_inherit = 'account.fiscal.position'
def _inverse_foreign_vat(self):
# EXTENDS account
super()._inverse_foreign_vat()
for fpos in self:
if fpos.foreign_vat:
fpos._create_draft_closing_move_for_foreign_vat()
def _create_draft_closing_move_for_foreign_vat(self):
self.ensure_one()
existing_draft_closings = self.env['account.move'].search([('tax_closing_report_id', '!=', False), ('state', '=', 'draft')])
for closing_date, entries in existing_draft_closings.grouped('date').items():
for entry in entries:
self.company_id._get_and_update_tax_closing_moves(closing_date, entry.tax_closing_report_id, fiscal_positions=self)

View File

@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from odoo.exceptions import ValidationError
from odoo import api, fields, models, _
from datetime import datetime
class AccountFiscalYear(models.Model):
_name = 'account.fiscal.year'
_description = 'Fiscal Year'
name = fields.Char(string='Name', required=True)
date_from = fields.Date(string='Start Date', required=True,
help='Start Date, included in the fiscal year.')
date_to = fields.Date(string='End Date', required=True,
help='Ending Date, included in the fiscal year.')
company_id = fields.Many2one('res.company', string='Company', required=True,
default=lambda self: self.env.company)
@api.constrains('date_from', 'date_to', 'company_id')
def _check_dates(self):
'''
Check interleaving between fiscal years.
There are 3 cases to consider:
s1 s2 e1 e2
( [----)----]
s2 s1 e2 e1
[----(----] )
s1 s2 e2 e1
( [----] )
'''
for fy in self:
# Starting date must be prior to the ending date
date_from = fy.date_from
date_to = fy.date_to
if date_to < date_from:
raise ValidationError(_('The ending date must not be prior to the starting date.'))
if fy.company_id.parent_id:
raise ValidationError(_('You cannot have a fiscal year on a child company.'))
domain = [
('id', '!=', fy.id),
('company_id', '=', fy.company_id.id),
'|', '|',
'&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_from),
'&', ('date_from', '<=', fy.date_to), ('date_to', '>=', fy.date_to),
'&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_to),
]
if self.search_count(domain) > 0:
raise ValidationError(_('You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years.'))

View File

@@ -0,0 +1,744 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from odoo import models, fields, api, _
from odoo.tools.misc import format_date
from odoo.tools import get_lang, SQL
from odoo.exceptions import UserError
from datetime import timedelta
from collections import defaultdict
class GeneralLedgerCustomHandler(models.AbstractModel):
_name = 'account.general.ledger.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'General Ledger Custom Handler'
def _get_custom_display_config(self):
return {
'templates': {
'AccountReportLineName': 'at_accounting.GeneralLedgerLineName',
},
}
def _custom_options_initializer(self, report, options, previous_options):
# Remove multi-currency columns if needed
super()._custom_options_initializer(report, options, previous_options=previous_options)
if self.env.user.has_group('base.group_multi_currency'):
options['multi_currency'] = True
else:
options['columns'] = [
column for column in options['columns']
if column['expression_label'] != 'amount_currency'
]
# Automatically unfold the report when printing it, unless some specific lines have been unfolded
options['unfold_all'] = (options['export_mode'] == 'print' and not options.get('unfolded_lines')) or options['unfold_all']
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
lines = []
date_from = fields.Date.from_string(options['date']['date_from'])
company_currency = self.env.company.currency_id
totals_by_column_group = defaultdict(lambda: {'debit': 0, 'credit': 0, 'balance': 0})
for account, column_group_results in self._query_values(report, options):
eval_dict = {}
has_lines = False
for column_group_key, results in column_group_results.items():
account_sum = results.get('sum', {})
account_un_earn = results.get('unaffected_earnings', {})
account_debit = account_sum.get('debit', 0.0) + account_un_earn.get('debit', 0.0)
account_credit = account_sum.get('credit', 0.0) + account_un_earn.get('credit', 0.0)
account_balance = account_sum.get('balance', 0.0) + account_un_earn.get('balance', 0.0)
eval_dict[column_group_key] = {
'amount_currency': account_sum.get('amount_currency', 0.0) + account_un_earn.get('amount_currency', 0.0),
'debit': account_debit,
'credit': account_credit,
'balance': account_balance,
}
max_date = account_sum.get('max_date')
has_lines = has_lines or (max_date and max_date >= date_from)
totals_by_column_group[column_group_key]['debit'] += account_debit
totals_by_column_group[column_group_key]['credit'] += account_credit
totals_by_column_group[column_group_key]['balance'] += account_balance
lines.append(self._get_account_title_line(report, options, account, has_lines, eval_dict))
# Report total line.
for totals in totals_by_column_group.values():
totals['balance'] = company_currency.round(totals['balance'])
# Tax Declaration lines.
journal_options = report._get_options_journals(options)
if len(options['column_groups']) == 1 and len(journal_options) == 1 and journal_options[0]['type'] in ('sale', 'purchase'):
lines += self._tax_declaration_lines(report, options, journal_options[0]['type'])
# Total line
lines.append(self._get_total_line(report, options, totals_by_column_group))
return [(0, line) for line in lines]
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
account_ids_to_expand = []
for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_general_ledger', []):
model, model_id = report._get_model_info_from_id(line_dict['id'])
if model == 'account.account':
account_ids_to_expand.append(model_id)
limit_to_load = report.load_more_limit if report.load_more_limit and not options.get('export_mode') else None
has_more_per_account_id = {}
unlimited_aml_results_per_account_id = self._get_aml_values(report, options, account_ids_to_expand)[0]
if limit_to_load:
# Apply the load_more_limit.
# load_more_limit cannot be passed to the call to _get_aml_values, otherwise it won't be applied per account but on the whole result.
# We gain perf from batching, but load every result ; then we need to filter them.
aml_results_per_account_id = {}
for account_id, account_aml_results in unlimited_aml_results_per_account_id.items():
account_values = {}
for key, value in account_aml_results.items():
if len(account_values) == limit_to_load:
has_more_per_account_id[account_id] = True
break
account_values[key] = value
aml_results_per_account_id[account_id] = account_values
else:
aml_results_per_account_id = unlimited_aml_results_per_account_id
return {
'initial_balances': self._get_initial_balance_values(report, account_ids_to_expand, options),
'aml_results': aml_results_per_account_id,
'has_more': has_more_per_account_id,
}
def _tax_declaration_lines(self, report, options, tax_type):
labels_replacement = {
'debit': _("Base Amount"),
'credit': _("Tax Amount"),
}
rslt = [{
'id': report._get_generic_line_id(None, None, markup='tax_decl_header_1'),
'name': _('Tax Declaration'),
'columns': [{} for column in options['columns']],
'level': 1,
'unfoldable': False,
'unfolded': False,
}, {
'id': report._get_generic_line_id(None, None, markup='tax_decl_header_2'),
'name': _('Name'),
'columns': [{'name': labels_replacement.get(col['expression_label'], '')} for col in options['columns']],
'level': 3,
'unfoldable': False,
'unfolded': False,
}]
# Call the generic tax report
generic_tax_report = self.env.ref('account.generic_tax_report')
tax_report_options = generic_tax_report.get_options({**options, 'selected_variant_id': generic_tax_report.id, 'forced_domain': [('tax_line_id.type_tax_use', '=', tax_type)]})
tax_report_lines = generic_tax_report._get_lines(tax_report_options)
tax_type_parent_line_id = generic_tax_report._get_generic_line_id(None, None, markup=tax_type)
for tax_report_line in tax_report_lines:
if tax_report_line.get('parent_id') == tax_type_parent_line_id:
original_columns = tax_report_line['columns']
row_column_map = {
'debit': original_columns[0],
'credit': original_columns[1],
}
tax_report_line['columns'] = [row_column_map.get(col['expression_label'], {}) for col in options['columns']]
rslt.append(tax_report_line)
return rslt
def _query_values(self, report, options):
""" Executes the queries, and performs all the computations.
:return: [(record, values_by_column_group), ...], where
- record is an account.account record.
- values_by_column_group is a dict in the form {column_group_key: values, ...}
- column_group_key is a string identifying a column group, as in options['column_groups']
- values is a list of dictionaries, one per period containing:
- sum: {'debit': float, 'credit': float, 'balance': float}
- (optional) initial_balance: {'debit': float, 'credit': float, 'balance': float}
- (optional) unaffected_earnings: {'debit': float, 'credit': float, 'balance': float}
"""
# Execute the queries and dispatch the results.
query = self._get_query_sums(report, options)
if not query:
return []
groupby_accounts = {}
groupby_companies = {}
self._cr.execute(query)
for res in self._cr.dictfetchall():
# No result to aggregate.
if res['groupby'] is None:
continue
column_group_key = res['column_group_key']
key = res['key']
if key == 'sum':
groupby_accounts.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']})
groupby_accounts[res['groupby']][column_group_key][key] = res
elif key == 'initial_balance':
groupby_accounts.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']})
groupby_accounts[res['groupby']][column_group_key][key] = res
elif key == 'unaffected_earnings':
groupby_companies.setdefault(res['groupby'], {col_group_key: {} for col_group_key in options['column_groups']})
groupby_companies[res['groupby']][column_group_key] = res
# Affect the unaffected earnings to the first fetched account of type 'account.data_unaffected_earnings'.
# It's less costly to fetch all candidate accounts in a single search and then iterate it.
if groupby_companies:
unaffected_earnings_accounts = self.env['account.account'].search([
('display_name', 'ilike', options.get('filter_search_bar')),
*self.env['account.account']._check_company_domain(list(groupby_companies.keys())),
('account_type', '=', 'equity_unaffected'),
])
for company_id, groupby_company in groupby_companies.items():
if equity_unaffected_account := unaffected_earnings_accounts.filtered(lambda a: self.env['res.company'].browse(company_id).root_id in a.company_ids):
for column_group_key in options['column_groups']:
groupby_accounts.setdefault(
equity_unaffected_account.id,
{col_group_key: {'unaffected_earnings': {}} for col_group_key in options['column_groups']},
)
if unaffected_earnings := groupby_company.get(column_group_key):
if groupby_accounts[equity_unaffected_account.id][column_group_key].get('unaffected_earnings'):
for key in ['amount_currency', 'debit', 'credit', 'balance']:
groupby_accounts[equity_unaffected_account.id][column_group_key]['unaffected_earnings'][key] += unaffected_earnings[key]
else:
groupby_accounts[equity_unaffected_account.id][column_group_key]['unaffected_earnings'] = unaffected_earnings
# Retrieve the accounts to browse.
# groupby_accounts.keys() contains all account ids affected by:
# - the amls in the current period.
# - the amls affecting the initial balance.
# - the unaffected earnings allocation.
# Note a search is done instead of a browse to preserve the table ordering.
if groupby_accounts:
accounts = self.env['account.account'].search([('id', 'in', list(groupby_accounts.keys()))])
else:
accounts = []
return [(account, groupby_accounts[account.id]) for account in accounts]
def _get_query_sums(self, report, options) -> SQL:
""" Construct a query retrieving all the aggregated sums to build the report. It includes:
- sums for all accounts.
- sums for the initial balances.
- sums for the unaffected earnings.
- sums for the tax declaration.
:return: query as SQL object
"""
options_by_column_group = report._split_options_per_column_group(options)
queries = []
# ============================================
# 1) Get sums for all accounts.
# ============================================
for column_group_key, options_group in options_by_column_group.items():
# Sum is computed including the initial balance of the accounts configured to do so, unless a special option key is used
# (this is required for trial balance, which is based on general ledger)
sum_date_scope = 'strict_range' if options_group.get('general_ledger_strict_range') else 'from_beginning'
query_domain = []
if not options_group.get('general_ledger_strict_range'):
date_from = fields.Date.from_string(options_group['date']['date_from'])
current_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from)
query_domain += [
'|',
('date', '>=', current_fiscalyear_dates['date_from']),
('account_id.include_initial_balance', '=', True),
]
if options_group.get('export_mode') == 'print' and options_group.get('filter_search_bar'):
query_domain.append(('account_id', 'ilike', options_group['filter_search_bar']))
if options_group.get('include_current_year_in_unaff_earnings'):
query_domain += [('account_id.include_initial_balance', '=', True)]
query = report._get_report_query(options_group, sum_date_scope, domain=query_domain)
queries.append(SQL(
"""
SELECT
account_move_line.account_id AS groupby,
'sum' AS key,
MAX(account_move_line.date) AS max_date,
%(column_group_key)s AS column_group_key,
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.account_id
""",
column_group_key=column_group_key,
table_references=query.from_clause,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
currency_table_join=report._currency_table_aml_join(options_group),
search_condition=query.where_clause,
))
# ============================================
# 2) Get sums for the unaffected earnings.
# ============================================
if not options_group.get('general_ledger_strict_range'):
unaff_earnings_domain = [('account_id.include_initial_balance', '=', False)]
# The period domain is expressed as:
# [
# ('date' <= fiscalyear['date_from'] - 1),
# ('account_id.include_initial_balance', '=', False),
# ]
new_options = self._get_options_unaffected_earnings(options_group)
query = report._get_report_query(new_options, 'strict_range', domain=unaff_earnings_domain)
queries.append(SQL(
"""
SELECT
account_move_line.company_id AS groupby,
'unaffected_earnings' AS key,
NULL AS max_date,
%(column_group_key)s AS column_group_key,
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.company_id
""",
column_group_key=column_group_key,
table_references=query.from_clause,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
currency_table_join=report._currency_table_aml_join(options_group),
search_condition=query.where_clause,
))
return SQL(" UNION ALL ").join(queries)
def _get_options_unaffected_earnings(self, options):
''' Create options used to compute the unaffected earnings.
The unaffected earnings are the amount of benefits/loss that have not been allocated to
another account in the previous fiscal years.
The resulting dates domain will be:
[
('date' <= fiscalyear['date_from'] - 1),
('account_id.include_initial_balance', '=', False),
]
:param options: The report options.
:return: A copy of the options.
'''
new_options = options.copy()
new_options.pop('filter_search_bar', None)
fiscalyear_dates = self.env.company.compute_fiscalyear_dates(fields.Date.from_string(options['date']['date_from']))
# Trial balance uses the options key, general ledger does not
new_date_to = fields.Date.from_string(new_options['date']['date_to']) if options.get('include_current_year_in_unaff_earnings') else fiscalyear_dates['date_from'] - timedelta(days=1)
new_options['date'] = self.env['account.report']._get_dates_period(None, new_date_to, 'single')
return new_options
def _get_aml_values(self, report, options, expanded_account_ids, offset=0, limit=None):
rslt = {account_id: {} for account_id in expanded_account_ids}
aml_query = self._get_query_amls(report, options, expanded_account_ids, offset=offset, limit=limit)
self._cr.execute(aml_query)
aml_results_number = 0
has_more = False
for aml_result in self._cr.dictfetchall():
aml_results_number += 1
if aml_results_number == limit:
has_more = True
break
# For asset_receivable the name will already contains the ref with the _compute_name
if aml_result['ref'] and aml_result['account_type'] != 'asset_receivable':
aml_result['communication'] = f"{aml_result['ref']} - {aml_result['name']}"
else:
aml_result['communication'] = aml_result['name']
# The same aml can return multiple results when using account_report_cash_basis module, if the receivable/payable
# is reconciled with multiple payments. In this case, the date shown for the move lines actually corresponds to the
# reconciliation date. In order to keep distinct lines in this case, we include date in the grouping key.
aml_key = (aml_result['id'], aml_result['date'])
account_result = rslt[aml_result['account_id']]
if not aml_key in account_result:
account_result[aml_key] = {col_group_key: {} for col_group_key in options['column_groups']}
already_present_result = account_result[aml_key][aml_result['column_group_key']]
if already_present_result:
# In case the same move line gives multiple results at the same date, add them.
# This does not happen in standard GL report, but could because of custom shadowing of account.move.line,
# such as the one done in account_report_cash_basis (if the payable/receivable line is reconciled twice at the same date).
already_present_result['debit'] += aml_result['debit']
already_present_result['credit'] += aml_result['credit']
already_present_result['balance'] += aml_result['balance']
already_present_result['amount_currency'] += aml_result['amount_currency']
else:
account_result[aml_key][aml_result['column_group_key']] = aml_result
return rslt, has_more
def _get_query_amls(self, report, options, expanded_account_ids, offset=0, limit=None) -> SQL:
""" Construct a query retrieving the account.move.lines when expanding a report line with or without the load
more.
:param options: The report options.
:param expanded_account_ids: The account.account ids corresponding to consider. If None, match every account.
:param offset: The offset of the query (used by the load more).
:param limit: The limit of the query (used by the load more).
:return: (query, params)
"""
additional_domain = [('account_id', 'in', expanded_account_ids)] if expanded_account_ids is not None else None
queries = []
journal_name = self.env['account.journal']._field_to_sql('journal', 'name')
for column_group_key, group_options in report._split_options_per_column_group(options).items():
# Get sums for the account move lines.
# period: [('date' <= options['date_to']), ('date', '>=', options['date_from'])]
query = report._get_report_query(group_options, domain=additional_domain, date_scope='strict_range')
account_alias = query.join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
account_type = self.env['account.account']._field_to_sql(account_alias, 'account_type')
query = SQL(
'''
SELECT
account_move_line.id,
account_move_line.date,
account_move_line.date_maturity,
account_move_line.name,
account_move_line.ref,
account_move_line.company_id,
account_move_line.account_id,
account_move_line.payment_id,
account_move_line.partner_id,
account_move_line.currency_id,
account_move_line.amount_currency,
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
account_move_line.date AS date,
%(debit_select)s AS debit,
%(credit_select)s AS credit,
%(balance_select)s AS balance,
move.name AS move_name,
company.currency_id AS company_currency_id,
partner.name AS partner_name,
move.move_type AS move_type,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
%(account_type)s AS account_type,
journal.code AS journal_code,
%(journal_name)s AS journal_name,
full_rec.id AS full_rec_name,
%(column_group_key)s AS column_group_key
FROM %(table_references)s
JOIN account_move move ON move.id = account_move_line.move_id
%(currency_table_join)s
LEFT JOIN res_company company ON company.id = account_move_line.company_id
LEFT JOIN res_partner partner ON partner.id = account_move_line.partner_id
LEFT JOIN account_journal journal ON journal.id = account_move_line.journal_id
LEFT JOIN account_full_reconcile full_rec ON full_rec.id = account_move_line.full_reconcile_id
WHERE %(search_condition)s
ORDER BY account_move_line.date, account_move_line.move_name, account_move_line.id
''',
account_code=account_code,
account_name=account_name,
account_type=account_type,
journal_name=journal_name,
column_group_key=column_group_key,
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(group_options),
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
search_condition=query.where_clause,
)
queries.append(query)
full_query = SQL(" UNION ALL ").join(SQL("(%s)", query) for query in queries)
if offset:
full_query = SQL('%s OFFSET %s ', full_query, offset)
if limit:
full_query = SQL('%s LIMIT %s ', full_query, limit)
return full_query
def _get_initial_balance_values(self, report, account_ids, options):
"""
Get sums for the initial balance.
"""
queries = []
for column_group_key, options_group in report._split_options_per_column_group(options).items():
new_options = self._get_options_initial_balance(options_group)
domain = [
('account_id', 'in', account_ids),
]
if not new_options.get('general_ledger_strict_range'):
domain += [
'|',
('date', '>=', new_options['date']['date_from']),
('account_id.include_initial_balance', '=', True),
]
if new_options.get('include_current_year_in_unaff_earnings'):
domain += [('account_id.include_initial_balance', '=', True)]
query = report._get_report_query(new_options, 'from_beginning', domain=domain)
queries.append(SQL(
"""
SELECT
account_move_line.account_id AS groupby,
'initial_balance' AS key,
NULL AS max_date,
%(column_group_key)s AS column_group_key,
COALESCE(SUM(account_move_line.amount_currency), 0.0) AS amount_currency,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.account_id
""",
column_group_key=column_group_key,
table_references=query.from_clause,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
currency_table_join=report._currency_table_aml_join(options_group),
search_condition=query.where_clause,
))
self._cr.execute(SQL(" UNION ALL ").join(queries))
init_balance_by_col_group = {
account_id: {column_group_key: {} for column_group_key in options['column_groups']}
for account_id in account_ids
}
for result in self._cr.dictfetchall():
init_balance_by_col_group[result['groupby']][result['column_group_key']] = result
accounts = self.env['account.account'].browse(account_ids)
return {
account.id: (account, init_balance_by_col_group[account.id])
for account in accounts
}
def _get_options_initial_balance(self, options):
""" Create options used to compute the initial balances.
The initial balances depict the current balance of the accounts at the beginning of
the selected period in the report.
The resulting dates domain will be:
[
('date' <= options['date_from'] - 1),
'|',
('date' >= fiscalyear['date_from']),
('account_id.include_initial_balance', '=', True)
]
:param options: The report options.
:return: A copy of the options.
"""
#pylint: disable=sql-injection
new_options = options.copy()
date_to = new_options['comparison']['periods'][-1]['date_from'] if new_options.get('comparison', {}).get('periods') else new_options['date']['date_from']
new_date_to = fields.Date.from_string(date_to) - timedelta(days=1)
# Date from computation
# We have two case:
# 1) We are choosing a date that starts at the beginning of a fiscal year and we want the initial period to be
# the previous fiscal year
# 2) We are choosing a date that starts in the middle of a fiscal year and in that case we want the initial period
# to be the beginning of the fiscal year
date_from = fields.Date.from_string(new_options['date']['date_from'])
current_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from)
if date_from == current_fiscalyear_dates['date_from']:
# We want the previous fiscal year
previous_fiscalyear_dates = self.env.company.compute_fiscalyear_dates(date_from - timedelta(days=1))
new_date_from = previous_fiscalyear_dates['date_from']
include_current_year_in_unaff_earnings = True
else:
# We want the current fiscal year
new_date_from = current_fiscalyear_dates['date_from']
include_current_year_in_unaff_earnings = False
new_options['date'] = self.env['account.report']._get_dates_period(
new_date_from,
new_date_to,
'range',
)
new_options['include_current_year_in_unaff_earnings'] = include_current_year_in_unaff_earnings
return new_options
####################################################
# COLUMN/LINE HELPERS
####################################################
def _get_account_title_line(self, report, options, account, has_lines, eval_dict):
line_columns = []
for column in options['columns']:
col_value = eval_dict.get(column['column_group_key'], {}).get(column['expression_label'])
col_expr_label = column['expression_label']
value = None if col_value is None or (col_expr_label == 'amount_currency' and not account.currency_id) else col_value
line_columns.append(report._build_column_dict(
value,
column,
options=options,
currency=account.currency_id if col_expr_label == 'amount_currency' else None,
))
line_id = report._get_generic_line_id('account.account', account.id)
is_in_unfolded_lines = any(
report._get_res_id_from_line_id(line_id, 'account.account') == account.id
for line_id in options.get('unfolded_lines')
)
return {
'id': line_id,
'name': account.display_name,
'columns': line_columns,
'level': 1,
'unfoldable': has_lines,
'unfolded': has_lines and (is_in_unfolded_lines or options.get('unfold_all')),
'expand_function': '_report_expand_unfoldable_line_general_ledger',
}
def _get_aml_line(self, report, parent_line_id, options, eval_dict, init_bal_by_col_group):
line_columns = []
for column in options['columns']:
col_expr_label = column['expression_label']
col_value = eval_dict[column['column_group_key']].get(col_expr_label)
col_currency = None
if col_value is not None:
if col_expr_label == 'amount_currency':
col_currency = self.env['res.currency'].browse(eval_dict[column['column_group_key']]['currency_id'])
col_value = None if col_currency == self.env.company.currency_id else col_value
elif col_expr_label == 'balance':
col_value += (init_bal_by_col_group[column['column_group_key']] or 0)
line_columns.append(report._build_column_dict(
col_value,
column,
options=options,
currency=col_currency,
))
aml_id = None
move_name = None
caret_type = None
for column_group_dict in eval_dict.values():
aml_id = column_group_dict.get('id', '')
if aml_id:
if column_group_dict.get('payment_id'):
caret_type = 'account.payment'
else:
caret_type = 'account.move.line'
move_name = column_group_dict['move_name']
date = str(column_group_dict.get('date', ''))
break
return {
'id': report._get_generic_line_id('account.move.line', aml_id, parent_line_id=parent_line_id, markup=date),
'caret_options': caret_type,
'parent_id': parent_line_id,
'name': move_name,
'columns': line_columns,
'level': 3,
}
@api.model
def _get_total_line(self, report, options, eval_dict):
line_columns = []
for column in options['columns']:
col_value = eval_dict[column['column_group_key']].get(column['expression_label'])
col_value = None if col_value is None else col_value
line_columns.append(report._build_column_dict(col_value, column, options=options))
return {
'id': report._get_generic_line_id(None, None, markup='total'),
'name': _('Total'),
'level': 1,
'columns': line_columns,
}
def caret_option_audit_tax(self, options, params):
return self.env['account.generic.tax.report.handler'].caret_option_audit_tax(options, params)
def _report_expand_unfoldable_line_general_ledger(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
def init_load_more_progress(line_dict):
return {
column['column_group_key']: line_col.get('no_format', 0)
for column, line_col in zip(options['columns'], line_dict['columns'])
if column['expression_label'] == 'balance'
}
report = self.env.ref('at_accounting.general_ledger_report')
model, model_id = report._get_model_info_from_id(line_dict_id)
if model != 'account.account':
raise UserError(_("Wrong ID for general ledger line to expand: %s", line_dict_id))
lines = []
# Get initial balance
if offset == 0:
if unfold_all_batch_data:
account, init_balance_by_col_group = unfold_all_batch_data['initial_balances'][model_id]
else:
account, init_balance_by_col_group = self._get_initial_balance_values(report, [model_id], options)[model_id]
initial_balance_line = report._get_partner_and_general_ledger_initial_balance_line(options, line_dict_id, init_balance_by_col_group, account.currency_id)
if initial_balance_line:
lines.append(initial_balance_line)
# For the first expansion of the line, the initial balance line gives the progress
progress = init_load_more_progress(initial_balance_line)
# Get move lines
limit_to_load = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None
if unfold_all_batch_data:
aml_results = unfold_all_batch_data['aml_results'][model_id]
has_more = unfold_all_batch_data['has_more'].get(model_id, False)
else:
aml_results, has_more = self._get_aml_values(report, options, [model_id], offset=offset, limit=limit_to_load)
aml_results = aml_results[model_id]
next_progress = progress
for aml_result in aml_results.values():
new_line = self._get_aml_line(report, line_dict_id, options, aml_result, next_progress)
lines.append(new_line)
next_progress = init_load_more_progress(new_line)
return {
'lines': lines,
'offset_increment': report.load_more_limit,
'has_more': has_more,
'progress': next_progress,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
from odoo import models, tools, _
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.exceptions import UserError, RedirectWarning
class AccountJournal(models.Model):
_inherit = "account.journal"
def _get_bank_statements_available_import_formats(self):
""" Returns a list of strings representing the supported import formats.
"""
return []
def __get_bank_statements_available_sources(self):
rslt = super(AccountJournal, self).__get_bank_statements_available_sources()
formats_list = self._get_bank_statements_available_import_formats()
if formats_list:
formats_list.sort()
import_formats_str = ', '.join(formats_list)
rslt.append(("file_import", _("Manual (or import %(import_formats)s)", import_formats=import_formats_str)))
return rslt
def create_document_from_attachment(self, attachment_ids=None):
journal = self or self.browse(self.env.context.get('default_journal_id'))
if journal.type in ('bank', 'credit', 'cash'):
attachments = self.env['ir.attachment'].browse(attachment_ids)
if not attachments:
raise UserError(_("No attachment was provided"))
return journal._import_bank_statement(attachments)
return super().create_document_from_attachment(attachment_ids)
def _import_bank_statement(self, attachments):
""" Process the file chosen in the wizard, create bank statement(s) and go to reconciliation. """
if any(not a.raw for a in attachments):
raise UserError(_("You uploaded an invalid or empty file."))
statement_ids_all = []
notifications_all = {}
errors = {}
# Let the appropriate implementation module parse the file and return the required data
# The active_id is passed in context in case an implementation module requires information about the wizard state (see QIF)
for attachment in attachments:
try:
currency_code, account_number, stmts_vals = self._parse_bank_statement_file(attachment)
# Check raw data
self._check_parsed_data(stmts_vals, account_number)
# Try to find the currency and journal in odoo
journal = self._find_additional_data(currency_code, account_number)
# If no journal found, ask the user about creating one
if not journal.default_account_id:
raise UserError(_('You have to set a Default Account for the journal: %s', journal.name))
# Prepare statement data to be used for bank statements creation
stmts_vals = self._complete_bank_statement_vals(stmts_vals, journal, account_number, attachment)
# Create the bank statements
statement_ids, dummy, notifications = self._create_bank_statements(stmts_vals)
statement_ids_all.extend(statement_ids)
# Now that the import worked out, set it as the bank_statements_source of the journal
if journal.bank_statements_source != 'file_import':
# Use sudo() because only 'account.group_account_manager'
# has write access on 'account.journal', but 'account.group_account_user'
# must be able to import bank statement files
journal.sudo().bank_statements_source = 'file_import'
msg = ""
for notif in notifications:
msg += (
f"{notif['message']}"
)
if notifications:
notifications_all[attachment.name] = msg
except (UserError, RedirectWarning) as e:
errors[attachment.name] = e.args[0]
statements = self.env['account.bank.statement'].browse(statement_ids_all)
line_to_reconcile = statements.line_ids
if line_to_reconcile:
# 'limit_time_real_cron' defaults to -1.
# Manual fallback applied for non-POSIX systems where this key is disabled (set to None).
cron_limit_time = tools.config['limit_time_real_cron'] or -1
limit_time = cron_limit_time if 0 < cron_limit_time < 180 else 180
line_to_reconcile._cron_try_auto_reconcile_statement_lines(limit_time=limit_time)
result = self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
extra_domain=[('statement_id', 'in', statements.ids)],
default_context={
'search_default_not_matched': True,
'default_journal_id': statements[:1].journal_id.id,
'notifications': notifications_all,
},
)
if errors:
error_msg = _("The following files could not be imported:\n")
error_msg += "\n".join([f"- {attachment_name}: {msg}" for attachment_name, msg in errors.items()])
if statements:
self.env.cr.commit() # save the correctly uploaded statements to the db before raising the errors
raise RedirectWarning(error_msg, result, _('View successfully imported statements'))
else:
raise UserError(error_msg)
return result
def _parse_bank_statement_file(self, attachment) -> tuple:
""" Each module adding a file support must extends this method. It processes the file if it can, returns super otherwise, resulting in a chain of responsability.
This method parses the given file and returns the data required by the bank statement import process, as specified below.
rtype: triplet (if a value can't be retrieved, use None)
- currency code: string (e.g: 'EUR')
The ISO 4217 currency code, case insensitive
- account number: string (e.g: 'BE1234567890')
The number of the bank account which the statement belongs to
- bank statements data: list of dict containing (optional items marked by o) :
- 'name': string (e.g: '000000123')
- 'date': date (e.g: 2013-06-26)
-o 'balance_start': float (e.g: 8368.56)
-o 'balance_end_real': float (e.g: 8888.88)
- 'transactions': list of dict containing :
- 'name': string (e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'date': date
- 'amount': float
- 'unique_import_id': string
-o 'account_number': string
Will be used to find/create the res.partner.bank in odoo
-o 'note': string
-o 'partner_name': string
-o 'ref': string
"""
raise RedirectWarning(
message=_("Could not make sense of the given file.\nDid you install the module to support this type of file?"),
action=self.env.ref('base.open_module_tree').id,
button_text=_("Go to Apps"),
additional_context={
'search_default_name': 'account_bank_statement_import',
'search_default_extra': True,
},
)
def _check_parsed_data(self, stmts_vals, account_number):
""" Basic and structural verifications """
if len(stmts_vals) == 0:
raise UserError(_(
'This file doesn\'t contain any statement for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.',
account_number,
))
no_st_line = True
for vals in stmts_vals:
if vals['transactions'] and len(vals['transactions']) > 0:
no_st_line = False
break
if no_st_line:
raise UserError(_(
'This file doesn\'t contain any transaction for account %s.\nIf it contains transactions for more than one account, it must be imported on each of them.',
account_number,
))
def _statement_import_check_bank_account(self, account_number):
# Needed for CH to accommodate for non-unique account numbers
sanitized_acc_number = self.bank_account_id.sanitized_acc_number.split(" ")[0]
# Needed for BNP France
if len(sanitized_acc_number) == 27 and len(account_number) == 11 and sanitized_acc_number[:2].upper() == "FR":
return sanitized_acc_number[14:-2] == account_number
# Needed for Credit Lyonnais (LCL)
if len(sanitized_acc_number) == 27 and len(account_number) == 7 and sanitized_acc_number[:2].upper() == "FR":
return sanitized_acc_number[18:-2] == account_number
return sanitized_acc_number == account_number
def _find_additional_data(self, currency_code, account_number):
""" Look for the account.journal using values extracted from the
statement and make sure it's consistent.
"""
company_currency = self.env.company.currency_id
currency = None
sanitized_account_number = sanitize_account_number(account_number)
if currency_code:
currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1)
if not currency:
raise UserError(_("No currency found matching '%s'.", currency_code))
if currency == company_currency:
currency = False
journal = self
if account_number:
# No bank account on the journal : create one from the account number of the statement
if journal and not journal.bank_account_id:
journal.set_bank_account(account_number)
# No journal passed to the wizard : try to find one using the account number of the statement
elif not journal:
journal = self.search([('bank_account_id.sanitized_acc_number', '=', sanitized_account_number)])
if not journal:
# Sometimes the bank returns only part of the full account number (e.g. local account number instead of full IBAN)
partial_match = self.search([('bank_account_id.sanitized_acc_number', 'ilike', sanitized_account_number)])
if len(partial_match) == 1:
journal = partial_match
# Already a bank account on the journal : check it's the same as on the statement
else:
if not self._statement_import_check_bank_account(sanitized_account_number):
raise UserError(_('The account of this statement (%(account)s) is not the same as the journal (%(journal)s).', account=account_number, journal=journal.bank_account_id.acc_number))
# If importing into an existing journal, its currency must be the same as the bank statement
if journal:
journal_currency = journal.currency_id or journal.company_id.currency_id
if currency is None:
currency = journal_currency
if currency and currency != journal_currency:
statement_cur_code = not currency and company_currency.name or currency.name
journal_cur_code = not journal_currency and company_currency.name or journal_currency.name
raise UserError(_('The currency of the bank statement (%(code)s) is not the same as the currency of the journal (%(journal)s).', code=statement_cur_code, journal=journal_cur_code))
if not journal:
raise UserError(_('Cannot find in which journal import this statement. Please manually select a journal.'))
return journal
def _complete_bank_statement_vals(self, stmts_vals, journal, account_number, attachment):
for st_vals in stmts_vals:
if not st_vals.get('reference'):
st_vals['reference'] = attachment.name
for line_vals in st_vals['transactions']:
line_vals['journal_id'] = journal.id
unique_import_id = line_vals.get('unique_import_id')
if unique_import_id:
sanitized_account_number = sanitize_account_number(account_number)
line_vals['unique_import_id'] = (sanitized_account_number and sanitized_account_number + '-' or '') + str(journal.id) + '-' + unique_import_id
if not line_vals.get('partner_bank_id'):
# Find the partner and his bank account or create the bank account. The partner selected during the
# reconciliation process will be linked to the bank when the statement is closed.
identifying_string = line_vals.get('account_number')
if identifying_string:
if line_vals.get('partner_id'):
partner_bank = self.env['res.partner.bank'].search([
('acc_number', '=', identifying_string),
('partner_id', '=', line_vals['partner_id'])
])
else:
partner_bank = self.env['res.partner.bank'].search([
('acc_number', '=', identifying_string),
('company_id', 'in', (False, journal.company_id.id))
])
# If multiple partners share the same account number, do not try to guess and just avoid setting it
if partner_bank and len(partner_bank) == 1:
line_vals['partner_bank_id'] = partner_bank.id
line_vals['partner_id'] = partner_bank.partner_id.id
return stmts_vals
def _create_bank_statements(self, stmts_vals, raise_no_imported_file=True):
""" Create new bank statements from imported values, filtering out already imported transactions, and returns data used by the reconciliation widget """
BankStatement = self.env['account.bank.statement']
BankStatementLine = self.env['account.bank.statement.line']
# Filter out already imported transactions and create statements
statement_ids = []
statement_line_ids = []
ignored_statement_lines_import_ids = []
for st_vals in stmts_vals:
filtered_st_lines = []
for line_vals in st_vals['transactions']:
if (line_vals['amount'] != 0
and ('unique_import_id' not in line_vals
or not line_vals['unique_import_id']
or not bool(BankStatementLine.sudo().search([('unique_import_id', '=', line_vals['unique_import_id'])], limit=1)))):
filtered_st_lines.append(line_vals)
else:
ignored_statement_lines_import_ids.append(line_vals)
if st_vals.get('balance_start') is not None:
st_vals['balance_start'] += float(line_vals['amount'])
if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records
st_vals.pop('transactions', None)
# Create the statement
st_vals['line_ids'] = [[0, False, line] for line in filtered_st_lines]
statement = BankStatement.with_context(default_journal_id=self.id).create(st_vals)
if not statement.name:
statement.name = st_vals['reference']
statement_ids.append(statement.id)
statement_line_ids.extend(statement.line_ids.ids)
# Create the report.
if statement.is_complete and not self._context.get('skip_pdf_attachment_generation'):
statement.action_generate_attachment()
if len(statement_line_ids) == 0 and raise_no_imported_file:
raise UserError(_('You already have imported that file.'))
# Prepare import feedback
notifications = []
num_ignored = len(ignored_statement_lines_import_ids)
if num_ignored > 0:
notifications += [{
'type': 'warning',
'message': _("%d transactions had already been imported and were ignored.", num_ignored)
if num_ignored > 1
else _("1 transaction had already been imported and was ignored."),
}]
return statement_ids, statement_line_ids, notifications

View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, models
from odoo.exceptions import UserError
class AccountJournal(models.Model):
_inherit = 'account.journal'
def _get_bank_statements_available_import_formats(self):
rslt = super()._get_bank_statements_available_import_formats()
rslt.extend(['CSV', 'XLS', 'XLSX'])
return rslt
def _check_file_format(self, filename):
return filename and filename.lower().strip().endswith(('.csv', '.xls', '.xlsx'))
def _import_bank_statement(self, attachments):
# In case of CSV files, only one file can be imported at a time.
if len(attachments) > 1:
csv = [bool(self._check_file_format(att.name)) for att in attachments]
if True in csv and False in csv:
raise UserError(_('Mixing CSV files with other file types is not allowed.'))
if csv.count(True) > 1:
raise UserError(_('Only one CSV file can be selected.'))
return super()._import_bank_statement(attachments)
if not self._check_file_format(attachments.name):
return super()._import_bank_statement(attachments)
ctx = dict(self.env.context)
import_wizard = self.env['base_import.import'].create({
'res_model': 'account.bank.statement.line',
'file': attachments.raw,
'file_name': attachments.name,
'file_type': attachments.mimetype,
})
ctx['wizard_id'] = import_wizard.id
ctx['default_journal_id'] = self.id
return {
'type': 'ir.actions.client',
'tag': 'import_bank_stmt',
'params': {
'model': 'account.bank.statement.line',
'context': ctx,
'filename': 'bank_statement_import.csv',
}
}

View File

@@ -0,0 +1,78 @@
from odoo import models
import ast
class account_journal(models.Model):
_inherit = "account.journal"
def action_open_reconcile(self):
self.ensure_one()
if self.type in ('bank', 'cash', 'credit'):
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
default_context={
'default_journal_id': self.id,
'search_default_journal_id': self.id,
'search_default_not_matched': True,
},
)
else:
# Open reconciliation view for customers/suppliers
return self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled')
def action_open_to_check(self):
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
default_context={
'search_default_to_check': True,
'search_default_journal_id': self.id,
'default_journal_id': self.id,
},
)
def action_open_bank_transactions(self):
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
default_context={
'search_default_journal_id': self.id,
'default_journal_id': self.id
},
kanban_first=False,
)
def action_open_reconcile_statement(self):
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
default_context={
'search_default_statement_id': self.env.context.get('statement_id'),
},
)
def open_action(self):
# EXTENDS account
# set default action for liquidity journals in dashboard
if self.type in ('bank', 'cash', 'credit') and not self._context.get('action_name'):
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
extra_domain=[('line_ids.account_id', '=', self.default_account_id.id)],
default_context={
'default_journal_id': self.id,
'search_default_journal_id': self.id,
},
)
return super().open_action()
def _fill_general_dashboard_data(self, dashboard_data):
super()._fill_general_dashboard_data(dashboard_data)
for journal in self.filtered(lambda journal: journal.type == 'general'):
dashboard_data[journal.id]['is_account_tax_periodicity_journal'] = journal == journal.company_id._get_tax_closing_journal()
def action_open_bank_balance_in_gl(self):
''' Show the bank balance inside the General Ledger report.
:return: An action opening the General Ledger.
'''
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("at_accounting.action_account_report_general_ledger")
action['context'] = dict(ast.literal_eval(action['context']), default_filter_accounts=self.default_account_id.code)
return action

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
from odoo.exceptions import UserError
from odoo.tools import SQL
class AccountMoveLine(models.Model):
_name = "account.move.line"
_inherit = "account.move.line"
exclude_bank_lines = fields.Boolean(compute='_compute_exclude_bank_lines', store=True)
@api.depends('journal_id')
def _compute_exclude_bank_lines(self):
for move_line in self:
move_line.exclude_bank_lines = move_line.account_id != move_line.journal_id.default_account_id
@api.constrains('tax_ids', 'tax_tag_ids')
def _check_taxes_on_closing_entries(self):
for aml in self:
if aml.move_id.tax_closing_report_id and (aml.tax_ids or aml.tax_tag_ids):
raise UserError(_("You cannot add taxes on a tax closing move line."))
@api.depends('product_id', 'product_uom_id', 'move_id.tax_closing_report_id')
def _compute_tax_ids(self):
""" Some special cases may see accounts used in tax closing having default taxes.
They would trigger the constrains above, which we don't want. Instead, we don't trigger
the tax computation in this case.
"""
# EXTEND account
lines_to_compute = self.filtered(lambda line: not line.move_id.tax_closing_report_id)
(self - lines_to_compute).tax_ids = False
super(AccountMoveLine, lines_to_compute)._compute_tax_ids()
@api.model
def _prepare_aml_shadowing_for_report(self, change_equivalence_dict):
""" Prepares the fields lists for creating a temporary table shadowing the account_move_line one.
This is used to switch the computation mode of the reports, with analytics or financial budgets, for example.
:param change_equivalence_dict: A dict, in the form {aml_field: sql_equivalence}, where:
- aml_field: is a string containing the name of field of account.move.line
- sql_equivalence: is the value to use to shadow aml_field. It can be an SQL object; if
it's not, it'll be escaped in the query.
:return: A tuple of 2 SQL objects, so that:
- The first one is the fields list to pass into the INSERT TO part of the query filling up the temporary table
- The second one contains the field values to insert into the SELECT clause of the same query, in the same order
as in the first element of the returned tuple.
"""
line_fields = self.env['account.move.line'].fields_get()
self.env.cr.execute("SELECT column_name FROM information_schema.columns WHERE table_name='account_move_line'")
stored_fields = {f[0] for f in self.env.cr.fetchall() if f[0] in line_fields}
fields_to_insert = []
for fname in stored_fields:
if fname in change_equivalence_dict:
fields_to_insert.append(SQL(
"%(original)s AS %(asname)s",
original=change_equivalence_dict[fname],
asname=SQL('"account_move_line.%s"', SQL(fname)),
))
else:
line_field = line_fields[fname]
if line_field.get("translate"):
typecast = SQL('jsonb')
else:
typecast = SQL(self.env['account.move.line']._fields[fname].column_type[0])
fields_to_insert.append(SQL(
"CAST(NULL AS %(typecast)s) AS %(fname)s",
typecast=typecast,
fname=SQL('"account_move_line.%s"', SQL(fname)),
))
return SQL(', ').join(SQL.identifier(fname) for fname in stored_fields), SQL(', ').join(fields_to_insert)

View File

@@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.tools import float_is_zero, SQL
from odoo.exceptions import UserError
from itertools import chain
class MulticurrencyRevaluationReportCustomHandler(models.AbstractModel):
"""Manage Unrealized Gains/Losses.
In multi-currencies environments, we need a way to control the risk related
to currencies (in case some are higthly fluctuating) and, in some countries,
some laws also require to create journal entries to record the provisionning
of a probable future expense related to currencies. Hence, people need to
create a journal entry at the beginning of a period, to make visible the
probable expense in reports (and revert it at the end of the period, to
recon the real gain/loss.
"""
_name = 'account.multicurrency.revaluation.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Multicurrency Revaluation Report Custom Handler'
def _get_custom_display_config(self):
return {
'components': {
'AccountReportFilters': 'at_accounting.MulticurrencyRevaluationReportFilters',
},
'templates': {
'AccountReportLineName': 'at_accounting.MulticurrencyRevaluationReportLineName',
},
}
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
active_currencies = self.env['res.currency'].search([('active', '=', True)])
if len(active_currencies) < 2:
raise UserError(_("You need to activate more than one currency to access this report."))
rates = active_currencies._get_rates(self.env.company, options.get('date').get('date_to'))
# Normalize the rates to the company's currency
company_rate = rates[self.env.company.currency_id.id]
for key in rates.keys():
rates[key] /= company_rate
options['currency_rates'] = {
str(currency_id.id): {
'currency_id': currency_id.id,
'currency_name': currency_id.name,
'currency_main': self.env.company.currency_id.name,
'rate': (rates[currency_id.id]
if not previous_options.get('currency_rates', {}).get(str(currency_id.id), {}).get('rate') else
float(previous_options['currency_rates'][str(currency_id.id)]['rate'])),
} for currency_id in active_currencies
}
for currency_rates in options['currency_rates'].values():
if currency_rates['rate'] == 0:
raise UserError(_("The currency rate cannot be equal to zero"))
options['company_currency'] = options['currency_rates'].pop(str(self.env.company.currency_id.id))
options['custom_rate'] = any(
not float_is_zero(cr['rate'] - rates[cr['currency_id']], 20)
for cr in options['currency_rates'].values()
)
options['multi_currency'] = True
options['buttons'].append({'name': _('Adjustment Entry'), 'sequence': 30, 'action': 'action_multi_currency_revaluation_open_revaluation_wizard', 'always_show': True})
def _customize_warnings(self, report, options, all_column_groups_expression_totals, warnings):
if len(self.env.companies) > 1:
warnings['at_accounting.multi_currency_revaluation_report_warning_multicompany'] = {'alert_type': 'warning'}
if options['custom_rate']:
warnings['at_accounting.multi_currency_revaluation_report_warning_custom_rate'] = {'alert_type': 'warning'}
def _custom_line_postprocessor(self, report, options, lines):
line_to_adjust_id = self.env.ref('at_accounting.multicurrency_revaluation_to_adjust').id
line_excluded_id = self.env.ref('at_accounting.multicurrency_revaluation_excluded').id
rslt = []
for index, line in enumerate(lines):
res_model_name, res_id = report._get_model_info_from_id(line['id'])
if res_model_name == 'account.report.line' and (
(res_id == line_to_adjust_id and report._get_model_info_from_id(lines[index + 1]['id']) == ('account.report.line', line_excluded_id)) or
(res_id == line_excluded_id and index == len(lines) - 1)
):
# 'To Adjust' and 'Excluded' lines need to be hidden if they have no child
continue
elif res_model_name == 'res.currency':
# Include the rate in the currency_id group lines
line['name'] = '{for_cur} (1 {comp_cur} = {rate:.6} {for_cur})'.format(
for_cur=line['name'],
comp_cur=self.env.company.currency_id.display_name,
rate=float(options['currency_rates'][str(res_id)]['rate']),
)
elif res_model_name == 'account.account':
# Mark the included/excluded lines, so that the custom component templates knows what label to put on them
line['is_included_line'] = report._get_res_id_from_line_id(line['id'], 'account.account') == line_to_adjust_id
# Inject the related model into the line dict in order to use it on the custom component template on js side to display buttons
line['cur_revaluation_line_model'] = res_model_name
rslt.append(line)
return rslt
def _custom_groupby_line_completer(self, report, options, line_dict):
model_info_from_id = report._get_model_info_from_id(line_dict['id'])
if model_info_from_id[0] == 'res.currency':
line_dict['unfolded'] = True
line_dict['unfoldable'] = False
def action_multi_currency_revaluation_open_revaluation_wizard(self, options):
"""Open the revaluation wizard."""
form = self.env.ref('at_accounting.view_account_multicurrency_revaluation_wizard', False)
return {
'name': _("Make Adjustment Entry"),
'type': 'ir.actions.act_window',
'res_model': 'account.multicurrency.revaluation.wizard',
'view_mode': 'form',
'view_id': form.id,
'views': [(form.id, 'form')],
'multi': 'True',
'target': 'new',
'context': {
**self._context,
'multicurrency_revaluation_report_options': options,
},
}
# ACTIONS
def action_multi_currency_revaluation_open_general_ledger(self, options, params):
report = self.env['account.report'].browse(options['report_id'])
account_id = report._get_res_id_from_line_id(params['line_id'], 'account.account')
account_line_id = report._get_generic_line_id('account.account', account_id)
general_ledger_options = self.env.ref('at_accounting.general_ledger_report').get_options(options)
general_ledger_options['unfolded_lines'] = [account_line_id]
general_ledger_action = self.env['ir.actions.actions']._for_xml_id('at_accounting.action_account_report_general_ledger')
general_ledger_action['params'] = {
'options': general_ledger_options,
'ignore_session': True,
}
return general_ledger_action
def action_multi_currency_revaluation_toggle_provision(self, options, params):
""" Include/exclude an account from the provision. """
res_ids_map = self.env['account.report']._get_res_ids_from_line_id(params['line_id'], ['res.currency', 'account.account'])
account = self.env['account.account'].browse(res_ids_map['account.account'])
currency = self.env['res.currency'].browse(res_ids_map['res.currency'])
if currency in account.exclude_provision_currency_ids:
account.exclude_provision_currency_ids -= currency
else:
account.exclude_provision_currency_ids += currency
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
def action_multi_currency_revaluation_open_currency_rates(self, options, params=None):
""" Open the currency rate list. """
currency_id = self.env['account.report']._get_res_id_from_line_id(params['line_id'], 'res.currency')
return {
'type': 'ir.actions.act_window',
'name': _('Currency Rates (%s)', self.env['res.currency'].browse(currency_id).display_name),
'views': [(False, 'list')],
'res_model': 'res.currency.rate',
'context': {**self.env.context, **{'default_currency_id': currency_id, 'active_id': currency_id}},
'domain': [('currency_id', '=', currency_id)],
}
def _report_custom_engine_multi_currency_revaluation_to_adjust(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._multi_currency_revaluation_get_custom_lines(options, 'to_adjust', current_groupby, next_groupby, offset=offset, limit=limit)
def _report_custom_engine_multi_currency_revaluation_excluded(self, expressions, options, date_scope, current_groupby, next_groupby, offset=0, limit=None, warnings=None):
return self._multi_currency_revaluation_get_custom_lines(options, 'excluded', current_groupby, next_groupby, offset=offset, limit=limit)
def _multi_currency_revaluation_get_custom_lines(self, options, line_code, current_groupby, next_groupby, offset=0, limit=None):
def build_result_dict(report, query_res):
return {
'balance_currency': query_res['balance_currency'] if len(query_res['currency_id']) == 1 else None,
'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None,
'balance_operation': query_res['balance_operation'],
'balance_current': query_res['balance_current'],
'adjustment': query_res['adjustment'],
'has_sublines': query_res['aml_count'] > 0,
}
report = self.env['account.report'].browse(options['report_id'])
report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else []))
# No need to run any SQL if we're computing the main line: it does not display any total
if not current_groupby:
return {
'balance_currency': None,
'currency_id': None,
'balance_operation': None,
'balance_current': None,
'adjustment': None,
'has_sublines': False,
}
query = "(VALUES {})".format(', '.join("(%s, %s)" for rate in options['currency_rates']))
params = list(chain.from_iterable((cur['currency_id'], cur['rate']) for cur in options['currency_rates'].values()))
custom_currency_table_query = SQL(query, *params)
date_to = options['date']['date_to']
select_part_not_an_exchange_move_id = SQL(
"""
NOT EXISTS (
SELECT 1
FROM account_partial_reconcile part_exch
WHERE part_exch.exchange_move_id = account_move_line.move_id
AND part_exch.max_date <= %s
)
""",
date_to
)
query = report._get_report_query(options, 'strict_range')
tail_query = report._get_engine_query_tail(offset, limit)
full_query = SQL(
"""
WITH custom_currency_table(currency_id, rate) AS (%(custom_currency_table_query)s)
-- Final select that gets the following lines:
-- (where there is a change in the rates of currency between the creation of the move and the full payments)
-- - Moves that don't have a payment yet at a certain date
-- - Moves that have a partial but are not fully paid at a certain date
SELECT
subquery.grouping_key,
ARRAY_AGG(DISTINCT(subquery.currency_id)) AS currency_id,
SUM(subquery.balance_currency) AS balance_currency,
SUM(subquery.balance_operation) AS balance_operation,
SUM(subquery.balance_current) AS balance_current,
SUM(subquery.adjustment) AS adjustment,
COUNT(subquery.aml_id) AS aml_count
FROM (
-- Get moves that have at least one partial at a certain date and are not fully paid at that date
SELECT
""" + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f"""
ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) AS balance_operation,
ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) AS balance_currency,
ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate AS balance_current,
(
-- adjustment is computed as: balance_current - balance_operation
ROUND( account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) / custom_currency_table.rate
- ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places)
) AS adjustment,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM %(table_references)s,
account_account AS account,
res_currency AS aml_currency,
res_currency AS aml_comp_currency,
custom_currency_table,
-- Get for each move line the amount residual and amount_residual currency
-- both for matched "debit" and matched "credit" the same way as account.move.line
-- '_compute_amount_residual()' method does
-- (using LATERAL greatly reduce the number of lines for which we have to compute it)
LATERAL (
-- Get sum of matched "debit" amount and amount in currency for related move line at date
SELECT COALESCE(SUM(part.amount), 0.0) AS amount_debit,
ROUND(
SUM(part.debit_amount_currency),
curr.decimal_places
) AS amount_debit_currency,
0.0 AS amount_credit,
0.0 AS amount_credit_currency,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM account_partial_reconcile part
JOIN res_currency curr ON curr.id = part.debit_currency_id
WHERE account_move_line.id = part.debit_move_id
AND part.max_date <= %(date_to)s
GROUP BY aml_id,
curr.decimal_places
UNION
-- Get sum of matched "credit" amount and amount in currency for related move line at date
SELECT 0.0 AS amount_debit,
0.0 AS amount_debit_currency,
COALESCE(SUM(part.amount), 0.0) AS amount_credit,
ROUND(
SUM(part.credit_amount_currency),
curr.decimal_places
) AS amount_credit_currency,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM account_partial_reconcile part
JOIN res_currency curr ON curr.id = part.credit_currency_id
WHERE account_move_line.id = part.credit_move_id
AND part.max_date <= %(date_to)s
GROUP BY aml_id,
curr.decimal_places
) AS ara
WHERE %(search_condition)s
AND account_move_line.account_id = account.id
AND account_move_line.currency_id = aml_currency.id
AND account_move_line.company_currency_id = aml_comp_currency.id
AND account_move_line.currency_id = custom_currency_table.currency_id
AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
AND (
account.currency_id != account_move_line.company_currency_id
OR (
account.account_type IN ('asset_receivable', 'liability_payable')
AND (account_move_line.currency_id != account_move_line.company_currency_id)
)
)
AND {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} (
SELECT 1
FROM account_account_exclude_res_currency_provision
WHERE account_account_id = account_move_line.account_id
AND res_currency_id = account_move_line.currency_id
)
AND (%(select_part_not_an_exchange_move_id)s)
GROUP BY account_move_line.id, aml_comp_currency.decimal_places, aml_currency.decimal_places, custom_currency_table.rate
HAVING ROUND(account_move_line.balance - SUM(ara.amount_debit) + SUM(ara.amount_credit), aml_comp_currency.decimal_places) != 0
OR ROUND(account_move_line.amount_currency - SUM(ara.amount_debit_currency) + SUM(ara.amount_credit_currency), aml_currency.decimal_places) != 0.0
UNION
-- Moves that don't have a payment yet at a certain date
SELECT
""" + (f"account_move_line.{current_groupby} AS grouping_key," if current_groupby else '') + f"""
account_move_line.balance AS balance_operation,
account_move_line.amount_currency AS balance_currency,
account_move_line.amount_currency / custom_currency_table.rate AS balance_current,
account_move_line.amount_currency / custom_currency_table.rate - account_move_line.balance AS adjustment,
account_move_line.currency_id AS currency_id,
account_move_line.id AS aml_id
FROM %(table_references)s
JOIN account_account account ON account_move_line.account_id = account.id
JOIN custom_currency_table ON custom_currency_table.currency_id = account_move_line.currency_id
WHERE %(search_condition)s
AND account.account_type NOT IN ('income', 'income_other', 'expense', 'expense_depreciation', 'expense_direct_cost', 'off_balance')
AND (
account.currency_id != account_move_line.company_currency_id
OR (
account.account_type IN ('asset_receivable', 'liability_payable')
AND (account_move_line.currency_id != account_move_line.company_currency_id)
)
)
AND {'NOT EXISTS' if line_code == 'to_adjust' else 'EXISTS'} (
SELECT 1
FROM account_account_exclude_res_currency_provision
WHERE account_account_id = account_id
AND res_currency_id = account_move_line.currency_id
)
AND (%(select_part_not_an_exchange_move_id)s)
AND NOT EXISTS (
SELECT 1 FROM account_partial_reconcile part
WHERE (part.debit_move_id = account_move_line.id OR part.credit_move_id = account_move_line.id)
AND part.max_date <= %(date_to)s
)
AND (account_move_line.balance != 0.0 OR account_move_line.amount_currency != 0.0)
) subquery
GROUP BY grouping_key
ORDER BY grouping_key
%(tail_query)s
""",
custom_currency_table_query=custom_currency_table_query,
table_references=query.from_clause,
date_to=date_to,
tail_query=tail_query,
search_condition=query.where_clause,
select_part_not_an_exchange_move_id=select_part_not_an_exchange_move_id,
)
self._cr.execute(full_query)
query_res_lines = self._cr.dictfetchall()
if not current_groupby:
return build_result_dict(report, query_res_lines and query_res_lines[0] or {})
else:
rslt = []
for query_res in query_res_lines:
grouping_key = query_res['grouping_key']
rslt.append((grouping_key, build_result_dict(report, query_res)))
return rslt

View File

@@ -0,0 +1,771 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _, fields
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import SQL
from datetime import timedelta
from collections import defaultdict
class PartnerLedgerCustomHandler(models.AbstractModel):
_name = 'account.partner.ledger.report.handler'
_inherit = 'account.report.custom.handler'
_description = 'Partner Ledger Custom Handler'
def _get_custom_display_config(self):
return {
'css_custom_class': 'partner_ledger',
'components': {
'AccountReportLineCell': 'at_accounting.PartnerLedgerLineCell',
},
'templates': {
'AccountReportFilters': 'at_accounting.PartnerLedgerFilters',
'AccountReportLineName': 'at_accounting.PartnerLedgerLineName',
},
}
def _dynamic_lines_generator(self, report, options, all_column_groups_expression_totals, warnings=None):
partner_lines, totals_by_column_group = self._build_partner_lines(report, options)
lines = report._regroup_lines_by_name_prefix(options, partner_lines, '_report_expand_unfoldable_line_partner_ledger_prefix_group', 0)
# Inject sequence on dynamic lines
lines = [(0, line) for line in lines]
# Report total line.
lines.append((0, self._get_report_line_total(options, totals_by_column_group)))
return lines
def _build_partner_lines(self, report, options, level_shift=0):
lines = []
totals_by_column_group = {
column_group_key: {
total: 0.0
for total in ['debit', 'credit', 'amount', 'balance']
}
for column_group_key in options['column_groups']
}
partners_results = self._query_partners(options)
search_filter = options.get('filter_search_bar', '')
accept_unknown_in_filter = search_filter.lower() in self._get_no_partner_line_label().lower()
for partner, results in partners_results:
if options['export_mode'] == 'print' and search_filter and not partner and not accept_unknown_in_filter:
# When printing and searching for a specific partner, make it so we only show its lines, not the 'Unknown Partner' one, that would be
# shown in case a misc entry with no partner was reconciled with one of the target partner's entries.
continue
partner_values = defaultdict(dict)
for column_group_key in options['column_groups']:
partner_sum = results.get(column_group_key, {})
partner_values[column_group_key]['debit'] = partner_sum.get('debit', 0.0)
partner_values[column_group_key]['credit'] = partner_sum.get('credit', 0.0)
partner_values[column_group_key]['amount'] = partner_sum.get('amount', 0.0)
partner_values[column_group_key]['balance'] = partner_sum.get('balance', 0.0)
totals_by_column_group[column_group_key]['debit'] += partner_values[column_group_key]['debit']
totals_by_column_group[column_group_key]['credit'] += partner_values[column_group_key]['credit']
totals_by_column_group[column_group_key]['amount'] += partner_values[column_group_key]['amount']
totals_by_column_group[column_group_key]['balance'] += partner_values[column_group_key]['balance']
lines.append(self._get_report_line_partners(options, partner, partner_values, level_shift=level_shift))
return lines, totals_by_column_group
def _report_expand_unfoldable_line_partner_ledger_prefix_group(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
report = self.env['account.report'].browse(options['report_id'])
matched_prefix = report._get_prefix_groups_matched_prefix_from_line_id(line_dict_id)
prefix_domain = [('partner_id.name', '=ilike', f'{matched_prefix}%')]
if self._get_no_partner_line_label().upper().startswith(matched_prefix):
prefix_domain = expression.OR([prefix_domain, [('partner_id', '=', None)]])
expand_options = {
**options,
'forced_domain': options.get('forced_domain', []) + prefix_domain
}
parent_level = len(matched_prefix) * 2
partner_lines, dummy = self._build_partner_lines(report, expand_options, level_shift=parent_level)
for partner_line in partner_lines:
partner_line['id'] = report._build_subline_id(line_dict_id, partner_line['id'])
partner_line['parent_id'] = line_dict_id
lines = report._regroup_lines_by_name_prefix(
options,
partner_lines,
'_report_expand_unfoldable_line_partner_ledger_prefix_group',
parent_level,
matched_prefix=matched_prefix,
parent_line_dict_id=line_dict_id,
)
return {
'lines': lines,
'offset_increment': len(lines),
'has_more': False,
}
def _custom_options_initializer(self, report, options, previous_options):
super()._custom_options_initializer(report, options, previous_options=previous_options)
domain = []
company_ids = report.get_report_company_ids(options)
exch_code = self.env['res.company'].browse(company_ids).mapped('currency_exchange_journal_id')
if exch_code:
domain += ['!', '&', '&', '&', ('credit', '=', 0.0), ('debit', '=', 0.0), ('amount_currency', '!=', 0.0), ('journal_id', 'in', exch_code.ids)]
if options['export_mode'] == 'print' and options.get('filter_search_bar'):
domain += [
'|', ('matched_debit_ids.debit_move_id.partner_id.name', 'ilike', options['filter_search_bar']),
'|', ('matched_credit_ids.credit_move_id.partner_id.name', 'ilike', options['filter_search_bar']),
('partner_id.name', 'ilike', options['filter_search_bar']),
]
options['forced_domain'] = options.get('forced_domain', []) + domain
if self.env.user.has_group('base.group_multi_currency'):
options['multi_currency'] = True
columns_to_hide = []
options['hide_account'] = (previous_options or {}).get('hide_account', False)
columns_to_hide += ['journal_code', 'account_code', 'matching_number'] if options['hide_account'] else []
options['hide_debit_credit'] = (previous_options or {}).get('hide_debit_credit', False)
columns_to_hide += ['debit', 'credit'] if options['hide_debit_credit'] else ['amount']
options['columns'] = [col for col in options['columns'] if col['expression_label'] not in columns_to_hide]
options['buttons'].append({
'name': _('Send'),
'action': 'action_send_statements',
'sequence': 90,
'always_show': True,
})
def _custom_unfold_all_batch_data_generator(self, report, options, lines_to_expand_by_function):
partner_ids_to_expand = []
# Regular case
for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_partner_ledger', []):
markup, model, model_id = self.env['account.report']._parse_line_id(line_dict['id'])[-1]
if model == 'res.partner':
partner_ids_to_expand.append(model_id)
elif markup == 'no_partner':
partner_ids_to_expand.append(None)
# In case prefix groups are used
no_partner_line_label = self._get_no_partner_line_label().upper()
partner_prefix_domains = []
for line_dict in lines_to_expand_by_function.get('_report_expand_unfoldable_line_partner_ledger_prefix_group', []):
prefix = report._get_prefix_groups_matched_prefix_from_line_id(line_dict['id'])
partner_prefix_domains.append([('name', '=ilike', f'{prefix}%')])
# amls without partners are regrouped "Unknown Partner", which is also used to create prefix groups
if no_partner_line_label.startswith(prefix):
partner_ids_to_expand.append(None)
if partner_prefix_domains:
partner_ids_to_expand += self.env['res.partner'].with_context(active_test=False).search(expression.OR(partner_prefix_domains)).ids
return {
'initial_balances': self._get_initial_balance_values(partner_ids_to_expand, options) if partner_ids_to_expand else {},
# load_more_limit cannot be passed to this call, otherwise it won't be applied per partner but on the whole result.
# We gain perf from batching, but load every result, even if the limit restricts them later.
'aml_values': self._get_aml_values(options, partner_ids_to_expand) if partner_ids_to_expand else {},
}
def _get_report_send_recipients(self, options):
partners = options.get('partner_ids', [])
if not partners:
self._cr.execute(self._get_query_sums(options))
partners = [row['groupby'] for row in self._cr.dictfetchall() if row['groupby']]
return self.env['res.partner'].browse(partners)
def action_send_statements(self, options):
template = self.env.ref('at_accounting.email_template_customer_statement', False)
return {
'name': _("Send Partner Ledgers"),
'type': 'ir.actions.act_window',
'views': [[False, 'form']],
'res_model': 'account.report.send',
'target': 'new',
'context': {
'default_mail_template_id': template.id if template else False,
'default_report_options': options,
},
}
@api.model
def action_open_partner(self, options, params):
dummy, record_id = self.env['account.report']._get_model_info_from_id(params['id'])
return {
'type': 'ir.actions.act_window',
'res_model': 'res.partner',
'res_id': record_id,
'views': [[False, 'form']],
'view_mode': 'form',
'target': 'current',
}
def _query_partners(self, options):
""" Executes the queries and performs all the computation.
:return: A list of tuple (partner, column_group_values) sorted by the table's model _order:
- partner is a res.parter record.
- column_group_values is a dict(column_group_key, fetched_values), where
- column_group_key is a string identifying a column group, like in options['column_groups']
- fetched_values is a dictionary containing:
- sum: {'debit': float, 'credit': float, 'balance': float}
- (optional) initial_balance: {'debit': float, 'credit': float, 'balance': float}
- (optional) lines: [line_vals_1, line_vals_2, ...]
"""
def assign_sum(row):
fields_to_assign = ['balance', 'debit', 'credit', 'amount']
if any(not company_currency.is_zero(row[field]) for field in fields_to_assign):
groupby_partners.setdefault(row['groupby'], defaultdict(lambda: defaultdict(float)))
for field in fields_to_assign:
groupby_partners[row['groupby']][row['column_group_key']][field] += row[field]
company_currency = self.env.company.currency_id
# Execute the queries and dispatch the results.
query = self._get_query_sums(options)
groupby_partners = {}
self._cr.execute(query)
for res in self._cr.dictfetchall():
assign_sum(res)
# Correct the sums per partner, for the lines without partner reconciled with a line having a partner
query = self._get_sums_without_partner(options)
self._cr.execute(query)
totals = {}
for total_field in ['debit', 'credit', 'amount', 'balance']:
totals[total_field] = {col_group_key: 0 for col_group_key in options['column_groups']}
for row in self._cr.dictfetchall():
totals['debit'][row['column_group_key']] += row['debit']
totals['credit'][row['column_group_key']] += row['credit']
totals['amount'][row['column_group_key']] += row['amount']
totals['balance'][row['column_group_key']] += row['balance']
if row['groupby'] not in groupby_partners:
continue
assign_sum(row)
if None in groupby_partners:
# Debit/credit are inverted for the unknown partner as the computation is made regarding the balance of the known partner
for column_group_key in options['column_groups']:
groupby_partners[None][column_group_key]['debit'] += totals['credit'][column_group_key]
groupby_partners[None][column_group_key]['credit'] += totals['debit'][column_group_key]
groupby_partners[None][column_group_key]['amount'] += totals['amount'][column_group_key]
groupby_partners[None][column_group_key]['balance'] -= totals['balance'][column_group_key]
# Retrieve the partners to browse.
# groupby_partners.keys() contains all account ids affected by:
# - the amls in the current period.
# - the amls affecting the initial balance.
if groupby_partners:
# Note a search is done instead of a browse to preserve the table ordering.
partners = self.env['res.partner'].with_context(active_test=False).search_fetch([('id', 'in', list(groupby_partners.keys()))], ["id", "name", "trust", "company_registry", "vat"])
else:
partners = []
# Add 'Partner Unknown' if needed
if None in groupby_partners.keys():
partners = [p for p in partners] + [None]
return [(partner, groupby_partners[partner.id if partner else None]) for partner in partners]
def _get_query_sums(self, options) -> SQL:
""" Construct a query retrieving all the aggregated sums to build the report. It includes:
- sums for all partners.
- sums for the initial balances.
:param options: The report options.
:return: query as SQL object
"""
queries = []
report = self.env.ref('at_accounting.partner_ledger_report')
# Create the currency table.
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
query = report._get_report_query(column_group_options, 'from_beginning')
queries.append(SQL(
"""
SELECT
account_move_line.partner_id AS groupby,
%(column_group_key)s AS column_group_key,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS amount,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.partner_id
""",
column_group_key=column_group_key,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(column_group_options),
search_condition=query.where_clause,
))
return SQL(' UNION ALL ').join(queries)
def _get_initial_balance_values(self, partner_ids, options):
queries = []
report = self.env.ref('at_accounting.partner_ledger_report')
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
# Get sums for the initial balance.
# period: [('date' <= options['date_from'] - 1)]
new_options = self._get_options_initial_balance(column_group_options)
query = report._get_report_query(new_options, 'from_beginning', domain=[('partner_id', 'in', partner_ids)])
queries.append(SQL(
"""
SELECT
account_move_line.partner_id,
%(column_group_key)s AS column_group_key,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS amount,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
%(currency_table_join)s
WHERE %(search_condition)s
GROUP BY account_move_line.partner_id
""",
column_group_key=column_group_key,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(column_group_options),
search_condition=query.where_clause,
))
self._cr.execute(SQL(" UNION ALL ").join(queries))
init_balance_by_col_group = {
partner_id: {column_group_key: {} for column_group_key in options['column_groups']}
for partner_id in partner_ids
}
for result in self._cr.dictfetchall():
init_balance_by_col_group[result['partner_id']][result['column_group_key']] = result
return init_balance_by_col_group
def _get_options_initial_balance(self, options):
""" Create options used to compute the initial balances for each partner.
The resulting dates domain will be:
[('date' <= options['date_from'] - 1)]
:param options: The report options.
:return: A copy of the options, modified to match the dates to use to get the initial balances.
"""
new_date_to = fields.Date.from_string(options['date']['date_from']) - timedelta(days=1)
new_date_options = dict(options['date'], date_from=False, date_to=fields.Date.to_string(new_date_to))
return dict(options, date=new_date_options)
def _get_sums_without_partner(self, options):
""" Get the sum of lines without partner reconciled with a line with a partner, grouped by partner. Those lines
should be considered as belonging to the partner for the reconciled amount as it may clear some of the partner
invoice/bill and they have to be accounted in the partner balance."""
queries = []
report = self.env.ref('at_accounting.partner_ledger_report')
for column_group_key, column_group_options in report._split_options_per_column_group(options).items():
query = report._get_report_query(column_group_options, 'from_beginning')
queries.append(SQL(
"""
SELECT
%(column_group_key)s AS column_group_key,
aml_with_partner.partner_id AS groupby,
SUM(%(debit_select)s) AS debit,
SUM(%(credit_select)s) AS credit,
SUM(%(balance_select)s) AS amount,
SUM(%(balance_select)s) AS balance
FROM %(table_references)s
JOIN account_partial_reconcile partial
ON account_move_line.id = partial.debit_move_id OR account_move_line.id = partial.credit_move_id
JOIN account_move_line aml_with_partner ON
(aml_with_partner.id = partial.debit_move_id OR aml_with_partner.id = partial.credit_move_id)
AND aml_with_partner.partner_id IS NOT NULL
%(currency_table_join)s
WHERE partial.max_date <= %(date_to)s AND %(search_condition)s
AND account_move_line.partner_id IS NULL
GROUP BY aml_with_partner.partner_id
""",
column_group_key=column_group_key,
debit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance > 0 THEN 0 ELSE partial.amount END")),
credit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance < 0 THEN 0 ELSE partial.amount END")),
balance_select=report._currency_table_apply_rate(SQL("-SIGN(aml_with_partner.balance) * partial.amount")),
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(column_group_options, aml_alias=SQL("aml_with_partner")),
date_to=column_group_options['date']['date_to'],
search_condition=query.where_clause,
))
return SQL(" UNION ALL ").join(queries)
def _report_expand_unfoldable_line_partner_ledger(self, line_dict_id, groupby, options, progress, offset, unfold_all_batch_data=None):
def init_load_more_progress(line_dict):
return {
column['column_group_key']: line_col.get('no_format', 0)
for column, line_col in zip(options['columns'], line_dict['columns'])
if column['expression_label'] == 'balance'
}
report = self.env.ref('at_accounting.partner_ledger_report')
markup, model, record_id = report._parse_line_id(line_dict_id)[-1]
if model != 'res.partner':
raise UserError(_("Wrong ID for partner ledger line to expand: %s", line_dict_id))
prefix_groups_count = 0
for markup, dummy1, dummy2 in report._parse_line_id(line_dict_id):
if isinstance(markup, dict) and 'groupby_prefix_group' in markup:
prefix_groups_count += 1
level_shift = prefix_groups_count * 2
lines = []
# Get initial balance
if offset == 0:
if unfold_all_batch_data:
init_balance_by_col_group = unfold_all_batch_data['initial_balances'][record_id]
else:
init_balance_by_col_group = self._get_initial_balance_values([record_id], options)[record_id]
initial_balance_line = report._get_partner_and_general_ledger_initial_balance_line(options, line_dict_id, init_balance_by_col_group, level_shift=level_shift)
if initial_balance_line:
lines.append(initial_balance_line)
# For the first expansion of the line, the initial balance line gives the progress
progress = init_load_more_progress(initial_balance_line)
limit_to_load = report.load_more_limit + 1 if report.load_more_limit and options['export_mode'] != 'print' else None
if unfold_all_batch_data:
aml_results = unfold_all_batch_data['aml_values'][record_id]
else:
aml_results = self._get_aml_values(options, [record_id], offset=offset, limit=limit_to_load)[record_id]
has_more = False
treated_results_count = 0
next_progress = progress
for result in aml_results:
if options['export_mode'] != 'print' and report.load_more_limit and treated_results_count == report.load_more_limit:
# We loaded one more than the limit on purpose: this way we know we need a "load more" line
has_more = True
break
new_line = self._get_report_line_move_line(options, result, line_dict_id, next_progress, level_shift=level_shift)
lines.append(new_line)
next_progress = init_load_more_progress(new_line)
treated_results_count += 1
return {
'lines': lines,
'offset_increment': treated_results_count,
'has_more': has_more,
'progress': next_progress
}
def _get_additional_column_aml_values(self):
"""
Allows customization of additional fields in the partner ledger query.
This method is intended to be overridden by other modules to add custom fields
to the partner ledger query, e.g. SQL("account_move_line.date AS date,").
By default, it returns an empty SQL object.
"""
return SQL()
def _get_aml_values(self, options, partner_ids, offset=0, limit=None):
rslt = {partner_id: [] for partner_id in partner_ids}
partner_ids_wo_none = [x for x in partner_ids if x]
directly_linked_aml_partner_clauses = []
indirectly_linked_aml_partner_clause = SQL('aml_with_partner.partner_id IS NOT NULL')
if None in partner_ids:
directly_linked_aml_partner_clauses.append(SQL('account_move_line.partner_id IS NULL'))
if partner_ids_wo_none:
directly_linked_aml_partner_clauses.append(SQL('account_move_line.partner_id IN %s', tuple(partner_ids_wo_none)))
indirectly_linked_aml_partner_clause = SQL('aml_with_partner.partner_id IN %s', tuple(partner_ids_wo_none))
directly_linked_aml_partner_clause = SQL('(%s)', SQL(' OR ').join(directly_linked_aml_partner_clauses))
queries = []
journal_name = self.env['account.journal']._field_to_sql('journal', 'name')
report = self.env.ref('at_accounting.partner_ledger_report')
additional_columns = self._get_additional_column_aml_values()
for column_group_key, group_options in report._split_options_per_column_group(options).items():
query = report._get_report_query(group_options, 'strict_range')
account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
account_name = self.env['account.account']._field_to_sql(account_alias, 'name')
# For the move lines directly linked to this partner
# ruff: noqa: FURB113
queries.append(SQL(
'''
SELECT
account_move_line.id,
account_move_line.date_maturity,
account_move_line.name,
account_move_line.ref,
account_move_line.company_id,
account_move_line.account_id,
account_move_line.payment_id,
account_move_line.partner_id,
account_move_line.currency_id,
account_move_line.amount_currency,
account_move_line.matching_number,
%(additional_columns)s
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
%(debit_select)s AS debit,
%(credit_select)s AS credit,
%(balance_select)s AS amount,
%(balance_select)s AS balance,
account_move.name AS move_name,
account_move.move_type AS move_type,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
journal.code AS journal_code,
%(journal_name)s AS journal_name,
%(column_group_key)s AS column_group_key,
'directly_linked_aml' AS key,
0 AS partial_id
FROM %(table_references)s
JOIN account_move ON account_move.id = account_move_line.move_id
%(currency_table_join)s
LEFT JOIN res_company company ON company.id = account_move_line.company_id
LEFT JOIN res_partner partner ON partner.id = account_move_line.partner_id
LEFT JOIN account_journal journal ON journal.id = account_move_line.journal_id
WHERE %(search_condition)s AND %(directly_linked_aml_partner_clause)s
ORDER BY account_move_line.date, account_move_line.id
''',
additional_columns=additional_columns,
debit_select=report._currency_table_apply_rate(SQL("account_move_line.debit")),
credit_select=report._currency_table_apply_rate(SQL("account_move_line.credit")),
balance_select=report._currency_table_apply_rate(SQL("account_move_line.balance")),
account_code=account_code,
account_name=account_name,
journal_name=journal_name,
column_group_key=column_group_key,
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(group_options),
search_condition=query.where_clause,
directly_linked_aml_partner_clause=directly_linked_aml_partner_clause,
))
# For the move lines linked to no partner, but reconciled with this partner. They will appear in grey in the report
queries.append(SQL(
'''
SELECT
account_move_line.id,
account_move_line.date_maturity,
account_move_line.name,
account_move_line.ref,
account_move_line.company_id,
account_move_line.account_id,
account_move_line.payment_id,
aml_with_partner.partner_id,
account_move_line.currency_id,
account_move_line.amount_currency,
account_move_line.matching_number,
%(additional_columns)s
COALESCE(account_move_line.invoice_date, account_move_line.date) AS invoice_date,
%(debit_select)s AS debit,
%(credit_select)s AS credit,
%(balance_select)s AS amount,
%(balance_select)s AS balance,
account_move.name AS move_name,
account_move.move_type AS move_type,
%(account_code)s AS account_code,
%(account_name)s AS account_name,
journal.code AS journal_code,
%(journal_name)s AS journal_name,
%(column_group_key)s AS column_group_key,
'indirectly_linked_aml' AS key,
partial.id AS partial_id
FROM %(table_references)s
%(currency_table_join)s,
account_partial_reconcile partial,
account_move,
account_move_line aml_with_partner,
account_journal journal
WHERE
(account_move_line.id = partial.debit_move_id OR account_move_line.id = partial.credit_move_id)
AND account_move_line.partner_id IS NULL
AND account_move.id = account_move_line.move_id
AND (aml_with_partner.id = partial.debit_move_id OR aml_with_partner.id = partial.credit_move_id)
AND %(indirectly_linked_aml_partner_clause)s
AND journal.id = account_move_line.journal_id
AND %(account_alias)s.id = account_move_line.account_id
AND %(search_condition)s
AND partial.max_date BETWEEN %(date_from)s AND %(date_to)s
ORDER BY account_move_line.date, account_move_line.id
''',
additional_columns=additional_columns,
debit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance > 0 THEN 0 ELSE partial.amount END")),
credit_select=report._currency_table_apply_rate(SQL("CASE WHEN aml_with_partner.balance < 0 THEN 0 ELSE partial.amount END")),
balance_select=report._currency_table_apply_rate(SQL("-SIGN(aml_with_partner.balance) * partial.amount")),
account_code=account_code,
account_name=account_name,
journal_name=journal_name,
column_group_key=column_group_key,
table_references=query.from_clause,
currency_table_join=report._currency_table_aml_join(group_options),
indirectly_linked_aml_partner_clause=indirectly_linked_aml_partner_clause,
account_alias=SQL.identifier(account_alias),
search_condition=query.where_clause,
date_from=group_options['date']['date_from'],
date_to=group_options['date']['date_to'],
))
query = SQL(" UNION ALL ").join(SQL("(%s)", query) for query in queries)
if offset:
query = SQL('%s OFFSET %s ', query, offset)
if limit:
query = SQL('%s LIMIT %s ', query, limit)
self._cr.execute(query)
for aml_result in self._cr.dictfetchall():
if aml_result['key'] == 'indirectly_linked_aml':
# Append the line to the partner found through the reconciliation.
if aml_result['partner_id'] in rslt:
rslt[aml_result['partner_id']].append(aml_result)
# Balance it with an additional line in the Unknown Partner section but having reversed amounts.
if None in rslt:
rslt[None].append({
**aml_result,
'debit': aml_result['credit'],
'credit': aml_result['debit'],
'amount': aml_result['credit'] - aml_result['debit'],
'balance': -aml_result['balance'],
})
else:
rslt[aml_result['partner_id']].append(aml_result)
return rslt
####################################################
# COLUMNS/LINES
####################################################
def _get_report_line_partners(self, options, partner, partner_values, level_shift=0):
company_currency = self.env.company.currency_id
partner_data = next(iter(partner_values.values()))
unfoldable = not company_currency.is_zero(partner_data.get('debit', 0) or partner_data.get('credit', 0))
column_values = []
report = self.env['account.report'].browse(options['report_id'])
for column in options['columns']:
col_expr_label = column['expression_label']
value = partner_values[column['column_group_key']].get(col_expr_label)
unfoldable = unfoldable or (col_expr_label in ('debit', 'credit', 'amount') and not company_currency.is_zero(value))
column_values.append(report._build_column_dict(value, column, options=options))
line_id = report._get_generic_line_id('res.partner', partner.id) if partner else report._get_generic_line_id('res.partner', None, markup='no_partner')
return {
'id': line_id,
'name': partner is not None and (partner.name or '')[:128] or self._get_no_partner_line_label(),
'columns': column_values,
'level': 1 + level_shift,
'trust': partner.trust if partner else None,
'unfoldable': unfoldable,
'unfolded': line_id in options['unfolded_lines'] or options['unfold_all'],
'expand_function': '_report_expand_unfoldable_line_partner_ledger',
}
def _get_no_partner_line_label(self):
return _('Unknown Partner')
@api.model
def _format_aml_name(self, line_name, move_ref, move_name=None):
''' Format the display of an account.move.line record. As its very costly to fetch the account.move.line
records, only line_name, move_ref, move_name are passed as parameters to deal with sql-queries more easily.
:param line_name: The name of the account.move.line record.
:param move_ref: The reference of the account.move record.
:param move_name: The name of the account.move record.
:return: The formatted name of the account.move.line record.
'''
return self.env['account.move.line']._format_aml_name(line_name, move_ref, move_name=move_name)
def _get_report_line_move_line(self, options, aml_query_result, partner_line_id, init_bal_by_col_group, level_shift=0):
if aml_query_result['payment_id']:
caret_type = 'account.payment'
else:
caret_type = 'account.move.line'
columns = []
report = self.env['account.report'].browse(options['report_id'])
for column in options['columns']:
col_expr_label = column['expression_label']
if col_expr_label not in aml_query_result:
raise UserError(_("The column '%s' is not available for this report.", col_expr_label))
col_value = aml_query_result[col_expr_label] if column['column_group_key'] == aml_query_result['column_group_key'] else None
if col_value is None:
columns.append(report._build_column_dict(None, None))
else:
currency = False
if col_expr_label == 'balance':
col_value += init_bal_by_col_group[column['column_group_key']]
if col_expr_label == 'amount_currency':
currency = self.env['res.currency'].browse(aml_query_result['currency_id'])
if currency == self.env.company.currency_id:
col_value = ''
columns.append(report._build_column_dict(col_value, column, options=options, currency=currency))
return {
'id': report._get_generic_line_id('account.move.line', aml_query_result['id'], parent_line_id=partner_line_id, markup=aml_query_result['partial_id']),
'parent_id': partner_line_id,
'name': self._format_aml_name(aml_query_result['name'], aml_query_result['ref'], aml_query_result['move_name']),
'columns': columns,
'caret_options': caret_type,
'level': 3 + level_shift,
}
def _get_report_line_total(self, options, totals_by_column_group):
column_values = []
report = self.env['account.report'].browse(options['report_id'])
for column in options['columns']:
col_value = totals_by_column_group[column['column_group_key']].get(column['expression_label'])
column_values.append(report._build_column_dict(col_value, column, options=options))
return {
'id': report._get_generic_line_id(None, None, markup='total'),
'name': _('Total'),
'level': 1,
'columns': column_values,
}
def open_journal_items(self, options, params):
params['view_ref'] = 'account.view_move_line_tree_grouped_partner'
report = self.env['account.report'].browse(options['report_id'])
action = report.open_journal_items(options=options, params=params)
action.get('context', {}).update({'search_default_group_by_account': 0})
return action

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import ast
from odoo import models, _
class AccountPayment(models.Model):
_inherit = "account.payment"
def action_open_manual_reconciliation_widget(self):
''' Open the manual reconciliation widget for the current payment.
:return: A dictionary representing an action.
'''
self.ensure_one()
action_values = self.env['ir.actions.act_window']._for_xml_id('at_accounting.action_move_line_posted_unreconciled')
if self.partner_id:
context = ast.literal_eval(action_values['context'])
context.update({'search_default_partner_id': self.partner_id.id})
if self.partner_type == 'customer':
context.update({'search_default_trade_receivable': 1})
elif self.partner_type == 'supplier':
context.update({'search_default_trade_payable': 1})
action_values['context'] = context
return action_values
def button_open_statement_lines(self):
# OVERRIDE
""" Redirect the user to the statement line(s) reconciled to this payment.
:return: An action to open the view of the payment in the reconciliation widget.
"""
self.ensure_one()
return self.env['account.bank.statement.line']._action_open_bank_reconciliation_widget(
extra_domain=[('id', 'in', self.reconciled_statement_line_ids.ids)],
default_context={
'create': False,
'default_st_line_id': self.reconciled_statement_line_ids.ids[-1],
},
name=_("Matched Transactions")
)

View File

@@ -0,0 +1,557 @@
from odoo import fields, models, Command, tools
from odoo.tools import SQL
import re
from collections import defaultdict
from dateutil.relativedelta import relativedelta
class AccountReconcileModel(models.Model):
_inherit = 'account.reconcile.model'
####################################################
# RECONCILIATION PROCESS
####################################################
def _apply_lines_for_bank_widget(self, residual_amount_currency, partner, st_line):
""" Apply the reconciliation model lines to the statement line passed as parameter.
:param residual_amount_currency: The open balance of the statement line in the bank reconciliation widget
expressed in the statement line currency.
:param partner: The partner set on the wizard.
:param st_line: The statement line processed by the bank reconciliation widget.
:return: A list of python dictionaries (one per reconcile model line) representing
the journal items to be created by the current reconcile model.
"""
self.ensure_one()
currency = st_line.foreign_currency_id or st_line.journal_id.currency_id or st_line.company_currency_id
vals_list = []
for line in self.line_ids:
vals = line._apply_in_bank_widget(residual_amount_currency, partner, st_line)
amount_currency = vals['amount_currency']
if currency.is_zero(amount_currency):
continue
vals_list.append(vals)
residual_amount_currency -= amount_currency
return vals_list
####################################################
# RECONCILIATION CRITERIA
####################################################
def _apply_rules(self, st_line, partner):
available_models = self.filtered(lambda m: m.rule_type != 'writeoff_button').sorted()
for rec_model in available_models:
if not rec_model._is_applicable_for(st_line, partner):
continue
if rec_model.rule_type == 'invoice_matching':
rules_map = rec_model._get_invoice_matching_rules_map()
for rule_index in sorted(rules_map.keys()):
for rule_method in rules_map[rule_index]:
candidate_vals = rule_method(st_line, partner)
if not candidate_vals:
continue
if candidate_vals.get('amls'):
res = rec_model._get_invoice_matching_amls_result(st_line, partner, candidate_vals)
if res:
return {
**res,
'model': rec_model,
}
else:
return {
**candidate_vals,
'model': rec_model,
}
elif rec_model.rule_type == 'writeoff_suggestion':
return {
'model': rec_model,
'status': 'write_off',
'auto_reconcile': rec_model.auto_reconcile,
}
return {}
def _is_applicable_for(self, st_line, partner):
""" Returns true iff this reconciliation model can be used to search for matches
for the provided statement line and partner.
"""
self.ensure_one()
# Filter on journals, amount nature, amount and partners
# All the conditions defined in this block are non-match conditions.
if ((self.match_journal_ids and st_line.move_id.journal_id not in self.match_journal_ids)
or (self.match_nature == 'amount_received' and st_line.amount < 0)
or (self.match_nature == 'amount_paid' and st_line.amount > 0)
or (self.match_amount == 'lower' and abs(st_line.amount) >= self.match_amount_max)
or (self.match_amount == 'greater' and abs(st_line.amount) <= self.match_amount_min)
or (self.match_amount == 'between' and (abs(st_line.amount) > self.match_amount_max or abs(st_line.amount) < self.match_amount_min))
or (self.match_partner and not partner)
or (self.match_partner and self.match_partner_ids and partner not in self.match_partner_ids)
or (self.match_partner and self.match_partner_category_ids and not (partner.category_id & self.match_partner_category_ids))
):
return False
# Filter on label, note and transaction_type
for record, rule_field, record_field in [(st_line, 'label', 'payment_ref'), (st_line.move_id, 'note', 'narration'), (st_line, 'transaction_type', 'transaction_type')]:
rule_term = (self['match_' + rule_field + '_param'] or '').lower()
record_term = (record[record_field] or '').lower()
# This defines non-match conditions
if ((self['match_' + rule_field] == 'contains' and rule_term not in record_term)
or (self['match_' + rule_field] == 'not_contains' and rule_term in record_term)
or (self['match_' + rule_field] == 'match_regex' and not re.match(rule_term, record_term))
):
return False
return True
def _get_invoice_matching_amls_domain(self, st_line, partner):
aml_domain = st_line._get_default_amls_matching_domain()
if st_line.amount > 0.0:
aml_domain.append(('balance', '>', 0.0))
else:
aml_domain.append(('balance', '<', 0.0))
currency = st_line.foreign_currency_id or st_line.currency_id
if self.match_same_currency:
aml_domain.append(('currency_id', '=', currency.id))
if partner:
aml_domain.append(('partner_id', '=', partner.id))
if self.past_months_limit:
date_limit = fields.Date.context_today(self) - relativedelta(months=self.past_months_limit)
aml_domain.append(('date', '>=', fields.Date.to_string(date_limit)))
return aml_domain
def _get_st_line_text_values_for_matching(self, st_line):
""" Collect the strings that could be used on the statement line to perform some matching.
:param st_line: The current statement line.
:return: A list of strings.
"""
self.ensure_one()
allowed_fields = []
if self.match_text_location_label:
allowed_fields.append('payment_ref')
if self.match_text_location_note:
allowed_fields.append('narration')
if self.match_text_location_reference:
allowed_fields.append('ref')
return st_line._get_st_line_strings_for_matching(allowed_fields=allowed_fields)
def _get_invoice_matching_st_line_tokens(self, st_line):
""" Parse the textual information from the statement line passed as parameter
in order to extract from it the meaningful information in order to perform the matching.
:param st_line: A statement line.
:return: A tuple of list of tokens, each one being a string.
The first element is a list of tokens you may match on numerical information.
The second element is a list of tokens you may match exactly.
"""
st_line_text_values = self._get_st_line_text_values_for_matching(st_line)
significant_token_size = 4
numerical_tokens = []
exact_tokens = set() # preventing duplicates
text_tokens = []
for text_value in st_line_text_values:
split_text = (text_value or '').split()
# Exact tokens
exact_tokens.add(text_value)
exact_tokens.update(
token for token in split_text
if len(token) >= significant_token_size
)
# Text tokens
tokens = [
''.join(x for x in token if re.match(r'[0-9a-zA-Z\s]', x))
for token in split_text
]
# Numerical tokens
for token in tokens:
# The token is too short to be significant.
if len(token) < significant_token_size:
continue
text_tokens.append(token)
formatted_token = ''.join(x for x in token if x.isdecimal())
# The token is too short after formatting to be significant.
if len(formatted_token) < significant_token_size:
continue
numerical_tokens.append(formatted_token)
return numerical_tokens, list(exact_tokens), text_tokens
def _get_invoice_matching_amls_candidates(self, st_line, partner):
""" Returns the match candidates for the 'invoice_matching' rule, with respect to the provided parameters.
:param st_line: A statement line.
:param partner: The partner associated to the statement line.
"""
def get_order_by_clause(prefix=SQL()):
direction = SQL(' DESC') if self.matching_order == 'new_first' else SQL(' ASC')
return SQL(", ").join(
SQL("%s%s%s", prefix, SQL(field), direction)
for field in ('date_maturity', 'date', 'id')
)
assert self.rule_type == 'invoice_matching'
self.env['account.move'].flush_model()
self.env['account.move.line'].flush_model()
aml_domain = self._get_invoice_matching_amls_domain(st_line, partner)
query = self.env['account.move.line']._where_calc(aml_domain)
tables = query.from_clause
where_clause = query.where_clause or SQL("TRUE")
aml_cte = SQL()
sub_queries: list[SQL] = []
numerical_tokens, exact_tokens, _text_tokens = self._get_invoice_matching_st_line_tokens(st_line)
if numerical_tokens or exact_tokens:
aml_cte = SQL('''
WITH aml_cte AS (
SELECT
account_move_line.id as account_move_line_id,
account_move_line.date as account_move_line_date,
account_move_line.date_maturity as account_move_line_date_maturity,
account_move_line.name as account_move_line_name,
account_move_line__move_id.name as account_move_line__move_id_name,
account_move_line__move_id.ref as account_move_line__move_id_ref
FROM %s
JOIN account_move account_move_line__move_id ON account_move_line__move_id.id = account_move_line.move_id
WHERE %s
)
''', tables, where_clause)
if numerical_tokens:
for table_alias, field in (
('account_move_line', 'name'),
('account_move_line__move_id', 'name'),
('account_move_line__move_id', 'ref'),
):
sub_queries.append(SQL(r'''
SELECT
account_move_line_id as id,
account_move_line_date as date,
account_move_line_date_maturity as date_maturity,
UNNEST(
REGEXP_SPLIT_TO_ARRAY(
SUBSTRING(
REGEXP_REPLACE(%(field)s, '[^0-9\s]', '', 'g'),
'\S(?:.*\S)*'
),
'\s+'
)
) AS token
FROM aml_cte
WHERE %(field)s IS NOT NULL
''', field=SQL("%s_%s", SQL(table_alias), SQL(field))))
if exact_tokens:
for table_alias, field in (
('account_move_line', 'name'),
('account_move_line__move_id', 'name'),
('account_move_line__move_id', 'ref'),
):
sub_queries.append(SQL('''
SELECT
account_move_line_id as id,
account_move_line_date as date,
account_move_line_date_maturity as date_maturity,
%(field)s AS token
FROM aml_cte
WHERE %(field)s != ''
''', field=SQL("%s_%s", SQL(table_alias), SQL(field))))
if sub_queries:
order_by = get_order_by_clause(prefix=SQL('sub.'))
candidate_ids = [r[0] for r in self.env.execute_query(SQL(
'''
%s
SELECT
sub.id,
COUNT(*) AS nb_match
FROM (%s) AS sub
WHERE sub.token IN %s
GROUP BY sub.date_maturity, sub.date, sub.id
HAVING COUNT(*) > 0
ORDER BY nb_match DESC, %s
''',
aml_cte,
SQL(" UNION ALL ").join(sub_queries),
tuple(numerical_tokens + exact_tokens),
order_by,
))]
if candidate_ids:
return {
'allow_auto_reconcile': True,
'amls': self.env['account.move.line'].browse(candidate_ids),
}
elif self.match_text_location_label or self.match_text_location_note or self.match_text_location_reference:
# In the case any of the Label, Note or Reference matching rule has been toggled, and the query didn't return
# any candidates, the model should not try to mount another aml instead.
return
if not partner:
st_line_currency = st_line.foreign_currency_id or st_line.journal_id.currency_id or st_line.company_currency_id
if st_line_currency == self.company_id.currency_id:
aml_amount_field = SQL('amount_residual')
else:
aml_amount_field = SQL('amount_residual_currency')
order_by = get_order_by_clause(prefix=SQL('account_move_line.'))
rows = self.env.execute_query(SQL(
'''
SELECT account_move_line.id
FROM %s
WHERE
%s
AND account_move_line.currency_id = %s
AND ROUND(account_move_line.%s, %s) = ROUND(%s, %s)
ORDER BY %s
''',
tables,
where_clause,
st_line_currency.id,
aml_amount_field,
st_line_currency.decimal_places,
-st_line.amount_residual,
st_line_currency.decimal_places,
order_by,
))
amls = self.env['account.move.line'].browse([row[0] for row in rows])
else:
amls = self.env['account.move.line'].search(aml_domain, order=get_order_by_clause().code)
if amls:
return {
'allow_auto_reconcile': False,
'amls': amls,
}
def _get_invoice_matching_rules_map(self):
""" Get a mapping <priority_order, rule> that could be overridden in others modules.
:return: a mapping <priority_order, rule> where:
* priority_order: Defines in which order the rules will be evaluated, the lowest comes first.
This is extremely important since the algorithm stops when a rule returns some candidates.
* rule: Method taking <st_line, partner> as parameters and returning the candidates journal items found.
"""
rules_map = defaultdict(list)
rules_map[10].append(self._get_invoice_matching_amls_candidates)
return rules_map
def _get_partner_from_mapping(self, st_line):
"""Find partner with mapping defined on model.
For invoice matching rules, matches the statement line against each
regex defined in partner mapping, and returns the partner corresponding
to the first one matching.
:param st_line (Model<account.bank.statement.line>):
The statement line that needs a partner to be found
:return Model<res.partner>:
The partner found from the mapping. Can be empty an empty recordset
if there was nothing found from the mapping or if the function is
not applicable.
"""
self.ensure_one()
if self.rule_type not in ('invoice_matching', 'writeoff_suggestion'):
return self.env['res.partner']
for partner_mapping in self.partner_mapping_line_ids:
match_payment_ref = True
if partner_mapping.payment_ref_regex:
match_payment_ref = re.match(partner_mapping.payment_ref_regex, st_line.payment_ref) if st_line.payment_ref else False
match_narration = True
if partner_mapping.narration_regex:
match_narration = re.match(
partner_mapping.narration_regex,
tools.html2plaintext(st_line.narration or '').rstrip(),
flags=re.DOTALL, # Ignore '/n' set by online sync.
)
if match_payment_ref and match_narration:
return partner_mapping.partner_id
return self.env['res.partner']
def _get_invoice_matching_amls_result(self, st_line, partner, candidate_vals):
def _create_result_dict(amls_values_list, status):
if 'rejected' in status:
return
result = {'amls': self.env['account.move.line']}
for aml_values in amls_values_list:
result['amls'] |= aml_values['aml']
if 'allow_write_off' in status and self.line_ids:
result['status'] = 'write_off'
if 'allow_auto_reconcile' in status and candidate_vals['allow_auto_reconcile'] and self.auto_reconcile:
result['auto_reconcile'] = True
return result
st_line_currency = st_line.foreign_currency_id or st_line.currency_id
st_line_amount = st_line._prepare_move_line_default_vals()[1]['amount_currency']
sign = 1 if st_line_amount > 0.0 else -1
amls = candidate_vals['amls']
amls_values_list = []
amls_with_epd_values_list = []
same_currency_mode = amls.currency_id == st_line_currency
for aml in amls:
aml_values = {
'aml': aml,
'amount_residual': aml.amount_residual,
'amount_residual_currency': aml.amount_residual_currency,
}
amls_values_list.append(aml_values)
# Manage the early payment discount.
if aml.move_id.invoice_payment_term_id:
last_discount_date = aml.move_id.invoice_payment_term_id._get_last_discount_date(aml.move_id.date)
else:
last_discount_date = False
if same_currency_mode \
and aml.move_id.move_type in ('out_invoice', 'out_receipt', 'in_invoice', 'in_receipt') \
and not aml.matched_debit_ids \
and not aml.matched_credit_ids \
and last_discount_date \
and st_line.date <= last_discount_date:
rate = abs(aml.amount_currency) / abs(aml.balance) if aml.balance else 1.0
amls_with_epd_values_list.append({
**aml_values,
'amount_residual': st_line.company_currency_id.round(aml.discount_amount_currency / rate),
'amount_residual_currency': aml.discount_amount_currency,
})
else:
amls_with_epd_values_list.append(aml_values)
def match_batch_amls(amls_values_list):
if not same_currency_mode:
return None, []
kepts_amls_values_list = []
sum_amount_residual_currency = 0.0
for aml_values in amls_values_list:
if st_line_currency.compare_amounts(st_line_amount, -aml_values['amount_residual_currency']) == 0:
# Special case: the amounts are the same, submit the line directly.
return 'perfect', [aml_values]
if st_line_currency.compare_amounts(sign * (st_line_amount + sum_amount_residual_currency), 0.0) > 0:
# Here, we still have room for other candidates ; so we add the current one to the list we keep.
# Then, we continue iterating, even if there is no room anymore, just in case one of the following candidates
# is an exact match, which would then be preferred on the current candidates.
kepts_amls_values_list.append(aml_values)
sum_amount_residual_currency += aml_values['amount_residual_currency']
if st_line_currency.is_zero(sign * (st_line_amount + sum_amount_residual_currency)):
return 'perfect', kepts_amls_values_list
elif kepts_amls_values_list:
return 'partial', kepts_amls_values_list
else:
return None, []
# Try to match a batch with the early payment feature. Only a perfect match is allowed.
match_type, kepts_amls_values_list = match_batch_amls(amls_with_epd_values_list)
if match_type != 'perfect':
kepts_amls_values_list = []
# Try to match the amls having the same currency as the statement line.
if not kepts_amls_values_list:
_match_type, kepts_amls_values_list = match_batch_amls(amls_values_list)
# Try to match the whole candidates.
if not kepts_amls_values_list:
kepts_amls_values_list = amls_values_list
# Try to match the amls having the same currency as the statement line.
if kepts_amls_values_list:
status = self._check_rule_propositions(st_line, kepts_amls_values_list)
result = _create_result_dict(kepts_amls_values_list, status)
if result:
return result
def _check_rule_propositions(self, st_line, amls_values_list):
""" Check restrictions that can't be handled for each move.line separately.
Note: Only used by models having a type equals to 'invoice_matching'.
:param st_line: The statement line.
:param amls_values_list: The candidates account.move.line as a list of dict:
* aml: The record.
* amount_residual: The amount residual to consider.
* amount_residual_currency: The amount residual in foreign currency to consider.
:return: A string representing what to do with the candidates:
* rejected: Reject candidates.
* allow_write_off: Allow to generate the write-off from the reconcile model lines if specified.
* allow_auto_reconcile: Allow to automatically reconcile entries if 'auto_validate' is enabled.
"""
self.ensure_one()
if not self.allow_payment_tolerance:
return {'allow_write_off', 'allow_auto_reconcile'}
st_line_currency = st_line.foreign_currency_id or st_line.currency_id
st_line_amount_curr = st_line._prepare_move_line_default_vals()[1]['amount_currency']
amls_amount_curr = sum(
st_line._prepare_counterpart_amounts_using_st_line_rate(
aml_values['aml'].currency_id,
aml_values['amount_residual'],
aml_values['amount_residual_currency'],
)['amount_currency']
for aml_values in amls_values_list
)
sign = 1 if st_line_amount_curr > 0.0 else -1
amount_curr_after_rec = st_line_currency.round(
sign * (amls_amount_curr + st_line_amount_curr)
)
# The statement line will be fully reconciled.
if st_line_currency.is_zero(amount_curr_after_rec):
return {'allow_auto_reconcile'}
# The payment amount is higher than the sum of invoices.
# In that case, don't check the tolerance and don't try to generate any write-off.
if amount_curr_after_rec > 0.0:
return {'allow_auto_reconcile'}
# No tolerance, reject the candidates.
if self.payment_tolerance_param == 0:
return {'rejected'}
# If the tolerance is expressed as a fixed amount, check the residual payment amount doesn't exceed the
# tolerance.
if self.payment_tolerance_type == 'fixed_amount' and st_line_currency.compare_amounts(-amount_curr_after_rec, self.payment_tolerance_param) <= 0:
return {'allow_write_off', 'allow_auto_reconcile'}
# The tolerance is expressed as a percentage between 0 and 100.0.
reconciled_percentage_left = (abs(amount_curr_after_rec / amls_amount_curr)) * 100.0
if self.payment_tolerance_type == 'percentage' and st_line_currency.compare_amounts(reconciled_percentage_left, self.payment_tolerance_param) <= 0:
return {'allow_write_off', 'allow_auto_reconcile'}
return {'rejected'}
def run_auto_reconciliation(self):
""" Tries to auto-reconcile as many statements as possible within time limit
arbitrary set to 3 minutes (the rest will be reconciled asynchronously with the regular cron).
"""
# 'limit_time_real_cron' defaults to -1.
# Manual fallback applied for non-POSIX systems where this key is disabled (set to None).
cron_limit_time = tools.config['limit_time_real_cron'] or -1
limit_time = cron_limit_time if 0 < cron_limit_time < 180 else 180
self.env['account.bank.statement.line']._cron_try_auto_reconcile_statement_lines(limit_time=limit_time)

Some files were not shown because too many files have changed in this diff Show More