692 Commits
v1.0.0 ... dev

Author SHA1 Message Date
AzureAD\SylvainDUVERNAY
1c8abc7c1e Trade Finance 2026-04-12 17:22:25 +02:00
AzureAD\SylvainDUVERNAY
37bf6ba23b Trade Finance 2026-04-12 17:15:08 +02:00
AzureAD\SylvainDUVERNAY
2e649aa61b Commit 2026-04-12 17:06:26 +02:00
AzureAD\SylvainDUVERNAY
735a72d23e Trade Finance 2026-04-12 16:58:45 +02:00
AzureAD\SylvainDUVERNAY
ebf9b6c495 Trade Finance 2026-04-12 16:47:59 +02:00
AzureAD\SylvainDUVERNAY
13d26ac41b Trade Finance 2026-04-12 16:36:08 +02:00
AzureAD\SylvainDUVERNAY
b829b11791 Trade Finance - Commit 2026-04-12 14:41:12 +02:00
AzureAD\SylvainDUVERNAY
4a056ef402 Change on Facility definition 2026-04-07 21:53:10 +02:00
AzureAD\SylvainDUVERNAY
da65da79c0 Trade Finance - Facility - Limit ordering 2026-03-31 22:06:58 +02:00
AzureAD\SylvainDUVERNAY
aa6b3fb9ad Trade Finance - TF Facility - Order limits 2026-03-31 21:55:34 +02:00
AzureAD\SylvainDUVERNAY
b1dd118628 Trade Finance - Facility - Improve Limits table layout 2026-03-31 21:39:56 +02:00
AzureAD\SylvainDUVERNAY
f1f9d157cc Trade Finance - Facility - Fix error on Save 2026-03-31 16:50:45 +02:00
AzureAD\SylvainDUVERNAY
283b71fda9 Trade Finance - Sub Limit 2026-03-31 14:14:05 +02:00
AzureAD\SylvainDUVERNAY
f27dd5620e Trade Finance - Adjustment on sub-limit 2026-03-31 14:10:43 +02:00
AzureAD\SylvainDUVERNAY
143f59c62e Trade Finance - Facility Fix 3 2026-03-31 13:37:42 +02:00
AzureAD\SylvainDUVERNAY
be6b6517a5 Trade Finance - Facility Fix 2 2026-03-31 13:34:31 +02:00
AzureAD\SylvainDUVERNAY
d96973310b Trade Finance Facility - Fix 1 2026-03-31 13:31:33 +02:00
AzureAD\SylvainDUVERNAY
39278c4483 Trade Finance - Facility 2026-03-31 13:22:18 +02:00
AzureAD\SylvainDUVERNAY
4534ad86f1 Trade Finance - Fix 2026-03-31 12:24:17 +02:00
AzureAD\SylvainDUVERNAY
10e8e5be9b Trade Finance - Initial Commit 2026-03-31 12:14:51 +02:00
AzureAD\SylvainDUVERNAY
2fa541e962 Remove groups 2026-03-29 18:41:12 +02:00
AzureAD\SylvainDUVERNAY
ad2f7e6f78 Commit IFRS adjustments 2026-03-29 18:34:54 +02:00
43c62607a8 29.03.26 2026-03-29 18:13:56 +02:00
5cff728d79 29.03.26 2026-03-29 18:05:00 +02:00
AzureAD\SylvainDUVERNAY
d6382f624b Commit 2 2026-03-29 18:02:31 +02:00
08febb904f 29.03.26 2026-03-29 17:34:42 +02:00
20d733e787 Merge pull request 'main' (#7) from main into dev
Reviewed-on: #7
2026-03-29 13:03:24 +00:00
984b2ba56f 27.03.26 2026-03-27 14:08:07 +01:00
f67e5d8ccc 27.03.26 2026-03-27 08:12:03 +01:00
2bf02e687c 27.03.26 2026-03-27 07:56:03 +01:00
3c45ebd50f 27.03.26 2026-03-27 07:40:40 +01:00
22d186f0ac 27.03.26 2026-03-27 07:30:15 +01:00
0979021f41 27.03.26 2026-03-27 06:51:43 +01:00
af4ae99dc0 26.03.26 2026-03-26 22:08:56 +01:00
11c489f79d 26.03.26 2026-03-26 21:57:02 +01:00
620d6bb604 26.03.26 2026-03-26 21:50:47 +01:00
97cfd13da2 26.03.26 2026-03-26 21:43:25 +01:00
126455bf0f 26.03.26 2026-03-26 21:01:14 +01:00
97cc447e0a 26.03.26 2026-03-26 20:54:08 +01:00
e2bbd0e522 26.03.26 2026-03-26 20:31:45 +01:00
51778bda9e 26.03.26 2026-03-26 20:09:33 +01:00
c06ea3bd99 26.03.26 2026-03-26 19:58:14 +01:00
f0b0666773 26.03.26 2026-03-26 19:50:27 +01:00
edca5fed55 26.03.26 2026-03-26 19:30:48 +01:00
8b6b93171f 26.03.26 2026-03-26 19:15:14 +01:00
841f7a1c20 26.03.26 2026-03-26 19:02:01 +01:00
91acaba3dc 26.03.26 2026-03-26 18:28:43 +01:00
3e5320cf9e 26.03.26 2026-03-26 15:43:45 +01:00
f9010ddefd 26.03.26 2026-03-26 15:23:42 +01:00
d722b58f4e 26.03.26 2026-03-26 15:20:36 +01:00
3e646ea035 26.03.26 2026-03-26 15:16:34 +01:00
90f97daa7f 26.03.26 2026-03-26 11:13:35 +01:00
e1488e9677 26.03.26 2026-03-26 09:25:34 +01:00
0bcec5d3c4 25.03.26 2026-03-25 20:32:07 +01:00
10848ed533 25.03.26 2026-03-25 20:17:57 +01:00
b90d65d245 25.03.26 2026-03-25 20:09:51 +01:00
357478e74c 25.03.26 2026-03-25 19:29:32 +01:00
1f49c00cff 25.03.26 2026-03-25 18:45:54 +01:00
d38d0324d5 25.03.26 2026-03-25 16:49:05 +01:00
b8ec7a3d2d 25.03.26 2026-03-25 11:01:26 +01:00
0ac261b670 25.03.26 2026-03-25 09:59:15 +01:00
51411faff2 24.03.26 2026-03-24 11:49:14 +01:00
a94906bb53 23.03.26 2026-03-23 21:16:51 +01:00
2262e79361 23.03.26 2026-03-23 20:10:30 +01:00
5327ca4f21 23.03.26 2026-03-23 18:27:54 +01:00
03997db3e4 23.03.26 2026-03-23 18:03:59 +01:00
74e4ac0c1b 23.03.26 2026-03-23 17:57:26 +01:00
353cf17af7 23.03.26 2026-03-23 17:47:36 +01:00
412f8f00e0 23.03.26 2026-03-23 17:38:04 +01:00
6b841db4ce 23.03.26 2026-03-23 17:09:11 +01:00
4e3818d46a 23.03.26 2026-03-23 16:51:32 +01:00
8bb34aa4fb 23.03.26 2026-03-23 16:44:46 +01:00
0392b191c5 23.03.26 2026-03-23 16:34:28 +01:00
f98b0fd010 23.03.26 2026-03-23 16:25:22 +01:00
97dd672e6a 23.03.26 2026-03-23 16:09:39 +01:00
bde600e86a 23.03.26 2026-03-23 15:55:39 +01:00
0e7cabdc00 23.03.26 2026-03-23 14:13:09 +01:00
15f9f4b724 23.03.26 2026-03-23 14:00:28 +01:00
c24de18fcd 23.03.26 2026-03-23 13:49:32 +01:00
cb9c868e88 23.03.26 2026-03-23 11:26:23 +01:00
0e76aa6312 23.03.26 2026-03-23 11:18:58 +01:00
062ca10340 23.03.26 2026-03-23 11:13:50 +01:00
175c7a5e30 23.03.26 2026-03-23 10:49:36 +01:00
e6519d30ee 23.03.26 2026-03-23 10:43:02 +01:00
f9ee416d99 23.03.26 2026-03-23 10:36:33 +01:00
80deac62f8 23.03.26 2026-03-23 10:16:56 +01:00
b5d771341d 23.03.26 2026-03-23 10:10:53 +01:00
afdbd05375 23.03.26 2026-03-23 09:31:27 +01:00
36b94b98af 23.03.26 2026-03-23 09:10:55 +01:00
c655eb4170 22.03.26 2026-03-22 21:10:26 +01:00
ed11b23f79 22.03.26 2026-03-22 20:53:42 +01:00
140eae06a0 22.03.26 2026-03-22 19:11:12 +01:00
234084f073 22.03.26 2026-03-22 19:06:19 +01:00
2fc6fbbd76 22.03.26 2026-03-22 19:02:53 +01:00
0b9c85f5ad 22.03.26 2026-03-22 18:58:46 +01:00
717b51ad19 22.03.26 2026-03-22 18:51:48 +01:00
dfdaf7a1cd 22.03.26 2026-03-22 17:03:58 +01:00
7dba25c6d7 22.03.26 2026-03-22 15:25:08 +01:00
765d90512d 22.03.26 2026-03-22 15:02:24 +01:00
090b4ea5c6 22.03.26 2026-03-22 14:53:53 +01:00
1326c8df4c 22.03.26 2026-03-22 11:27:48 +01:00
d10d805753 22.03.26 2026-03-22 11:22:09 +01:00
85bb272edf 22.03.26 2026-03-22 09:43:36 +01:00
5f475d5714 22.03.26 2026-03-22 09:17:30 +01:00
8cc091ef57 22.03.26 2026-03-22 09:15:19 +01:00
7ba170e072 21.03.26 2026-03-21 22:18:29 +01:00
931b5b9ea2 21.03.26 2026-03-21 22:11:23 +01:00
eb98ef87bb 21.03.26 2026-03-21 22:03:43 +01:00
834ae46a14 21.03.26 2026-03-21 21:56:40 +01:00
7c746ad931 21.03.26 2026-03-21 20:10:55 +01:00
95f5c4af57 21.03.26 2026-03-21 20:00:42 +01:00
f83c62ffa8 21.03.26 2026-03-21 19:46:50 +01:00
1f4b36633b 21.03.26 2026-03-21 19:39:15 +01:00
36dc48bee0 21.03.26 2026-03-21 19:10:08 +01:00
fc52aa8bbb 21.03.26 2026-03-21 18:48:17 +01:00
5c516465e2 21.03.26 2026-03-21 18:36:48 +01:00
d827fa6140 21.03.26 2026-03-21 18:31:04 +01:00
93d27059af 21.03.26 2026-03-21 18:25:30 +01:00
d2a13b3c01 20.03.26 2026-03-20 14:38:42 +01:00
22c4766e66 20.03.26 2026-03-20 14:36:43 +01:00
73eeee0f72 20.03.26 2026-03-20 14:29:46 +01:00
260300681f 20.03.26 2026-03-20 13:53:10 +01:00
a2e0c36a3c 20.03.26 2026-03-20 13:50:31 +01:00
4f6641ceeb 20.03.26 2026-03-20 13:43:47 +01:00
cabd032bc6 20.03.26 2026-03-20 10:45:34 +01:00
b2e095aad5 20.03.26 2026-03-20 10:36:27 +01:00
8c3ba0a23d 20.03.26 2026-03-20 10:28:09 +01:00
13072de28f 20.03.26 2026-03-20 10:19:50 +01:00
a63a46cc40 20.03.26 2026-03-20 10:05:30 +01:00
bf6e9f6a78 20.03.26 2026-03-20 10:01:22 +01:00
6c7947c7c1 20.03.26 2026-03-20 09:11:57 +01:00
6954044f7e 20.03.26 2026-03-20 09:08:13 +01:00
d6207855a0 18.03.26 2026-03-18 18:27:29 +01:00
a41ec4412d 18.03.26 2026-03-18 18:02:49 +01:00
8c3c224c07 18.03.26 2026-03-18 11:06:05 +01:00
3581b7ea80 18.03.26 2026-03-18 10:53:29 +01:00
f7d87d3a78 17.03.26 2026-03-17 20:55:55 +01:00
5a52f09f97 17.03.26 2026-03-17 20:53:32 +01:00
420577cd72 17.03.26 2026-03-17 20:26:11 +01:00
01777f1536 17.03.26 2026-03-17 20:04:11 +01:00
24c8cd8075 17.03.26 2026-03-17 18:16:29 +01:00
7ace327d3f 17.03.26 2026-03-17 14:36:55 +01:00
a8d37bd766 17.03.26 2026-03-17 14:11:33 +01:00
6b96cdbc88 17.03.26 2026-03-17 13:58:08 +01:00
5bb1618b30 16.03.26 2026-03-16 22:35:09 +01:00
3ef819bf86 16.03.26 2026-03-16 22:26:45 +01:00
7ae54f9977 16.03.26 2026-03-16 22:19:15 +01:00
410604d890 16.03.26 2026-03-16 22:14:30 +01:00
c8bbf9a12e 16.03.26 2026-03-16 22:04:21 +01:00
f32cce6177 16.03.26 2026-03-16 21:33:57 +01:00
32b48cf6d1 16.03.26 2026-03-16 21:01:43 +01:00
eebcf936fe 16.03.26 2026-03-16 20:43:05 +01:00
127a38f857 16.03.26 2026-03-16 20:34:16 +01:00
1cedd11304 16.03.26 2026-03-16 20:28:10 +01:00
8881caccd0 16.03.26 2026-03-16 19:47:43 +01:00
59a6554fa3 16.03.26 2026-03-16 19:35:22 +01:00
3d80a6ba5e 16.03.26 2026-03-16 19:25:56 +01:00
07816b9cfe 16.03.26 2026-03-16 19:19:32 +01:00
5bbd68448a 16.03.26 2026-03-16 18:38:15 +01:00
185f26f31b 16.03.26 2026-03-16 15:40:19 +01:00
bbb88dbd22 16.03.26 2026-03-16 15:26:08 +01:00
3ad2ae2624 16.03.26 2026-03-16 15:24:46 +01:00
e78eb80d08 16.03.26 2026-03-16 14:46:11 +01:00
498da9a728 16.03.26 2026-03-16 14:41:29 +01:00
eac35b849f 16.03.26 2026-03-16 14:21:41 +01:00
13770cc299 16.03.26 2026-03-16 14:15:42 +01:00
72b71675ae 16.03.26 2026-03-16 11:08:47 +01:00
5cf5ca520a 16.03.26 2026-03-16 10:59:28 +01:00
5fe0be5c59 16.03.26 2026-03-16 10:48:54 +01:00
c504b4b2d7 16.03.26 2026-03-16 10:41:50 +01:00
bbd3d30b37 16.03.26 2026-03-16 10:32:27 +01:00
b4d09d3a69 16.03.26 2026-03-16 10:23:13 +01:00
8e7f9648fc 16.03.26 2026-03-16 10:06:58 +01:00
2183d8d5a4 16.03.26 2026-03-16 09:52:15 +01:00
8398a8c212 15.03.26 2026-03-15 15:59:16 +01:00
8859ccad1a 15.03.26 2026-03-15 15:53:38 +01:00
c2adc96fbf 15.03.26 2026-03-15 15:26:56 +01:00
9330662b30 15.03.26 2026-03-15 15:21:23 +01:00
eadbbbbbc8 15.03.26 2026-03-15 15:15:22 +01:00
5b12e5f8db 15.03.26 2026-03-15 13:06:10 +01:00
1d10591cf6 15.03.26 2026-03-15 12:02:50 +01:00
fd3ce2188a 15.03.26 2026-03-15 11:59:15 +01:00
4695a93e44 15.03.26 2026-03-15 11:48:43 +01:00
93f0e6b0af 15.03.26 2026-03-15 11:45:42 +01:00
319d8a4afb 15.03.26 2026-03-15 11:28:29 +01:00
b6e85ee710 15.03.26 2026-03-15 10:58:53 +01:00
c349441874 13.03.26 2026-03-13 14:58:00 +01:00
73fdbe9eba 13.03.26 2026-03-13 14:51:55 +01:00
b20b0c8df6 13.03.26 2026-03-13 14:47:13 +01:00
cc5f4da38c 13.03.26 2026-03-13 14:28:23 +01:00
016112a355 13.03.26 2026-03-13 10:43:58 +01:00
0cf03a75b0 13.03.26 2026-03-13 10:24:54 +01:00
7f6b93094f 12.03.26 2026-03-12 15:20:29 +01:00
ad58d40da7 12.03.26 2026-03-12 15:14:03 +01:00
f0c1b8909e 12.03.26 2026-03-12 14:56:09 +01:00
8e8afe39d0 12.03.26 2026-03-12 14:44:04 +01:00
8da50c72c7 12.03.26 2026-03-12 11:39:43 +01:00
683d3600ac 12.03.26 2026-03-12 10:56:52 +01:00
e2cb840844 12.03.26 2026-03-12 10:51:39 +01:00
19a4363d10 12.03.26 2026-03-12 10:46:48 +01:00
23c4edfec5 12.03.26 2026-03-12 10:24:34 +01:00
b07ad57a36 11.03.26 2026-03-11 22:17:48 +01:00
6d06125360 11.03.26 2026-03-11 22:12:38 +01:00
4a14a78f78 11.03.26 2026-03-11 21:33:42 +01:00
2fba795b11 11.03.26 2026-03-11 21:27:43 +01:00
b23dba865f 11.03.26 2026-03-11 21:14:53 +01:00
0b1bb2ffa5 11.03.26 2026-03-11 21:06:45 +01:00
6890d0de07 11.03.26 2026-03-11 20:53:20 +01:00
c180e8926f 11.03.26 2026-03-11 20:51:46 +01:00
0fe16df326 11.03.26 2026-03-11 20:31:47 +01:00
3e15052520 11.03.26 2026-03-11 20:14:47 +01:00
dee1896a6c 10.03.26 2026-03-10 21:18:19 +01:00
faf5fa605e 10.03.26 2026-03-10 15:33:22 +01:00
a7846b359c 10.03.26 2026-03-10 15:21:47 +01:00
c0cffde079 10.03.26 2026-03-10 15:01:09 +01:00
814071e4c1 10.03.26 2026-03-10 14:49:10 +01:00
30ae86a987 10.03.26 2026-03-10 14:44:16 +01:00
1de11a846b 10.03.26 2026-03-10 14:29:12 +01:00
93acacb955 10.03.26 2026-03-10 13:04:13 +01:00
6d053dfe03 10.03.26 2026-03-10 12:59:07 +01:00
5f49e01495 10.03.26 2026-03-10 12:54:46 +01:00
d609864822 10.03.26 2026-03-10 12:46:03 +01:00
aff4459942 09.03.26 2026-03-09 20:35:42 +01:00
840f03e5d8 09.03.26 2026-03-09 20:32:51 +01:00
039b278757 09.03.26 2026-03-09 20:27:08 +01:00
265b41b206 09.03.26 2026-03-09 20:15:09 +01:00
5235b381c6 09.03.26 2026-03-09 20:10:53 +01:00
52c58df547 09.03.26 2026-03-09 20:01:20 +01:00
cc8540ee91 09.03.26 2026-03-09 18:17:32 +01:00
d70a784db8 09.03.26 2026-03-09 18:05:08 +01:00
e6984e3393 09.03.26 2026-03-09 17:59:26 +01:00
f4ae7ebd7c 09.03.26 2026-03-09 17:56:21 +01:00
5f744ec25c 09.03.26 2026-03-09 17:53:54 +01:00
1dc69008d8 09.03.26 2026-03-09 17:50:26 +01:00
96aab15a7a 09.03.26 2026-03-09 16:52:19 +01:00
dd06fc523c 09.03.26 2026-03-09 16:11:42 +01:00
7cafd8381f 09.03.26 2026-03-09 15:49:51 +01:00
c009b15ed0 09.03.26 2026-03-09 15:35:27 +01:00
6717feb22d 09.03.26 2026-03-09 15:29:29 +01:00
7998467adf 09.03.26 2026-03-09 15:24:15 +01:00
83ef3c6ae9 09.03.26 2026-03-09 15:17:34 +01:00
372d04b30f 08.03.26 2026-03-08 09:09:10 +01:00
4e6ddc39c1 08.03.26 2026-03-08 08:53:02 +01:00
a3f15c30e6 06.03.26 2026-03-06 14:24:18 +01:00
cdb0bf7254 05.03.26 2026-03-05 19:49:16 +01:00
843ade6f3d 05.03.26 2026-03-05 19:38:03 +01:00
62f731530b 05.03.26 2026-03-05 19:34:46 +01:00
14c2548c16 05.03.26 2026-03-05 19:32:55 +01:00
13000f110d 05.03.26 2026-03-05 19:25:17 +01:00
915fd17d83 05.03.26 2026-03-05 18:38:24 +01:00
12de27d105 05.03.26 2026-03-05 18:16:22 +01:00
1a1f675cf6 05.03.26 2026-03-05 18:12:22 +01:00
3b67f3c251 05.03.26 2026-03-05 18:10:49 +01:00
551a040df0 05.03.26 2026-03-05 18:07:54 +01:00
b6c3279917 05.03.26 2026-03-05 18:01:05 +01:00
2b956e6142 05.03.26 2026-03-05 17:53:49 +01:00
ae0817e0ac 05.03.26 2026-03-05 15:30:11 +01:00
7451e19125 05.03.26 2026-03-05 12:30:47 +01:00
52418b6244 05.03.26 2026-03-05 12:27:01 +01:00
a577595ee6 05.03.26 2026-03-05 11:24:40 +01:00
8db31ddaf0 05.03.26 2026-03-05 11:03:44 +01:00
e371b116f7 05.03.26 2026-03-05 10:58:01 +01:00
3212d551d2 05.03.26 2026-03-05 10:54:10 +01:00
8d557a63a0 05.03.26 2026-03-05 10:50:48 +01:00
ee9866c28e 05.03.26 2026-03-05 10:37:46 +01:00
8d9dd6c275 05.03.26 2026-03-05 10:21:37 +01:00
0841fb4609 05.03.26 2026-03-05 10:13:43 +01:00
5b059b70e6 04.03.26 2026-03-04 18:59:39 +01:00
3319563b22 04.06.26 2026-03-04 18:52:02 +01:00
ddc2884f61 04.03.26 2026-03-04 18:29:56 +01:00
5dfd24b5de 04.03.26 2026-03-04 18:05:42 +01:00
7ae145b6c1 04.03.26 2026-03-04 17:56:58 +01:00
7909b10636 04.03.26 2026-03-04 17:43:53 +01:00
bab6c14499 04.03.26 2026-03-04 17:32:49 +01:00
c816ecdbb7 04.03.26 2026-03-04 17:23:14 +01:00
024f8f78bd 04.03.26 2026-03-04 17:09:32 +01:00
94595346b7 04.03.26 2026-03-04 16:54:32 +01:00
8a4d730c5a 04.03.26 2026-03-04 16:35:29 +01:00
3226f55a7d 04.03.26 2026-03-04 16:34:25 +01:00
8eba77b26c 04.03.26 2026-03-04 16:19:40 +01:00
a0c1408f8e 04.03.26 2026-03-04 16:18:42 +01:00
7e726010b8 03.03.26 2026-03-03 20:10:02 +01:00
4abd9b7e85 03.03.26 2026-03-03 19:49:15 +01:00
e984c026b9 03.03.26 2026-03-03 19:48:09 +01:00
4affffaf0a 03.03.26 2026-03-03 19:34:31 +01:00
c55338e879 03.03.26 2026-03-03 16:30:37 +01:00
1c288d9646 03.03.26 2026-03-03 16:20:38 +01:00
d37bad643d 03.03.26 2026-03-03 16:08:37 +01:00
f493901e8d 03.03.26 2026-03-03 14:00:05 +01:00
934f3511f7 03.03.26 2026-03-03 13:56:57 +01:00
62a1bc69f7 03.03.26 2026-03-03 13:54:04 +01:00
741389b71c 03.03.26 2026-03-03 13:50:58 +01:00
9dd4543fc1 03.03.26 2026-03-03 13:47:46 +01:00
af89743b40 03.03.26 2026-03-03 13:23:19 +01:00
7f0400400c 03.03.26 2026-03-03 12:05:07 +01:00
d9cf6c9a49 03.03.26 2026-03-03 12:00:35 +01:00
1a9114da7e 02.03.26 2026-03-02 10:11:37 +01:00
b5bc559b11 27.02.26 2026-02-27 07:21:16 +01:00
8e67c7aecb 27.02.26 2026-02-27 07:17:24 +01:00
6b884af9cc 27.02.26 2026-02-27 07:04:54 +01:00
e768fe71a3 27.02.26 2026-02-27 06:55:07 +01:00
5f8d081b0e 27.02.26 2026-02-27 06:32:45 +01:00
20c3dacded 27.02.26 2026-02-27 06:24:01 +01:00
6fdf5e1994 27.02.26 2026-02-27 06:17:55 +01:00
92210a381a 27.02.26 2026-02-27 06:12:39 +01:00
cd2a82f61c 27.02.26 2026-02-27 05:56:00 +01:00
4eae444c93 27.02.26 2026-02-27 05:48:07 +01:00
02993336de 26.02.26 2026-02-26 21:32:41 +01:00
2c38d2742b 26.02.26 2026-02-26 20:32:42 +01:00
a724e0a086 26.02.26 2026-02-26 20:21:42 +01:00
399d9da9e5 26.02.26 2026-02-26 16:38:38 +01:00
6f0e0d65ee 26.02.26 2026-02-26 16:28:43 +01:00
20f2b87d99 24.02.26 2026-02-24 20:25:32 +01:00
56f2c3a4a6 24.02.26 2026-02-24 19:48:12 +01:00
a1a13a6846 24.02.26 2026-02-24 19:24:31 +01:00
c244cf658c 23.02.26 2026-02-23 15:33:09 +01:00
6fc0e5982a 19.02.26 2026-02-19 22:53:36 +01:00
84fa03bf77 19.02.26 2026-02-19 22:19:27 +01:00
11cabcee32 18.02.26 2026-02-18 16:58:53 +01:00
1cdc54e59e 18.02.26 2026-02-18 16:37:20 +01:00
d2288e009f 18.02.26 2026-02-18 16:04:35 +01:00
d81b19bfc6 18.02.26 2026-02-18 15:13:50 +01:00
b2fb1e9c4c 18.02.26 2026-02-18 13:32:48 +01:00
cac84abbef 18.02.26 2026-02-18 12:14:35 +01:00
191abf022f 18.02.26 2026-02-18 12:03:40 +01:00
0011a7f943 18.02.26 2026-02-18 11:03:27 +01:00
697b53e68a 16.02.26 2026-02-16 20:01:57 +01:00
0e5c40e9a2 16.02.26 2026-02-16 19:55:09 +01:00
bb313d29bf 1602.26 2026-02-16 15:46:43 +01:00
3f54cbef6a 15.02.26 2026-02-15 15:54:44 +01:00
00cae2f82b 15.02.26 2026-02-15 15:49:40 +01:00
d1d37ee4b3 15.02.26 2026-02-15 15:33:32 +01:00
08a66bc219 15.02.26 2026-02-15 15:20:55 +01:00
35c9731a85 15.02.26 2026-02-15 15:02:42 +01:00
d458e9849d 15.02.26 2026-02-15 14:54:10 +01:00
b94d6e911d 15.02.26 2026-02-15 14:45:31 +01:00
4fb39d564e 15.02.26 2026-02-15 14:37:04 +01:00
10b370f11b 15.02.26 2026-02-15 14:33:14 +01:00
479e0d4d5a 15.02.26 2026-02-15 14:23:33 +01:00
4059cd591e 15.02.26 2026-02-15 12:47:07 +01:00
9565e82850 15.02.26 2026-02-15 11:37:46 +01:00
29ada1899e 15.02.26 2026-02-15 11:27:59 +01:00
67d3ce545e 15.02.26 2026-02-15 10:16:08 +01:00
9b8d372fa6 15.02.26 2026-02-15 10:13:02 +01:00
dda6a63e74 15.02.26 2026-02-15 10:08:31 +01:00
2fc89282b9 15.02.26 2026-02-15 10:02:55 +01:00
cca82dd9f7 14.02.26 2026-02-15 09:40:05 +01:00
1e51061ddc 14.02.26 2026-02-15 09:36:08 +01:00
267e5a3509 14.12.26 2026-02-14 21:30:28 +01:00
e040a4fc11 14.02.26 2026-02-14 20:59:35 +01:00
6f4649c778 14.02.26 2026-02-14 20:19:52 +01:00
177f862263 14.02.26 2026-02-14 20:16:28 +01:00
93456e9530 14.02.26 2026-02-14 20:12:56 +01:00
8ee9354327 14.02.26 2026-02-14 18:46:52 +01:00
6f4ad3723f 14.02.26 2026-02-14 18:41:16 +01:00
f3370e89ea 14.02.26 2026-02-14 18:36:26 +01:00
330fc8d320 14.02.26 2026-02-14 18:28:15 +01:00
f0b979ac8c 14.02.26 2026-02-14 18:16:29 +01:00
f3ebeb7cd3 14.02.26 2026-02-14 18:02:08 +01:00
981a97ed05 14.02.26 2026-02-14 17:53:36 +01:00
9ed4d5f6bb 14.02.26 2026-02-14 17:38:43 +01:00
e43189f052 14.02.26 2026-02-14 16:07:08 +01:00
0fcaca2b5a 14.02.26 2026-02-14 08:55:57 +01:00
3db2f8c96e 14.02.26 2026-02-14 08:52:31 +01:00
b0ebc02434 14.02.26 2026-02-14 08:47:57 +01:00
0be0194ab5 14.02.26 2026-02-14 08:32:38 +01:00
7d99f56fa5 13.02.26 2026-02-13 17:15:48 +01:00
6eec004781 13.02.26 2026-02-13 09:53:54 +01:00
70603d95d4 11.02.26 2026-02-11 13:14:58 +01:00
c78f8b1079 11.02.26 2026-02-11 12:10:29 +01:00
9ec76410f7 11.02.26 2026-02-11 08:41:58 +01:00
e47023c4ba 11.02.26 2026-02-11 08:32:24 +01:00
133805eab5 11.02.26 2026-02-11 08:23:00 +01:00
878588d567 11.02.26 2026-02-11 08:20:23 +01:00
d88a9f204c 11.02.26 2026-02-11 08:13:20 +01:00
1ab2b56f32 11.02.26 2026-02-11 07:58:02 +01:00
b41fe0fa54 11.02.26 2026-02-11 07:43:25 +01:00
1326669f01 11.02.26 2026-02-11 07:36:47 +01:00
e37cd4a1cc 11.02.26 2026-02-11 07:26:48 +01:00
1eb86892de 10.02.26 2026-02-10 15:08:45 +01:00
91095f28e9 09.02.26 2026-02-09 20:55:10 +01:00
e41ad51634 09.02.26 2026-02-09 20:30:56 +01:00
2eabfe2587 09.02.26 2026-02-09 15:13:57 +01:00
a71d666619 09.02.26 2026-02-09 14:50:27 +01:00
38254fee48 09.02.26 2026-02-09 14:49:01 +01:00
5cc66b3a3a 08.02.26 2026-02-08 22:49:57 +01:00
d007db7a1b 08.02.26 2026-02-08 22:46:43 +01:00
935862a66c 08.02.26 2026-02-08 22:31:50 +01:00
0c83fcf35e 08.02.26 2026-02-08 22:26:24 +01:00
bc5aa57319 08.02.26 2026-02-08 22:18:54 +01:00
231b733222 08.02.26 2026-02-08 22:06:19 +01:00
c02d7212bb 08.02.26 2026-02-08 21:54:51 +01:00
df5ef074ad 08.02.26 2026-02-08 21:51:04 +01:00
04179fde77 08.02.26 2026-02-08 21:36:01 +01:00
0cd9d63b15 08.02.26 2026-02-08 21:28:17 +01:00
b5b2f1c362 08.02.26 2026-02-08 21:25:22 +01:00
14dd557e58 08.02.26 2026-02-08 19:24:31 +01:00
1e7cd1c7be 08.02.26 2026-02-08 19:19:57 +01:00
0a04774fac 08.02.26 2026-02-08 19:15:12 +01:00
720ad804a4 08.02.26 2026-02-08 18:10:36 +01:00
175b10e59d 08.02.26 2026-02-08 18:05:29 +01:00
03d923441e 08.02.26 2026-02-08 16:55:56 +01:00
3da61070eb 08.02.26 2026-02-08 15:11:53 +01:00
6a0acb1c20 08.02.26 2026-02-08 10:58:08 +01:00
3195994444 08.02.26 2026-02-08 10:42:18 +01:00
7ab361c2b8 08.02.26 2026-02-08 10:27:59 +01:00
894375c143 08.02.26 2026-02-08 10:15:36 +01:00
225df5c6da 08.02.26 2026-02-08 10:08:37 +01:00
1a2d595303 08.02.26 2026-02-08 09:40:37 +01:00
cccff5726c 08.02.26 2026-02-08 09:37:53 +01:00
9a354181b0 08.02.26 2026-02-08 09:34:35 +01:00
c7dce8bedc 08.02.26 2026-02-08 09:31:31 +01:00
adecdbdb4f 08.02.26 2026-02-08 09:28:30 +01:00
06d40ddbae 08.02.26 2026-02-08 09:25:34 +01:00
e009a9e1e3 08.02.26 2026-02-08 09:22:43 +01:00
d22610abb3 08.02.26 2026-02-08 09:18:35 +01:00
8c033f4eab 08.02.26 2026-02-08 09:14:52 +01:00
cb6c1819f4 08.02.26 2026-02-08 09:09:03 +01:00
762cfc66c1 08.02.26 2026-02-08 09:05:47 +01:00
1d653b2756 08.02.26 2026-02-08 09:04:04 +01:00
b7927b787d 08.02.26 2026-02-08 09:00:54 +01:00
e57019b39c 08.02.26 2026-02-08 08:58:03 +01:00
a09a88ff5f 08.02.26 2026-02-08 08:52:30 +01:00
c1a4b442a3 07.02.26 2026-02-07 20:21:51 +01:00
34166dedc6 07.02.26 2026-02-07 19:31:54 +01:00
9095f3b470 07.02.26 2026-02-07 19:18:42 +01:00
5255959614 07.02.26 2026-02-07 17:11:13 +01:00
1b92b1d207 07.02.26 2026-02-07 17:08:29 +01:00
67eca35a9a 07.02.26 2026-02-07 16:40:48 +01:00
a35e304561 07.02.26 2026-02-07 16:39:34 +01:00
f93c81624d 07.02.26 2026-02-07 16:32:12 +01:00
d47c2cb5fb 07.02.26 2026-02-07 16:28:00 +01:00
45ab8e8904 07.02.26 2026-02-07 16:25:37 +01:00
0f3497e867 07.02.26 2026-02-07 16:23:58 +01:00
b0e19226eb 07.02.26 2026-02-07 16:13:36 +01:00
95589bcf73 07.02.26 2026-02-07 16:10:00 +01:00
03c7f41457 07.02.26 2026-02-07 16:07:54 +01:00
cd0c068b3f 05.02.26 2026-02-05 19:54:14 +01:00
05d5d85bee 05.02.26 2026-02-05 19:46:23 +01:00
d213328fd9 05.02.26 2026-02-05 19:40:38 +01:00
9235c22e04 05.02.26 2026-02-05 19:36:55 +01:00
c1ca8c707c 05.02.26 2026-02-05 19:29:06 +01:00
6c4ded50d0 05.02.26 2026-02-05 19:19:45 +01:00
86b8d3e250 05.02.26 2026-02-05 18:19:09 +01:00
7b94c56e7c 05.02.26 2026-02-05 18:13:10 +01:00
c6f963b6be 05.02.26 2026-02-05 18:04:07 +01:00
181217e755 05.02.26 2026-02-05 17:09:54 +01:00
00a6a5debe 05.02.26 2026-02-05 17:06:00 +01:00
91f85ea7c1 05.02.26 2026-02-05 16:48:16 +01:00
aaf4a264a1 04.02.26 2026-02-04 21:43:53 +01:00
b03a97d02d 04.02.26 2026-02-04 21:34:46 +01:00
a69a9dcb57 04.02.26 2026-02-04 21:20:34 +01:00
0a88b26160 04.02.26 2026-02-04 21:06:10 +01:00
3a79652c59 04.02.26 2026-02-04 18:40:28 +01:00
a0ed097b6c 04.02.26 2026-02-04 18:34:16 +01:00
a9d6f8fa58 04.02.26 2026-02-04 18:18:50 +01:00
2ebad01847 04.02.26 2026-02-04 18:02:09 +01:00
71716c1ad5 04.02.26 2026-02-04 15:40:30 +01:00
240f35af26 04.02.26 2026-02-04 15:18:06 +01:00
1ebbb874ef 04.02.26 2026-02-04 15:00:37 +01:00
eef972c679 03.02.26 2026-02-03 20:26:05 +01:00
011af03e8a 03.02.26 2026-02-03 20:18:55 +01:00
24843ab72d 03.02.26 2026-02-03 19:33:01 +01:00
65c0053df4 03.02.26 2026-02-03 16:01:54 +01:00
f391e17fb6 03.02.26 2026-02-03 15:57:12 +01:00
44fc7dc855 03.02.26 2026-02-03 15:53:32 +01:00
99d895c951 03.02.26 2026-02-03 15:43:38 +01:00
39e59b5b8b 03.02.26 2026-02-03 15:09:24 +01:00
7bc3e350ba 03.02.26 2026-02-03 15:06:27 +01:00
fe7c10d527 03.02.26 2026-02-03 14:33:40 +01:00
272875eb69 03.02.26 2026-02-03 14:30:36 +01:00
16df1c99a6 03.02.26 2026-02-03 14:03:46 +01:00
403db5133e 03.02.26 2026-02-03 13:58:00 +01:00
43312ba412 03.02.26 2026-02-03 13:49:05 +01:00
f4a53f2705 03.02.26 2026-02-03 12:19:44 +01:00
0a41765551 02.02.26 2026-02-02 19:27:23 +01:00
28789200c3 02.02.26 2026-02-02 18:25:39 +01:00
350e41714f 02.02.26 2026-02-02 17:56:51 +01:00
84b70b73c5 02.02.26 2026-02-02 17:51:26 +01:00
68e0000afc 02.02.26 2026-02-02 17:44:47 +01:00
33eeabf5e1 02.02.26 2026-02-02 17:33:40 +01:00
a4f0a08469 02.02.26 2026-02-02 17:27:14 +01:00
7f7b103945 02.02.26 2026-02-02 17:17:49 +01:00
8fddb599ef 02.02.26 2026-02-02 17:10:30 +01:00
313d574a08 02.02.26 2026-02-02 17:05:27 +01:00
f7d6ed00fd 02.02.26 2026-02-02 17:02:14 +01:00
6c570acfe6 02.02.26 2026-02-02 16:49:23 +01:00
2ba836c284 02.02.26 2026-02-02 16:42:57 +01:00
5b32a402a6 02.02.26 2026-02-02 16:23:27 +01:00
10aaff4cc8 02.02.26 2026-02-02 16:08:35 +01:00
40025169d4 02.02.26 2026-02-02 16:04:07 +01:00
38a903469c 02.02.26 2026-02-02 15:59:20 +01:00
70a0a41787 02.02.26 2026-02-02 15:26:05 +01:00
318f9e5564 02.02.26 2026-02-02 15:16:37 +01:00
d73816285e 02.02.26 2026-02-02 12:50:57 +01:00
86ac172cfd 02.02.26 2026-02-02 12:33:54 +01:00
703c7e46fa 02.02.26 2026-02-02 12:26:58 +01:00
747c2e4a0e 02.02.26 2026-02-02 12:08:46 +01:00
7d89418874 02.02.26 2026-02-02 10:42:08 +01:00
64806b143b 02.02.26 2026-02-02 08:44:09 +01:00
7b669a27a8 02.02.26 2026-02-02 08:42:48 +01:00
62c2080fee 01.02.26 2026-02-01 21:36:39 +01:00
f5d6d4ef55 01.02.26 2026-02-01 20:58:57 +01:00
4513ce8ac7 01.02.26 2026-02-01 18:03:12 +01:00
88eb890c46 01.02.26 2026-02-01 17:55:54 +01:00
9a66952f50 30.01.26 2026-01-30 08:07:41 +01:00
d0db0abdd4 30.01.26 2026-01-30 07:47:15 +01:00
d9cb2b7961 30.01.26 2026-01-30 07:27:45 +01:00
3eedc0b30e 30.01.26 2026-01-30 06:31:16 +01:00
b51ee12331 29.01.26 2026-01-29 17:49:20 +01:00
744e147a1a 29.01.26 2026-01-29 17:23:40 +01:00
8cc0bffcae 29.01.26 2026-01-29 14:05:17 +01:00
2b3cf5c095 29.01.26 2026-01-29 13:17:11 +01:00
94f15d66da 29.01.26 2026-01-29 12:39:47 +01:00
8edd5ea901 29.01.26 2026-01-29 11:26:21 +01:00
1e23528258 29.01.26 2026-01-29 11:06:23 +01:00
9447a685f2 29.01.26 2026-01-29 10:31:42 +01:00
7155708d4b 28.01.26 2026-01-28 16:52:54 +01:00
a76b8798ae 28.01.26 2026-01-28 15:57:49 +01:00
016b92e5aa 28.01.26 2026-01-28 15:55:04 +01:00
6085879d6f 28.01.26 2026-01-28 15:51:43 +01:00
42652d0fcb 28.01.26 2026-01-28 15:39:15 +01:00
5e8917430b 28.01.26 2026-01-28 11:46:00 +01:00
cdcefe6fbc 28.01.26 2026-01-28 11:33:18 +01:00
0678263f3e 27.01.26 2026-01-27 23:15:45 +01:00
aa69b8157e 27.01.26 2026-01-27 23:03:54 +01:00
47ce23f4e8 27.01.26 2026-01-27 22:56:39 +01:00
f6fe3fb2bd 27.01.26 2026-01-27 22:50:34 +01:00
fabc380784 27.01.26 2026-01-27 22:42:31 +01:00
7c845d0f9a 27.01.26 2026-01-27 20:36:18 +01:00
50da3d7a68 27.01.26 2026-01-27 20:31:34 +01:00
da1b268862 27.01.26 2026-01-27 19:05:30 +01:00
73e46b399b 27.01.26 2026-01-27 18:54:53 +01:00
431062f123 27.01.26 2026-01-27 18:45:09 +01:00
3846932517 27.01.26 2026-01-27 18:29:50 +01:00
aa10a58b60 27.01.26 2026-01-27 18:21:00 +01:00
9db6d2cb45 27.01.26 2026-01-27 18:13:12 +01:00
509dcac22e 27.01.26 2026-01-27 18:07:41 +01:00
f3102ee835 27.01.26 2026-01-27 18:00:46 +01:00
4c3056326b 27.01.26 2026-01-27 17:56:54 +01:00
1339f51256 27.01.26 2026-01-27 17:47:37 +01:00
408c5d82e4 27.01.26 2026-01-27 17:26:42 +01:00
71a58814b4 27.01.26 2026-01-27 17:05:39 +01:00
870f2e93f4 27.01.26 2026-01-27 16:45:55 +01:00
842a2b1da6 27.01.27 2026-01-27 10:28:36 +01:00
bf7de1e5d1 26.01.26 2026-01-26 23:06:55 +01:00
ba0dd0e0fb 26.01.26 2026-01-26 23:03:52 +01:00
e0c7480dd2 26.01.26 2026-01-26 23:01:04 +01:00
9b324ca7cd 26.01.26 2026-01-26 22:57:35 +01:00
0cb824076b 26.01.26 2026-01-26 22:51:59 +01:00
02320ce9c8 26.01.26 2026-01-26 22:45:26 +01:00
6d82621a34 26.01.26 2026-01-26 22:41:31 +01:00
0dae857364 26.01.26 2026-01-26 22:38:27 +01:00
9a974365a7 26.01.26 2026-01-26 22:32:59 +01:00
e9e57803ae 26.01.26 2026-01-26 22:29:04 +01:00
a3b7d11ae3 26.01.26 2026-01-26 22:23:08 +01:00
2248f49f9d 26.01.26 2026-01-26 22:18:39 +01:00
f96e73d7b9 26.01.26 2026-01-26 22:15:30 +01:00
ece0e55f98 26.01.26 2026-01-26 22:09:09 +01:00
db207f4995 26.01.26 2026-01-26 22:05:58 +01:00
f94f8d3abe 26.01.26 2026-01-26 21:50:32 +01:00
017eb92c6a 26.01.26 2026-01-26 21:43:41 +01:00
765d526c16 26.01.26 2026-01-26 21:31:31 +01:00
e71c6b14d0 26.01.26 2026-01-26 21:26:39 +01:00
210adad3f6 26.01.26 2026-01-26 21:18:51 +01:00
72b1d4ffcd 26.01.26 2026-01-26 21:14:21 +01:00
e2fc7178c4 26.01.26 2026-01-26 21:06:23 +01:00
58b360ed6c 26.01.26 2026-01-26 21:00:36 +01:00
985f4dc19d 26.01.26 2026-01-26 20:48:41 +01:00
757b9f724d 26.01.26 2026-01-26 17:32:33 +01:00
fbfc943301 26.01.26 2026-01-26 13:16:42 +01:00
4c70f0bc5f 26.01.26 2026-01-26 12:42:49 +01:00
ef27c020e0 25.01.26 2026-01-25 14:41:47 +01:00
23d9d88492 22.01.26 2026-01-22 13:55:09 +01:00
fffbb9708a 20.01.26 2026-01-20 22:14:35 +01:00
e7ff1fd56c 20.01.26 2026-01-20 21:56:09 +01:00
5b962eeb1d 20.01.26 2026-01-20 21:38:23 +01:00
08581f24ea 20.01.26 2026-01-20 21:35:00 +01:00
5e9141f03a 20.01.26 2026-01-20 21:30:14 +01:00
2269fc7709 20.01.26 2026-01-20 20:59:36 +01:00
b966366bc4 20.01.26 2026-01-20 20:53:34 +01:00
e2b0695cb8 20.01.26 2026-01-20 20:48:21 +01:00
000547ae21 20.01.26 2026-01-20 20:42:59 +01:00
8cc19b67f9 20.01.26 2026-01-20 20:31:49 +01:00
fcb6377b21 20.01.26 2026-01-20 20:19:56 +01:00
8a54b6fcbc 20.01.26 2026-01-20 20:13:08 +01:00
ecdc4b283d 20.01.26 2026-01-20 19:46:48 +01:00
d9ab3cfe47 20.01.26 2026-01-20 19:36:31 +01:00
72fc0c5431 20.01.26 2026-01-20 19:15:22 +01:00
044096c8d7 20.01.26 2026-01-20 19:11:43 +01:00
96b6be3398 20.01.26 2026-01-20 19:08:19 +01:00
42fb2e0d22 20.01.26 2026-01-20 19:06:46 +01:00
227eb3cf46 20.01.26 2026-01-20 19:04:54 +01:00
0e007afed3 20.01.26 2026-01-20 19:02:44 +01:00
ddf023f078 20.01.26 2026-01-20 18:59:40 +01:00
a9d1fe7baf 20.01.26 2026-01-20 17:30:13 +01:00
aeadca6f60 20.01.26 2026-01-20 17:12:07 +01:00
17233d8ded 20.01.26 2026-01-20 17:02:55 +01:00
797783b59e 20.01.26 2026-01-20 16:42:52 +01:00
e5e76e2dcb 20.01.26 2026-01-20 16:13:52 +01:00
0fc80958af 20.01.26 2026-01-20 16:06:15 +01:00
b43ab082b4 20.01.26 2026-01-20 15:36:55 +01:00
fcf61605d6 20.01.26 2026-01-20 15:30:40 +01:00
c5053638df 20.01.26 2026-01-20 15:25:15 +01:00
c5fbd3e528 20.01.26 2026-01-20 14:57:16 +01:00
117c33a2ea 20.01.26 2026-01-20 14:55:04 +01:00
327de18f1a 20.01.26 2026-01-20 14:50:32 +01:00
437904bcd8 20.01.26 2026-01-20 13:08:52 +01:00
6d75b4660e 20.01.26 2026-01-20 11:56:38 +01:00
a110019345 20.01.26 2026-01-20 11:40:44 +01:00
da530a2211 20.01.26 2026-01-20 11:30:53 +01:00
51a512e568 20.01.26 2026-01-20 11:25:29 +01:00
5df6378e80 20.01.26 2026-01-20 11:14:09 +01:00
a479dc718b 20.01.26 2026-01-20 11:04:53 +01:00
c39bb1d6d1 18.01.26 2026-01-18 23:59:24 +01:00
5c1ca6a895 18.01.26 2026-01-18 23:55:08 +01:00
24c1186061 18.01.26 2026-01-18 23:30:49 +01:00
8ae4aeb3e8 18.01.26 2026-01-18 23:00:29 +01:00
9daa701a28 18.01.26 2026-01-18 22:23:09 +01:00
336c2415b3 18.01.26 2026-01-18 22:08:41 +01:00
49731501d6 18.01.26 2026-01-18 22:00:54 +01:00
661d38404a 18.01.26 2026-01-18 21:28:40 +01:00
e869338703 18.01.26 2026-01-18 21:22:41 +01:00
b55c1e3c8f 18.01.26 2026-01-18 21:12:27 +01:00
3820f00182 18.01.26 2026-01-18 21:07:55 +01:00
178fb25a2f 18.01.26 2026-01-18 20:52:20 +01:00
6b2d5aebf8 18.01.26 2026-01-18 20:02:49 +01:00
0d3bfda08c 18.01.26 2026-01-18 12:07:27 +01:00
11f33236cf 18.01.26 2026-01-18 11:05:00 +01:00
db7288c709 18.01.26 2026-01-18 10:51:40 +01:00
a31e51152b 18.01.26 2026-01-18 10:27:55 +01:00
97b8632b74 18.01.26 2026-01-18 10:18:57 +01:00
2ebbe334bf 18.01.26 2026-01-18 10:06:48 +01:00
7571a1cb80 17.01.26 2026-01-17 18:15:29 +01:00
b4f794c275 17.01.26 2026-01-17 17:55:07 +01:00
2f1adffba3 17.01.26 2026-01-17 17:44:01 +01:00
0047e6e879 17.01.26 2026-01-17 17:36:39 +01:00
e8453c76a7 17.01.26 2026-01-17 17:00:35 +01:00
5d7d0ffe5b 17.01.26 2026-01-17 16:54:51 +01:00
39c9d83f1b 17.01.26 2026-01-17 14:12:11 +01:00
b38f7553a6 17.01.26 2026-01-17 14:03:45 +01:00
9b887a6b4c 17.01.26 2026-01-17 09:59:48 +01:00
b43db20bc4 17.01.26 2026-01-17 09:53:19 +01:00
8e38ceb13e 17.01.26 2026-01-17 09:47:06 +01:00
18c3cf21c6 17.01.26 2026-01-17 09:36:45 +01:00
a73564a24d Merge pull request '17.01.26' (#6) from dev into main
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/6
2026-01-17 08:26:19 +00:00
9c6029d152 17.01.26 2026-01-17 09:25:00 +01:00
7a35413da5 Merge pull request 'dev' (#5) from dev into main
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/5
2026-01-17 05:48:03 +00:00
660e714983 16.01.26 2026-01-16 21:07:56 +01:00
9edfcd5058 Merge pull request 'main' (#4) from main into dev
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/4
2026-01-16 08:15:31 +00:00
cd8f785881 13.01.26 2026-01-13 14:45:04 +01:00
24895173d6 11.01.26 2026-01-11 14:15:12 +01:00
6a1c26ce70 11.01.26 2026-01-11 11:11:55 +01:00
ba06fb60c2 11.01.26 2026-01-11 11:06:13 +01:00
3204eee7ac 10.01.26 2026-01-10 11:04:13 +01:00
815b8696d2 10.01.26 2026-01-10 10:58:17 +01:00
2e80c19e0e 10.01.26 2026-01-10 10:34:00 +01:00
170cc09627 Merge pull request 'Prod => Dev' (#3) from main into dev
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/3
2026-01-10 09:18:03 +00:00
8fd8de607f 10.01.26 2026-01-10 09:22:47 +01:00
ebdfd7f499 09.01.26 2026-01-09 17:49:20 +01:00
19669281d5 07.01.26 2026-01-07 18:04:18 +01:00
ccbf545fe2 07.01.26 2026-01-07 17:59:01 +01:00
550ad57354 07.01.26 2026-01-07 16:17:21 +01:00
dbb488ba52 Merge pull request 'Dev adjusted by Prod' (#2) from main into dev
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/2
2026-01-05 20:04:35 +00:00
49d2438ea0 05.01.26 2026-01-05 20:33:56 +01:00
92ba297bb7 05.01.26 2026-01-05 17:45:16 +01:00
cad3d04d08 Merge pull request 'Merge dev from main' (#1) from main into dev
Reviewed-on: https://srv413259.hstgr.cloud/admin/tradon/pulls/1
2026-01-05 13:03:45 +00:00
f1e002998e 05.01.26 2026-01-05 13:25:24 +01:00
c42edc4efd 05.01.26 2026-01-05 13:16:06 +01:00
e5056c6ea1 05.01.26 2026-01-05 13:09:58 +01:00
56a4bd9e82 04.01.26 2026-01-04 21:06:11 +01:00
9449f4f0ec 04.01.26 2026-01-04 20:15:27 +01:00
5111265019 04.01.26 2026-01-04 19:46:26 +01:00
750db2fe4d 04.01.26 2026-01-04 18:13:35 +01:00
36f9fb48a8 04.01.26 2026-01-04 18:09:22 +01:00
b857ab3eed 04.01.26 2026-01-04 18:07:36 +01:00
dfc2ea3a4e 04.01.26 2026-01-04 18:01:15 +01:00
dfc2ae4896 04.01.26 2026-01-04 17:30:28 +01:00
272b91609c 04.01.26 2026-01-04 17:27:49 +01:00
f47036b6df 04.01.26 2026-01-04 17:24:36 +01:00
bed9c9deac 04.01.26 2026-01-04 15:47:30 +01:00
837c75f8f0 04.01.26 2026-01-04 15:37:24 +01:00
dee3d2ff90 04.01.26 2026-01-04 14:49:22 +01:00
02b99b4622 04.01.26 2026-01-04 13:31:17 +01:00
37d73b6962 03.01.26 2026-01-03 17:17:25 +01:00
35c56538dd 03.01.26 2026-01-03 14:15:50 +01:00
13b7553641 03.01.26 2026-01-03 13:35:06 +01:00
bb117d4ee6 03.01.26 2026-01-03 13:25:11 +01:00
b0b2741422 29_12_25 2025-12-29 19:57:54 +01:00
ca83f0ec7b 29_12_25 2025-12-29 16:35:43 +01:00
1ab5e57017 28_12_25 2025-12-28 11:32:32 +01:00
a34f02db00 v1.0.1 2025-12-26 20:39:32 +01:00
255 changed files with 31627 additions and 1159 deletions

View File

@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules)",
"Bash(ls -d */)",
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules/purchase_trade)",
"Bash(ls -1d */)",
"Bash(for f:*)",
"Bash(do echo:*)",
"Read(//c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/**)",
"Bash(done)",
"Bash(cd /c/Users/SylvainDUVERNAY/Documents/Visual Studio Code/Tradon DEV/tradon/modules/purchase_trade/view)",
"Bash(ls -1 *.xml)",
"Bash(py --version)"
]
}
}

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
deployment/vps-TradonDev_Instructions.md
deployment/vps/46.202.173.47-credentials.md

107
AGENTS.md Normal file
View File

@@ -0,0 +1,107 @@
# AGENTS.md
Guide rapide pour les agents qui codent dans ce repository.
## 1) Contexte du projet
- Codebase Tryton monolithique (coeur + modules metier).
- Noyau serveur a la racine: `application.py`, `wsgi.py`, `admin.py`, `worker.py`, `cron.py`.
- Couches framework importantes:
- ORM: `model/`
- Meta/systeme (`ir`): `ir/`
- Protocoles RPC: `protocols/`
- Backend DB: `backend/`
- Modules metier: `modules/<module_name>/` (~220 modules).
## 2) Regles de travail pour agent
- Ne jamais toucher des fichiers sans rapport avec la demande.
- Limiter le scope de modif au minimum necessaire.
- Respecter le style existant du module cible.
- Ne pas supprimer du code legacy sans verifier les usages.
- Si comportement incertain: preferer un patch conservateur + test.
## 3) Zones de bruit a ignorer pendant l'exploration
- `.venv/`
- `__pycache__/`
- `build/` (quand present dans des sous-modules)
- Fichiers temporaires editeur (ex: `*.swp`)
## 4) Comment choisir ou coder selon le besoin
- Si bug ORM/champs:
- Lire `model/fields/*.py` et les tests `tests/test_field_*.py`.
- Si bug transaction/DB:
- Lire `transaction.py`, `backend/*/database.py`, `tests/test_backend.py`.
- Si bug API/RPC/HTTP:
- Lire `wsgi.py`, `rpc.py`, `protocols/*`, `tests/test_rpc.py`, `tests/test_wsgi.py`.
- Si bug metier:
- Modifier uniquement `modules/<module>/` + ses tests.
- Si bug template Relatorio (`.fodt`):
- Lire d'abord le template standard voisin du meme domaine (`invoice.fodt`, `sale.fodt`, etc.).
- Preferer des proprietes Python simples exposees par le modele plutot que des expressions Genshi complexes dans le template.
- Dans les placeholders XML, utiliser `&quot;` et `&apos;` plutot que des antislashs type `\'`.
- Si un document facture depend fortement d'une vente/achat, ajouter au besoin un petit pont Python pour exposer des `report_*` stables au template.
- Si plusieurs actions de report pointent vers `report_name = 'account.invoice'`, verifier aussi le cache `invoice_report_cache` dans `modules/account_invoice/invoice.py`: un mauvais cache peut faire croire que plusieurs actions utilisent le meme `.fodt`.
- Avant de conclure qu'un template ou une action est faux, verifier si le report alternatif doit bypasser le cache standard.
- Dans `purchase_trade`, pour remonter d'une facture vers shipment, pro forma, freight ou autres donnees logistiques, privilegier le lot physique comme pont entre `purchase.line`, `sale.line` et shipment.
- Pour `FREIGHT VALUE`, ne pas lire un champ direct sur la facture: retrouver le fee de shipment (`shipment_in`) dont le produit est `Maritime freight`, puis utiliser `fee.get_amount()`.
## 5) Workflow de modification (obligatoire)
1. Identifier le module et le flux impacte.
2. Localiser un test existant proche du comportement a changer.
3. Implementer le plus petit patch possible.
4. Ajouter/adapter les tests au plus pres du changement.
5. Lancer la validation ciblee (pas toute la suite si inutile).
6. Donner un resume du risque residuel.
## 6) Checklist avant de rendre une modif
- Le changement est-il limite au domaine demande ?
- Le comportement existant non cible est-il preserve ?
- Les droits/regles (`ir.rule`, acces) sont-ils impactes ?
- Les vues XML et labels sont-ils coherents si un champ change ?
- Les tests modifies couvrent-ils le bug/la feature ?
- Le message de commit (si demande) explique clairement le pourquoi ?
## 7) Tests: point de depart pratique
- Suite coeur: `tests/test_tryton.py`
- Tests coeur par domaine: `tests/test_*.py`
- Tests module:
- `modules/<module>/tests/test_module.py`
- `modules/<module>/tests/test_scenario.py`
- `modules/<module>/tests/scenario_*.rst`
Quand possible, lancer d'abord la cible minimale:
- fichier de test touche
- puis fichier voisin de regression
- puis suite plus large uniquement si necessaire
## 8) Contrat de sortie attendu de l'agent
Toujours fournir:
- Liste des fichiers modifies
- Resume fonctionnel (ce qui change)
- Resume technique (pourquoi ce design)
- Tests executes + resultat
- Risques residuels et impacts potentiels
## 9) Cas sensibles (demander confirmation humaine)
- Changement schema/structure de donnees
- Changement de logique de securite/acces
- Changement de comportement transverse (transaction, pool, RPC, worker)
- Refactor multi-modules sans ticket explicite
## 10) Raccourci de demarrage pour agent
1. Lire ce fichier.
2. Lire le(s) fichier(s) touche(s) et leurs tests.
3. Proposer le patch minimal.
4. Implementer + tester cible.
5. Rendre avec le contrat de sortie (section 8).

5
deployment/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Deployment Notes
- Runbook onboarding SSH: `deployment/runbooks/vps-onboarding.md`
- VPS 46.202.173.47 credentials: `deployment/vps/46.202.173.47-credentials.md`
- VPS 46.202.173.47 quickstart: `deployment/vps/46.202.173.47-quickstart.md`

View File

@@ -0,0 +1,57 @@
# Procedure - Ajouter un nouveau VPS (SSH)
Date de reference: 2026-04-07
## 1) Preparation locale (Windows)
1. Creer le dossier SSH local si absent:
`New-Item -ItemType Directory -Force $env:USERPROFILE\.ssh`
2. Generer une cle dediee VPS:
`ssh-keygen -t ed25519 -C "vps-deploy" -f $env:USERPROFILE\.ssh\vps_deploy_key`
3. Lire la cle publique:
`Get-Content $env:USERPROFILE\.ssh\vps_deploy_key.pub`
## 2) Installer la cle sur le VPS
1. Se connecter au VPS avec mot de passe (premiere fois):
`ssh <user>@<ip_vps>`
2. Sur le VPS, preparer le dossier SSH:
`mkdir -p ~/.ssh`
`chmod 700 ~/.ssh`
3. Ajouter la cle publique (une ligne complete):
`echo "ssh-ed25519 ... vps-deploy" >> ~/.ssh/authorized_keys`
`chmod 600 ~/.ssh/authorized_keys`
## 3) Tester la connexion par cle
Depuis Windows:
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@<ip_vps>`
## 4) Test operationnel minimal
Creer un dossier distant:
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@<ip_vps> "mkdir -p ~/test_codex_deploy && ls -ld ~/test_codex_deploy"`
## 5) Durcissement recommande
- Desactiver l'authentification par mot de passe apres validation de la cle.
- Utiliser une cle dediee par environnement (dev/staging/prod).
- Documenter user + IP + chemin de cle dans une fiche VPS separee.
## 6) Note importante - Contrainte sandbox Codex
- Si une commande SSH/SCP echoue avec un message proche de:
- `Identity file ... not accessible: Permission denied`
- Cause probable:
- la session est en sandbox et ne peut pas lire la cle locale dans
`C:\Users\<user>\.ssh\...`.
- Action:
- relancer la commande en mode `require_escalated` (hors sandbox) pour
autoriser l'acces a la cle locale.
- Exemple observe le 2026-04-07:
- creation du dossier `/root/test` sur `46.202.173.47` reussie uniquement
apres escalation.

View File

@@ -0,0 +1,39 @@
# Commandes Rapides - VPS 46.202.173.47
Date de reference: 2026-04-07
## Cles SSH par utilisateur
| Utilisateur | Fichier cle locale |
|---|---|
| Laurent Barontini | `$env:USERPROFILE\.ssh\vps_deploy_key` |
| Sylvain Duvernay | `$env:USERPROFILE\.ssh\id_ed25519` |
> Les commandes ci-dessous utilisent `id_ed25519` (Sylvain Duvernay).
## 1) Test SSH
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "echo ok"`
## 2) Creer un dossier test distant
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "mkdir -p ~/test_codex_deploy && ls -ld ~/test_codex_deploy"`
## 3) Lister home distant
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47 "ls -la ~"`
## 4) Copier un fichier local vers le VPS
`scp -i $env:USERPROFILE\.ssh\id_ed25519 .\local.txt <user>@46.202.173.47:~/local.txt`
## 5) Recuperer un fichier du VPS
`scp -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47:~/remote.txt .\remote.txt`
## 6) Depannage sandbox (Codex)
- Symptome:
- `Identity file ... not accessible: Permission denied`
- Correctif:
- relancer la commande SSH/SCP en mode escalade (`require_escalated`).

View File

@@ -0,0 +1,3 @@
Serveur: 'VPS-62.72.36.116'
Alias Name: 'VPS DEV'
IP Address:'62.72.36.116'

View File

@@ -93,6 +93,7 @@ class Model(
cursor.execute(*ir_model.select(ir_model.id,
where=ir_model.model == model.__name__))
model_id = None
logger.info("MODEL_NAME:%s",model.__name__)
if cursor.rowcount == -1 or cursor.rowcount is None:
data = cursor.fetchone()
if data:

View File

@@ -30,8 +30,8 @@ class Binary(Field):
on_change_with=None, depends=None, context=None, loading='lazy',
filename=None, file_id=None, store_prefix=None):
self.filename = filename
self.file_id = file_id
self.store_prefix = store_prefix
self.file_id = None #file_id
self.store_prefix = None #store_prefix
super(Binary, self).__init__(string=string, help=help,
required=required, readonly=readonly, domain=domain, states=states,
on_change=on_change, on_change_with=on_change_with,

View File

@@ -103,15 +103,26 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
@classmethod
def _get_name(cls):
'''
Returns the first non-empty line of the model docstring.
'''
assert cls.__doc__, '%s has no docstring' % cls
if cls.__doc__ is None:
print("\n💥 MODELE SANS DOCSTRING :", cls.__name__, " (module:", cls.__module__, ")")
raise Exception("MODELE SANS DOCSTRING")
lines = cls.__doc__.splitlines()
for line in lines:
line = line.strip()
if line:
return line
if lines:
return lines[0]
return cls.__name__
# @classmethod
# def _get_name(cls):
# '''
# Returns the first non-empty line of the model docstring.
# '''
# assert cls.__doc__, '%s has no docstring' % cls
# lines = cls.__doc__.splitlines()
# for line in lines:
# line = line.strip()
# if line:
# return line
@classmethod
def __register__(cls, module_name):

View File

@@ -117,6 +117,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
post_date = fields.Date('Post Date', readonly=True)
description = fields.Char('Description', states=_MOVE_STATES)
ext_ref = fields.Char('Ext. Ref')
origin = fields.Reference('Origin', selection='get_origin',
states=_MOVE_STATES)
state = fields.Selection([
@@ -921,6 +922,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
fields.Reference("Move Origin", selection='get_move_origin'),
'get_move_field', searcher='search_move_field')
description = fields.Char('Description', states=_states)
ext_ref = fields.Char('Ext. Ref')
move_description_used = fields.Function(
fields.Char("Move Description", states=_states),
'get_move_field',

View File

@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="origin" colspan="3"/>
<label name="description_used"/>
<field name="description_used" colspan="3"/>
<label name="ext_ref"/>
<field name="ext_ref" colspan="3"/>
<notebook>
<page name="lines">
<field name="lines" colspan="4"

View File

@@ -26,6 +26,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="origin"/>
<label name="description_used"/>
<field name="description_used" colspan="3"/>
<label name="ext_ref"/>
<field name="ext_ref" colspan="3"/>
<notebook colspan="4">
<page string="Other Info" id="info">
<label name="date"/>

View File

@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
<tree editable="1">
<field name="move"/>
<field name="account" expand="1"/>
<field name="ext_ref" expand="1" optional="1"/>
<field name="party" expand="1"/>
<field name="debit" sum="1"/>
<field name="credit" sum="1"/>

View File

@@ -1286,9 +1286,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
remainder = sum(l.debit - l.credit for l in move_lines)
if self.payment_term:
payment_date = self.payment_term_date or self.invoice_date or today
purchase_line = int(str(self.lines[0].origin).split(",")[1]) if self.lines[0].origin else None
term_lines = self.payment_term.compute(
self.total_amount, self.currency, payment_date, purchase_line)
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
logger.info("MODEL:%s",model)
if model:
Line = Pool().get(model)
line = Line(int(str(self.lines[0].origin).split(",")[1]))
logger.info("LINE:%s",line)
term_lines = self.payment_term.compute(
self.total_amount, self.currency, payment_date, line)
else:
term_lines = [(self.payment_term_date or today, self.total_amount)]
past_payment_term_dates = []
@@ -1960,14 +1965,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
if amount < 0:
move_line.debit = Decimal(0)
move_line.credit = -amount
move_line.account = gl.product.account_stock_used
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.credit = Decimal(0)
move_line_.debit = -amount
move_line_.account = gl.product.account_stock_in_used
else:
move_line.debit = amount
move_line.credit = Decimal(0)
move_line.account = gl.product.account_stock_used
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
move_line_.debit = Decimal(0)
move_line_.credit = amount
move_line_.account = gl.product.account_stock_in_used
@@ -2031,7 +2038,11 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
var_qt = sum([i.quantity for i in gl])
logger.info("LOT_TO_PROCESS:%s",lot)
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
if lot:
if (gl[0].fee and not gl[0].product.landed_cost):
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
Move.save([account_move])
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
adjust_move_lines = []
mov = None
if self.type == 'in':
@@ -3684,13 +3695,19 @@ class InvoiceReport(Report):
Invoice = pool.get('account.invoice')
# Re-instantiate because records are TranslateModel
invoice, = Invoice.browse(records)
if invoice.invoice_report_cache:
report_path = cls._get_action_report_path(action)
use_cache = (
report_path in (None, 'account_invoice/invoice.fodt')
and invoice.invoice_report_cache
)
if use_cache:
return (
invoice.invoice_report_format,
invoice.invoice_report_cache)
else:
result = super()._execute(records, header, data, action)
if invoice.invoice_report_versioned:
if (invoice.invoice_report_versioned
and report_path in (None, 'account_invoice/invoice.fodt')):
format_, data = result
if isinstance(data, str):
data = bytes(data, 'utf-8')
@@ -3707,6 +3724,12 @@ class InvoiceReport(Report):
with Transaction().set_context(language=False):
return super().render(*args, **kwargs)
@staticmethod
def _get_action_report_path(action):
if isinstance(action, dict):
return action.get('report')
return getattr(action, 'report', None)
@classmethod
def execute(cls, ids, data):
pool = Pool()

View File

@@ -264,11 +264,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
<field name="model">account.invoice</field>
</record>
<record model="ir.action.keyword" id="refresh_invoice_report_keyword">
<field name="keyword">form_print</field>
<field name="model">account.invoice,-1</field>
<field name="action" ref="refresh_invoice_report_wizard"/>
</record>
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
<field name="action" ref="refresh_invoice_report_wizard"/>
<field name="group" ref="account.group_account_admin"/>
@@ -293,7 +288,7 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.action.report" id="report_invoice">
<field name="name">Invoice</field>
<field name="name">Provisional Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice.fodt</field>
@@ -318,6 +313,19 @@ this repository contains the full copyright notices and license terms. -->
<field name="action" ref="report_prepayment"/>
</record>
<record model="ir.action.report" id="report_invoice_ict_final">
<field name="name">Final Invoice</field>
<field name="model">account.invoice</field>
<field name="report_name">account.invoice</field>
<field name="report">account_invoice/invoice_ict_final.fodt</field>
<field name="single" eval="True"/>
</record>
<record model="ir.action.keyword" id="report_invoice_ict_final_keyword">
<field name="keyword">form_print</field>
<field name="model">account.invoice,-1</field>
<field name="action" ref="report_invoice_ict_final"/>
</record>
<record model="ir.sequence.type" id="sequence_type_account_invoice">
<field name="name">Invoice</field>
</record>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,10 @@ from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateView, Wizard
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
import logging
logger = logging.getLogger(__name__)
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
@@ -46,7 +48,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
'.msg_payment_term_missing_last_remainder',
payment_term=term.rec_name))
def compute(self, amount, currency, date, purchase_line = None):
def compute(self, amount, currency, date, line_ = None):
"""Calculate payment terms and return a list of tuples
with (date, amount) for each payment term line.
@@ -59,7 +61,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
remainder = amount
for line in self.lines:
value = line.get_value(remainder, amount, currency)
value_date = line.get_date(date, purchase_line)
value_date = line.get_date(date, line_)
if value is None or not value_date:
continue
if ((remainder - value) * sign) < Decimal(0):
@@ -155,12 +157,11 @@ class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView):
self.ratio = self.round(1 / self.divisor,
self.__class__.ratio.digits[1])
def get_date(self, date, purchase_line = None):
def get_date(self, date, line = None):
#find date based on trigger:
if purchase_line and self.trigger_event:
PurchaseLine = Pool().get('purchase.line')
purchase_line = PurchaseLine(purchase_line)
trigger_date = purchase_line.get_date(self.trigger_event)
if line and self.trigger_event:
trigger_date = line.get_date(self.trigger_event)
logger.info("DATE_FROM_LINE:%s",trigger_date)
if trigger_date:
date = trigger_date

View File

@@ -92,8 +92,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="invoice_report_revisions" colspan="4"/>
</page>
<page string="Rate management" id="rate">
<label name="warning"/>
<field name="warning"/>
<!-- <label name="warning"/>
<field name="warning"/> -->
<newline/>
<label name="rate"/>
<field name="rate"/>

View File

@@ -0,0 +1,14 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import account
def register():
Pool.register(
account.AccountTemplate,
module='account_itsa', type_='model')
Pool.register(
account.CreateChart,
module='account_itsa', type_='wizard')

View File

@@ -0,0 +1,40 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import csv
from io import BytesIO, TextIOWrapper
from sql import Table
from sql.aggregate import Sum
from sql.conditionals import Coalesce
from trytond.config import config
from trytond.model import ModelStorage, ModelView, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
class AccountTemplate(metaclass=PoolMeta):
__name__ = 'account.account.template'
@classmethod
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
model_data = Table('ir_model_data')
super().__register__(module_name)
class CreateChart(metaclass=PoolMeta):
__name__ = 'account.create_chart'
def default_properties(self, fields):
pool = Pool()
ModelData = pool.get('ir.model.data')
defaults = super().default_properties(fields)
# template_id = ModelData.get_id('account_ch.root')
# if self.account.account_template.id == template_id:
# defaults['account_receivable'] = self.get_account(
# 'account_ch.3400')
# defaults['account_payable'] = self.get_account(
# 'account_ch.6040')
return defaults

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
[tryton]
version=7.2.3
depends:
account
extras_depend:
account_invoice
xml:
account_itsa.xml
#tax_ict.xml

View File

@@ -6,7 +6,7 @@ from decimal import Decimal
from trytond.i18n import gettext
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.exceptions import UserWarning, UserError
from .exceptions import COGSWarning
import logging
@@ -74,8 +74,8 @@ class InvoiceLine(metaclass=PoolMeta):
if move_line.second_currency:
move_line.amount_second_currency = amount
else:
move_line.debit = Decimal(0)
move_line.credit = -amount_converted
move_line.debit = -amount_converted
move_line.credit = Decimal(0)
move_line.account = self.product.account_stock_out_used
if move_line.second_currency:
move_line.amount_second_currency = amount
@@ -171,10 +171,28 @@ class InvoiceLine(metaclass=PoolMeta):
cost = self.amount
else:
cost = self.lot.get_cog()
if not cost or cost == 0:
raise UserError('No COG for this invoice, please generate the reception of the goods')
if self.amount < 0 :
cost *= -1
logger.info("GETMOVELINES_COST:%s",cost)
anglo_saxon_move_lines_ = []
with Transaction().set_context(
company=self.invoice.company.id, date=accounting_date):
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
cost, type_)
if type_ == 'in_supplier' and (self.lot.sale_invoice_line_prov or self.lot.sale_invoice_line) and not self.fee:
anglo_saxon_move_lines_ = self._get_anglo_saxon_move_lines(cost, 'out_customer')
result.extend(anglo_saxon_move_lines)
result.extend(anglo_saxon_move_lines_)
#Fee inventoried delivery management
if self.lot and type_ != 'in_supplier':
FeeLots = Pool().get('fee.lots')
fees = FeeLots.search(['lot','=',self.lot.id])
for fl in fees:
if fl.fee.type == 'ordered' and fl.fee.product.template.landed_cost:
AccountMove = Pool().get('account.move')
account_move = fl.fee._get_account_move_fee(fl.lot,'out')
AccountMove.save([account_move])
return result

View File

@@ -16,11 +16,11 @@ account_names = [
class Category(metaclass=PoolMeta):
__name__ = 'product.category'
account_stock = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock",
'account.account', "Account Stock/Cost Income",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
('type.statement', '=', 'balance'),
# ('type.stock', '=', True),
# ('type.statement', '=', 'balance'),
('company', '=', Eval('context', {}).get('company', -1)),
],
states={
@@ -29,7 +29,7 @@ class Category(metaclass=PoolMeta):
| ~Eval('accounting', False)),
}))
account_stock_in = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock IN",
'account.account', "Account Stock IN/Cost liability",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
@@ -41,7 +41,7 @@ class Category(metaclass=PoolMeta):
| ~Eval('accounting', False)),
}))
account_stock_out = fields.MultiValue(fields.Many2One(
'account.account', "Account Stock OUT",
'account.account', "Account Stock OUT/Cost liability",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
@@ -103,8 +103,8 @@ class CategoryAccount(metaclass=PoolMeta):
'account.account', "Account Stock",
domain=[
('closed', '!=', True),
('type.stock', '=', True),
('type.statement', '=', 'balance'),
# ('type.stock', '=', True),
# ('type.statement', '=', 'balance'),
('company', '=', Eval('company', -1)),
])
account_stock_in = fields.Many2One(

View File

@@ -1,8 +1,11 @@
from trytond.pool import Pool
from . import automation,rules #, document
from . import automation,rules,freight_booking,cron #, document
def register():
Pool.register(
automation.AutomationDocument,
rules.AutomationRuleSet,
freight_booking.FreightBookingInfo,
cron.Cron,
cron.AutomationCron,
module='automation', type_='model')

View File

@@ -1,10 +1,15 @@
from trytond.model import ModelSQL, ModelView, fields, Workflow
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.wizard import Button
from trytond.transaction import Transaction
from sql import Table
from decimal import getcontext, Decimal, ROUND_HALF_UP
import requests
import io
import logging
import json
import re
logger = logging.getLogger(__name__)
@@ -17,6 +22,7 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
('invoice', 'Invoice'),
('statement_of_facts', 'Statement of Facts'),
('weight_report', 'Weight Report'),
('controller', 'Controller'),
('bol', 'Bill of Lading'),
('controller_invoice', 'Controller Invoice'),
], 'Type')
@@ -57,25 +63,53 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
def run_ocr(cls, docs):
for doc in docs:
try:
# Décoder le fichier depuis le champ Binary
file_data = doc.document.data or b""
logger.info(f"File size: {len(file_data)} bytes")
logger.info(f"First 20 bytes: {file_data[:20]}")
logger.info(f"Last 20 bytes: {file_data[-20:]}")
if doc.type == 'weight_report':
# Décoder le fichier depuis le champ Binary
file_data = doc.document.data or b""
logger.info(f"File size: {len(file_data)} bytes")
logger.info(f"First 20 bytes: {file_data[:20]}")
logger.info(f"Last 20 bytes: {file_data[-20:]}")
file_name = doc.document.name or "document"
file_name = doc.document.name or "document"
# Envoyer le fichier au service OCR
response = requests.post(
"http://automation-service:8006/ocr",
files={"file": (file_name, io.BytesIO(file_data))}
)
response.raise_for_status()
data = response.json()
logger.info("RUN_OCR_RESPONSE:%s",data)
doc.ocr_text = data.get("ocr_text", "")
doc.state = "ocr_done"
doc.notes = (doc.notes or "") + "OCR done\n"
# Envoyer le fichier au service OCR
response = requests.post(
"http://automation-service:8006/ocr",
files={"file": (file_name, io.BytesIO(file_data))}
)
response.raise_for_status()
data = response.json()
logger.info("RUN_OCR_RESPONSE:%s",data)
doc.ocr_text = data.get("ocr_text", "")
doc.state = "ocr_done"
doc.notes = (doc.notes or "") + "OCR done\n"
else:
doc.ocr_text = (doc.document.data or b"").decode('utf-8', errors='replace')
match = re.search(r"\bID\s*:\s*(\d+)", doc.ocr_text)
if match:
request_id = match.group(1)
match = re.search(r"\bBL\s*number\s*:\s*([A-Za-z0-9_-]+)", doc.ocr_text, re.IGNORECASE)
if match:
bl_number = match.group(1)
ShipmentIn = Pool().get('stock.shipment.in')
sh = ShipmentIn.search(['bl_number','=',bl_number])
if sh:
sh[0].returned_id = request_id
ShipmentIn.save(sh)
doc.notes = (doc.notes or "") + "Id returned: " + request_id
so_payload = {
"ServiceOrderKey": sh[0].service_order_key,
"ID_Number": request_id
}
response = requests.post(
"http://automation-service:8006/service-order-update",
json=so_payload,
timeout=10
)
response.raise_for_status()
doc.notes = (doc.notes or "") + " SO updated"
except Exception as e:
doc.state = "error"
@@ -154,7 +188,8 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
response = requests.post(
"http://automation-service:8006/metadata",
#"http://automation-service:8006/metadata",
"http://automation-service:8006/parse",
json={"text": doc.ocr_text or ""}
)
response.raise_for_status()
@@ -176,6 +211,18 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
logger.error("Metadata processing error: %s", e)
doc.save()
def create_weight_report(self,wr_payload):
response = requests.post(
"http://automation-service:8006/weight-report",
json=wr_payload, # 👈 ICI la correction
timeout=10
)
response.raise_for_status()
return response.json()
# -------------------------------------------------------
# FULL PIPELINE
# -------------------------------------------------------
@@ -184,18 +231,66 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
def run_pipeline(cls, docs):
for doc in docs:
try:
if cls.rule_set.ocr_required:
cls.run_ocr([doc])
if cls.rule_set.structure_required and doc.state != "error":
cls.run_structure([doc])
if cls.rule_set.table_required and doc.state != "error":
cls.run_tables([doc])
if cls.rule_set.metadata_required and doc.state != "error":
cls.run_metadata([doc])
if doc.state != "error":
doc.state = "validated"
doc.notes = (doc.notes or "") + "Pipeline completed\n"
logger.info("DATA_TYPE:%s",type(doc.metadata_json))
metadata = json.loads(str(doc.metadata_json))
logger.info("JSON STRUCTURE:%s",metadata)
WeightReport = Pool().get('weight.report')
wr = WeightReport.create_from_json(metadata)
ShipmentIn = Pool().get('stock.shipment.in')
ShipmentWR = Pool().get('shipment.wr')
sh = ShipmentIn.search([('bl_number','ilike',wr.bl_no)])
if sh:
swr = ShipmentWR()
swr.shipment_in = sh[0]
swr.wr = wr
ShipmentWR.save([swr])
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
logger.info("BL_NUMBER:%s",sh[0].bl_number)
if sh[0].incoming_moves:
factor_net = wr.net_landed_kg / wr.bales if wr.bales else 1
factor_gross = wr.gross_landed_kg / wr.bales if wr.bales else 1
for move in sh[0].incoming_moves:
lot = move.lot
if lot.lot_type == 'physic':
wr_payload = {
"chunk_key": lot.lot_chunk_key,
"gross_weight": float(round(Decimal(lot.lot_qt) * factor_gross,5)),
"net_weight": float(round(Decimal(lot.lot_qt) * factor_net,5)),
"tare_total": float(round(wr.tare_kg * (Decimal(lot.lot_qt) / wr.bales),5)) ,
"bags": int(lot.lot_qt),
"surveyor_code": sh[0].controller.get_alf(),
"place_key": sh[0].to_location.get_places(),
"report_date": int(wr.report_date.strftime("%Y%m%d")),#wr.report_date.isoformat() if wr.report_date else None,
"weight_date": int(wr.weight_date.strftime("%Y%m%d")),#wr.weight_date.isoformat() if wr.weight_date else None,
"agent": sh[0].agent.get_alf(),
"forwarder_ref": sh[0].returned_id
}
logger.info("PAYLOAD:%s",wr_payload)
data = doc.create_weight_report(wr_payload)
doc.notes = (doc.notes or "") + f"WR created in Fintrade: {data.get('success')}\n"
doc.notes = (doc.notes or "") + f"WR key: {data.get('weight_report_key')}\n"
# if cls.rule_set.ocr_required:[]
# cls.run_ocr([doc])
# if cls.rule_set.structure_required and doc.state != "error":
# cls.run_structure([doc])
# if cls.rule_set.table_required and doc.state != "error":
# cls.run_tables([doc])
# if cls.rule_set.metadata_required and doc.state != "error":
# cls.run_metadata([doc])
# if doc.state != "error":
# doc.state = "validated"
# doc.notes = (doc.notes or "") + "Pipeline completed\n"
except Exception as e:
logger.exception("PIPELINE FAILED") # 👈 TRACE COMPLETE
doc.state = "error"
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
doc.save()
raise
# except Exception as e:
# doc.state = "error"
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
doc.save()

View File

@@ -75,7 +75,7 @@
<record model="ir.model.button" id="auto_button1">
<field name="model">automation.document</field>
<field name="name">run_pipeline</field>
<field name="string">Run Full Pipeline</field>
<field name="string">Create Weight Report</field>
</record>
<record model="ir.model.button" id="auto_button2">
<field name="model">automation.document</field>

377
modules/automation/cron.py Normal file
View File

@@ -0,0 +1,377 @@
import requests
from decimal import getcontext, Decimal, ROUND_HALF_UP
from datetime import datetime, timedelta
from trytond.model import fields
from trytond.model import ModelSQL, ModelView
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
import logging
from sql import Table
import traceback
logger = logging.getLogger(__name__)
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.append(
('automation.cron|update_shipment', "Update Shipment from freight booking info")
)
class AutomationCron(ModelSQL, ModelView):
"Automation Cron"
__name__ = 'automation.cron'
frequency = fields.Selection([
('daily', "Daily"),
('weekly', "Weekly"),
('monthly', "Monthly"),
], "Frequency", required=True,
help="How frequently rates must be updated.")
last_update = fields.Date("Last Update", required=True)
@classmethod
def run(cls, crons):
cls.update_shipment()
@classmethod
def update_shipment(cls):
PoolObj = Pool()
ShipmentIn = PoolObj.get('stock.shipment.in')
Party = PoolObj.get('party.party')
Vessel = PoolObj.get('trade.vessel')
Location = PoolObj.get('stock.location')
# Table externe
t = Table('freight_booking_info')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.ShippingInstructionNumber,
t.ShippingInstructionDate,
t.ShippingInstructionQuantity,
t.ShippingInstructionQuantityUnit,
t.NumberOfContainers,
t.ContainerType,
t.Loading,
t.Destination,
t.BookingAgent,
t.Carrier,
t.Vessel,
t.BL_Number,
t.ETD_Date,
t.BL_Date,
t.ExpectedController,
t.Comments,
t.FintradeBookingKey,
))
rows = cursor.fetchall()
logger.info(f"Nombre total de lignes à traiter : {len(rows)}")
# ---- PREMIÈRE TRANSACTION : Création des objets de référence ----
with Transaction().new_transaction() as trans1:
try:
logger.info("Début de la création des objets de référence...")
parties_to_save = []
vessels_to_save = []
locations_to_save = []
parties_cache = {}
vessels_cache = {}
locations_cache = {}
# Collecter les données des objets de référence
for row in rows:
(
si_number, si_date, si_quantity, si_unit,
container_number, container_type,
loading_name, destination_name,
agent_name, carrier_name,
vessel_name, bl_number,
etd_date, bl_date, controller,
comments, fintrade_booking_key
) = row
# Fonction pour obtenir ou créer un Party
def get_or_create_party(name):
if not name:
return None
name_upper = str(name).strip().upper()
if name_upper in parties_cache:
return parties_cache[name_upper]
# Chercher d'abord dans la base
existing = Party.search([('name', '=', name_upper)], limit=1)
if existing:
parties_cache[name_upper] = existing[0]
return existing[0]
# Créer un nouveau
new_p = Party()
new_p.name = name_upper
parties_cache[name_upper] = new_p
parties_to_save.append(new_p)
return new_p
# Fonction pour obtenir ou créer un Vessel
def get_or_create_vessel(name):
if not name:
return None
name_upper = str(name).strip().upper()
if name_upper in vessels_cache:
return vessels_cache[name_upper]
existing = Vessel.search([('vessel_name', '=', name_upper)], limit=1)
if existing:
vessels_cache[name_upper] = existing[0]
return existing[0]
new_v = Vessel()
new_v.vessel_name = name_upper
vessels_cache[name_upper] = new_v
vessels_to_save.append(new_v)
return new_v
# Fonction pour obtenir ou créer une Location
def get_or_create_location(name, type_):
if not name:
return None
name_upper = str(name).strip().upper()
key = f"{name_upper}_{type_}"
if key in locations_cache:
return locations_cache[key]
existing = Location.search([
('name', '=', name_upper),
('type', '=', type_)
], limit=1)
if existing:
locations_cache[key] = existing[0]
return existing[0]
new_loc = Location()
new_loc.name = name_upper
new_loc.type = type_
locations_cache[key] = new_loc
locations_to_save.append(new_loc)
return new_loc
# Collecter les objets à créer
_ = get_or_create_party(carrier_name)
_ = get_or_create_party(agent_name)
_ = get_or_create_vessel(vessel_name)
_ = get_or_create_location(loading_name, 'supplier')
_ = get_or_create_location(destination_name, 'customer')
# Sauvegarder tous les objets de référence
if parties_to_save:
logger.info(f"Création de {len(parties_to_save)} parties...")
Party.save(parties_to_save)
if vessels_to_save:
logger.info(f"Création de {len(vessels_to_save)} vessels...")
Vessel.save(vessels_to_save)
if locations_to_save:
logger.info(f"Création de {len(locations_to_save)} locations...")
Location.save(locations_to_save)
trans1.commit()
logger.info("Première transaction commitée : objets de référence créés")
except Exception as e:
trans1.rollback()
logger.error(f"Erreur dans la création des objets de référence : {e}")
logger.error(traceback.format_exc())
raise
# ---- TRANSACTIONS INDIVIDUELLES pour chaque shipment ----
successful_shipments = 0
failed_shipments = []
# Recréer le curseur après la nouvelle transaction
cursor2 = Transaction().connection.cursor()
cursor2.execute(*t.select(
t.ShippingInstructionNumber,
t.ShippingInstructionDate,
t.ShippingInstructionQuantity,
t.ShippingInstructionQuantityUnit,
t.NumberOfContainers,
t.ContainerType,
t.Loading,
t.Destination,
t.BookingAgent,
t.Carrier,
t.Vessel,
t.BL_Number,
t.ETD_Date,
t.BL_Date,
t.ExpectedController,
t.Comments,
t.FintradeBookingKey,
))
rows2 = cursor2.fetchall()
for i, row in enumerate(rows2, 1):
(
si_number, si_date, si_quantity, si_unit,
container_number, container_type,
loading_name, destination_name,
agent_name, carrier_name,
vessel_name, bl_number,
etd_date, bl_date, controller,
comments, fintrade_booking_key
) = row
logger.info(f"Traitement shipment {i}/{len(rows2)} : SI {si_number}")
# ---- TRANSACTION INDIVIDUELLE pour ce shipment ----
try:
with Transaction().new_transaction() as trans_shipment:
logger.info(f"Début transaction pour SI {si_number}")
# Vérifier si le shipment existe déjà
existing_shipment = ShipmentIn.search([
('reference', '=', si_number)
], limit=1)
if existing_shipment:
logger.info(f"Shipment {si_number} existe déjà, ignoré")
trans_shipment.commit()
continue
# Récupérer les objets (maintenant ils existent dans la base)
carrier = None
if carrier_name:
carrier_list = Party.search([('name', '=', str(carrier_name).strip().upper())], limit=1)
if carrier_list:
carrier = carrier_list[0]
logger.info(f"Carrier trouvé pour {si_number}: {carrier.name}")
else:
logger.warning(f"Carrier NON TROUVÉ pour {si_number}: '{carrier_name}'")
agent = None
agent_list = Party.search([('name', '=', str(agent_name or 'TBN').strip().upper())], limit=1)
if agent_list:
agent = agent_list[0]
vessel = None
if vessel_name:
vessel_list = Vessel.search([('vessel_name', '=', str(vessel_name).strip().upper())], limit=1)
if vessel_list:
vessel = vessel_list[0]
loc_from = None
if loading_name:
loc_from_list = Location.search([
('name', '=', str(loading_name).strip().upper()),
('type', '=', 'supplier')
], limit=1)
if loc_from_list:
loc_from = loc_from_list[0]
loc_to = None
if destination_name:
loc_to_list = Location.search([
('name', '=', str(destination_name).strip().upper()),
('type', '=', 'customer')
], limit=1)
if loc_to_list:
loc_to = loc_to_list[0]
# Vérification critique du carrier
if not carrier:
error_msg = f"ERREUR CRITIQUE: Carrier manquant pour SI {si_number} (valeur: '{carrier_name}')"
logger.error(error_msg)
raise ValueError(error_msg)
# Créer le shipment
shipment = ShipmentIn()
shipment.reference = si_number
shipment.from_location = loc_from
shipment.to_location = loc_to
shipment.carrier = None #carrier
shipment.supplier = agent
shipment.agent = agent
shipment.vessel = vessel
shipment.cargo_mode = 'bulk'
shipment.bl_number = bl_number
shipment.bl_date = bl_date
shipment.etd = etd_date
shipment.etad = shipment.bl_date + timedelta(days=20)
# Sauvegarder ce shipment uniquement
ShipmentIn.save([shipment])
inv_date,inv_nb = shipment._create_lots_from_fintrade()
shipment.controller = shipment.get_controller()
shipment.controller_target = controller
shipment.create_fee(shipment.controller)
shipment.instructions = shipment.get_instructions_html(inv_date,inv_nb)
ShipmentIn.save([shipment])
trans_shipment.commit()
successful_shipments += 1
logger.info(f"✓ Shipment {si_number} créé avec succès")
except Exception as e:
# Cette transaction échoue mais les autres continuent
error_details = {
'si_number': si_number,
'carrier_name': carrier_name,
'error': str(e),
'traceback': traceback.format_exc()
}
failed_shipments.append(error_details)
logger.error(f"✗ ERREUR pour shipment {si_number}: {e}")
logger.error(f" Carrier: '{carrier_name}'")
logger.error(f" Agent: '{agent_name}'")
logger.error(f" Vessel: '{vessel_name}'")
logger.error(" Traceback complet:")
for line in traceback.format_exc().split('\n'):
if line.strip():
logger.error(f" {line}")
# ---- RÉSUMÉ FINAL ----
logger.info("=" * 60)
logger.info("RÉSUMÉ DE L'EXÉCUTION")
logger.info("=" * 60)
logger.info(f"Total de shipments à traiter : {len(rows2)}")
logger.info(f"Shipments créés avec succès : {successful_shipments}")
logger.info(f"Shipments en échec : {len(failed_shipments)}")
if failed_shipments:
logger.info("\nDétail des échecs :")
for i, error in enumerate(failed_shipments, 1):
logger.info(f" {i}. SI {error['si_number']}:")
logger.info(f" Carrier: '{error['carrier_name']}'")
logger.info(f" Erreur: {error['error']}")
# Log supplémentaire pour debug
logger.info("\nAnalyse des carriers problématiques :")
problematic_carriers = {}
for error in failed_shipments:
carrier = error['carrier_name']
if carrier in problematic_carriers:
problematic_carriers[carrier] += 1
else:
problematic_carriers[carrier] = 1
for carrier, count in problematic_carriers.items():
logger.info(f" Carrier '{carrier}' : {count} échec(s)")
# Vérifier si ce carrier existe dans la base
existing = Party.search([('name', '=', str(carrier).strip().upper())], limit=1)
if existing:
logger.info(f" → EXISTE DANS LA BASE (ID: {existing[0].id})")
else:
logger.info(f" → N'EXISTE PAS DANS LA BASE")
logger.info("=" * 60)

View File

@@ -0,0 +1,37 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="cron_view_list">
<field name="model">automation.cron</field>
<field name="type">tree</field>
<field name="name">cron_list</field>
</record>
<record model="ir.ui.view" id="cron_view_form">
<field name="model">automation.cron</field>
<field name="type">form</field>
<field name="name">cron_form</field>
</record>
<record model="ir.action.act_window" id="act_cron_form">
<field name="name">Update shipment from freight booking</field>
<field name="res_model">automation.cron</field>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="cron_view_list"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.action.act_window.view" id="act_cron_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="cron_view_form"/>
<field name="act_window" ref="act_cron_form"/>
</record>
<record model="ir.cron" id="cron_cron">
<field name="method">automation.cron|update_shipment</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">days</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,72 @@
from trytond.model import ModelSQL, fields
class ExecutionFollowUp(ModelSQL):
"Execution Follow Up"
__name__ = 'execution.automation'
port_of_loading = fields.Char("Port of Loading")
fb_loading = fields.Char("FB Loading")
warehouse = fields.Char("Warehouse")
origin = fields.Char("Origin")
agent = fields.Char("Agent")
operator = fields.Char("Operator")
fintrade_lc_nb = fields.Char("Fintrade LC Nb")
lc_number = fields.Char("LC Number")
si = fields.Char("SI")
port_of_destination = fields.Char("Port of Destination")
fb_destination = fields.Char("FB Destination")
status = fields.Char("Status")
etd_date = fields.Date("ETD Date")
bl_date = fields.Date("BL Date")
etd_sob = fields.Date("ETD SOB")
sale_contract_no = fields.Char("Sale Contract No")
customer = fields.Char("Customer")
elt_quantity = fields.Float("Elt Quantity")
number_of_container = fields.Integer("Containers")
vessel = fields.Char("Vessel")
shipping_company = fields.Char("Shipping Company")
booking_ref = fields.Char("Booking Ref")
freight_forwarder = fields.Char("Freight Forwarder")
instrument_status = fields.Char("Instrument Status")
ip_date = fields.Date("IP Date")
ip_status = fields.Char("IP Status")
latest_shipment_date = fields.Date("Latest Shipment Date")
countersigned = fields.Boolean("Countersigned")
comments = fields.Text("Comments")
docs_internal_comments = fields.Text("Docs Internal Comments")
alloc_quantity = fields.Float("Allocated Quantity")
si_comments = fields.Text("SI Comments")
is_archived = fields.Boolean("Archived")
price_cont = fields.Numeric("Price / Cont")
price_cont_curr = fields.Char("Currency")
ct_period_start = fields.Date("CT Start")
ct_period_end = fields.Date("CT End")
lsd_check = fields.Char("LSD Check")
bl2lsd_delta = fields.Integer("BL → LSD Delta")
fintrade_booking = fields.Char("Fintrade Booking")
alloc_unit_price = fields.Numeric("Alloc Unit Price")
alloc_price_curr = fields.Char("Alloc Price Curr")
alloc_price_unit = fields.Char("Alloc Price Unit")
left_time = fields.Integer("Days Left")
@classmethod
def table_query(cls):
return (
"SELECT "
"row_number() OVER () AS id, "
"* "
"FROM singa_execution_follow_up"
)

View File

@@ -0,0 +1,9 @@
<tryton>
<data>
<record model="ir.ui.view" id="execution_followup_tree">
<field name="model">execution.automation</field>
<field name="type">tree</field>
<field name="name">execution_automation_tree</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,56 @@
from trytond.model import ModelSQL, ModelView, fields
from sql import Table
from sql.functions import CurrentTimestamp
from sql import Column, Literal
import logging
logger = logging.getLogger(__name__)
class FreightBookingInfo(ModelSQL, ModelView):
"Freight Booking"
__name__ = 'freight.booking.info'
booking_number = fields.Char("Booking Number")
agent = fields.Char("Agent")
controller = fields.Char("Customer")
origin = fields.Char("Origin")
destination = fields.Char("Destination")
etd = fields.Date("ETD")
bl_date = fields.Date("BL date")
bl_number = fields.Char("BL Nb")
carrier = fields.Char("Carrier")
vessel = fields.Char("Vessel")
container_count = fields.Float("Containers")
quantity = fields.Float("Gross Weight")
@classmethod
def table_query(cls):
t = Table('freight_booking_info')
query = t.select(
Literal(None).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Column(t, 'FintradeBookingKey').as_('id'),
Column(t, 'ShippingInstructionNumber').as_('booking_number'),
Column(t, 'BookingAgent').as_('agent'),
Column(t, 'ExpectedController').as_('controller'),
Column(t, 'Loading').as_('origin'),
Column(t, 'Destination').as_('destination'),
Column(t, 'ETD_Date').as_('etd'),
Column(t, 'BL_Date').as_('bl_date'),
Column(t, 'BL_Number').as_('bl_number'),
Column(t, 'Carrier').as_('carrier'),
Column(t, 'Vessel').as_('vessel'),
Column(t, 'NumberOfContainers').as_('container_count'),
Column(t, 'ShippingInstructionQuantity').as_('quantity'),
)
#logger.info("*****QUERY*****:%s",query)
return query
@classmethod
def __setup__(cls):
super().__setup__()
cls._order = [
('etd', 'DESC'),
]

View File

@@ -0,0 +1,25 @@
<tryton>
<data>
<record model="ir.ui.view" id="freight_booking_info_tree">
<field name="model">freight.booking.info</field>
<field name="type">tree</field>
<field name="name">freight_booking_info_tree</field>
</record>
<record model="ir.action.act_window" id="act_freight_booking_info">
<field name="name">Freight Bookings</field>
<field name="res_model">freight.booking.info</field>
</record>
<record model="ir.action.act_window.view" id="act_freight_booking_info_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="freight_booking_info_tree"/>
<field name="act_window" ref="act_freight_booking_info"/>
</record>
<menuitem
name="Freight Booking"
action="act_freight_booking_info"
parent="menu_automation"
sequence="10"
id="menu_freight_booking" />
</data>
</tryton>

View File

@@ -5,4 +5,6 @@ depends:
res
document_incoming
xml:
automation.xml
automation.xml
freight_booking.xml
cron.xml

View File

@@ -0,0 +1,60 @@
<tree>
<field name="port_of_loading"/>
<field name="fb_loading"/>
<field name="warehouse"/>
<field name="origin"/>
<field name="agent"/>
<field name="operator"/>
<field name="fintrade_lc_nb"/>
<field name="lc_number"/>
<field name="si"/>
<field name="port_of_destination"/>
<field name="fb_destination"/>
<field name="status"/>
<field name="etd_date"/>
<field name="bl_date"/>
<field name="etd_sob"/>
<field name="sale_contract_no"/>
<field name="customer"/>
<field name="elt_quantity"/>
<field name="number_of_container"/>
<field name="vessel"/>
<field name="shipping_company"/>
<field name="booking_ref"/>
<field name="freight_forwarder"/>
<field name="instrument_status"/>
<field name="ip_date"/>
<field name="ip_status"/>
<field name="latest_shipment_date"/>
<field name="countersigned"/>
<field name="comments"/>
<field name="docs_internal_comments"/>
<field name="alloc_quantity"/>
<field name="si_comments"/>
<field name="is_archived"/>
<field name="price_cont"/>
<field name="price_cont_curr"/>
<field name="ct_period_start"/>
<field name="ct_period_end"/>
<field name="lsd_check"/>
<field name="bl2lsd_delta"/>
<field name="fintrade_booking"/>
<field name="alloc_unit_price"/>
<field name="alloc_price_curr"/>
<field name="alloc_price_unit"/>
<field name="left_time"/>
</tree>

View File

@@ -0,0 +1,13 @@
<tree>
<field name="booking_number"/>
<field name="agent"/>
<field name="controller"/>
<field name="origin"/>
<field name="destination"/>
<field name="etd"/>
<field name="bl_date"/>
<field name="bl_number"/>
<field name="carrier"/>
<field name="vessel"/>
<field name="container_count"/>
</tree>

View File

@@ -27,6 +27,14 @@ class Sale(metaclass=PoolMeta):
invoice.save()
return invoice
@property
def report_agent(self):
if self.agent:
return (self.agent.party.address_get(
type='delivery')).full_address
else:
return ''
@classmethod
@ModelView.button
@Workflow.transition('quotation')

View File

@@ -47,6 +47,8 @@ class Company(ModelSQL, ModelView):
help="Used to compute the today date.")
employees = fields.One2Many('company.employee', 'company', 'Employees',
help="Add employees to the company.")
logo = fields.Binary("Logo")
@property
def header_used(self):

View File

@@ -17,6 +17,8 @@ this repository contains the full copyright notices and license terms. -->
<field name="header"/>
<separator name="footer"/>
<field name="footer"/>
<separator name="logo"/>
<field name="logo" widget="image" stretch="true"/>
</page>
</notebook>
</form>

View File

@@ -137,6 +137,13 @@ class Currency(
closer = date
return res
@classmethod
def get_by_name(cls, name):
currencies = cls.search([('symbol', '=', name)], limit=1)
if not currencies:
return None
return currencies[0]
@staticmethod
def _get_rate(currencies, tdate=None):
'''

View File

@@ -17,6 +17,8 @@ from trytond.transaction import Transaction
from trytond.wizard import Button, StateTransition, StateView, Wizard
from .exceptions import DocumentIncomingSplitError
import logging
logger = logging.getLogger(__name__)
if config.getboolean('document_incoming', 'filestore', default=True):
file_id = 'file_id'
@@ -179,30 +181,112 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
def _split_mime_types(cls):
return ['application/pdf']
# @classmethod
# def from_inbound_email(cls, email_, rule):
# message = email_.as_dict()
# attachments = message.get('attachments')
# active = False
# data = message.get('text', message.get('html'))
# logger.info("DATA_FROM_INBOUND_MAIL:%s",data)
# if isinstance(data, str):
# data = data.encode()
# body = message.get('text') or message.get('html') or ''
# if isinstance(body, str):
# body_bytes = body.encode('utf-8')
# else:
# body_bytes = body
# document = cls(
# active=active,
# name=message.get('subject', 'No Subject'),
# company=rule.document_incoming_company,
# data=data,
# type=rule.document_incoming_type if active else None,
# source='inbound_email',
# )
# children = []
# if attachments:
# for attachment in attachments:
# child = cls(
# name=attachment['filename'] or 'data.bin',
# company=rule.document_incoming_company,
# data=attachment['data'],
# type=rule.document_incoming_type,
# source='inbound_email')
# children.append(child)
# else:
# child = cls(
# name='mail_' + message.get('subject', 'No Subject') + '.txt',
# company=rule.document_incoming_company,
# data=body_bytes,
# type=rule.document_incoming_type,
# source='inbound_email',
# )
# children.append(child)
# document.children = children
# document.save()
# return document
@classmethod
def from_inbound_email(cls, email_, rule):
message = email_.as_dict()
active = not message.get('attachments')
def clean(value):
if not value:
return value
return (
value
.replace('\n', ' ')
.replace('\r', ' ')
.replace("'", '')
.replace('"', '')
.strip()
)
subject = clean(message.get('subject', 'No Subject'))
attachments = message.get('attachments')
active = False
data = message.get('text', message.get('html'))
logger.info("DATA_FROM_INBOUND_MAIL:%s", data)
if isinstance(data, str):
data = data.encode()
body = message.get('text') or message.get('html') or ''
if isinstance(body, str):
body_bytes = body.encode('utf-8')
else:
body_bytes = body
document = cls(
active=active,
name=message.get('subject', 'No Subject'),
name=subject,
company=rule.document_incoming_company,
data=data,
type=rule.document_incoming_type if active else None,
source='inbound_email',
)
)
children = []
for attachment in message.get('attachments', []):
if attachments:
for attachment in attachments:
filename = clean(attachment['filename'] or 'data.bin')
child = cls(
name=filename,
company=rule.document_incoming_company,
data=attachment['data'],
type=rule.document_incoming_type,
source='inbound_email')
children.append(child)
else:
child = cls(
name=attachment['filename'] or 'data.bin',
name='mail_' + subject + '.txt',
company=rule.document_incoming_company,
data=attachment['data'],
data=body_bytes,
type=rule.document_incoming_type,
source='inbound_email')
source='inbound_email',
)
children.append(child)
document.children = children
document.save()
return document
@@ -265,7 +349,6 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
default.setdefault('children')
return super().copy(documents, default=default)
def iter_pages(expression, size):
ranges = set()
for pages in expression.split(','):

View File

@@ -4,7 +4,8 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
import logging
logger = logging.getLogger(__name__)
class Rule(metaclass=PoolMeta):
__name__ = 'inbound.email.rule'
@@ -53,4 +54,4 @@ class Rule(metaclass=PoolMeta):
if (self.action == 'document.incoming|from_inbound_email'
and self.document_incoming_process):
document = email_.result
DocumentIncoming.process([document], with_children=True)
DocumentIncoming.process([document], with_children=True)

View File

@@ -0,0 +1,15 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import document
__all__ = ['register']
def register():
Pool.register(
document.IncomingConfiguration,
document.Incoming,
module='document_incoming_wr', type_='model')

View File

@@ -0,0 +1,63 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.i18n import gettext
from trytond.model import fields
from trytond.modules.document_incoming.exceptions import (
DocumentIncomingProcessError)
from trytond.pool import Pool, PoolMeta
import json
class IncomingConfiguration(metaclass=PoolMeta):
__name__ = 'document.incoming.configuration'
default_controller = fields.Many2One('party.party', "Default Controller")
class Incoming(metaclass=PoolMeta):
__name__ = 'document.incoming'
@classmethod
def __setup__(cls):
super().__setup__()
cls.type.selection.append(
('weight_report', "Weight Report"))
cls.type.selection.append(
('controller', "Controller"))
@classmethod
def _get_results(cls):
return super()._get_results() | {'automation.document'}
def _process_weight_report(self):
WR = Pool().get('automation.document')
wr = WR()
wr.document = self.id
wr.type = 'weight_report'
wr.state = 'draft'
WR.save([wr])
WR.run_ocr([wr])
WR.run_metadata([wr])
return wr
def _process_controller(self):
WR = Pool().get('automation.document')
wr = WR()
wr.document = self.id
wr.type = 'controller'
wr.state = 'draft'
WR.save([wr])
WR.run_ocr([wr])
# WR.run_metadata([wr])
return wr
# @property
# def supplier_invoice_company(self):
# pass
# @property
# def supplier_invoice_party(self):
# pass

View File

@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="ddocument_incoming_configuration_view_form">
<field name="model">document.incoming.configuration</field>
<field name="inherit" ref="document_incoming.document_incoming_configuration_view_form"/>
<field name="name">document_incoming_configuration_form</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,8 @@
[tryton]
version=7.2.0
depends:
document_incoming
ir
party
xml:
document.xml

View File

@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form" position="inside">
<separator string="Weight Report" id="weight_report" colspan="4"/>
<label name="default_controller"/>
<field name="default_controller"/>
</xpath>
</data>

View File

@@ -17,6 +17,9 @@ from trytond.pyson import Eval
from trytond.transaction import Transaction
from trytond.url import http_host
import logging
logger = logging.getLogger(__name__)
if config.getboolean('inbound_email', 'filestore', default=True):
file_id = 'data_id'
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
@@ -74,6 +77,7 @@ class Inbox(ModelSQL, ModelView):
assert email_.inbox == self
for rule in self.rules:
if rule.match(email_.as_dict()):
logger.info("RULE_MATCHED:%s",rule)
email_.rule = rule
rule.run(email_)
return

View File

@@ -12,7 +12,7 @@ __all__ = ['IncotermMixin', 'IncotermAvailableMixin']
class IncotermMixin(Model):
incoterm = fields.Many2One(
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'),
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'), required=False,
ondelete='RESTRICT')
incoterm_location = fields.Many2One(
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),

View File

@@ -477,15 +477,16 @@ class Lot(ModelSQL, ModelView):
else:
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
def get_hist_quantity(self,seq):
def get_hist_quantity(self,state_id=0):
qt = Decimal(0)
gross_qt = Decimal(0)
if self.lot_state:
if self.lot_hist:
if seq != 0:
st = seq
if state_id != 0:
st = state_id
else:
st = self.lot_state.id
logger.info("GET_HIST_QT:%s",st)
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
qt = round(lot.quantity,5)
gross_qt = round(lot.gross_quantity,5)
@@ -499,24 +500,48 @@ class Lot(ModelSQL, ModelView):
physic_sum = Decimal(0)
for l in line.lots:
if l.lot_type == 'physic' :
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit)),5)
factor = None
rate = None
if l.lot_unit_line.category.id != l.line.unit.category.id:
factor = 1
rate = 1
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit, True, factor, rate)),5)
return line.quantity_theorical - physic_sum
def get_current_quantity(self,name=None):
# if self.lot_type == 'physic':
qt, gross_qt = self.get_hist_quantity(0)
qt, gross_qt = self.get_hist_quantity()
return qt
# else:
# return self.get_virtual_diff()
def get_current_quantity_converted(self,name=None):
def get_current_quantity_converted(self,state_id=0,unit=None):
Uom = Pool().get('product.uom')
unit = self.line.unit if self.line else self.sale_line.unit
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
if not unit:
unit = self.line.unit if self.line else self.sale_line.unit
qt, gross_qt = self.get_hist_quantity(state_id)
factor = None
rate = None
if self.lot_unit_line.category.id != unit.category.id:
factor = 1
rate = 1
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(qt), unit, True, factor, rate)),5)
def get_current_gross_quantity_converted(self,state_id=0,unit=None):
Uom = Pool().get('product.uom')
if not unit:
unit = self.line.unit if self.line else self.sale_line.unit
qt, gross_qt = self.get_hist_quantity(state_id)
factor = None
rate = None
if self.lot_unit_line.category.id != unit.category.id:
factor = 1
rate = 1
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(gross_qt), unit, True, factor, rate)),5)
def get_current_gross_quantity(self,name=None):
if self.lot_type == 'physic':
qt, gross_qt = self.get_hist_quantity(0)
qt, gross_qt = self.get_hist_quantity()
return gross_qt
else:
return None
@@ -526,6 +551,7 @@ class Lot(ModelSQL, ModelView):
lqh = LotQtHist()
lqh.quantity_type = qt_type
lqh.quantity = net
logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
lqh.gross_quantity = gross
lqh.lot = self
return lqh
@@ -542,6 +568,7 @@ class Lot(ModelSQL, ModelView):
if existing:
hist = existing[0]
hist.quantity = net
logger.info("SET_CURRENT_HIST:%s",gross)
hist.gross_quantity = gross
else:
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
@@ -633,6 +660,7 @@ class SplitLine(ModelView):
weight = fields.Numeric('Weight', digits=(16,5))
class SplitWizardStart(ModelView):
"Split Line Start"
__name__ = 'lot.split.wizard.start'
mode = fields.Selection([

View File

@@ -44,7 +44,7 @@ class Price(
price_composite = fields.One2Many('price.composite','price',"Composites")
price_product = fields.One2Many('price.product', 'price', "Product")
price_ct_size = fields.Numeric("Ct size")
def get_qt(self,nb_ct,unit):
Uom = Pool().get('product.uom')
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
@@ -71,7 +71,6 @@ class Price(
def get_price(self,dt,unit,currency,last=False):
price = float(0)
PV = Pool().get('price.price_value')
logger.info("ASKED_PRICE_FOR:%s",dt)
if self.price_values:
dt = dt.strftime("%Y-%m-%d")
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
@@ -115,7 +114,6 @@ class Calendar(DeactivableMixin,ModelSQL,ModelView,MultiValueMixin):
dt = dt.strftime("%Y-%m-%d")
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
if cl:
#logger.info("ISQUOTE:%s",cl)
return False
else:
return True
@@ -136,3 +134,20 @@ class Product(ModelSQL,ModelView):
__name__ = 'price.product'
price = fields.Many2One('price.price',"Price index")
product = fields.Many2One('product.product',"Product")
attributes = fields.Many2One('product.attribute',"Attribute",domain=[
('sets', '=', Eval('attribute_set')),
],
states={
'readonly': ~Eval('attribute_set'),
},
depends=['product', 'attribute_set'])
attribute_set = fields.Function(
fields.Many2One('product.attribute.set', "Attribute Set"),
'on_change_with_attribute_set'
)
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
if self.product and self.product.template and self.product.template.attribute_set:
return self.product.template.attribute_set.id

View File

@@ -1,6 +1,6 @@
<form>
<label name="price"/>
<field name="price"/>
<label name="product"/>
<field name="product"/>
<label name="attributes"/>
<field name="attributes"/>
</form>

View File

@@ -1,4 +1,5 @@
<tree>
<field name="price"/>
<field name="product"/>
<field name="attributes"/>
</tree>

View File

@@ -609,6 +609,26 @@ class Product(
('template.code', operator, code_value, *extra),
]
@classmethod
def get_by_name(cls, name, type_='goods'):
pool = Pool()
Template = pool.get('product.template')
Uom = pool.get('product.uom')
templates = Template.search([('name', '=', name)], limit=1)
if templates:
return templates[0].products[0]
unit_uom, = Uom.search([('name', '=', 'Mt')], limit=1)
template, = Template.create([{
'name': name,
'type': type_,
'default_uom': unit_uom.id,
'cost_price_method': 'fixed',
}])
return template.products[0]
@staticmethod
def get_price_uom(products, name):
Uom = Pool().get('product.uom')

View File

@@ -92,6 +92,13 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
def default_digits():
return 2
@classmethod
def get_by_name(cls, name):
uom = cls.search([('symbol', '=', name)], limit=1)
if not uom:
return None
return uom[0]
@fields.depends('factor')
def on_change_factor(self):
if (self.factor or 0.0) == 0.0:

View File

@@ -22,6 +22,7 @@ class Month(ModelView, ModelSQL):
is_cotation = fields.Boolean("Cotation month")
beg_date = fields.Date("Date from")
end_date = fields.Date("Date end")
description = fields.Char("Description")
class ProductMonth(ModelView, ModelSQL):
"Product month"

105
modules/purchase/AGENTS.md Normal file
View File

@@ -0,0 +1,105 @@
# AGENTS.md - Module `purchase`
Ce guide complete le `AGENTS.md` racine.
Pour ce module, les regles locales ci-dessous priment.
## 1) Perimetre metier
Le module `purchase` gere le cycle d'achat fournisseur:
- commande d'achat (`purchase.purchase`, `purchase.line`)
- facturation fournisseur (`account.invoice` liee a l'achat)
- reception/retour de stock (`stock.move`, `stock.shipment.in`, `stock.shipment.in.return`)
- reporting achats (axes temporels, fournisseur, produit)
## 2) Fichiers pivots
- Logique coeur:
- `modules/purchase/purchase.py`
- Extensions metier connexes:
- `modules/purchase/product.py`
- `modules/purchase/stock.py`
- `modules/purchase/invoice.py`
- `modules/purchase/party.py`
- `modules/purchase/configuration.py`
- `modules/purchase/purchase_reporting.py`
- Vues et actions:
- `modules/purchase/purchase.xml`
- `modules/purchase/stock.xml`
- `modules/purchase/invoice.xml`
- `modules/purchase/purchase_reporting.xml`
- Manifest et dependances:
- `modules/purchase/tryton.cfg`
- Documentation metier:
- `modules/purchase/docs/business-rules.template.md` (template)
- `modules/purchase/docs/business-rules.md` (instance a remplir)
## 3) Etats et flux critiques a preserver
Workflow de commande (dans `purchase.py`):
- `draft -> quotation -> confirmed -> processing -> done`
- transitions de retour existent aussi (`cancelled`, retour a `draft`, etc.)
Invariants importants:
- `invoice_state` et `shipment_state` doivent rester coherents apres `process()`.
- `process()` orchestre facture + stock + recalcul d'etats, ne pas contourner sans raison.
- `delete()` exige une commande annulee.
- Les methodes `create_invoice()` et `create_move()` sont sensibles (gestion `lots` et `action`).
## 4) Couplages a surveiller
- Facture:
- `purchase.py` <-> `invoice.py`
- gestion des exceptions facture (`purchase_exception_state`)
- Stock:
- `purchase.py` <-> `stock.py`
- liens `moves`, expeditions entrantes, retours
- Produit/fournisseur/prix:
- `product.py` impacte prix d'achat, UoM, fournisseurs
- Tiers:
- `party.py` impacte adresses/parametres fournisseur et contraintes d'effacement
## 5) Convention de modification pour ce module
1. Modifier d'abord le coeur minimal dans `purchase.py` ou le fichier specialise adequat.
2. Mettre a jour XML uniquement si comportement UI/action change.
3. Si regle metier impactee, mettre a jour `docs/business-rules.md`.
4. Ajouter un test proche du flux reel (scenario `.rst` prioritaire si possible).
5. Verifier les impacts transverses facture/stock avant rendu.
## 6) Strategie de test recommandee
Priorite 1 (rapide):
- `modules/purchase/tests/test_module.py`
Priorite 2 (comportement metier):
- `modules/purchase/tests/test_scenario.py`
- Scenarios cibles selon la modif:
- `scenario_purchase.rst`
- `scenario_purchase_manual_invoice.rst`
- `scenario_purchase_line_cancelled.rst`
- `scenario_purchase_line_cancelled_on_shipment.rst`
- `scenario_purchase_return_wizard.rst`
- `scenario_purchase_reporting.rst`
Si la modif touche prix/UoM/fournisseur:
- ajouter un cas dans `test_module.py` ou un scenario dedie.
## 7) Cas qui exigent validation humaine
- Changement du workflow d'etats
- Changement des regles de creation facture/mouvement
- Changement de logique sur retours fournisseur
- Changement qui altere les ecritures comptables ou le statut de paiement
## 8) Definition of done (module `purchase`)
- Le flux metier cible fonctionne de bout en bout.
- Les etats `state`, `invoice_state`, `shipment_state` restent coherents.
- Les tests du module pertinents passent.
- Le patch est limite aux fichiers necessaires.

View File

@@ -0,0 +1,122 @@
# Business Rules Template - Purchase
Statut: `draft` | `reviewed` | `approved`
Version: `v0.1`
Derniere mise a jour: `YYYY-MM-DD`
Owner metier: `Nom / Equipe`
Owner technique: `Nom / Equipe`
## 1) Scope
- Domaine: `ex: achats fournisseur`
- Hors scope: `ex: achats intercompany`
- Modules impactes:
- `purchase`
- `stock` (si applicable)
- `account_invoice` (si applicable)
## 2) Glossaire
- `Purchase`: commande d'achat fournisseur.
- `Line`: ligne de commande.
- `Invoice State`: etat facture calcule.
- `Shipment State`: etat reception calcule.
- Ajouter ici les termes metier propres a ton contexte.
## 3) Regles metier (source de verite)
### BR-001 - [Titre court]
- Intent: `Pourquoi cette regle existe`
- Description:
- `Enonce clair et testable`
- Conditions d'entree:
- `Etat`
- `Type de ligne (goods/service)`
- `Contexte (societe, devise, fournisseur, lot, etc.)`
- Resultat attendu:
- `Etat/valeur/action attendue`
- Exceptions:
- `Cas ou la regle ne s'applique pas`
- Priorite:
- `bloquante | importante | informative`
- Source:
- `Ticket / spec / decision metier`
### BR-002 - [Titre court]
- Intent:
- Description:
- Conditions d'entree:
- Resultat attendu:
- Exceptions:
- Priorite:
- Source:
## 4) Matrice d'etats (optionnel mais recommande)
| Regle | Etat initial | Evenement | Etat attendu | Notes |
|---|---|---|---|---|
| BR-001 | `draft` | `quote` | `quotation` | |
| BR-002 | `quotation` | `confirm` | `confirmed/processing` | |
## 5) Exemples concrets
### Exemple E1 - Cas nominal
- Donnees:
- `fournisseur = X`
- `produit = Y`
- `quantite = 10`
- Attendu:
- `invoice_state = pending`
- `shipment_state = waiting`
### Exemple E2 - Cas limite
- Donnees:
- Attendu:
## 6) Impact code attendu
- Fichiers Python potentiellement concernes:
- `modules/purchase/purchase.py`
- `modules/purchase/stock.py`
- `modules/purchase/invoice.py`
- `modules/purchase/product.py`
- Fichiers XML potentiellement concernes:
- `modules/purchase/purchase.xml`
- `modules/purchase/stock.xml`
- `modules/purchase/invoice.xml`
## 7) Strategie de tests
- Unitaires:
- `modules/purchase/tests/test_module.py`
- Scenarios:
- `modules/purchase/tests/scenario_purchase.rst`
- `modules/purchase/tests/scenario_purchase_manual_invoice.rst`
- `modules/purchase/tests/scenario_purchase_return_wizard.rst`
Pour chaque regle BR-xxx, lister le test associe:
| Regle | Test existant | Nouveau test a ajouter | Statut |
|---|---|---|---|
| BR-001 | `...` | `...` | `todo` |
## 8) Compatibilite et migration
- Effet retroactif sur commandes existantes: `oui/non`
- Migration necessaire: `oui/non`
- Plan de rollback:
- `comment revenir en arriere sans corruption metier`
## 9) Validation
- Valide par metier:
- `Nom` - `date`
- Valide par technique:
- `Nom` - `date`
- Decision finale:
- `approved / rejected / needs update`

View File

@@ -89,14 +89,14 @@ class Purchase(
number = fields.Char("Number", readonly=True)
reference = fields.Char("Reference")
description = fields.Char('Description', size=None, states=_states)
purchase_date = fields.Date('Purchase Date',
purchase_date = fields.Date('Purchase Date', required=True,
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
'required': ~Eval('state').in_(
['draft', 'quotation', 'cancelled']),
})
payment_term = fields.Many2One(
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
'account.invoice.payment_term', "Payment Term", required=True, ondelete='RESTRICT',
states={
'readonly': ~Eval('state').in_(['draft', 'quotation']),
})
@@ -389,6 +389,11 @@ class Purchase(
def default_state():
return 'draft'
@classmethod
def default_purchase_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_currency(cls, **pattern):
pool = Pool()
@@ -462,6 +467,8 @@ class Purchase(
self.tol_min = self.party.tol_min
if self.party.tol_max:
self.tol_max = self.party.tol_max
if self.party.origin:
self.product_origin = self.party.origin
if self.party.wb:
self.wb = self.party.wb
if self.party.association:
@@ -734,6 +741,7 @@ class Purchase(
@classmethod
def copy(cls, purchases, default=None):
Date = Pool().get('ir.date')
if default is None:
default = {}
else:
@@ -742,7 +750,7 @@ class Purchase(
default.setdefault('invoice_state', 'none')
default.setdefault('invoices_ignored', None)
default.setdefault('shipment_state', 'none')
default.setdefault('purchase_date', None)
default.setdefault('purchase_date', Date.today())
default.setdefault('quoted_by')
default.setdefault('confirmed_by')
default.setdefault('untaxed_amount_cache')
@@ -1021,9 +1029,15 @@ class Purchase(
pool = Pool()
Invoice = pool.get('account.invoice')
Invoice.save(invoices.values())
Invoice.save(invoices.values())
for purchase, invoice in invoices.items():
#check if forex
forex_rate = invoice.get_forex()
if forex_rate:
invoice.selection_rate = 'forex'
invoice.rate = invoice.on_change_with_rate()
Invoice.save([invoice])
purchase.copy_resources_to(invoice)
if len(invoices)==1:
if prepayment:
@@ -1215,7 +1229,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
()),
If(Eval('type') != 'line',
('id', '=', None),
()),
())
],
states={
'invisible': Eval('type') != 'line',
@@ -1685,7 +1699,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
@fields.depends(
'type', 'quantity', 'unit_price',
'purchase', '_parent_purchase.currency')
'purchase', '_parent_purchase.currency','premium')
def on_change_with_amount(self):
if (self.type == 'line'
and self.quantity is not None
@@ -1857,77 +1871,93 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
else:
lots_to_invoice = self.lots
for l in lots_to_invoice:
#if l.lot_type == 'physic':
invoice_line = InvoiceLine()
invoice_line.type = self.type
invoice_line.currency = self.currency
invoice_line.company = self.company
invoice_line.description = self.description
invoice_line.note = self.note
invoice_line.origin = self
qt, gross_qt = l.get_hist_quantity(0)
quantity = float(qt)
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
if self.unit:
quantity = self.unit.round(quantity)
invoice_line.unit_price = l.get_lot_price()
invoice_line.product = l.lot_product
invoice_line.quantity = quantity
if not invoice_line.quantity:
return []
invoice_line.unit = self.unit
invoice_line.taxes = self.taxes
if self.company.purchase_taxes_expense:
invoice_line.taxes_deductible_rate = 0
elif self.product:
invoice_line.taxes_deductible_rate = (
self.product.supplier_taxes_deductible_rate_used)
invoice_line.invoice_type = 'in'
if self.product:
invoice_line.account = self.product.account_stock_in_used
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_product_missing_account_expense',
purchase=self.purchase.rec_name,
product=self.product.rec_name))
else:
invoice_line.account = account_config.get_multivalue(
'default_category_account_expense', company=self.company.id)
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_missing_account_expense',
purchase=self.purchase.rec_name))
if action == 'prov':
invoice_line.description = 'Pro forma'
elif action == 'final':
invoice_line.description = 'Final'
elif action == 'service':
invoice_line.description = 'Service'
#invoice_line.stock_moves = self._get_invoice_line_moves()
#invoice_line.stock_moves = [l.get_current_supplier_move()]
invoice_line.lot = l.id
if self.product.type == 'service':
invoice_line.unit_price = self.unit_price
invoice_line.product = self.product
invoice_line.stock_moves = []
Fee = Pool().get('fee.fee')
fee = Fee.search(['purchase','=',self.purchase.id])
if fee:
invoice_line.fee = fee[0]
lines.append(invoice_line)
logger.info("GETINVLINE:%s",self.product.type)
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
if l.invoice_line_prov and self.product.type != 'service':
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
'invoice': None,
'quantity': -l.invoice_line_prov.quantity,
'unit_price': l.invoice_line_prov.unit_price,
'party': l.invoice_line_prov.invoice.party,
'origin': str(self),
})
lines.append(invoice_line_)
if l.lot_type == 'physic':
invoice_line = InvoiceLine()
invoice_line.type = self.type
invoice_line.currency = self.currency
invoice_line.company = self.company
invoice_line.description = self.description
invoice_line.note = self.note
invoice_line.origin = self
qt, gross_qt = l.get_hist_quantity(0)
quantity = float(qt)
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
if self.unit:
quantity = self.unit.round(quantity)
invoice_line.unit_price = l.get_lot_price()
invoice_line.product = l.lot_product
invoice_line.quantity = quantity
logger.info("GETINVOICELINE_QT:%s",quantity)
if not invoice_line.quantity:
return []
invoice_line.unit = self.unit
invoice_line.taxes = self.taxes
if self.company.purchase_taxes_expense:
invoice_line.taxes_deductible_rate = 0
elif self.product:
invoice_line.taxes_deductible_rate = (
self.product.supplier_taxes_deductible_rate_used)
invoice_line.invoice_type = 'in'
if self.product:
if self.product.type == 'service' and not self.product.landed_cost:
invoice_line.account = self.product.account_stock_in_used
else:
invoice_line.account = self.product.account_stock_in_used
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_product_missing_account_expense',
purchase=self.purchase.rec_name,
product=self.product.rec_name))
else:
invoice_line.account = account_config.get_multivalue(
'default_category_account_expense', company=self.company.id)
if not invoice_line.account:
raise AccountError(
gettext('purchase'
'.msg_purchase_missing_account_expense',
purchase=self.purchase.rec_name))
if action == 'prov':
invoice_line.description = 'Pro forma'
elif action == 'final':
invoice_line.description = 'Final'
elif action == 'service':
invoice_line.description = 'Service'
#invoice_line.stock_moves = self._get_invoice_line_moves()
#invoice_line.stock_moves = [l.get_current_supplier_move()]
invoice_line.lot = l.id
if self.product.type == 'service':
invoice_line.unit_price = self.unit_price
invoice_line.product = self.product
invoice_line.stock_moves = []
Fee = Pool().get('fee.fee')
fee = Fee.search(['purchase','=',self.purchase.id])
if fee:
invoice_line.fee = fee[0]
if fee[0].mode == 'lumpsum':
invoice_line.quantity = 1
elif fee[0].mode == 'ppack':
invoice_line.quantity = fee[0].quantity
else:
state_id = 0
LotQtType = Pool().get('lot.qt.type')
lqt = LotQtType.search([('name','=','BL')])
if lqt:
state_id = lqt[0].id
invoice_line.quantity = fee[0].get_fee_lots_qt(state_id)
lines.append(invoice_line)
logger.info("GETINVLINE:%s",self.product.type)
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
if l.invoice_line_prov and self.product.type != 'service':
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
'invoice': None,
'quantity': -l.invoice_line_prov.quantity,
'unit_price': l.invoice_line_prov.unit_price,
'party': l.invoice_line_prov.invoice.party,
'origin': str(self),
})
lines.append(invoice_line_)
return lines
def _get_invoice_line_quantity(self):
@@ -1990,8 +2020,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
'''
pool = Pool()
Move = pool.get('stock.move')
InvoiceLine = pool.get('account.invoice.line')
Uom = pool.get('product.uom')
Location = pool.get('stock.location')
if self.type != 'line':
return
if not self.product:
@@ -2047,26 +2076,28 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
to_location = self.purchase.to_location
move.from_location = from_location
if to_location.type != 'customer':
move.to_location = 8
else:
move.to_location = to_location
logger.info("FROM_LOCATION:%s",self.purchase.from_location)
logger.info("TO_LOCATION:%s",self.purchase.to_location)
if to_location:
if to_location.type != 'customer':
move.to_location = Location.get_transit_id()
else:
move.to_location = to_location
unit_price = l.get_lot_price()
if l.invoice_line_prov != None :
prov_inv = InvoiceLine(l.invoice_line_prov)
quantity -= prov_inv.quantity
if quantity < 0 :
move.from_location = self.purchase.to_location
move.to_location = 16
elif quantity > 0 :
move.from_location = 16
move.to_location = self.purchase.to_location
quantity = abs(quantity)
unit_price = prov_inv.unit_price
if quantity == 0:
continue
# if l.invoice_line_prov != None :
# prov_inv = InvoiceLine(l.invoice_line_prov)
# quantity -= prov_inv.quantity
# if quantity < 0 :
# move.from_location = self.purchase.to_location
# move.to_location = 16
# elif quantity > 0 :
# move.from_location = 16
# move.to_location = self.purchase.to_location
# quantity = abs(quantity)
# unit_price = prov_inv.unit_price
# if quantity == 0:
# continue
move.quantity = quantity
move.unit = self.unit
move.product = l.lot_product
@@ -2086,7 +2117,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
moves.append(move)
if move.to_location.type != 'customer':
move_to, = Move.copy([move.id], default={
'from_location': 8,
'from_location': Location.get_transit_id(),
'to_location': to_location,
})
moves.append(move_to)

View File

@@ -17,9 +17,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="payment_term"/>
<label name="currency"/>
<field name="currency"/>
<newline/>
<label name="certif"/>
<field name="certif"/>
</group>
<group col="2" colspan="2" id="hd" yfill="1">
<field name="viewer" widget="html_viewer" height="300" width="600"/>

View File

@@ -16,13 +16,21 @@ this repository contains the full copyright notices and license terms. -->
<label name="product_supplier"/>
<field name="product_supplier"/>
<newline/>
<label name="attributes_name"/>
<field name="attributes_name"/>
<label name="concentration"/>
<field name="concentration"/>
<newline/>
<label name="del_period"/>
<field name="del_period"/>
<label name="period_at"/>
<field name="period_at"/>
<newline/>
<label id="delivery_date" string="Delivery Date:"/>
<group id="delivery_date" col="-1">
<field name="delivery_date" xexpand="0"/>
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
</group>
<label name="del_period"/>
<field name="del_period"/>
<newline/>
<label name="from_del"/>
<field name="from_del"/>
@@ -40,6 +48,10 @@ this repository contains the full copyright notices and license terms. -->
<separator name="description" colspan="4"/>
<field name="description" colspan="4"/>
</page>
<page string="Attributes" id="att">
<label name="attributes"/>
<field name="attributes"/>
</page>
<page string="Taxes" id="taxes">
<field name="taxes" colspan="4"/>
</page>

View File

@@ -3,7 +3,38 @@
from trytond.pool import Pool
from . import purchase,sale,global_reporting,stock,derivative,lot,pricing,workflow,lc,dashboard,fee,payment_term,purchase_prepayment,cron,party,forex,outgoing,incoming,optional,association_tables, document_tracking, open_position, credit_risk
from . import (
account,
purchase,
sale,
global_reporting,
stock,
derivative,
lot,
pricing,
workflow,
lc,
dashboard,
fee,
payment_term,
purchase_prepayment,
cron,
party,
forex,
outgoing,
incoming,
optional,
association_tables,
document_tracking,
open_position,
credit_risk,
valuation,
dimension,
weight_report,
backtoback,
service,
invoice,
)
def register():
Pool.register(
@@ -47,6 +78,9 @@ def register():
dashboard.News,
dashboard.Demos,
party.Party,
party.PartyExecution,
party.PartyExecutionSla,
party.PartyExecutionPlace,
payment_term.PaymentTerm,
payment_term.PaymentTermLine,
purchase.Purchase,
@@ -69,8 +103,11 @@ def register():
fee.Fee,
fee.FeeLots,
purchase.FeeLots,
fee.Valuation,
fee.ValuationDyn,
valuation.Valuation,
valuation.ValuationLine,
valuation.ValuationDyn,
valuation.ValuationReport,
valuation.ValuationReportContext,
derivative.Derivative,
derivative.DerivativeMatch,
derivative.MatchWizardStart,
@@ -82,9 +119,12 @@ def register():
forex.PForex,
forex.ForexBI,
purchase.PnlBI,
purchase.PositionBI,
stock.Move,
stock.Location,
stock.InvoiceLine,
stock.ShipmentIn,
stock.ShipmentWR,
stock.ShipmentInternal,
stock.ShipmentOut,
stock.StatementOfFacts,
@@ -128,14 +168,41 @@ def register():
purchase.ContractDocumentType,
purchase.DocTemplate,
purchase.DocTypeTemplate,
purchase.Mtm,
purchase.PurchaseStrategy,
purchase.PriceComposition,
purchase.QualityAnalysis,
purchase.Assay,
purchase.AssayLine,
purchase.AssayElement,
purchase.AssayUnit,
purchase.PayableRule,
purchase.PenaltyRule,
purchase.PenaltyRuleTier,
purchase.ConcentrateTerm,
backtoback.Backtoback,
dimension.AnalyticDimension,
dimension.AnalyticDimensionValue,
dimension.AnalyticDimensionAssignment,
weight_report.WeightReport,
module='purchase', type_='model')
Pool.register(
account.PhysicalTradeIFRS,
module='purchase_trade', type_='model')
Pool.register(
invoice.Invoice,
invoice.InvoiceLine,
module='account_invoice', type_='model')
Pool.register(
forex.Forex,
forex.ForexCoverFees,
forex.ForexCategory,
pricing.Component,
pricing.Mtm,
pricing.MtmStrategy,
pricing.MtmScenario,
pricing.MtmSnapshot,
pricing.PriceMatrix,
pricing.PriceMatrixLine,
pricing.Estimated,
pricing.Pricing,
pricing.Period,
@@ -151,6 +218,9 @@ def register():
sale.SaleCreatePurchaseInput,
sale.Derivative,
sale.Valuation,
sale.ValuationLine,
sale.ValuationDyn,
sale.ValuationReport,
sale.Fee,
sale.Lot,
sale.FeeLots,
@@ -161,8 +231,11 @@ def register():
forex.SForex,
forex.ForexCoverPhysicalSale,
sale.ContractDocumentType,
sale.Mtm,
sale.SaleStrategy,
sale.OpenPosition,
sale.Backtoback,
sale.AnalyticDimensionAssignment,
sale.PriceComposition,
module='sale', type_='model')
Pool.register(
lot.LotShipping,
@@ -188,6 +261,7 @@ def register():
dashboard.DashboardLoader,
forex.ForexReport,
purchase.PnlReport,
purchase.PositionReport,
derivative.DerivativeMatchWizard,
module='purchase', type_='wizard')
Pool.register(

View File

@@ -0,0 +1,30 @@
# account.py
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
from trytond.pyson import Eval
__all__ = ['PhysicalTradeIFRS']
__metaclass__ = PoolMeta
class PhysicalTradeIFRS(ModelSQL, ModelView):
'Physical Trade - IFRS Adjustment'
__name__ = 'account.physical_trade_ifrs'
date = fields.Date('Date', required=True)
comment = fields.Text('Comment', required=True)
currency = fields.Many2One('currency.currency', 'Currency', required=True)
currency_digits = fields.Function(
fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
amount = fields.Numeric(
'Amount',
digits=(16, Eval('currency_digits', 2)),
depends=['currency_digits'],
required=True)
@fields.depends('currency')
def on_change_with_currency_digits(self, name=None):
if self.currency:
return self.currency.digits
return 2

View File

@@ -0,0 +1,69 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="res.group" id="group_physical_trade_ifrs">
<field name="name">Physical Trade IFRS</field>
</record>
<record model="res.group" id="group_physical_trade_ifrs_admin">
<field name="name">Physical Trade IFRS Administration</field>
<field name="parent" ref="group_physical_trade_ifrs"/>
</record>
<record model="res.user-res.group" id="user_admin_group_physical_trade_ifrs">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_physical_trade_ifrs"/>
</record>
<record model="res.user-res.group" id="user_admin_group_physical_trade_ifrs_admin">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_physical_trade_ifrs_admin"/>
</record>
<record model="ir.ui.view" id="view_physical_trade_ifrs_form">
<field name="model">account.physical_trade_ifrs</field>
<field name="type">form</field>
<field name="name">physical_trade_IFRS_form</field>
</record>
<record model="ir.ui.view" id="view_physical_trade_ifrs_tree">
<field name="model">account.physical_trade_ifrs</field>
<field name="type">tree</field>
<field name="name">physical_trade_IFRS_tree</field>
</record>
<record model="ir.action.act_window" id="act_physical_trade_ifrs_form">
<field name="name">Physical Trade - IFRS Adjustment</field>
<field name="res_model">account.physical_trade_ifrs</field>
</record>
<record model="ir.action.act_window.view" id="act_physical_trade_ifrs_view_tree">
<field name="sequence" eval="10"/>
<field name="view" ref="view_physical_trade_ifrs_tree"/>
<field name="act_window" ref="act_physical_trade_ifrs_form"/>
</record>
<record model="ir.action.act_window.view" id="act_physical_trade_ifrs_view_form">
<field name="sequence" eval="20"/>
<field name="view" ref="view_physical_trade_ifrs_form"/>
<field name="act_window" ref="act_physical_trade_ifrs_form"/>
</record>
<menuitem
name="Physical Trade - IFRS Adjustment"
parent="account.menu_processing"
action="act_physical_trade_ifrs_form"
sequence="30"
id="menu_physical_trade_ifrs"/>
<record model="ir.model.access" id="access_physical_trade_ifrs">
<field name="model">account.physical_trade_ifrs</field>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_physical_trade_ifrs_group">
<field name="model">account.physical_trade_ifrs</field>
<field name="group" ref="group_physical_trade_ifrs"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,22 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
import datetime
import logging
class Backtoback(ModelSQL, ModelView):
'Back To Back'
__name__ = 'back.to.back'
reference = fields.Char("Reference")
purchase = fields.One2Many('purchase.purchase','btb', "Purchase")

View File

@@ -0,0 +1,65 @@
<tryton>
<data>
<record model="ir.ui.icon" id="btb_icon">
<field name="name">tradon-btb</field>
<field name="path">icons/tradon-btb.svg</field>
</record>
<record model="ir.ui.view" id="btb_view_form">
<field name="model">back.to.back</field>
<field name="type">form</field>
<field name="name">btb_form</field>
</record>
<record model="ir.ui.view" id="purchase_btb_view_form">
<field name="model">purchase.purchase</field>
<field name="type">form</field>
<field name="name">purchase_btb_form</field>
</record>
<record model="ir.ui.view" id="purchase_line_btb_view_form">
<field name="model">purchase.line</field>
<field name="type">form</field>
<field name="name">purchase_line_btb_form</field>
</record>
<record model="ir.ui.view" id="sale_line_btb_view_form">
<field name="model">sale.line</field>
<field name="type">form</field>
<field name="name">sale_line_btb_form</field>
</record>
<record model="ir.ui.view" id="btb_view_list">
<field name="model">back.to.back</field>
<field name="type">tree</field>
<field name="name">btb_tree</field>
</record>
<record model="ir.action.act_window" id="act_btb_form">
<field name="name">Back to back</field>
<field name="res_model">back.to.back</field>
</record>
<record model="ir.action.act_window.view" id="act_btb_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="btb_view_list"/>
<field name="act_window" ref="act_btb_form"/>
</record>
<record model="ir.action.act_window.view" id="act_btb_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="btb_view_form"/>
<field name="act_window" ref="act_btb_form"/>
</record>
<menuitem
name="Back to back"
sequence="99"
id="menu_btb_main"
icon="tradon-btb" />
<menuitem
name="Back to back"
action="act_btb_form"
parent="menu_btb_main"
sequence="10"
id="menu_btb" />
</data>
</tryton>

View File

@@ -155,7 +155,7 @@ class Party(metaclass=PoolMeta):
if overdue > 0:
# scale by overdue relative to limit
limit = self.credit_limit or 1
score += int(min(40, (overdue / float(limit)) * 100))
score += int(min(40, (float(overdue) / float(limit)) * 100))
# cap
if score > 100:

View File

@@ -1,5 +1,5 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
# this repository contains the full copyright notices and license terms.
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
from trytond.pyson import Eval
from trytond.transaction import Transaction
@@ -193,7 +193,7 @@ class Dashboard(ModelSQL, ModelView):
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
logger.info("EXITONCHANGE",self.chatbot)
def get_last_two_fx_rates(self, from_code='USD', to_code='EUR'):
def get_last_five_fx_rates(self, from_code='USD', to_code='EUR'):
"""
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
"""
@@ -208,29 +208,93 @@ class Dashboard(ModelSQL, ModelView):
rates = CurrencyRate.search(
[('currency', '=', to_currency.id)],
order=[('date', 'DESC')],
limit=2,
limit=5,
)
if not rates:
return None, None
return None, None, None, None, None
# Calcul du taux EUR/USD
# Si la devise principale de la société est EUR, et que le taux stocké est
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
last_rate = rates[0].rate
prev_rate = rates[1].rate if len(rates) > 1 else None
f1 = rates[0].rate
f2 = rates[1].rate if len(rates) > 1 else None
f3 = rates[2].rate if len(rates) > 2 else None
f4 = rates[3].rate if len(rates) > 3 else None
f5 = rates[4].rate if len(rates) > 4 else None
d1 = rates[0].date
d2 = rates[1].date if len(rates) > 1 else None
d3 = rates[2].date if len(rates) > 2 else None
d4 = rates[3].date if len(rates) > 3 else None
d5 = rates[4].date if len(rates) > 4 else None
# if from_currency != to_currency:
# last_rate = 1 / last_rate if last_rate else None
# prev_rate = 1 / prev_rate if prev_rate else None
if last_rate and prev_rate:
return round(1/last_rate,6), round(1/prev_rate,6)
return round(1/f1,6), round(1/f2,6) if f2 else None, round(1/f3,6) if f3 else None, round(1/f4,6) if f4 else None, round(1/f5,6) if f5 else None, d1, d2, d3, d4, d5
def get_tremor(self,name):
Pnl = Pool().get('valuation.valuation')
pnls = Pnl.search(['id','>',0])
pnl_amount = "{:,.0f}".format(round(sum([e.amount for e in pnls]),0))
Date = Pool().get('ir.date')
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
Shipment = Pool().get('stock.shipment.in')
DocumentIncoming = Pool().get('document.incoming')
Fee = Pool().get('fee.fee')
WR = Pool().get('weight.report')
if config.automation:
shipment = Shipment.search([('state','!=','received')])
shipment_trend = [sh for sh in shipment if sh.create_date == Date.today()]
controller = Shipment.search([('controller','!=',None)])
controller_trend = [co for co in controller if co.create_date == Date.today()]
instruction = Shipment.search([('result','!=',None)])
instruction_trend = [si for si in instruction if si.create_date == Date.today()]
id_received = Shipment.search([('returned_id','!=',None)])
id_received_trend = [i for i in id_received if i.create_date == Date.today()]
wr = WR.search([('id','>',0)])
wr_trend = [w for w in wr if w.create_date == Date.today()]
so = Fee.search(['id','=',25])
so_trend = [s for s in so if s.create_date == Date.today()]
di = DocumentIncoming.search(['id','>',0])
di_trend = [d for d in di if d.create_date == Date.today()]
return (
config.dashboard +
"/dashboard/index.html?shipment="
+ str(len(shipment))
+ "&shipment_trend="
+ str(len(shipment_trend))
+ "&controller="
+ str(len(controller))
+ "&controller_trend="
+ str(len(controller_trend))
+ "&instruction="
+ str(len(instruction))
+ "&instruction_trend="
+ str(len(instruction_trend))
+ "&wr="
+ str(len(wr))
+ "&wr_trend="
+ str(len(wr_trend))
+ "&so="
+ str(len(so))
+ "&so_trend="
+ str(len(so_trend))
+ "&di="
+ str(len(di))
+ "&di_trend="
+ str(len(di_trend))
+ "&id_received="
+ str(len(id_received))
+ "&id_received_trend="
+ str(len(id_received_trend)))
f1,f2,f3,f4,f5,d1,d2,d3,d4,d5 = self.get_last_five_fx_rates()
Valuation = Pool().get('valuation.valuation')
last_total,last_variation = Valuation.get_totals()
pnl_amount = "{:,.0f}".format(round(last_total,0))
pnl_variation = 0
if last_total and last_variation:
pnl_variation = "{:,.2f}".format(round((last_variation/last_total)*100,0))
Open = Pool().get('open.position')
opens = Open.search(['id','>',0])
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
@@ -258,38 +322,69 @@ class Dashboard(ModelSQL, ModelView):
val_s = len(val)
conf = Sale.search(['state','=','confirmed'])
conf_s = len(conf)
Shipment = Pool().get('stock.shipment.in')
draft = Shipment.search(['state','=','draft'])
shipment_d = len(draft)
val = Purchase.search(['state','=','started'])
val = Shipment.search(['state','=','started'])
shipment_s = len(val)
conf = Purchase.search(['state','=','received'])
conf = Shipment.search(['state','=','received'])
shipment_r = len(conf)
Lot = Pool().get('lot.lot')
lots = Lot.search(['sale_line','!=',None])
lots = Lot.search([('sale_line','!=',None),('line','!=',None),('lot_type','=','physic')])
lot_m = len(lots)
val = Lot.search(['sale_line','=',None])
val = Lot.search([('sale_line','=',None),('line','!=',None),('lot_type','=','physic')])
lot_a = len(val)
conf = Lot.search(['lot_type','=','physic'])
lot_al = len(conf)
Invoice = Pool().get('account.invoice')
invs = Invoice.search(['type','=','in'])
inv_p = len(invs)
invs = Invoice.search([('type','=','in'),('state','=','paid')])
inv_p_p = len(invs)
invs = Invoice.search([('type','=','in'),('state','!=','paid')])
inv_p_np = len(invs)
invs = Invoice.search(['type','=','out'])
inv_s = len(invs)
invs = Invoice.search([('type','=','out'),('state','=','paid')])
inv_s_p = len(invs)
invs = Invoice.search([('type','=','out'),('state','!=','paid')])
inv_s_np = len(invs)
AccountMove = Pool().get('account.move')
accs = AccountMove.search(['id','>',0])
move_cash = len(accs)
accs = AccountMove.search([('journal','=',3),('state','!=','posted')])
pay_val = len(accs)
accs = AccountMove.search([('journal','=',3),('state','=','posted')])
pay_posted = len(accs)
return (
"https://srv413259.hstgr.cloud/dashboard/index.html?pnl_amount="
config.dashboard +
"/dashboard/index.html?pnl_amount="
+ str(pnl_amount)
+ "&pnl_variation="
+ str(pnl_variation)
+ "&exposure="
+ str(exposure)
+ "&topay="
+ str(topay)
+ "&toreceive="
+ str(toreceive)
+ "&eurusd="
+ str(f1)
+ "&eurusd="
+ str(f2)
+ "&eurusd="
+ str(f3)
+ "&eurusd="
+ str(f4)
+ "&eurusd="
+ str(f5)
+ "&eurusd_date="
+ str(d1)
+ "&eurusd_date="
+ str(d2)
+ "&eurusd_date="
+ str(d3)
+ "&eurusd_date="
+ str(d4)
+ "&eurusd_date="
+ str(d5)
+ "&draft_p="
+ str(draft_p)
+ "&val_p="
@@ -312,14 +407,22 @@ class Dashboard(ModelSQL, ModelView):
+ str(lot_m)
+ "&lot_a="
+ str(lot_a)
+ "&lot_al="
+ str(lot_al)
+ "&inv_p="
+ str(inv_p)
+ "&inv_p_p="
+ str(inv_p_p)
+ "&inv_p_np="
+ str(inv_p_np)
+ "&inv_s="
+ str(inv_s)
+ "&move_cash="
+ str(move_cash)
+ "&inv_s_p="
+ str(inv_s_p)
+ "&inv_s_np="
+ str(inv_s_np)
+ "&pay_val="
+ str(pay_val)
+ "&pay_posted="
+ str(pay_posted)
)
@@ -327,7 +430,7 @@ class Dashboard(ModelSQL, ModelView):
News = Pool().get('news.news')
Date = Pool().get('ir.date')
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
last_rate,prev_rate = self.get_last_two_fx_rates()
last_rate,prev_rate, = self.get_last_five_fx_rates()
if last_rate and prev_rate:
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
direction = "📈" if variation > 0 else "📉"
@@ -412,7 +515,7 @@ class Dashboard(ModelSQL, ModelView):
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
]
demos = Demos.search([('active', '=', True)])
demos = Demos.search([('active', '=', True)],order=[('id', 'DESC')])
for n in demos:
icon = n.icon or "📰"
category = n.category or "General"
@@ -499,12 +602,14 @@ class Dashboard(ModelSQL, ModelView):
return pu
def gen_url(self,name=None):
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
payload = {
"resource": {"dashboard": self.bi_id},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
token = jwt.encode(payload, config.payload, algorithm="HS256")
logger.info("TOKEN:%s",token)
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
@@ -663,6 +768,7 @@ class BotWizard(Wizard):
l.lot_quantity = l.lot_qt
l.lot_gross_quantity = l.lot_qt
l.lot_premium = Decimal(0)
l.lot_chunk_key = None
lot_id = LotQt.add_physical_lots(lqt,[l])
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
Dashboard.save([d])

View File

@@ -0,0 +1,79 @@
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard
import datetime
import logging
class AnalyticDimension(ModelSQL, ModelView):
'Analytic Dimension'
__name__ = 'analytic.dimension'
name = fields.Char('Name', required=True)
code = fields.Char('Code', required=True)
active = fields.Boolean('Active')
class AnalyticDimensionValue(ModelSQL, ModelView):
'Analytic Dimension Value'
__name__ = 'analytic.dimension.value'
dimension = fields.Many2One(
'analytic.dimension',
'Dimension',
required=True,
ondelete='CASCADE'
)
name = fields.Char('Name', required=True)
code = fields.Char('Code')
parent = fields.Many2One(
'analytic.dimension.value',
'Parent',
domain=[
('dimension', '=', Eval('dimension')),
],
depends=['dimension']
)
children = fields.One2Many(
'analytic.dimension.value',
'parent',
'Children'
)
active = fields.Boolean('Active')
class AnalyticDimensionAssignment(ModelSQL, ModelView):
'Analytic Dimension Assignment'
__name__ = 'analytic.dimension.assignment'
dimension = fields.Many2One(
'analytic.dimension',
'Dimension',
required=True
)
value = fields.Many2One(
'analytic.dimension.value',
'Value',
required=True,
domain=[
('dimension', '=', Eval('dimension')),
],
depends=['dimension']
)
purchase = fields.Many2One(
'purchase.purchase',
'Purchase',
ondelete='CASCADE'
)

View File

@@ -0,0 +1,34 @@
<tryton>
<data>
<record model="ir.ui.view" id="dimension_view_form">
<field name="model">analytic.dimension</field>
<field name="type">form</field>
<field name="name">dimension_form</field>
</record>
<record model="ir.ui.view" id="dimension_view_list">
<field name="model">analytic.dimension</field>
<field name="type">tree</field>
<field name="name">dimension_tree</field>
</record>
<record model="ir.ui.view" id="dimension_value_view_form">
<field name="model">analytic.dimension.value</field>
<field name="type">form</field>
<field name="name">dimension_value_form</field>
</record>
<record model="ir.ui.view" id="dimension_value_view_list">
<field name="model">analytic.dimension.value</field>
<field name="type">tree</field>
<field name="name">dimension_value_tree</field>
</record>
<record model="ir.ui.view" id="dimension_ass_view_form">
<field name="model">analytic.dimension.assignment</field>
<field name="type">form</field>
<field name="name">dimension_ass_form</field>
</record>
<record model="ir.ui.view" id="dimension_ass_view_list">
<field name="model">analytic.dimension.assignment</field>
<field name="type">tree</field>
<field name="name">dimension_ass_tree</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,157 @@
# Business Rules - Purchase Trade
Statut: `draft`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
Owner metier: `a completer`
Owner technique: `a completer`
## 1) Scope
- Domaine: `purchase_trade`
- Hors scope:
- Modules impactes:
- `purchase_trade`
- `lot`
## 2) Glossaire
- `Purchase Line`: ligne d'achat.
- `quantity_theorical`: quantite theorique contractuelle de la ligne.
- `Virtual Lot`: lot unique de type `virtual` rattache a une `purchase.line`.
- `lot.qt`: table des quantites ouvertes, matchées ou shippées par lot.
- `lot.qt ouvert`: enregistrement `lot.qt` avec `lot_p = virtual lot`, `lot_s = None` et sans shipment.
## 3) Regles metier
### BR-PT-001 - Ajustement de la quantite theorique apres creation du contrat
- Intent: conserver la coherence entre la quantite theorique de la ligne d'achat, le lot virtuel associe et les quantites ouvertes stockees dans `lot.qt`.
- Description:
- Quand `purchase.line.quantity_theorical` est modifiee apres creation du contrat, le systeme doit recalculer le delta entre l'ancienne et la nouvelle valeur.
- La regle s'applique au lot unique de type `virtual` rattache a la `purchase.line`.
- Conditions d'entree:
- Une `purchase.line` existe deja.
- Son champ `quantity_theorical` est modifie via `write`.
- Un lot `virtual` est rattache a la ligne.
- Resultat attendu:
- Si `delta > 0`:
- augmenter la quantite courante du lot `virtual` via `set_current_quantity` pour conserver l'historique `lot.qt.hist`
- augmenter le `lot.qt` ouvert existant
- si aucun `lot.qt` ouvert n'existe, en creer un nouveau avec le delta
- Si `delta < 0`:
- diminuer le `lot.qt` ouvert uniquement si la quantite ouverte disponible est suffisante
- diminuer la quantite courante du lot `virtual` du meme delta
- si aucun `lot.qt` ouvert n'existe ou si sa quantite est insuffisante, bloquer avec l'erreur `Please unlink or unmatch lot`
- Definition du `lot.qt` ouvert:
- `lot_p = virtual lot`
- `lot_s = None`
- `lot_shipment_in = None`
- `lot_shipment_internal = None`
- `lot_shipment_out = None`
- Exceptions:
- si aucun lot `virtual` n'est trouve sur la ligne, la regle ne fait rien
- Priorite:
- `bloquante`
- Source:
- `Decision metier documentee dans les commentaires de purchase_trade.purchase.Line.write`
### BR-PT-002 - Le lot physique est le pont metier entre purchase, sale et shipment
- Intent: disposer d'un chemin unique et stable pour retrouver les informations logistiques et de facturation reliees a un contrat d'achat ou de vente.
- Description:
- Le lot physique (`lot_type = physic`) porte simultanement le lien vers:
- la `purchase.line` via `lot.line`
- la `sale.line` via `lot.sale_line`
- le shipment via `lot.lot_shipment_in` / `lot.lot_shipment_internal` / `lot.lot_shipment_out`
- Pour toute logique qui doit naviguer entre achat, vente, shipment et facture, il faut privilegier ce lot physique comme source de verite.
- Resultat attendu:
- depuis une facture d'achat:
- remonter a la `purchase.line`
- puis au lot physique de la ligne
- puis au shipment et aux donnees logistiques associees
- depuis une facture de vente:
- remonter a la `sale.line`
- puis au lot physique matchant qui porte aussi la `purchase.line`
- puis au shipment et aux donnees logistiques associees
- Cas d'usage typiques:
- recuperer `bl_date`, `bl_number`, `controller`, `from_location`, `to_location`
- retrouver une facture provisoire liee au lot
- retrouver des fees rattaches au shipment
- Priorite:
- `structurante`
### BR-PT-003 - Le freight amount des templates facture vient du fee de shipment
- Intent: afficher dans les documents facture la vraie valeur de fret maritime rattachee au shipment du lot physique.
- Description:
- Le `FREIGHT VALUE` d'une facture ne doit pas etre pris sur la facture elle-meme.
- Il doit etre calcule a partir du `fee.fee` rattache au shipment (`shipment_in`) du lot physique relie a la facture.
- Regle de navigation:
- retrouver le lot physique pertinent depuis la facture
- retrouver son shipment
- chercher le `fee.fee` avec:
- `shipment_in = shipment.id`
- `product.name = 'Maritime freight'`
- utiliser `fee.get_amount()` comme montant de fret
- Portee:
- s'applique aussi bien aux factures d'achat qu'aux factures de vente
- cote vente, la remontee doit passer par le lot physique qui fait le lien entre `purchase.line` et `sale.line`
- Priorite:
- `importante`
## 4) Exemples concrets
### Exemple E1 - Augmentation simple
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 120`
- `lot.qt ouvert = 40`
- Attendu:
- lot `virtual` augmente de `20`
- `lot.qt ouvert` passe de `40` a `60`
### Exemple E2 - Augmentation sans lot.qt ouvert
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 110`
- aucun `lot.qt` ouvert
- Attendu:
- lot `virtual` augmente de `10`
- creation d'un `lot.qt` ouvert a `10`
### Exemple E3 - Diminution possible
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 90`
- `lot.qt ouvert = 25`
- Attendu:
- lot `virtual` diminue de `10`
- `lot.qt ouvert` passe de `25` a `15`
### Exemple E4 - Diminution impossible
- Donnees:
- `ancienne quantity_theorical = 100`
- `nouvelle quantity_theorical = 80`
- `lot.qt ouvert = 5`
- Attendu:
- blocage avec `Please unlink or unmatch lot`
## 5) Impact code attendu
- Fichiers Python concernes:
- `modules/purchase_trade/purchase.py`
- `modules/purchase_trade/lot.py`
## 6) Strategie de tests
Pour cette regle, couvrir au minimum:
- augmentation avec `lot.qt` ouvert existant
- augmentation sans `lot.qt` ouvert
- diminution possible
- diminution impossible avec erreur

View File

@@ -0,0 +1,149 @@
# Template Rules - Purchase Trade
Statut: `draft`
Version: `v0.2`
Derniere mise a jour: `2026-03-27`
## 1) Scope
- Domaine: `templates Relatorio .fodt`
- Modules concernes:
- `purchase_trade`
- `sale`
- `account_invoice`
## 2) Objectif
- Eviter les erreurs de parsing Relatorio/Genshi lors de la generation des documents.
- Standardiser la maniere d'alimenter les templates metier a partir du code Python.
## 3) Regles pratiques
### TR-001 - Toujours partir du template standard voisin
- Avant de modifier un template metier (`invoice_ict.fodt`, `sale_ict.fodt`, etc.), comparer avec le template standard du module source:
- `modules/account_invoice/invoice.fodt`
- `modules/sale/sale.fodt`
- Reprendre en priorite la syntaxe Relatorio deja validee dans ces templates.
### TR-002 - Eviter les expressions Genshi trop complexes dans le `.fodt`
- Preferer des proprietes Python simples exposees par le modele.
- Le template doit consommer au maximum des champs ou proprietes du type:
- `record.report_address`
- `record.report_price`
- `record.report_payment_date`
- Si un template a besoin de donnees issues d'un autre modele lie, creer un petit pont Python.
### TR-003 - Regles de syntaxe XML/Relatorio dans les placeholders
- Dans un `text:placeholder`, utiliser:
- `&quot;...&quot;` pour les guillemets doubles
- `&apos;...&apos;` pour les apostrophes
- Eviter les formes avec antislashs:
- interdit: `\'\'`
- interdit: `\'value\'`
- Exemples corrects:
- `&lt;replace text:p=&quot;set_lang(invoice.party.lang)&quot;&gt;`
- `&lt;if test=&quot;invoice.report_payment_description&quot;&gt;`
- `&lt;tax.description or &apos;&apos;&gt;`
### TR-004 - Pour une facture issue d'une vente, preferer un pont `account.invoice -> sale`
- Si le template facture doit reutiliser la logique de la pro forma vente, ne pas dupliquer les calculs directement dans le `.fodt`.
- Ajouter plutot dans `purchase_trade` une extension `account.invoice` avec des proprietes `report_*` qui relaient vers `invoice.sales[0]`.
- Exemple de proprietes utiles:
- `report_address`
- `report_contract_number`
- `report_shipment`
- `report_product_description`
- `report_crop_name`
- `report_attributes_name`
- `report_price`
- `report_payment_date`
- `report_nb_bale`
- `report_gross`
- `report_net`
- `report_lbs`
### TR-005 - Reutiliser les proprietes existantes du module `purchase_trade.sale`
- Avant d'ajouter une nouvelle logique pour un template vente ou facture issue d'une vente, verifier si une propriete existe deja sur `sale.sale`.
- Proprietes deja utiles:
- `report_terms`
- `report_gross`
- `report_net`
- `report_qt`
- `report_nb_bale`
- `report_deal`
- `report_packing`
- `report_price`
- `report_delivery`
- `report_payment_date`
- `report_shipment`
### TR-006 - Penser au cache des reports facture avant d'accuser le `.fodt`
- Les actions de report `account.invoice` peuvent partager le meme moteur de rendu.
- Dans `modules/account_invoice/invoice.py`, le champ `invoice_report_cache` peut reutiliser un document deja genere.
- Symptome typique:
- plusieurs actions differentes (`Provisional Invoice`, `Final Invoice`, `Prepayment`, etc.) semblent ouvrir le meme template ou le meme rendu
- Reflexe a avoir:
- verifier si le probleme vient du cache avant de modifier le `.fodt`
- pour un report alternatif, ne pas reutiliser le cache du report standard `account_invoice/invoice.fodt`
- si besoin, bypasser la lecture/ecriture du cache pour les templates alternatifs
### TR-007 - Pour une facture trade, privilegier le lot physique comme chemin de navigation
- Pour remonter d'une facture vers des donnees logistiques ou metier, ne pas dupliquer de chemins differents selon achat/vente.
- Regle pratique:
- partir de la ligne metier (`purchase.line` ou `sale.line`)
- retrouver le lot physique associe
- utiliser ce lot comme pont vers le shipment et les autres objets lies
- Ce chemin doit etre privilegie pour exposer des proprietes `report_*` comme:
- `report_bl_date`
- `report_loading_port`
- `report_discharge_port`
- `report_controller_name`
- `report_si_number`
- `report_proforma_invoice_number`
- `report_proforma_invoice_date`
### TR-008 - Le freight amount d'un template facture vient du fee de shipment
- Ne pas lire le fret directement sur `account.invoice`.
- Pour les templates `invoice_ict*`, le `FREIGHT VALUE` doit etre expose par une propriete Python du type `invoice.report_freight_amount`.
- La logique attendue est:
- retrouver le lot physique pertinent
- retrouver son shipment
- chercher le `fee.fee` du shipment avec `product.name = 'Maritime freight'`
- utiliser `fee.get_amount()`
- Si le fee a sa propre devise, preferer aussi exposer le symbole de devise depuis le fee plutot que depuis la facture.
## 4) Workflow recommande pour corriger un template en erreur
1. Identifier le placeholder exact qui provoque l'erreur Relatorio.
2. Comparer sa syntaxe avec le template standard equivalent.
3. Remplacer les guillemets/quotes non valides par `&quot;` / `&apos;`.
4. Si l'expression devient trop longue, la deplacer dans une propriete Python `report_*`.
5. Ne modifier que les placeholders necessaires.
6. Regenerer le document pour verifier la prochaine erreur eventuelle.
7. Si plusieurs actions affichent le meme rendu, verifier ensuite le cache `invoice_report_cache`.
## 5) Cas documentes dans ce repo
### Invoice ICT
- Fichier: `modules/account_invoice/invoice_ict.fodt`
- Strategie retenue:
- aligner la syntaxe sur `modules/account_invoice/invoice.fodt`
- reutiliser au maximum les proprietes metier deja exposees
- exposer dans `modules/purchase_trade/invoice.py` des proprietes de pont `account.invoice -> sale/purchase`
- pour les donnees shipment et freight, passer par le lot physique comme pont achat/vente
### Sale ICT
- Fichier: `modules/sale/sale_ict.fodt`
- Usage:
- reference principale pour les champs metier proches d'une pro forma / facture commerciale
- source de verite pratique pour les placeholders `report_*` issus de `purchase_trade.sale`

View File

@@ -6,7 +6,7 @@ from trytond.pyson import Bool, Eval, Id, If
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case
from sql import Column, Literal
@@ -18,120 +18,11 @@ import datetime
import logging
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
from trytond.modules.account.exceptions import PeriodNotFoundError
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('pur. priced', 'Pur. price'),
('pur. efp', 'Pur. efp'),
('sale priced', 'Sale price'),
('sale efp', 'Sale efp'),
('line fee', 'Line fee'),
('pur. fee', 'Pur. fee'),
('sale fee', 'Sale fee'),
('shipment fee', 'Shipment fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class Valuation(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation'
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Purch. Line")
date = fields.Date("Date")
type = fields.Selection(VALTYPE, "Type")
reference = fields.Char("Reference")
counterparty = fields.Many2One('party.party',"Counterparty")
product = fields.Many2One('product.product',"Product")
state = fields.Char("State")
price = fields.Numeric("Price",digits='unit')
currency = fields.Many2One('currency.currency',"Cur")
quantity = fields.Numeric("Quantity",digits='unit')
unit = fields.Many2One('product.uom',"Unit")
amount = fields.Numeric("Amount",digits='unit')
mtm = fields.Numeric("Mtm",digits='unit')
lot = fields.Many2One('lot.lot',"Lot")
class ValuationDyn(ModelSQL,ModelView):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
r_line = fields.Many2One('purchase.line',"Line")
r_date = fields.Date("Date")
r_type = fields.Selection(VALTYPE, "Type")
r_reference = fields.Char("Reference")
r_counterparty = fields.Many2One('party.party',"Counterparty")
r_product = fields.Many2One('product.product',"Product")
r_state = fields.Char("State")
r_price = fields.Numeric("Price",digits='r_unit')
r_currency = fields.Many2One('currency.currency',"Cur")
r_quantity = fields.Numeric("Quantity",digits='r_unit')
r_unit = fields.Many2One('product.uom',"Unit")
r_amount = fields.Numeric("Amount",digits='r_unit')
r_mtm = fields.Numeric("Mtm",digits='r_unit')
r_lot = fields.Many2One('lot.lot',"Lot")
@classmethod
def table_query(cls):
Valuation = Pool().get('valuation.valuation')
val = Valuation.__table__()
context = Transaction().context
group_pnl = context.get('group_pnl')
wh = (val.id > 0)
# query = val.select(
# Literal(0).as_('create_uid'),
# CurrentTimestamp().as_('create_date'),
# Literal(None).as_('write_uid'),
# Literal(None).as_('write_date'),
# val.id.as_('id'),
# val.purchase.as_('r_purchase'),
# val.line.as_('r_line'),
# val.date.as_('r_date'),
# val.type.as_('r_type'),
# val.reference.as_('r_reference'),
# val.counterparty.as_('r_counterparty'),
# val.product.as_('r_product'),
# val.state.as_('r_state'),
# val.price.as_('r_price'),
# val.currency.as_('r_currency'),
# val.quantity.as_('r_quantity'),
# val.unit.as_('r_unit'),
# val.amount.as_('r_amount'),
# val.mtm.as_('r_mtm'),
# val.lot.as_('r_lot'),
# where=wh)
#if group_pnl==True:
query = val.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.mtm).as_('r_mtm'),
Max(val.lot).as_('r_lot'),
where=wh,
group_by=[val.type,val.counterparty,val.state])
return query
def filter_state(state):
def filter(func):
@wraps(func)
@@ -166,16 +57,28 @@ class Fee(ModelSQL,ModelView):
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('rate', '% rate'),
('pcost', '% cost price'),
('ppack', 'Per packing'),
], 'Mode', required=True)
inherit_qt = fields.Boolean("Inh Qt")
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
unit = fields.Many2One('product.uom',"Unit",domain=[
If(Eval('mode') == 'ppack',
('category', '=', Eval('packing_category')),
()),
],
states={
'readonly': (Bool(Eval('mode') != 'ppack') & Bool(Eval('mode') != 'perqt')),
},
depends=['mode', 'packing_category'])
packing_category = fields.Function(fields.Many2One('product.uom.category',"Packing Category"),'on_change_with_packing_category')
inherit_shipment = fields.Boolean("Inh Sh",states={
'invisible': (Eval('shipment_in')),
})
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
qt_state = fields.Many2One('lot.qt.type',"Qt State")
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
@@ -191,9 +94,94 @@ class Fee(ModelSQL,ModelView):
weight_type = fields.Selection([
('net', 'Net'),
('brut', 'Brut'),
('brut', 'Gross'),
], string='W. type')
fee_date = fields.Date("Date")
@classmethod
def default_fee_date(cls):
Date = Pool().get('ir.date')
return Date.today()
@classmethod
def default_qt_state(cls):
LotQtType = Pool().get('lot.qt.type')
lqt = LotQtType.search([('name','=','BL')])
if lqt:
return lqt[0].id
@fields.depends('mode','unit')
def on_change_with_packing_category(self, name=None):
UnitCategory = Pool().get('product.uom.category')
packing = UnitCategory.search(['name','=','Packing'])
if packing:
return packing[0]
@fields.depends('line','sale_line','shipment_in','lots','price','unit','auto_calculation','mode','_parent_line.unit','_parent_line.lots','_parent_sale_line.unit','_parent_sale_line.lots','_parent_shipment_in.id')
def on_change_with_quantity(self, name=None):
qt = None
unit = None
line = self.line
logger.info("ON_CHANGE_WITH_LINE:%s",line)
if not line:
line = self.sale_line
if line:
if line.lots:
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots])
qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots])
unit = line.lots[0].lot_unit
logger.info("ON_CHANGE_WITH_QT0:%s",qt)
logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in)
if self.shipment_in:
Lot = Pool().get('lot.lot')
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
logger.info("ON_CHANGE_WITH_LOTS:%s",lots)
if lots:
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots])
qt_ = sum([e.get_current_quantity_converted(0) for e in lots])
unit = lots[0].lot_unit
if not qt:
logger.info("ON_CHANGE_WITH_QT1:%s",qt)
LotQt = Pool().get('lot.qt')
if self.shipment_in:
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
qt = Decimal(lqts[0].lot_quantity)
qt_ = qt
unit = lqts[0].lot_unit
logger.info("ON_CHANGE_WITH_QT2:%s",qt)
if self.mode != 'ppack':
return qt
else:
if self.auto_calculation:
logger.info("AUTOCALCULATION:%s",qt)
logger.info("AUTOCALCULATION2:%s",qt_)
logger.info("AUTOCALCULATION3:%s",Decimal(unit.factor))
logger.info("AUTOCALCULATION4:%s",Decimal(self.unit.factor))
return (qt_ * Decimal(unit.factor) / Decimal(self.unit.factor)).to_integral_value(rounding=ROUND_UP)
@fields.depends('price','mode','_parent_line.lots','_parent_sale_line.lots','shipment_in')
def on_change_with_unit(self, name=None):
if self.mode != 'ppack' and self.mode != 'perqt':
line = self.line
if not line:
line = self.sale_line
if line:
if line.lots:
if len(line.lots) == 1:
return line.lots[0].lot_unit_line
else:
return line.lots[1].lot_unit_line
if self.shipment_in:
Lot = Pool().get('lot.lot')
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
logger.info("ON_CHANGE_WITH_UNIT:%s",lots)
if lots:
return lots[0].lot_unit_line
else:
return self.unit
def get_lots(self, name):
logger.info("GET_LOTS_LINE:%s",self.line)
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
@@ -230,6 +218,23 @@ class Fee(ModelSQL,ModelView):
if ml:
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
def get_non_cog(self,lot):
MoveLine = Pool().get('account.move.line')
Currency = Pool().get('currency.currency')
Date = Pool().get('ir.date')
AccountConfiguration = Pool().get('account.configuration')
account_configuration = AccountConfiguration(1)
Uom = Pool().get('product.uom')
ml = MoveLine.search([
('lot', '=', lot.id),
('fee', '=', self.id),
('account', '=', self.product.account_stock_in_used.id),
])
logger.info("GET_NON_COG_FEE:%s",ml)
if ml:
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
@classmethod
def __setup__(cls):
super().__setup__()
@@ -248,13 +253,14 @@ class Fee(ModelSQL,ModelView):
def default_p_r(cls):
return 'pay'
def get_unit(self, name):
Lot = Pool().get('lot.lot')
if self.lots:
if self.lots[0].line:
return self.lots[0].line.unit
if self.lots[0].sale_line:
return self.lots[0].sale_line.unit
def get_unit(self, name=None):
FeeLots = Pool().get('fee.lots')
fl = FeeLots.search(['fee','=',self.id])
if fl:
if fl[0].lot.line:
return fl[0].lot.line.unit
if fl[0].lot.sale_line:
return fl[0].lot.sale_line.unit
@classmethod
@ModelView.button
@@ -285,7 +291,11 @@ class Fee(ModelSQL,ModelView):
return round(self.price / self.quantity,4)
elif self.mode == 'perqt':
return self.price
elif self.mode == 'pprice':
elif self.mode == 'ppack':
unit = self.get_unit()
if unit and self.unit:
return round(self.price / Decimal(self.unit.factor) * Decimal(unit.factor),4)
elif self.mode == 'pprice' or self.mode == 'pcost':
if self.line and self.price:
return round(self.price * Decimal(self.line.unit_price) / 100,4)
if self.sale_line and self.price:
@@ -305,8 +315,8 @@ class Fee(ModelSQL,ModelView):
def get_landed_status(self,name):
if self.product:
return self.product.landed_cost
return self.product.template.landed_cost
def get_quantity(self,name=None):
qt = self.get_fee_lots_qt()
if qt:
@@ -317,6 +327,7 @@ class Fee(ModelSQL,ModelView):
return Decimal(lqts[0].lot_quantity)
def get_amount(self,name=None):
Date = Pool().get('ir.date')
sign = Decimal(1)
if self.price:
# if self.p_r:
@@ -324,13 +335,54 @@ class Fee(ModelSQL,ModelView):
# sign = -1
if self.mode == 'lumpsum':
return self.price * sign
elif self.mode == 'ppack':
return round(self.price * self.quantity,2)
elif self.mode == 'rate':
#take period with estimated trigger date
if self.line:
if self.line.estimated_date:
beg_date = self.fee_date if self.fee_date else Date.today()
est_lines = [dd for dd in self.line.estimated_date if dd.trigger == 'bldate']
est_line = est_lines[0] if est_lines else None
if est_line and est_line.estimated_date:
est_date = est_line.estimated_date + datetime.timedelta(
days=est_line.fin_int_delta or 0
)
if est_date and beg_date:
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price/100,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
return round(factor * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
if self.sale_line:
if self.sale_line.sale.payment_term:
beg_date = self.fee_date if self.fee_date else Date.today()
est_date = self.sale_line.sale.payment_term.lines[0].get_date(beg_date,self.sale_line)
logger.info("EST_DATE:%s",est_date)
if est_date and beg_date:
factor = InterestCalculator.calculate(
start_date=beg_date,
end_date=est_date,
rate=self.price/100,
rate_type='annual',
convention='ACT/360',
compounding='simple'
)
logger.info("FACTOR:%s",factor)
return round(factor * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
elif self.mode == 'perqt':
if self.shipment_in:
StockMove = Pool().get('stock.move')
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
if sm:
unique_lots = {e.lot for e in sm if e.lot}
return round(self.price * Decimal(sum([e.get_current_quantity_converted() for e in unique_lots])) * sign,2)
return round(self.price * Decimal(sum([e.get_current_quantity_converted(0,self.unit) for e in unique_lots])) * sign,2)
LotQt = Pool().get('lot.qt')
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
if lqts:
@@ -373,12 +425,12 @@ class Fee(ModelSQL,ModelView):
return super().copy(fees, default=default)
def get_fee_lots_qt(self):
def get_fee_lots_qt(self,state_id=0):
qt = Decimal(0)
FeeLots = Pool().get('fee.lots')
fee_lots = FeeLots.search([('fee', '=', self.id)])
if fee_lots:
qt = sum([e.lot.get_current_quantity_converted() for e in fee_lots])
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
logger.info("GET_FEE_LOTS_QT:%s",qt)
return qt
@@ -388,10 +440,18 @@ class Fee(ModelSQL,ModelView):
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
if self.price != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.price
if self.quantity != self.purchase.lines[0].quantity:
self.purchase.lines[0].quantity = self.quantity
if self.mode == 'lumpsum':
if self.amount != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.amount
elif self.mode == 'ppack':
if self.amount != self.purchase.lines[0].amount:
self.purchase.lines[0].unit_price = self.price
self.purchase.lines[0].quantity = self.quantity
else:
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
self.purchase.lines[0].unit_price = self.get_price_per_qt()
if self.quantity != self.purchase.lines[0].quantity:
self.purchase.lines[0].quantity = self.quantity
if self.product != self.purchase.lines[0].product:
self.purchase.lines[0].product = self.product
PurchaseLine.save([self.purchase.lines[0]])
@@ -408,46 +468,47 @@ class Fee(ModelSQL,ModelView):
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
records = super(Fee, cls).create(vlist)
fees = super(Fee, cls).create(vlist)
qt_sh = Decimal(0)
qt_line = Decimal(0)
unit = None
for record in records:
for fee in fees:
FeeLots = Pool().get('fee.lots')
Lots = Pool().get('lot.lot')
LotQt = Pool().get('lot.qt')
if record.line:
for l in record.line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.line = l.line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.line.unit
if record.sale_line:
for l in record.sale_line.lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.lot = l.id
fl.sale_line = l.sale_line.id
FeeLots.save([fl])
if record.shipment_in:
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
if fee.line:
for l in fee.line.lots:
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
fl = FeeLots()
fl.fee = fee.id
fl.lot = l.id
fl.line = l.line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.line.unit
if fee.sale_line:
for l in fee.sale_line.lots:
if (l.lot_type == 'virtual' and len(fee.sale_line.lots)==1) or (l.lot_type == 'physic' and len(fee.sale_line.lots)>1):
fl = FeeLots()
fl.fee = fee.id
fl.lot = l.id
fl.sale_line = l.sale_line.id
FeeLots.save([fl])
qt_line += l.get_current_quantity_converted()
unit = l.sale_line.unit
if fee.shipment_in:
if fee.shipment_in.state == 'draft'or fee.shipment_in.state == 'started':
lots = Lots.search(['lot_shipment_in','=',fee.shipment_in.id])
if lots:
for l in lots:
#if l.lot_type == 'physic':
fl = FeeLots()
fl.fee = record.id
fl.fee = fee.id
fl.lot = l.id
FeeLots.save([fl])
qt_sh += l.get_current_quantity_converted()
unit = l.line.unit
else:
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
if lqts:
for l in lqts:
qt_sh += l.lot_p.get_current_quantity_converted()
@@ -455,32 +516,154 @@ class Fee(ModelSQL,ModelView):
else:
raise UserError("You cannot add fee on received shipment!")
type = record.type
type = fee.type
if type == 'ordered':
Purchase = Pool().get('purchase.purchase')
PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine()
pl.product = record.product
if record.line:
pl.product = fee.product
if fee.line or fee.sale_line:
pl.quantity = round(qt_line,5)
if record.shipment_in:
if fee.shipment_in:
pl.quantity = round(qt_sh,5)
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
pl.unit = unit
pl.fee_ = record.id
if record.price:
pl.unit_price = round(Decimal(record.price),4)
pl.fee_ = fee.id
if fee.price:
fee_price = fee.get_price_per_qt()
logger.info("GET_FEE_PRICE_PER_QT:%s",fee_price)
pl.unit_price = round(Decimal(fee_price),4)
if fee.mode == 'lumpsum':
pl.quantity = 1
pl.unit_price = round(Decimal(fee.amount),4)
elif fee.mode == 'ppack':
pl.unit_price = fee.price
p = Purchase()
p.lines = [pl]
p.party = record.supplier
p.party = fee.supplier
if p.party.addresses:
p.invoice_address = p.party.addresses[0]
p.currency = record.currency
p.currency = fee.currency
p.line_type = 'service'
p.from_location = fee.shipment_in.from_location if fee.shipment_in else (fee.line.purchase.from_location if fee.line else fee.sale_line.sale.from_location)
p.to_location = fee.shipment_in.to_location if fee.shipment_in else (fee.line.purchase.to_location if fee.line else fee.sale_line.sale.to_location)
if fee.shipment_in and fee.shipment_in.lotqt:
p.payment_term = fee.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
elif fee.line:
p.payment_term = fee.line.purchase.payment_term
elif fee.sale_line:
p.payment_term = fee.sale_line.sale.payment_term
Purchase.save([p])
return records
#if reception of moves done we need to generate accrual for fee
if not fee.sale_line:
feelots = FeeLots.search(['fee','=',fee.id])
for fl in feelots:
if fee.product.template.landed_cost:
move = fl.lot.get_received_move()
if move:
Warning = Pool().get('res.user.warning')
warning_name = Warning.format("Lot ever received", [])
if Warning.check(warning_name):
raise UserWarning(warning_name,
"By clicking yes, an accrual for this fee will be created")
AccountMove = Pool().get('account.move')
account_move = move._get_account_stock_move_fee(fee)
AccountMove.save([account_move])
else:
AccountMove = Pool().get('account.move')
account_move = fee._get_account_move_fee(fl.lot)
AccountMove.save([account_move])
return fees
def _get_account_move_fee(self,lot,in_out='in',amt = None):
pool = Pool()
AccountMove = pool.get('account.move')
Date = pool.get('ir.date')
Period = pool.get('account.period')
AccountConfiguration = pool.get('account.configuration')
if self.product.type != 'service':
return
today = Date.today()
company = lot.line.purchase.company if lot.line else lot.sale_line.sale.company
for date in [today]:
try:
period = Period.find(company, date=date, test_state=False)
except PeriodNotFoundError:
if date < today:
return
continue
break
else:
return
if period.state != 'open':
date = today
period = Period.find(company, date=date)
AccountMoveLine = pool.get('account.move.line')
Currency = pool.get('currency.currency')
move_line = AccountMoveLine()
move_line.lot = lot
move_line.fee = self
move_line.origin = None
move_line_ = AccountMoveLine()
move_line_.lot = lot
move_line_.fee = self
move_line_.origin = None
amount = amt if amt else self.amount
if self.currency != company.currency:
with Transaction().set_context(date=today):
amount_converted = amount
amount = Currency.compute(self.currency,
amount, company.currency)
move_line.second_currency = self.currency
if self.p_r == 'pay':
move_line.debit = amount
move_line.credit = Decimal(0)
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
if hasattr(move_line, 'second_currency') and move_line.second_currency:
move_line.amount_second_currency = amount_converted
move_line_.debit = Decimal(0)
move_line_.credit = amount
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
move_line_.amount_second_currency = -amount_converted
else:
move_line.debit = Decimal(0)
move_line.credit = amount
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
if hasattr(move_line, 'second_currency') and move_line.second_currency:
move_line.amount_second_currency = -amount_converted
move_line_.debit = amount
move_line_.credit = Decimal(0)
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
move_line_.amount_second_currency = amount_converted
logger.info("FEE_MOVELINES_1:%s",move_line)
logger.info("FEE_MOVELINES_2:%s",move_line_)
AccountJournal = Pool().get('account.journal')
journal = AccountJournal.search(['type','=','expense'])
if journal:
journal = journal[0]
description = None
description = 'Fee'
return AccountMove(
journal=journal,
period=period,
date=date,
origin=None,
description=description,
lines=[move_line,move_line_],
)
class FeeLots(ModelSQL,ModelView):
"Fee lots"

View File

@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">fee_tree_sequence</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
<field name="model">valuation.valuation</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence3</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph</field>
</record>
<record model="ir.ui.view" id="valuation_view_graph2">
<field name="model">valuation.valuation</field>
<field name="type">graph</field>
<field name="name">valuation_graph2</field>
</record>
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
<field name="model">valuation.valuation.dyn</field>
<field name="type">tree</field>
<field name="name">valuation_tree_sequence4</field>
</record>
<record model="ir.ui.view" id="fee_view_tree_sequence2">
<field name="model">fee.fee</field>
<field name="type">tree</field>

View File

@@ -0,0 +1,141 @@
from decimal import Decimal, getcontext
from datetime import date
from calendar import isleap
getcontext().prec = 28
class DayCount:
@staticmethod
def year_fraction(start_date, end_date, convention):
if end_date <= start_date:
return Decimal('0')
if convention == 'ACT/360':
return Decimal((end_date - start_date).days) / Decimal(360)
elif convention in ('ACT/365', 'ACT/365F'):
return Decimal((end_date - start_date).days) / Decimal(365)
elif convention == 'ACT/ACT_ISDA':
return DayCount._act_act_isda(start_date, end_date)
elif convention == '30/360_US':
return DayCount._30_360_us(start_date, end_date)
elif convention == '30E/360':
return DayCount._30e_360(start_date, end_date)
elif convention == '30E/360_ISDA':
return DayCount._30e_360_isda(start_date, end_date)
else:
raise ValueError(f"Unsupported convention {convention}")
# ---------- IMPLEMENTATIONS ----------
@staticmethod
def _act_act_isda(start_date, end_date):
total = Decimal('0')
current = start_date
while current < end_date:
year_end = date(current.year, 12, 31)
period_end = min(year_end.replace(day=31) +
(date(current.year + 1, 1, 1) - year_end),
end_date)
days_in_period = (period_end - current).days
days_in_year = 366 if isleap(current.year) else 365
total += Decimal(days_in_period) / Decimal(days_in_year)
current = period_end
return total
@staticmethod
def _30_360_us(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
if d1.day in (30, 31) and d2.day == 31:
d2_day = 30
else:
d2_day = d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360(d1, d2):
d1_day = min(d1.day, 30)
d2_day = min(d2.day, 30)
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
@staticmethod
def _30e_360_isda(d1, d2):
d1_day = 30 if d1.day == 31 else d1.day
d2_day = 30 if d2.day == 31 else d2.day
days = ((d2.year - d1.year) * 360 +
(d2.month - d1.month) * 30 +
(d2_day - d1_day))
return Decimal(days) / Decimal(360)
class InterestCalculator:
@staticmethod
def calculate(
start_date,
end_date,
rate,
rate_type='annual', # 'annual' or 'monthly'
convention='ACT/360',
compounding='simple', # simple, annual, monthly, continuous
):
"""
Retourne le facteur d'intérêt (pas le montant).
"""
if not start_date or not end_date:
return Decimal('0')
if end_date <= start_date:
return Decimal('0')
rate = Decimal(str(rate))
# Conversion en taux annuel si besoin
if rate_type == 'monthly':
annual_rate = rate * Decimal(12)
else:
annual_rate = rate
yf = DayCount.year_fraction(start_date, end_date, convention)
if compounding == 'simple':
return annual_rate * yf
elif compounding == 'annual':
return (Decimal(1) + annual_rate) ** yf - Decimal(1)
elif compounding == 'monthly':
monthly_rate = annual_rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
elif compounding == 'continuous':
from math import exp
return Decimal(exp(float(annual_rate * yf))) - Decimal(1)
else:
raise ValueError("Unsupported compounding mode")

View File

@@ -0,0 +1,259 @@
from decimal import Decimal, getcontext
from datetime import datetime, date
from calendar import isleap
from typing import Callable, Dict
import uuid
getcontext().prec = 28
# {
# "computation_type": "INTEREST_ACCRUAL",
# "input": {
# "start_date": "2026-01-01",
# "end_date": "2026-06-30",
# "notional": "1000000",
# "rate": {
# "value": "0.08",
# "type": "ANNUAL"
# },
# "day_count_convention": "ACT/360",
# "compounding_method": "SIMPLE"
# }
# }
# result = FinancialComputationService.execute(payload)
# interest = Decimal(result["result"]["interest_amount"])
# interest = currency.round(interest)
# ============================================================
# VERSIONING
# ============================================================
ENGINE_VERSION = "1.0.0"
# ============================================================
# REGISTRY (PLUGIN SYSTEM)
# ============================================================
DAY_COUNT_REGISTRY: Dict[str, Callable] = {}
COMPOUNDING_REGISTRY: Dict[str, Callable] = {}
def register_day_count(name: str):
def decorator(func):
DAY_COUNT_REGISTRY[name] = func
return func
return decorator
def register_compounding(name: str):
def decorator(func):
COMPOUNDING_REGISTRY[name] = func
return func
return decorator
# ============================================================
# DOMAIN DAY COUNT CONVENTIONS
# ============================================================
@register_day_count("ACT/360")
def act_360(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(360)
@register_day_count("ACT/365F")
def act_365f(start: date, end: date) -> Decimal:
return Decimal((end - start).days) / Decimal(365)
@register_day_count("ACT/ACT_ISDA")
def act_act_isda(start: date, end: date) -> Decimal:
total = Decimal("0")
current = start
while current < end:
year_end = date(current.year, 12, 31)
next_year = date(current.year + 1, 1, 1)
period_end = min(next_year, end)
days = (period_end - current).days
year_days = 366 if isleap(current.year) else 365
total += Decimal(days) / Decimal(year_days)
current = period_end
return total
@register_day_count("30E/360")
def thirty_e_360(start: date, end: date) -> Decimal:
d1 = min(start.day, 30)
d2 = min(end.day, 30)
days = (
(end.year - start.year) * 360 +
(end.month - start.month) * 30 +
(d2 - d1)
)
return Decimal(days) / Decimal(360)
# ============================================================
# DOMAIN COMPOUNDING STRATEGIES
# ============================================================
@register_compounding("SIMPLE")
def simple(rate: Decimal, yf: Decimal) -> Decimal:
return rate * yf
@register_compounding("ANNUAL")
def annual(rate: Decimal, yf: Decimal) -> Decimal:
return (Decimal(1) + rate) ** yf - Decimal(1)
@register_compounding("MONTHLY")
def monthly(rate: Decimal, yf: Decimal) -> Decimal:
monthly_rate = rate / Decimal(12)
months = yf * Decimal(12)
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
@register_compounding("CONTINUOUS")
def continuous(rate: Decimal, yf: Decimal) -> Decimal:
from math import exp
return Decimal(exp(float(rate * yf))) - Decimal(1)
# ============================================================
# DOMAIN INTEREST COMPUTATION OBJECT
# ============================================================
class InterestComputation:
def __init__(
self,
start_date: date,
end_date: date,
notional: Decimal,
rate_value: Decimal,
rate_type: str,
day_count: str,
compounding: str,
):
self.start_date = start_date
self.end_date = end_date
self.notional = notional
self.rate_value = rate_value
self.rate_type = rate_type
self.day_count = day_count
self.compounding = compounding
def compute(self):
if self.end_date <= self.start_date:
raise ValueError("end_date must be after start_date")
if self.day_count not in DAY_COUNT_REGISTRY:
raise ValueError("Unsupported day count convention")
if self.compounding not in COMPOUNDING_REGISTRY:
raise ValueError("Unsupported compounding method")
yf = DAY_COUNT_REGISTRY[self.day_count](
self.start_date,
self.end_date
)
# Normalize rate to annual
if self.rate_type == "MONTHLY":
annual_rate = self.rate_value * Decimal(12)
else:
annual_rate = self.rate_value
factor = COMPOUNDING_REGISTRY[self.compounding](
annual_rate,
yf
)
interest_amount = self.notional * factor
return {
"year_fraction": yf,
"interest_factor": factor,
"interest_amount": interest_amount,
}
# ============================================================
# APPLICATION LAYER JSON SERVICE (Camunda Ready)
# ============================================================
class FinancialComputationService:
@staticmethod
def execute(payload: dict) -> dict:
"""
Stateless JSON entrypoint.
Compatible Camunda / REST / Tryton bridge.
"""
try:
request_id = str(uuid.uuid4())
input_data = payload["input"]
start_date = datetime.strptime(
input_data["start_date"], "%Y-%m-%d"
).date()
end_date = datetime.strptime(
input_data["end_date"], "%Y-%m-%d"
).date()
notional = Decimal(input_data["notional"])
rate_value = Decimal(input_data["rate"]["value"])
rate_type = input_data["rate"]["type"].upper()
day_count = input_data["day_count_convention"]
compounding = input_data["compounding_method"]
computation = InterestComputation(
start_date=start_date,
end_date=end_date,
notional=notional,
rate_value=rate_value,
rate_type=rate_type,
day_count=day_count,
compounding=compounding,
)
result = computation.compute()
return {
"metadata": {
"engine_version": ENGINE_VERSION,
"request_id": request_id,
"timestamp": datetime.utcnow().isoformat() + "Z"
},
"result": {
"year_fraction": str(result["year_fraction"]),
"interest_factor": str(result["interest_factor"]),
"interest_amount": str(result["interest_amount"]),
},
"explainability": {
"formula": "Interest = Notional × Factor",
"factor_definition": f"{compounding} compounding applied to annualized rate",
"day_count_used": day_count
},
"status": "SUCCESS"
}
except Exception as e:
return {
"status": "ERROR",
"message": str(e),
"engine_version": ENGINE_VERSION
}

View File

@@ -385,11 +385,11 @@ class ForexBI(ModelSingleton,ModelSQL, ModelView):
config = Configuration.search(['id','>',0])[0]
payload = {
"resource": {"dashboard": 3},
"resource": {"dashboard": config.forex_id},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
token = jwt.encode(payload, config.payload, algorithm="HS256")
logger.info("TOKEN:%s",token)
if config.dark:
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"

View File

@@ -12,4 +12,10 @@ class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
__name__ = 'gr.configuration'
bi = fields.Char("BI connexion")
dark = fields.Boolean("Dark mode")
dashboard = fields.Char("Dashboard connexion")
dark = fields.Boolean("Dark mode")
pnl_id = fields.Integer("Pnl ID")
position_id = fields.Integer("Position ID")
forex_id = fields.Integer("Forex ID")
payload = fields.Char("Metabase payload")
automation = fields.Boolean("Automation")

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<!-- Barre centrale (contrat / position) -->
<rect x="11" y="4" width="2" height="16" fill="#267F82"/>
<!-- Flèche gauche (achat) -->
<path d="M9 7 L5 12 L9 17 L9 14 L11 14 L11 10 L9 10 Z"
fill="#267F82"/>
<!-- Flèche droite (vente) -->
<path d="M15 7 L19 12 L15 17 L15 14 L13 14 L13 10 L15 10 Z"
fill="#267F82"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="
M3 18 V7
L8 12
L12 8
L16 12
L21 7
V18
H18 V11
L12 15
L6 11
V18
Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="
M4 18 V6
H7 L12 13 L17 6
H20 V18
H17 V10.5
L12 16
L7 10.5
V18
Z
" fill="#267F82"/>
</svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1,430 @@
from decimal import Decimal
from trytond.pool import Pool, PoolMeta
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
def _get_report_invoice_line(self):
for line in self.lines or []:
if getattr(line, 'type', None) == 'line':
return line
return self.lines[0] if self.lines else None
def _get_report_purchase(self):
purchases = list(self.purchases or [])
return purchases[0] if purchases else None
def _get_report_sale(self):
# Bridge invoice templates to the originating sale so FODT files can
# reuse stable sale.report_* properties instead of complex expressions.
sales = list(self.sales or [])
return sales[0] if sales else None
def _get_report_trade(self):
return self._get_report_sale() or self._get_report_purchase()
def _get_report_purchase_line(self):
purchase = self._get_report_purchase()
if purchase and purchase.lines:
return purchase.lines[0]
def _get_report_sale_line(self):
sale = self._get_report_sale()
if sale and sale.lines:
return sale.lines[0]
def _get_report_trade_line(self):
return self._get_report_sale_line() or self._get_report_purchase_line()
def _get_report_lot(self):
line = self._get_report_trade_line()
if line and line.lots:
for lot in line.lots:
if lot.lot_type == 'physic':
return lot
return line.lots[0]
def _get_report_freight_fee(self):
pool = Pool()
Fee = pool.get('fee.fee')
shipment = self._get_report_shipment()
if not shipment:
return None
fees = Fee.search([
('shipment_in', '=', shipment.id),
('product.name', '=', 'Maritime freight'),
], limit=1)
return fees[0] if fees else None
def _get_report_shipment(self):
lot = self._get_report_lot()
if not lot:
return None
return (
getattr(lot, 'lot_shipment_in', None)
or getattr(lot, 'lot_shipment_out', None)
or getattr(lot, 'lot_shipment_internal', None)
)
@property
def report_address(self):
trade = self._get_report_trade()
if trade and trade.report_address:
return trade.report_address
if self.invoice_address and self.invoice_address.full_address:
return self.invoice_address.full_address
return ''
@property
def report_contract_number(self):
trade = self._get_report_trade()
if trade and trade.full_number:
return trade.full_number
return self.origins or ''
@property
def report_shipment(self):
trade = self._get_report_trade()
if trade and trade.report_shipment:
return trade.report_shipment
return self.description or ''
@property
def report_trader_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'trader', None):
return trade.trader.initial or ''
return ''
@property
def report_origin(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'product_origin', None):
return trade.product_origin or ''
return ''
@property
def report_operator_initial(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'operator', None):
return trade.operator.initial or ''
return ''
@property
def report_product_description(self):
line = self._get_report_trade_line()
if line and line.product:
return line.product.description or ''
return ''
@property
def report_description_upper(self):
if self.lines:
return (self.lines[0].description or '').upper()
return ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
line = self._get_report_trade_line()
if line:
return getattr(line, 'attributes_name', '') or ''
return ''
@property
def report_price(self):
trade = self._get_report_trade()
if trade and trade.report_price:
return trade.report_price
return ''
@property
def report_rate_currency_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_currency_upper
return ''
@property
def report_rate_value(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_value
return ''
@property
def report_rate_unit_upper(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_unit_upper
return ''
@property
def report_rate_price_words(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_price_words
return self.report_price or ''
@property
def report_rate_pricing_text(self):
line = self._get_report_invoice_line()
if line:
return line.report_rate_pricing_text
return ''
@property
def report_payment_date(self):
trade = self._get_report_trade()
if trade and trade.report_payment_date:
return trade.report_payment_date
return ''
@property
def report_payment_description(self):
trade = self._get_report_trade()
if trade and trade.payment_term:
return trade.payment_term.description or ''
if self.payment_term:
return self.payment_term.description or ''
return ''
@property
def report_nb_bale(self):
sale = self._get_report_sale()
if sale and sale.report_nb_bale:
return sale.report_nb_bale
line = self._get_report_trade_line()
if line and line.lots:
nb_bale = sum(
lot.lot_qt for lot in line.lots if lot.lot_type == 'physic'
)
return 'NB BALES: ' + str(int(nb_bale))
return ''
@property
def report_gross(self):
sale = self._get_report_sale()
if sale and sale.report_gross != '':
return sale.report_gross
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_gross_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
return ''
@property
def report_net(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_net', '') != '':
return trade.report_net
line = self._get_report_trade_line()
if line and line.lots:
return sum(
lot.get_current_quantity()
for lot in line.lots if lot.lot_type == 'physic'
)
if self.lines:
return self.lines[0].quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)
@property
def report_bl_date(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_date
@property
def report_bl_nb(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.bl_number
@property
def report_vessel(self):
shipment = self._get_report_shipment()
if shipment and shipment.vessel:
return shipment.vessel.vessel_name
@property
def report_loading_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.from_location:
return shipment.from_location.rec_name
return ''
@property
def report_discharge_port(self):
shipment = self._get_report_shipment()
if shipment and shipment.to_location:
return shipment.to_location.rec_name
return ''
@property
def report_incoterm(self):
trade = self._get_report_trade()
if not trade:
return ''
incoterm = trade.incoterm.code if getattr(trade, 'incoterm', None) else ''
location = (
trade.incoterm_location.party_name
if getattr(trade, 'incoterm_location', None) else ''
)
if incoterm and location:
return f"{incoterm} {location}"
return incoterm or location
@property
def report_proforma_invoice_number(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.number or ''
return ''
@property
def report_proforma_invoice_date(self):
lot = self._get_report_lot()
if lot:
line = (
getattr(lot, 'sale_invoice_line_prov', None)
or getattr(lot, 'invoice_line_prov', None)
)
if line and line.invoice:
return line.invoice.invoice_date
@property
def report_controller_name(self):
shipment = self._get_report_shipment()
if shipment and shipment.controller:
return shipment.controller.rec_name
return ''
@property
def report_si_number(self):
shipment = self._get_report_shipment()
if shipment:
return shipment.number or ''
return ''
@property
def report_freight_amount(self):
fee = self._get_report_freight_fee()
if fee:
return fee.get_amount()
return ''
@property
def report_freight_currency_symbol(self):
fee = self._get_report_freight_fee()
if fee and fee.currency:
return fee.currency.symbol or ''
if self.currency:
return self.currency.symbol or ''
return 'USD'
class InvoiceLine(metaclass=PoolMeta):
__name__ = 'account.invoice.line'
def _get_report_trade(self):
origin = getattr(self, 'origin', None)
if not origin:
return None
return getattr(origin, 'sale', None) or getattr(origin, 'purchase', None)
def _get_report_trade_line(self):
return getattr(self, 'origin', None)
@property
def report_product_description(self):
if self.product:
return self.product.description or ''
origin = getattr(self, 'origin', None)
if origin and getattr(origin, 'product', None):
return origin.product.description or ''
return ''
@property
def report_description_upper(self):
return (self.description or '').upper()
@property
def report_rate_currency_upper(self):
origin = self._get_report_trade_line()
currency = getattr(origin, 'linked_currency', None) or self.currency
if currency and currency.rec_name:
return currency.rec_name.upper()
return ''
@property
def report_rate_value(self):
return self.unit_price if self.unit_price is not None else ''
@property
def report_rate_unit_upper(self):
origin = self._get_report_trade_line()
unit = getattr(origin, 'linked_unit', None) or self.unit
if unit and unit.rec_name:
return unit.rec_name.upper()
return ''
@property
def report_rate_price_words(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'report_price', None):
return trade.report_price
return ''
@property
def report_rate_pricing_text(self):
origin = self._get_report_trade_line()
return getattr(origin, 'get_pricing_text', '') or ''
@property
def report_crop_name(self):
trade = self._get_report_trade()
if trade and getattr(trade, 'crop', None):
return trade.crop.name or ''
return ''
@property
def report_attributes_name(self):
origin = getattr(self, 'origin', None)
if origin:
return getattr(origin, 'attributes_name', '') or ''
return ''
@property
def report_net(self):
if self.type == 'line':
return self.quantity
return ''
@property
def report_lbs(self):
net = self.report_net
if net == '':
return ''
return round(Decimal(net) * Decimal('2204.62'),2)

View File

@@ -20,6 +20,7 @@ import datetime
import json
import logging
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.service import ContractFactory
logger = logging.getLogger(__name__)
@@ -47,7 +48,7 @@ class LotMove(ModelSQL,ModelView):
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
line = fields.Many2One('purchase.line',"Purchase")
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
lot_move = fields.One2Many('lot.move','lot',"Move")
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
@@ -58,6 +59,7 @@ class Lot(metaclass=PoolMeta):
delta_pr = fields.Numeric("Delta Pr")
delta_amt = fields.Numeric("Delta Amt")
warrant_nb = fields.Char("Warrant Nb")
lot_chunk_key = fields.Integer("Chunk key")
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
pivot = fields.Function(
@@ -201,7 +203,7 @@ class Lot(metaclass=PoolMeta):
)
pivot_data['options'] = {
"rows": ["lot","ct type","event_date","event","move","curr","rate"],
"rows": ["lot","ct type","event_date","event","move","Curr","rate"],
"cols": ["account"],
"aggregatorName": "Sum",
"vals": ["amount"]
@@ -578,6 +580,14 @@ class Lot(metaclass=PoolMeta):
return True
return False
def get_received_move(self):
if self.lot_move:
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
for m in lm:
if m.move.from_location.type == 'supplier' and m.move.state == 'done':
return m.move
return None
def GetShipment(self,type):
if type == 'in':
m = self.get_current_supplier_move()
@@ -1087,6 +1097,7 @@ class LotQt(
newlot.lot_shipment_internal = self.lot_shipment_internal
newlot.lot_shipment_out = self.lot_shipment_out
newlot.lot_product = self.lot_p.line.product
newlot.lot_chunk_key = l.lot_chunk_key
if self.lot_s:
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
newlot.lot_type = 'physic'
@@ -1168,6 +1179,7 @@ class LotQt(
@classmethod
def validate(cls, lotqts):
super(LotQt, cls).validate(lotqts)
Date = Pool().get('ir.date')
#Update Move
for lqt in lotqts:
cls.updateMove(lqt.lot_move)
@@ -1177,23 +1189,23 @@ class LotQt(
if lqt.lot_p and lqt.lot_quantity > 0:
pl = lqt.lot_p.line
logger.info("VALIDATE_LQT_PL:%s",pl)
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',pl.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(pl.get_pnl_fee_lines())
pnl_lines.extend(pl.get_pnl_price_lines())
pnl_lines.extend(pl.get_pnl_der_lines())
Pnl.save(pnl_lines)
# Pnl = Pool().get('valuation.valuation')
# pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
# if pnl:
# Pnl.delete(pnl)
# pnl_lines = []
# pnl_lines.extend(pl.get_pnl_fee_lines())
# pnl_lines.extend(pl.get_pnl_price_lines())
# pnl_lines.extend(pl.get_pnl_der_lines())
# Pnl.save(pnl_lines)
#Open position update
if pl.quantity_theorical:
OpenPosition = Pool().get('open.position')
OpenPosition.create_from_purchase_line(pl)
# if pl.quantity_theorical:
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_purchase_line(pl)
@classmethod
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None):
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None,finished=False):
pool = Pool()
LotQt = pool.get('lot.qt')
lqt = LotQt.__table__()
@@ -1241,8 +1253,12 @@ class LotQt(
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
if ps == 'P':
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
if not finished:
wh &= (pl.finished == False)
elif ps == 'S':
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
if not finished:
wh &= (sl.finished == False)
if purchase:
wh &= (pu.id == purchase)
if sale:
@@ -1832,7 +1848,8 @@ class LotReport(
supplier = context.get('supplier')
#asof = context.get('asof')
#todate = context.get('todate')
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin)
finished = context.get('finished')
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin,finished)
return query
@classmethod
@@ -1922,6 +1939,12 @@ class LotContext(ModelView):
('pnl', 'Pnl'),
],'Mode')
finished = fields.Boolean("Display finished")
@classmethod
def default_finished(cls):
return False
@classmethod
def default_asof(cls):
pool = Pool()
@@ -2002,17 +2025,24 @@ class LotShipping(Wizard):
if r.r_lot_shipment_in:
raise UserError("Please unlink before linking to a new shipment !")
else:
shipped_quantity = Decimal(r.r_lot_quantity)
shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
logger.info("LotShipping:%s",shipped_quantity)
shipment_origin = None
if self.ship.quantity:
shipped_quantity = self.ship.quantity
if shipped_quantity == 0:
shipped_quantity = Decimal(r.r_lot_matched)
shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
if self.ship.shipment == 'in':
if not self.ship.shipment_in:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
elif self.ship.shipment == 'out':
if not self.ship.shipment_out:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
elif self.ship.shipment == 'int':
if not self.ship.shipment_internal:
UserError("Shipment not known!")
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
if r.id < 10000000 :
l = Lot(r.id)
@@ -2030,15 +2060,22 @@ class LotShipping(Wizard):
move = Move(l.move)
move.shipment = shipment_origin
Move.save([move])
linked_transit_move = move.get_linked_transit_move()
if linked_transit_move:
linked_transit_move.shipment = shipment_origin
Move.save([linked_transit_move])
#Decrease forecasted virtual part shipped
vlot_p = l.getVlot_p()
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
l.lot_av = 'reserved'
Lot.save([l])
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
Lot.save([l])
else:
lqt = LotQt(r.id - 10000000)
#Increase forecasted virtual part shipped
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
logger.info("LotShipping2:%s",shipped_quantity)
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
#Decrease forecasted virtual part non shipped
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
@@ -2442,6 +2479,7 @@ class LotAddLine(ModelView):
lot_gross_quantity = fields.Numeric("Gross weight")
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
lot_premium = fields.Numeric("Premium")
lot_chunk_key = fields.Integer("Chunk key")
# @fields.depends('lot_qt')
# def on_change_with_lot_quantity(self):
@@ -2604,6 +2642,17 @@ class LotInvoice(Wizard):
invoicing = StateTransition()
message = StateView(
'purchase.create_prepayment.message',
'purchase_trade.create_prepayment_message_form',
[
Button('OK', 'end', 'tryton-ok'),
Button('See Invoice', 'see_invoice', 'tryton-go-next'),
]
)
see_invoice = StateAction('account_invoice.act_invoice_form')
def transition_start(self):
return 'inv'
@@ -2650,7 +2699,7 @@ class LotInvoice(Wizard):
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
val['lot_unit'] = lot.lot_unit_line.id
val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
unit = val['lot_unit']
val['lot_currency'] = lot.lot_price_ct_symbol
lot_p.append(val)
@@ -2666,6 +2715,7 @@ class LotInvoice(Wizard):
val_s['lot_diff_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
val_s['lot_diff_amount'] = val_s['lot_amount'] - Decimal(lot.sale_invoice_line_prov.amount)
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
lot_s.append(val_s)
if line:
if line.fees:
@@ -2747,12 +2797,27 @@ class LotInvoice(Wizard):
continue
lots.append(lot)
invoice_line = None
if self.inv.type == 'purchase':
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.invoice_line else r.r_lot_p.invoice_line_prov
else:
if sale:
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
return 'end'
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
invoice_line = r.r_lot_p.invoice_line if r.r_lot_p.sale_invoice_line else r.r_lot_p.sale_invoice_line_prov
self.message.invoice = invoice_line.invoice
return 'message'
def default_message(self, fields):
return {
'message': 'The invoice has been successfully created.',
}
def do_see_invoice(self, action):
action['views'].reverse() # pour ouvrir en form directement
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
return action, {'res_id':self.message.invoice.id}
def end(self):
return 'reload'
@@ -3129,136 +3194,13 @@ class CreateContracts(Wizard):
}
def transition_creating(self):
SaleLine = Pool().get('sale.line')
Sale = Pool().get('sale.sale')
PurchaseLine = Pool().get('purchase.line')
Purchase = Pool().get('purchase.purchase')
LotQt = Pool().get('lot.qt')
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
Lot = Pool().get('lot.lot')
Date = Pool().get('ir.date')
self.sale_lines = []
type = self.ct.type
base_contract = self.ct.lot.sale_line.sale if type == 'Purchase' else self.ct.lot.line.purchase
for c in self.ct.contracts:
contract = Purchase() if type == 'Purchase' else Sale()
contract_line = PurchaseLine() if type == 'Purchase' else SaleLine()
parts = c.currency_unit.split("_")
if int(parts[0]) != 0:
contract.currency = int(parts[0])
else:
contract.currency = 1
contract.party = c.party
contract.crop = c.crop
contract.tol_min = c.tol_min
contract.tol_max = c.tol_max
if type == 'Purchase':
contract.purchase_date = Date.today()
else:
contract.sale_date = Date.today()
contract.reference = c.reference
if base_contract.from_location and base_contract.to_location:
if type == 'Purchase':
contract.to_location = base_contract.from_location
else:
contract.from_location = base_contract.to_location
if base_contract.from_location.type == 'supplier' and base_contract.to_location.type == 'customer':
contract.from_location = base_contract.from_location
contract.to_location = base_contract.to_location
if c.party.wb:
contract.wb = c.party.wb
if c.party.association:
contract.association = c.party.association
if type == 'Purchase':
if c.party.supplier_payment_term:
contract.payment_term = c.party.supplier_payment_term
else:
if c.party.customer_payment_term:
contract.payment_term = c.party.customer_payment_term
contract.incoterm = c.incoterm
if c.party.addresses:
contract.invoice_address = c.party.addresses[0]
if type == 'Sale':
contract.shipment_address = c.party.addresses[0]
contract.__class__.save([contract])
contract_line.quantity = c.quantity
contract_line.quantity_theorical = c.quantity
contract_line.product = self.ct.product
contract_line.price_type = c.price_type
contract_line.unit = self.ct.unit
if type == 'Purchase':
contract_line.purchase = contract.id
else:
contract_line.sale = contract.id
contract_line.created_by_code = self.ct.matched
contract_line.premium = Decimal(0)
if int(parts[0]) == 0:
contract_line.enable_linked_currency = True
contract_line.linked_currency = 1
contract_line.linked_unit = int(parts[1])
contract_line.linked_price = c.price
contract_line.unit_price = contract_line.get_price_linked_currency()
else:
contract_line.unit_price = c.price if c.price else Decimal(0)
contract_line.del_period = c.del_period
contract_line.from_del = c.from_del
contract_line.to_del = c.to_del
contract_line.__class__.save([contract_line])
logger.info("CREATE_ID:%s",contract.id)
logger.info("CREATE_LINE_ID:%s",contract_line.id)
if self.ct.matched:
lot = Lot()
if type == 'Purchase':
lot.line = contract_line.id
else:
lot.sale_line = contract_line.id
lot.lot_qt = None
lot.lot_unit = None
lot.lot_unit_line = contract_line.unit
lot.lot_quantity = round(contract_line.quantity,5)
lot.lot_gross_quantity = None
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = contract_line.product
lqtt = LotQtType.search([('sequence','=',1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = round(lot.lot_quantity,5)
lqh.gross_quantity = round(lot.lot_quantity,5)
lot.lot_hist = [lqh]
Lot.save([lot])
vlot = self.ct.lot
shipment_origin = None
if self.ct.shipment_in:
shipment_origin = 'stock.shipment.in,' + str(self.ct.shipment_in.id)
elif self.ct.shipment_internal:
shipment_origin = 'stock.shipment.internal,' + str(self.ct.shipment_internal.id)
elif self.ct.shipment_out:
shipment_origin = 'stock.shipment.out,' + str(self.ct.shipment_out.id)
qt = c.quantity
if type == 'Purchase':
if not lot.updateVirtualPart(qt,shipment_origin,vlot):
lot.createVirtualPart(qt,shipment_origin,vlot)
#Decrease forecasted virtual part non matched
lot.updateVirtualPart(-qt,shipment_origin,vlot,'only sale')
else:
if not vlot.updateVirtualPart(qt,shipment_origin,lot):
vlot.createVirtualPart(qt,shipment_origin,lot)
#Decrease forecasted virtual part non matched
vlot.updateVirtualPart(-qt,shipment_origin,None)
ContractFactory.create_contracts(
self.ct.contracts,
type_=self.ct.type,
ct=self.ct,
)
return 'end'
# def do_matching(self, action):
# return action, {
# 'ids': self.sale_lines,
# 'model': str(self.ct.lot.id),
# }
def end(self):
return 'reload'
@@ -3288,7 +3230,6 @@ class ContractsStart(ModelView):
def default_matched(cls):
return True
class ContractDetail(ModelView):
"Contract Detail"
@@ -3296,26 +3237,29 @@ class ContractDetail(ModelView):
category = fields.Integer("Category")
cd = fields.Many2One('contracts.start',"Contracts")
party = fields.Many2One('party.party',"Party",domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
currency = fields.Many2One('currency.currency',"Currency")
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm")
quantity = fields.Numeric("Quantity",digits=(1,5))
unit = fields.Many2One('product.uom',"Unit")
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
currency = fields.Many2One('currency.currency',"Currency", required=True)
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
unit = fields.Many2One('product.uom',"Unit", required=True)
qt_unit = fields.Many2One('product.uom',"Unit")
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
tol_min = fields.Numeric("Tol - in %", required=True)
tol_max = fields.Numeric("Tol + in %", required=True)
crop = fields.Many2One('purchase.crop',"Crop")
del_period = fields.Many2One('product.month',"Delivery Period")
from_del = fields.Date("From")
to_del = fields.Date("To")
price = fields.Numeric("Price",digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
price_type = price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
], 'Price type')
], 'Price type', required=True)
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
reference = fields.Char("Reference")
from_location = fields.Many2One('stock.location',"From location")
to_location = fields.Many2One('stock.location',"To location")
payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
@classmethod
def default_category(cls):
@@ -3372,7 +3316,7 @@ class ContractDetail(ModelView):
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
return lqt.lot_p.line.purchase.crop.id
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
return lqt.lot_s.line.sale.crop.id
return lqt.lot_s.sale_line.sale.crop.id
@classmethod
def default_currency(cls):

View File

@@ -0,0 +1,152 @@
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
UNITS = (
"ZERO ONE TWO THREE FOUR FIVE SIX SEVEN EIGHT NINE TEN ELEVEN TWELVE "
"THIRTEEN FOURTEEN FIFTEEN SIXTEEN SEVENTEEN EIGHTEEN NINETEEN"
).split()
TENS = "ZERO TEN TWENTY THIRTY FORTY FIFTY SIXTY SEVENTY EIGHTY NINETY".split()
def format_date_en(d):
if not d:
return ''
day = d.day
# Gestion des suffixes ordinaux
if 10 <= day % 100 <= 20:
suffix = 'TH'
else:
suffix = {1: 'ST', 2: 'ND', 3: 'RD'}.get(day % 10, 'TH')
return f"{day}{suffix} {d.strftime('%B').upper()} {d.year}"
def _under_thousand(n):
words = []
hundreds = n // 100
remainder = n % 100
if hundreds:
words.append(UNITS[hundreds])
words.append("HUNDRED")
if remainder:
words.append("AND")
if remainder:
if remainder < 20:
words.append(UNITS[remainder])
else:
words.append(TENS[remainder // 10])
if remainder % 10:
words.append(UNITS[remainder % 10])
return " ".join(words)
def integer_to_words(n):
if n == 0:
return "ZERO"
parts = []
millions = n // 1_000_000
thousands = (n // 1_000) % 1_000
remainder = n % 1_000
if millions:
parts.append(_under_thousand(millions))
parts.append("MILLION")
if thousands:
parts.append(_under_thousand(thousands))
parts.append("THOUSAND")
if remainder:
parts.append(_under_thousand(remainder))
return " ".join(parts)
# ==============================
# 💰 MONETARY
# ==============================
def amount_to_currency_words(amount,
major_singular="DOLLAR",
major_plural="DOLLARS",
minor_singular="CENT",
minor_plural="CENTS"):
"""
Example:
1.20 → ONE DOLLAR AND TWENTY CENTS
2.00 → TWO DOLLARS
"""
amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
integer_part = int(amount)
decimal_part = int((amount - integer_part) * 100)
words = []
# Major unit
major_words = integer_to_words(integer_part)
words.append(major_words)
if integer_part == 1:
words.append(major_singular)
else:
words.append(major_plural)
# Minor unit
if decimal_part:
words.append("AND")
minor_words = integer_to_words(decimal_part)
words.append(minor_words)
if decimal_part == 1:
words.append(minor_singular)
else:
words.append(minor_plural)
return " ".join(words)
# ==============================
# ⚖️ QUANTITY WITH UNIT
# ==============================
def quantity_to_words(quantity,
unit_singular="METRIC TON",
unit_plural="METRIC TONS"):
"""
Example:
1 → ONE METRIC TON
23 → TWENTY THREE METRIC TONS
1.5 → ONE POINT FIVE METRIC TONS
"""
quantity = Decimal(str(quantity)).normalize()
if quantity == quantity.to_integral():
integer_part = int(quantity)
words = integer_to_words(integer_part)
if integer_part == 1:
unit = unit_singular
else:
unit = unit_plural
return f"{words} {unit}"
else:
# lecture décimale simple pour quantités
integer_part = int(quantity)
decimal_str = str(quantity).split(".")[1]
words = integer_to_words(integer_part)
decimal_words = " ".join(UNITS[int(d)] for d in decimal_str)
return f"{words} POINT {decimal_words} {unit_plural}"

View File

@@ -1,11 +1,60 @@
from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
from trytond.pool import PoolMeta, Pool
from trytond.exceptions import UserError
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
from trytond.transaction import Transaction
from decimal import getcontext, Decimal, ROUND_HALF_UP
from sql import Table
from trytond.pyson import Bool, Eval, Id, If
__all__ = ['Party']
__metaclass__ = PoolMeta
class PartyExecution(ModelSQL,ModelView):
"Party Execution"
__name__ = 'party.execution'
party = fields.Many2One('party.party',"Party")
area = fields.Many2One('country.region',"Area")
percent = fields.Numeric("% targeted")
achieved_percent = fields.Function(fields.Numeric("% achieved"),'get_percent')
def get_percent(self,name):
return 2
class PartyExecutionSla(ModelSQL,ModelView):
"Party Execution Sla"
__name__ = 'party.execution.sla'
party = fields.Many2One('party.party',"Party")
reference = fields.Char("Reference")
product = fields.Many2One('product.product',"Product")
date_from = fields.Date("From")
date_to = fields.Date("To")
places = fields.One2Many('party.execution.place','pes',"")
class PartyExecutionPlace(ModelSQL,ModelView):
"Party Sla Place"
__name__ = 'party.execution.place'
pes = fields.Many2One('party.execution.sla',"Sla")
location = fields.Many2One('stock.location',"Location")
cost = fields.Numeric("Cost",digits=(16,4))
mode = fields.Selection([
('lumpsum', 'Lump sum'),
('perqt', 'Per qt'),
('pprice', '% price'),
('rate', '% rate'),
('pcost', '% cost price'),
('ppack', 'Per packing'),
], 'Mode', required=True)
currency = fields.Many2One('currency.currency',"Currency")
unit = fields.Many2One('product.uom',"Unit",domain=[
If(Eval('mode') == 'ppack',
('category', '=', 8),
()),
],
states={
'readonly': Eval('mode') != 'ppack',
})
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
@@ -13,5 +62,53 @@ class Party(metaclass=PoolMeta):
tol_max = fields.Numeric("Tol + in %")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
origin =fields.Char("Origin")
execution = fields.One2Many('party.execution','party',"")
sla = fields.One2Many('party.execution.sla','party', "Sla")
initial = fields.Char("Initials")
def IsAvailableForControl(self,sh):
return True
def get_sla_cost(self,location):
if self.sla:
for sla in self.sla:
SlaPlace = Pool().get('party.execution.place')
sp = SlaPlace.search([('pes','=', sla.id),('location','=',location)])
if sp:
return sp[0].cost,sp[0].mode,sp[0].currency,sp[0].unit
def get_alf(self):
if self.name == 'CARGO CONTROL':
return 105
t = Table('alf')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.ALF_CODE,
where=t.SHORT_NAME.ilike(f'%{self.name}%')
))
rows = cursor.fetchall()
if rows:
return int(rows[0][0])
@classmethod
def getPartyByName(cls, party, category=None):
party = party.upper()
p = cls.search([('name', '=', party)], limit=1)
if p:
return p[0]
else:
p = cls()
p.name = party
cls.save([p])
if category:
Category = Pool().get('party.category')
cat = Category.search(['name','=',category])
if cat:
PartyCategory = Pool().get('party.party-party.category')
pc = PartyCategory()
pc.party = p.id
pc.category = cat[0].id
PartyCategory.save([pc])
return p

View File

@@ -1,5 +1,29 @@
<record model="ir.ui.view" id="party_view_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>
<tryton>
<data>
<record model="ir.ui.view" id="party_view_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>
<record model="ir.ui.view" id="party_exec_view_list">
<field name="model">party.execution</field>
<field name="type">tree</field>
<field name="name">party_exec_tree</field>
</record>
<record model="ir.ui.view" id="party_exec_sla_view_form">
<field name="model">party.execution.sla</field>
<field name="type">form</field>
<field name="name">party_exec_sla_form</field>
</record>
<record model="ir.ui.view" id="party_exec_sla_view_list">
<field name="model">party.execution.sla</field>
<field name="type">tree</field>
<field name="name">party_exec_sla_tree</field>
</record>
<record model="ir.ui.view" id="party_exec_place_view_form">
<field name="model">party.execution.place</field>
<field name="type">tree</field>
<field name="name">party_exec_place_tree</field>
</record>
</data>
</tryton>

View File

@@ -21,6 +21,7 @@ class PaymentTermLine(metaclass=PoolMeta):
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
term_type = fields.Selection([
(None, ''),
('advance', 'Advance'),
('cad', 'CAD'),
('open', 'Open'),
@@ -30,18 +31,21 @@ class PaymentTermLine(metaclass=PoolMeta):
trigger_offset = fields.Integer('Trigger Offset')
offset_unit = fields.Selection([
(None, ''),
('calendar', 'Calendar Days'),
('business', 'Business Days'),
], 'Offset Unit')
eom_flag = fields.Boolean('EOM Flag')
eom_mode = fields.Selection([
(None, ''),
('standard', 'Standard'),
('before', 'Before EOM'),
('after', 'After EOM'),
], 'EOM Mode')
risk_classification = fields.Selection([
(None, ''),
('fully_secured', 'Fully Secured'),
('partially_secured', 'Partially Secured'),
('unsecured', 'Unsecured'),

View File

@@ -50,37 +50,243 @@ DAYS = [
('sunday', 'Sunday'),
]
class Estimated(ModelSQL, ModelView):
"Estimated date"
__name__ = 'pricing.estimated'
trigger = fields.Selection(TRIGGERS,"Trigger")
estimated_date = fields.Date("Estimated date")
fin_int_delta = fields.Integer("Financing interests delta")
class MtmScenario(ModelSQL, ModelView):
"MtM Scenario"
__name__ = 'mtm.scenario'
name = fields.Char("Scenario", required=True)
valuation_date = fields.Date("Valuation Date", required=True)
use_last_price = fields.Boolean("Use Last Available Price")
calendar = fields.Many2One(
'price.calendar', "Calendar"
)
class MtmStrategy(ModelSQL, ModelView):
"Mark to Market Strategy"
__name__ = 'mtm.strategy'
name = fields.Char("Name", required=True)
active = fields.Boolean("Active")
scenario = fields.Many2One(
'mtm.scenario', "Scenario", required=True
)
currency = fields.Many2One(
'currency.currency', "Valuation Currency"
)
components = fields.One2Many(
'pricing.component', 'strategy', "Components"
)
@classmethod
def default_active(cls):
return True
def get_mtm(self,line,qty):
pool = Pool()
Currency = pool.get('currency.currency')
total = Decimal(0)
scenario = self.scenario
dt = scenario.valuation_date
for comp in self.components:
value = Decimal(0)
if comp.price_source_type == 'curve' and comp.price_index:
value = Decimal(
comp.price_index.get_price(
dt,
line.unit,
self.currency,
last=scenario.use_last_price
)
)
elif comp.price_source_type == 'matrix' and comp.price_matrix:
value = self._get_matrix_price(comp, line, dt)
if comp.ratio:
value *= Decimal(comp.ratio) / Decimal(100)
total += value * qty
return Decimal(str(total)).quantize(Decimal("0.01"))
def _get_matrix_price(self, comp, line, dt):
MatrixLine = Pool().get('price.matrix.line')
domain = [
('matrix', '=', comp.price_matrix.id),
]
if line:
domain += [
('origin', '=', line.purchase.from_location),
('destination', '=', line.purchase.to_location),
]
lines = MatrixLine.search(domain)
if lines:
return Decimal(lines[0].price_value)
return Decimal(0)
def run_daily_mtm():
Strategy = Pool().get('mtm.strategy')
Snapshot = Pool().get('mtm.snapshot')
for strat in Strategy.search([('active', '=', True)]):
amount = strat.compute_mtm()
Snapshot.create([{
'strategy': strat.id,
'valuation_date': strat.scenario.valuation_date,
'amount': amount,
'currency': strat.currency.id,
}])
class Mtm(ModelSQL, ModelView):
"Mtm"
"MtM Component"
__name__ = 'mtm.component'
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
price_index = fields.Many2One('price.price',"Curve")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=True, ondelete='CASCADE'
)
def get_cur(self,name):
name = fields.Char("Component", required=True)
component_type = fields.Selection([
('commodity', 'Commodity'),
('freight', 'Freight'),
('quality', 'Quality'),
('fx', 'FX'),
('storage', 'Storage'),
('other', 'Other'),
], "Type", required=True)
fix_type = fields.Many2One('price.fixtype', "Fixation Type")
price_source_type = fields.Selection([
('curve', 'Curve'),
('matrix', 'Matrix'),
('manual', 'Manual'),
], "Price Source", required=True)
price_index = fields.Many2One('price.price', "Price Curve")
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
ratio = fields.Numeric("Ratio / %", digits=(16, 6))
manual_price = fields.Numeric(
"Manual Price",
digits=(16, 6),
help="Price set manually if price_source_type is 'manual'"
)
currency = fields.Many2One('currency.currency', "Currency")
def get_cur(self, name=None):
if self.price_index:
PI = Pool().get('price.price')
pi = PI(self.price_index)
return pi.price_currency
return self.price_index.price_currency
if self.price_matrix:
return self.price_matrix.currency
return None
@fields.depends('price_index','price_matrix')
def on_change_with_currency(self):
return self.get_cur()
class PriceMatrix(ModelSQL, ModelView):
"Price Matrix"
__name__ = 'price.matrix'
name = fields.Char("Name", required=True)
matrix_type = fields.Selection([
('freight', 'Freight'),
('location', 'Location Spread'),
('quality', 'Quality'),
('storage', 'Storage'),
('other', 'Other'),
], "Matrix Type", required=True)
unit = fields.Many2One('product.uom', "Unit")
currency = fields.Many2One('currency.currency', "Currency")
calendar = fields.Many2One(
'price.calendar', "Calendar"
)
valid_from = fields.Date("Valid From")
valid_to = fields.Date("Valid To")
lines = fields.One2Many(
'price.matrix.line', 'matrix', "Lines"
)
class PriceMatrixLine(ModelSQL, ModelView):
"Price Matrix Line"
__name__ = 'price.matrix.line'
matrix = fields.Many2One(
'price.matrix', "Matrix",
required=True, ondelete='CASCADE'
)
origin = fields.Many2One('stock.location', "Origin")
destination = fields.Many2One('stock.location', "Destination")
product = fields.Many2One('product.product', "Product")
quality = fields.Many2One('product.category', "Quality")
price_value = fields.Numeric("Price", digits=(16, 6))
class MtmSnapshot(ModelSQL, ModelView):
"MtM Snapshot"
__name__ = 'mtm.snapshot'
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=True, ondelete='CASCADE'
)
valuation_date = fields.Date("Valuation Date", required=True)
amount = fields.Numeric("MtM Amount", digits=(16, 6))
currency = fields.Many2One('currency.currency', "Currency")
created_at = fields.DateTime("Created At")
class Component(ModelSQL, ModelView):
"Component"
__name__ = 'pricing.component'
strategy = fields.Many2One(
'mtm.strategy', "Strategy",
required=False, ondelete='CASCADE'
)
price_source_type = fields.Selection([
('curve', 'Curve'),
('matrix', 'Matrix'),
# ('manual', 'Manual'),
], "Price Source", required=True)
fix_type = fields.Many2One('price.fixtype',"Fixation type")
ratio = fields.Numeric("%")
ratio = fields.Numeric("%",digits=(16,7))
price_index = fields.Many2One('price.price',"Curve")
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
auto = fields.Boolean("Auto")
fallback = fields.Boolean("Fallback")
@@ -194,6 +400,7 @@ class Trigger(ModelSQL,ModelView):
'readonly': Eval('pricing_period') != None,
})
average = fields.Boolean("Avg")
last = fields.Boolean("Last")
application_period = fields.Many2One('pricing.period',"Application period")
from_a = fields.Date("From",
states={
@@ -217,14 +424,11 @@ class Trigger(ModelSQL,ModelView):
pp = PP(self.application_period)
CO = Pool().get('pricing.component')
co = CO(self.component)
logger.info("DELDATEEST_:%s",co)
if co.line:
d = co.getEstimatedTriggerPurchase(pp.trigger)
else:
d = co.getEstimatedTriggerSale(pp.trigger)
logger.info("DELDATEEST:%s",d)
date_from,date_to,dates = pp.getDates(d)
logger.info("DELDATEEST2:%s",dates)
return date_from,date_to,d,pp.include,dates
def getApplicationListDates(self, cal):
@@ -288,7 +492,7 @@ class Trigger(ModelSQL,ModelView):
pi = PI(pc.price_index)
val = {}
val['date'] = current_date
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency)
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency,self.last)
val['avg'] = val['price']
val['avg_minus_1'] = val['price']
val['isAvg'] = self.average
@@ -330,8 +534,6 @@ class Period(ModelSQL,ModelView):
date_from = None
date_to = None
dates = []
logger.info("GETDATES:%s",t)
logger.info("GETDATES:%s",self.every)
if t:
if self.every:
if t:
@@ -348,21 +550,18 @@ class Period(ModelSQL,ModelView):
while current.month == t.month:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation > 0:
days_to_add = (weekday_target - t.weekday()) % 7
current = t + datetime.timedelta(days=days_to_add)
while len(dates) < self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current += datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
elif self.nb_quotation < 0:
days_to_sub = (t.weekday() - weekday_target) % 7
current = t - datetime.timedelta(days=days_to_sub)
while len(dates) < -self.nb_quotation:
dates.append(datetime.datetime(current.year, current.month, current.day))
current -= datetime.timedelta(days=7)
logger.info("GETDATES:%s",dates)
else:
if self.startday == 'before':

View File

@@ -3,6 +3,11 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.icon" id="mtm_icon">
<field name="name">tradon-mtm</field>
<field name="path">icons/tradon-mtm.svg</field>
</record>
<record model="ir.ui.view" id="summary_view_tree_sequence">
<field name="model">sale.pricing.summary</field>
<field name="type">tree</field>
@@ -104,5 +109,84 @@ this repository contains the full copyright notices and license terms. -->
<field name="type">form</field>
<field name="name">period_form</field>
</record>
<record model="ir.ui.view" id="mtm_scenario_view_form">
<field name="model">mtm.scenario</field>
<field name="type">form</field>
<field name="name">mtm_scenario_form</field>
</record>
<record model="ir.ui.view" id="mtm_scenario_view_list">
<field name="model">mtm.scenario</field>
<field name="type">tree</field>
<field name="name">mtm_scenario_tree</field>
</record>
<record model="ir.ui.view" id="mtm_strategy_view_form">
<field name="model">mtm.strategy</field>
<field name="type">form</field>
<field name="name">mtm_strategy_form</field>
</record>
<record model="ir.ui.view" id="mtm_strategy_view_list">
<field name="model">mtm.strategy</field>
<field name="type">tree</field>
<field name="name">mtm_strategy_tree</field>
</record>
<record model="ir.ui.view" id="price_matrix_view_form">
<field name="model">price.matrix</field>
<field name="type">form</field>
<field name="name">price_matrix_form</field>
</record>
<record model="ir.ui.view" id="price_matrix_view_list">
<field name="model">price.matrix</field>
<field name="type">tree</field>
<field name="name">price_matrix_tree</field>
</record>
<record model="ir.ui.view" id="price_matrix_line_view_list">
<field name="model">price.matrix.line</field>
<field name="type">tree</field>
<field name="name">price_matrix_line_tree</field>
</record>
<!-- <record model="ir.ui.view" id="price_matrix_line_view_form">
<field name="model">price.matrix.line</field>
<field name="type">form</field>
<field name="name">price_matrix_line_form</field>
</record>
<record model="ir.ui.view" id="mtm_snapshot_view_form">
<field name="model">mtm.snapshot</field>
<field name="type">form</field>
<field name="name">mtm_snapshot_form</field>
</record>
<record model="ir.ui.view" id="mtm_snapshot_view_list">
<field name="model">mtm.snapshot</field>
<field name="type">tree</field>
<field name="name">mtm_snapshot_tree</field>
</record> -->
<record model="ir.action.act_window" id="act_strategy_form">
<field name="name">Strategy</field>
<field name="res_model">mtm.strategy</field>
</record>
<record model="ir.action.act_window.view" id="act_strategy_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="mtm_strategy_view_list"/>
<field name="act_window" ref="act_strategy_form"/>
</record>
<record model="ir.action.act_window.view" id="act_strategy_form_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="mtm_strategy_view_form"/>
<field name="act_window" ref="act_strategy_form"/>
</record>
<menuitem
name="Mtm"
sequence="99"
id="menu_mtm"
icon="tradon-mtm" />
<menuitem
name="Strategy"
action="act_strategy_form"
parent="menu_mtm"
sequence="10"
id="menu_strategy" />
</data>
</tryton>

File diff suppressed because it is too large Load Diff

View File

@@ -126,6 +126,25 @@ this repository contains the full copyright notices and license terms. -->
<field name="wiz_name">pnl.report</field>
</record>
<record model="ir.ui.view" id="position_bi_view_graph">
<field name="model">position.bi</field>
<field name="type">form</field>
<field name="name">position_bi_graph</field>
</record>
<record model="ir.action.act_window" id="act_position_bi">
<field name="name">Position BI</field>
<field name="res_model">position.bi</field>
</record>
<record model="ir.action.act_window.view" id="act_position_bi_view">
<field name="sequence" eval="30"/>
<field name="view" ref="position_bi_view_graph"/>
<field name="act_window" ref="act_position_bi"/>
</record>
<record model="ir.action.wizard" id="act_position_report">
<field name="name">Position report</field>
<field name="wiz_name">position.report</field>
</record>
<record model="ir.ui.view" id="mtm_view_form">
<field name="model">mtm.component</field>
<field name="type">form</field>
@@ -137,6 +156,77 @@ this repository contains the full copyright notices and license terms. -->
<field name="name">mtm_tree</field>
</record>
<record model="ir.ui.view" id="price_composition_view_tree">
<field name="model">price.composition</field>
<field name="type">tree</field>
<field name="name">price_composition_tree</field>
</record>
<record model="ir.ui.view" id="quality_analysis_view_tree">
<field name="model">quality.analysis</field>
<field name="type">tree</field>
<field name="name">quality_analysis_tree</field>
</record>
<record model="ir.ui.view" id="quality_analysis_view_form">
<field name="model">quality.analysis</field>
<field name="type">form</field>
<field name="name">quality_analysis_form</field>
</record>
<record model="ir.ui.view" id="assay_view_tree">
<field name="model">assay.assay</field>
<field name="type">tree</field>
<field name="name">assay_tree</field>
</record>
<record model="ir.ui.view" id="assay_view_form">
<field name="model">assay.assay</field>
<field name="type">form</field>
<field name="name">assay_form</field>
</record>
<record model="ir.ui.view" id="assay_line_view_tree">
<field name="model">assay.line</field>
<field name="type">tree</field>
<field name="name">assay_line_tree</field>
</record>
<record model="ir.ui.view" id="assay_element_view_form">
<field name="model">assay.element</field>
<field name="type">form</field>
<field name="name">assay_element_form</field>
</record>
<record model="ir.ui.view" id="concentrate_view_tree">
<field name="model">concentrate.term</field>
<field name="type">tree</field>
<field name="name">concentrate_tree</field>
</record>
<record model="ir.ui.view" id="concentrate_view_form">
<field name="model">concentrate.term</field>
<field name="type">form</field>
<field name="name">concentrate_form</field>
</record>
<record model="ir.ui.view" id="payable_rule_view_form">
<field name="model">payable.rule</field>
<field name="type">form</field>
<field name="name">payable_rule_form</field>
</record>
<record model="ir.ui.view" id="penalty_rule_view_form">
<field name="model">penalty.rule</field>
<field name="type">form</field>
<field name="name">penalty_rule_form</field>
</record>
<record model="ir.ui.view" id="penalty_rule_view_tree">
<field name="model">penalty.rule</field>
<field name="type">tree</field>
<field name="name">penalty_rule_tree</field>
</record>
<record model="ir.ui.view" id="penalty_rule_tier_view_tree">
<field name="model">penalty.rule.tier</field>
<field name="type">tree</field>
<field name="name">penalty_rule_tier_tree</field>
</record>
<menuitem
name="Pnl Report"
parent="purchase_trade.menu_global_reporting"
@@ -144,6 +234,13 @@ this repository contains the full copyright notices and license terms. -->
sequence="110"
id="menu_pnl_bi"/>
<menuitem
name="Position Report"
parent="purchase_trade.menu_global_reporting"
action="act_position_bi"
sequence="120"
id="menu_position_bi"/>
<menuitem
parent="purchase_trade.menu_global_reporting"
sequence="100"

View File

@@ -4,6 +4,7 @@ from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.i18n import gettext
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_HALF_UP
@@ -15,6 +16,7 @@ import datetime
import logging
import json
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
logger = logging.getLogger(__name__)
@@ -32,6 +34,11 @@ class ContractDocumentType(metaclass=PoolMeta):
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
sale = fields.Many2One('sale.sale', "Sale")
class AnalyticDimensionAssignment(metaclass=PoolMeta):
'Analytic Dimension Assignment'
__name__ = 'analytic.dimension.assignment'
sale = fields.Many2One('sale.sale', "Sale")
class Estimated(metaclass=PoolMeta):
"Estimated date"
__name__ = 'pricing.estimated'
@@ -45,6 +52,12 @@ class FeeLots(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Line")
class Backtoback(metaclass=PoolMeta):
'Back To Back'
__name__ = 'back.to.back'
sale = fields.One2Many('sale.sale','btb', "Sale")
class OpenPosition(metaclass=PoolMeta):
"Open position"
__name__ = 'open.position'
@@ -52,10 +65,11 @@ class OpenPosition(metaclass=PoolMeta):
sale_line = fields.Many2One('sale.line',"Sale Line")
client = fields.Many2One('party.party',"Client")
class Mtm(metaclass=PoolMeta):
"Mtm"
__name__ = 'mtm.component'
sale_line = fields.Many2One('sale.line',"Line")
class SaleStrategy(ModelSQL):
"Sale - Document Type"
__name__ = 'sale.strategy'
sale_line = fields.Many2One('sale.line', 'Sale Line')
strategy = fields.Many2One('mtm.strategy', "Strategy")
class Component(metaclass=PoolMeta):
"Component"
@@ -172,7 +186,7 @@ class Summary(ModelSQL,ModelView):
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
sale_line = fields.Many2One('sale.line',"Sale")
sale_line = fields.Many2One('sale.line',"Sale",ondelete='CASCADE')
lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt')
lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt')
@@ -203,34 +217,236 @@ class Lot(metaclass=PoolMeta):
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
from_location = fields.Many2One('stock.location', 'From location',domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location',domain=[('type', "!=", 'supplier')])
btb = fields.Many2One('back.to.back',"Back to back")
bank_accounts = fields.Function(
fields.Many2Many('bank.account', None, None, "Bank Accounts"),
'on_change_with_bank_accounts')
bank_account = fields.Many2One(
'bank.account', "Bank Account",
domain=[('id', 'in', Eval('bank_accounts', []))],
depends=['bank_accounts'])
from_location = fields.Many2One('stock.location', 'From location', required=True,domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location', required=True,domain=[('type', "!=", 'supplier')])
shipment_out = fields.Many2One('stock.shipment.out','Sales')
pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
#pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
pnl = fields.One2Many('valuation.valuation.dyn', 'r_sale', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
pnl_ = fields.One2Many('valuation.valuation.line', 'sale', 'Pnl',states={'invisible': Eval('group_pnl'),})
group_pnl = fields.Boolean("Group Pnl")
derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification")
# weight_basis = fields.Selection([
# (None, ''),
# ('ncsw', 'NCSW'),
# ('nlw', 'NLW'),
# ], 'Weight basis')
certif = fields.Many2One('purchase.certification',"Certification")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
crop = fields.Many2One('purchase.crop',"Crop")
tol_min = fields.Numeric("Tol - in %", required=True)
tol_max = fields.Numeric("Tol + in %", required=True)
tol_min_qt = fields.Numeric("Tol -")
tol_max_qt = fields.Numeric("Tol +")
certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),})
wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True)
association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),})
crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
viewer = fields.Function(fields.Text(""),'get_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
analytic_dimensions = fields.One2Many(
'analytic.dimension.assignment',
'sale',
'Analytic Dimensions'
)
trader = fields.Many2One('party.party',"Trader")
operator = fields.Many2One('party.party',"Operator")
our_reference = fields.Char("Our Reference")
company_visible = fields.Function(
fields.Boolean("Visible"), 'on_change_with_company_visible')
lc_date = fields.Date("LC date")
product_origin = fields.Char("Origin")
@fields.depends('company', '_parent_company.party')
def on_change_with_company_visible(self, name=None):
return bool(
self.company and self.company.party
and self.company.party.name == 'MELYA')
def _get_default_bank_account(self):
if not self.party or not self.party.bank_accounts:
return None
party_bank_accounts = list(self.party.bank_accounts)
if self.currency:
for account in party_bank_accounts:
if account.currency == self.currency:
return account
return party_bank_accounts[0]
@fields.depends('party', '_parent_party.bank_accounts')
def on_change_with_bank_accounts(self, name=None):
if self.party and self.party.bank_accounts:
return [account.id for account in self.party.bank_accounts]
return []
@fields.depends(
'company', 'party', 'invoice_party', 'shipment_party', 'warehouse',
'payment_term', 'lines', 'bank_account', '_parent_party.bank_accounts')
def on_change_party(self):
super().on_change_party()
self.bank_account = self._get_default_bank_account()
@fields.depends('party', 'currency', '_parent_party.bank_accounts')
def on_change_currency(self):
self.bank_account = self._get_default_bank_account()
@classmethod
def default_wb(cls):
WB = Pool().get('purchase.weight.basis')
wb = WB.search(['id','>',0])
if wb:
return wb[0].id
@classmethod
def default_certif(cls):
Certification = Pool().get('purchase.certification')
certification = Certification.search(['id','>',0])
if certification:
return certification[0].id
@classmethod
def default_association(cls):
Association = Pool().get('purchase.association')
association = Association.search(['id','>',0])
if association:
return association[0].id
@classmethod
def default_tol_min(cls):
return 0
@classmethod
def default_tol_max(cls):
return 0
@property
def report_terms(self):
if self.lines:
return self.lines[0].note
else:
return ''
@property
def report_gross(self):
if self.lines:
return sum([l.get_current_gross_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_net(self):
if self.lines:
return sum([l.get_current_quantity() for l in self.lines[0].lots if l.lot_type == 'physic'])
else:
return ''
@property
def report_qt(self):
if self.lines:
return quantity_to_words(self.lines[0].quantity)
else:
return ''
@property
def report_nb_bale(self):
text_bale = 'NB BALES: '
nb_bale = 0
if self.lines:
for line in self.lines:
if line.lots:
nb_bale += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
return text_bale + str(int(nb_bale))
@property
def report_deal(self):
if self.lines and self.lines[0].lots and len(self.lines[0].lots)>1:
return self.lines[0].lots[1].line.purchase.number + ' ' + self.number
else:
''
@property
def report_packing(self):
nb_packing = 0
unit = ''
if self.lines:
for line in self.lines:
if line.lots:
nb_packing += sum([l.lot_qt for l in line.lots if l.lot_type == 'physic'])
if len(line.lots)>1:
unit = line.lots[1].lot_unit.name
return str(int(nb_packing)) + unit
@property
def report_price(self):
if self.lines:
if self.lines[0].price_type == 'priced':
if self.lines[0].linked_price:
return amount_to_currency_words(self.lines[0].linked_price,'USC','USC')
else:
return amount_to_currency_words(self.lines[0].unit_price)
elif self.lines[0].price_type == 'basis':
return amount_to_currency_words(self.lines[0].unit_price) + ' ' + self.lines[0].get_pricing_text()
else:
return ''
@property
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_payment_date(self):
if self.lines:
if self.lc_date:
return format_date_en(self.lc_date)
Date = Pool().get('ir.date')
payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
if payment_date:
payment_date = format_date_en(payment_date)
return payment_date
@property
def report_shipment(self):
if self.lines:
if len(self.lines[0].lots)>1:
shipment = self.lines[0].lots[1].lot_shipment_in
lot = self.lines[0].lots[1].lot_name
if shipment:
info = ''
if shipment.bl_number:
info += ' B/L ' + shipment.bl_number
if shipment.supplier:
info += ' BY ' + shipment.supplier.name
if shipment.vessel:
info += ' (' + shipment.vessel.vessel_name + ')'
if shipment.container and shipment.container[0].container_no:
id = 1
for cont in shipment.container:
if id == 1:
info += ' Container(s)'
if cont.container_no:
info += ' ' + cont.container_no
else:
info += ' unnamed'
id += 1
# info += ' (LOT ' + lot + ')'
if shipment.note:
info += ' ' + shipment.note
return info
else:
return ''
@classmethod
def default_viewer(cls):
country_start = "Zobiland"
@@ -316,26 +532,20 @@ class Sale(metaclass=PoolMeta):
for sale in sales:
for line in sale.lines:
if not line.quantity_theorical and line.quantity > 0:
line.quantity_theorical = line.quantity
line.quantity_theorical = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))
Line.save([line])
if line.lots:
line_p = line.lots[0].line
line_p = line.get_matched_lines()#line.lots[0].line
if line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',line_p.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(line_p.get_pnl_fee_lines())
pnl_lines.extend(line_p.get_pnl_price_lines())
pnl_lines.extend(line_p.get_pnl_der_lines())
Pnl.save(pnl_lines)
for l in line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
Pnl.generate(l.lot_p.line)
if line.quantity_theorical:
OpenPosition = Pool().get('open.position')
OpenPosition.create_from_sale_line(line)
# if line.quantity_theorical:
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_sale_line(line)
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
@@ -348,6 +558,11 @@ class Sale(metaclass=PoolMeta):
for d in line.derivatives:
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
Line.save([line])
class PriceComposition(metaclass=PoolMeta):
__name__ = 'price.composition'
sale_line = fields.Many2One('sale.line',"Sale line")
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
@@ -369,8 +584,18 @@ class SaleLine(metaclass=PoolMeta):
}),'get_progress')
from_del = fields.Date("From")
to_del = fields.Date("To")
period_at = fields.Selection([
(None, ''),
('laycan', 'Laycan'),
('loading', 'Loading'),
('discharge', 'Discharge'),
('crossing_border', 'Crossing Border'),
('title_transfer', 'Title transfer'),
('arrival', 'Arrival'),
],"Period at")
concentration = fields.Numeric("Concentration")
price_components = fields.One2Many('pricing.component','sale_line',"Components")
mtm = fields.One2Many('mtm.component','sale_line',"Mtm")
mtm = fields.Many2Many('sale.strategy', 'sale_line', 'strategy', 'Mtm Strategy')
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
@@ -381,6 +606,12 @@ class SaleLine(metaclass=PoolMeta):
tol_max = fields.Numeric("Tol + in %",states={
'readonly': (Eval('inherit_tol')),
})
tol_min_qt = fields.Numeric("Tol -",states={
'readonly': (Eval('inherit_tol')),
})
tol_max_qt = fields.Numeric("Tol +",states={
'readonly': (Eval('inherit_tol')),
})
inherit_tol = fields.Boolean("Inherit tolerance")
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
@@ -403,6 +634,76 @@ class SaleLine(metaclass=PoolMeta):
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
attributes = fields.Dict(
'product.attribute', 'Attributes',
domain=[
('sets', '=', Eval('attribute_set')),
],
states={
'readonly': ~Eval('attribute_set'),
},
depends=['product', 'attribute_set'],
help="Add attributes to the variant."
)
attribute_set = fields.Function(
fields.Many2One('product.attribute.set', "Attribute Set"),
'on_change_with_attribute_set'
)
attributes_name = fields.Function(
fields.Char("Attributes Name"),
'on_change_with_attributes_name'
)
finished = fields.Boolean("Mark as finished")
pricing_rule = fields.Text("Pricing description")
price_composition = fields.One2Many('price.composition','sale_line',"Price composition")
@classmethod
def default_finished(cls):
return False
@property
def report_fixing_rule(self):
pricing_rule = ''
if self.pricing_rule:
pricing_rule = self.pricing_rule
return pricing_rule
@property
def get_pricing_text(self):
pricing_text = ''
if self.price_components:
for pc in self.price_components:
if pc.price_index:
pricing_text += 'ON ' + pc.price_index.price_desc + ' ' + (pc.price_index.price_period.description if pc.price_index.price_period else '')
return pricing_text
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
if self.product and self.product.template and self.product.template.attribute_set:
return self.product.template.attribute_set.id
@fields.depends('product', 'attributes')
def on_change_with_attributes_name(self, name=None):
if not self.product or not self.product.attribute_set or not self.attributes:
return
def key(attribute):
return getattr(attribute, 'sequence', attribute.name)
values = []
for attribute in sorted(self.product.attribute_set.attributes, key=key):
if attribute.name in self.attributes:
value = self.attributes[attribute.name]
values.append(gettext(
'product_attribute.msg_label_value',
label=attribute.string,
value=attribute.format(value)
))
return " | ".join(filter(None, values))
@classmethod
def default_price_type(cls):
return 'priced'
@@ -419,10 +720,20 @@ class SaleLine(metaclass=PoolMeta):
def default_inherit_cer(cls):
return True
# @fields.depends('quantity')
# def on_change_with_quantity_theorical(self):
# if not self.quantity_theorical:
# return self.quantity
def get_matched_lines(self):
if self.lots:
LotQt = Pool().get('lot.qt')
return LotQt.search([('lot_s','=',self.lots[0].id),('lot_p','>',0)])
def get_date(self,trigger_event):
trigger_date = None
if self.estimated_date:
logger.info("ESTIMATED_DATE:%s",self.estimated_date)
trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == trigger_event]
logger.info("TRIGGER_DATE:%s",trigger_date)
logger.info("TRIGGER_EVENT:%s",trigger_event)
trigger_date = trigger_date[0] if trigger_date else None
return trigger_date
def get_tol_min(self,name):
if self.inherit_tol:
@@ -659,9 +970,9 @@ class SaleLine(metaclass=PoolMeta):
valuations = Valuation.search([('lot','in',line.lots)])
if valuations:
Valuation.delete(valuations)
op = OpenPosition.search(['sale_line','=',line.id])
if op:
OpenPosition.delete(op)
# op = OpenPosition.search(['sale_line','=',line.id])
# if op:
# OpenPosition.delete(op)
super(SaleLine, cls).delete(lines)
@@ -698,7 +1009,7 @@ class SaleLine(metaclass=PoolMeta):
lot.sale_line = line.id
lot.lot_qt = line.quantity
lot.lot_unit_line = line.unit
lot.lot_quantity = line.quantity
lot.lot_quantity = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))#round(line.quantity,5)
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
@@ -733,14 +1044,7 @@ class SaleLine(metaclass=PoolMeta):
if purchase_lines:
for pl in purchase_lines:
Pnl = Pool().get('valuation.valuation')
pnl = Pnl.search([('line','=',pl.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(pl.get_pnl_fee_lines())
pnl_lines.extend(pl.get_pnl_price_lines())
pnl_lines.extend(pl.get_pnl_der_lines())
Pnl.save(pnl_lines)
Pnl.generate(pl)
class SaleCreatePurchase(Wizard):
"Create mirror purchase"
@@ -831,6 +1135,20 @@ class Valuation(metaclass=PoolMeta):
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationLine(metaclass=PoolMeta):
"Last Valuation"
__name__ = 'valuation.valuation.line'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationReport(metaclass=PoolMeta):
"Valuation Report"
__name__ = 'valuation.report'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationDyn(metaclass=PoolMeta):
"Valuation"
__name__ = 'valuation.valuation.dyn'
@@ -855,18 +1173,22 @@ class ValuationDyn(metaclass=PoolMeta):
Max(val.sale).as_('r_sale'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
val.type.as_('r_type'),
Literal(None).as_('r_type'),
Max(val.reference).as_('r_reference'),
val.counterparty.as_('r_counterparty'),
Literal(None).as_('r_counterparty'),
Max(val.product).as_('r_product'),
val.state.as_('r_state'),
Literal(None).as_('r_state'),
Avg(val.price).as_('r_price'),
Max(val.currency).as_('r_currency'),
Sum(val.quantity).as_('r_quantity'),
Max(val.unit).as_('r_unit'),
Sum(val.amount).as_('r_amount'),
Sum(val.base_amount).as_('r_base_amount'),
Sum(val.rate).as_('r_rate'),
Sum(val.mtm).as_('r_mtm'),
Max(val.strategy).as_('r_strategy'),
Max(val.lot).as_('r_lot'),
Max(val.sale_line).as_('r_sale_line'),
where=wh,
group_by=[val.purchase,val.sale])

View File

@@ -52,5 +52,11 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">sale.sale,-1</field>
<field name="action" ref="act_sale_allocations_wizard"/>
</record>
<record model="ir.ui.view" id="sale_btb_view_form">
<field name="model">sale.sale</field>
<field name="type">form</field>
<field name="name">sale_btb_form</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
from decimal import Decimal
import logging
from trytond.pool import Pool
from trytond.transaction import Transaction
logger = logging.getLogger(__name__)
class ContractFactory:
"""
Factory métier pour créer des Purchase depuis Sale
ou des Sale depuis Purchase.
Compatible :
- Wizard (n contrats)
- Appel direct depuis un modèle (1 contrat)
"""
@classmethod
def create_contracts(cls, contracts, *, type_, ct):
"""
:param contracts: iterable de contracts (wizard lines)
:param type_: 'Purchase' ou 'Sale'
:param ct: objet contenant le contexte (lot, product, unit, matched...)
:return: liste des contracts créés
"""
pool = Pool()
Sale = pool.get('sale.sale')
Purchase = pool.get('purchase.purchase')
SaleLine = pool.get('sale.line')
PurchaseLine = pool.get('purchase.line')
Date = pool.get('ir.date')
created = []
base_contract = (
ct.lot.sale_line.sale
if type_ == 'Purchase'
else ct.lot.line.purchase
)
for c in contracts:
contract = Purchase() if type_ == 'Purchase' else Sale()
line = PurchaseLine() if type_ == 'Purchase' else SaleLine()
# ---------- CONTRACT ----------
parts = c.currency_unit.split("_")
contract.currency = int(parts[0]) or 1
contract.party = c.party
contract.crop = c.crop
contract.tol_min = c.tol_min
contract.tol_max = c.tol_max
contract.payment_term = c.payment_term
contract.reference = c.reference
contract.from_location = c.from_location
contract.to_location = c.to_location
context = Transaction().context
contract.company = context.get('company') if context else None
if type_ == 'Purchase':
contract.purchase_date = Date.today()
else:
contract.sale_date = Date.today()
cls._apply_locations(contract, base_contract, type_)
cls._apply_party_data(contract, c.party, type_)
cls._apply_payment_term(contract, c.party, type_)
if type_ == 'Sale':
contract.product_origin = getattr(base_contract, 'product_origin', None)
contract.incoterm = c.incoterm
if c.party.addresses:
contract.invoice_address = c.party.addresses[0]
if type_ == 'Sale':
contract.shipment_address = c.party.addresses[0]
contract.save()
# ---------- LINE ----------
line.quantity = c.quantity
line.quantity_theorical = c.quantity
line.product = ct.product
line.unit = ct.unit
line.price_type = c.price_type
line.created_by_code = ct.matched
line.premium = Decimal(0)
if type_ == 'Purchase':
line.purchase = contract.id
else:
line.sale = contract.id
cls._apply_price(line, c, parts)
line.del_period = c.del_period
line.from_del = c.from_del
line.to_del = c.to_del
line.save()
logger.info("CREATE_ID:%s", contract.id)
logger.info("CREATE_LINE_ID:%s", line.id)
if ct.matched:
cls._create_lot(line, c, ct, type_)
created.append(contract)
return created
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
@staticmethod
def _apply_locations(contract, base, type_):
if not (base.from_location and base.to_location):
return
if type_ == 'Purchase':
contract.to_location = base.from_location
else:
contract.from_location = base.to_location
if (base.from_location.type == 'supplier'
and base.to_location.type == 'customer'):
contract.from_location = base.from_location
contract.to_location = base.to_location
@staticmethod
def _apply_party_data(contract, party, type_):
if party.wb:
contract.wb = party.wb
if party.association:
contract.association = party.association
@staticmethod
def _apply_payment_term(contract, party, type_):
if type_ == 'Purchase' and party.supplier_payment_term:
contract.payment_term = party.supplier_payment_term
elif type_ == 'Sale' and party.customer_payment_term:
contract.payment_term = party.customer_payment_term
@staticmethod
def _apply_price(line, c, parts):
if int(parts[0]) == 0:
line.enable_linked_currency = True
line.linked_currency = 1
line.linked_unit = int(parts[1])
line.linked_price = c.price
line.unit_price = line.get_price_linked_currency()
else:
line.unit_price = c.price if c.price else Decimal(0)
# -------------------------------------------------------------------------
# LOT / MATCHING (repris tel quel du wizard)
# -------------------------------------------------------------------------
@classmethod
def _create_lot(cls, line, c, ct, type_):
pool = Pool()
Lot = pool.get('lot.lot')
LotQtHist = pool.get('lot.qt.hist')
LotQtType = pool.get('lot.qt.type')
lot = Lot()
if type_ == 'Purchase':
lot.line = line.id
else:
lot.sale_line = line.id
lot.lot_qt = None
lot.lot_unit = None
lot.lot_unit_line = line.unit
lot.lot_quantity = round(line.quantity, 5)
lot.lot_gross_quantity = None
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
lqtt = LotQtType.search([('sequence', '=', 1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = round(lot.lot_quantity, 5)
lqh.gross_quantity = round(lot.lot_quantity, 5)
lot.lot_hist = [lqh]
lot.save()
vlot = ct.lot
shipment_origin = cls._get_shipment_origin(ct)
qt = c.quantity
if type_ == 'Purchase':
if not lot.updateVirtualPart(qt, shipment_origin, vlot):
lot.createVirtualPart(qt, shipment_origin, vlot)
# Decrease forecasted virtual part non matched
lot.updateVirtualPart(-qt, shipment_origin, vlot, 'only sale')
else:
if not vlot.updateVirtualPart(qt, shipment_origin, lot):
vlot.createVirtualPart(qt, shipment_origin, lot)
# Decrease forecasted virtual part non matched
vlot.updateVirtualPart(-qt, shipment_origin, None)
@staticmethod
def _get_shipment_origin(ct):
if ct.shipment_in:
return 'stock.shipment.in,%s' % ct.shipment_in.id
if ct.shipment_internal:
return 'stock.shipment.internal,%s' % ct.shipment_internal.id
if ct.shipment_out:
return 'stock.shipment.out,%s' % ct.shipment_out.id
return None

View File

@@ -67,10 +67,10 @@ setup(name=name,
+ ['trytond.modules.purchase_trade.%s' % p
for p in find_packages()]
),
package_data={
'trytond.modules.purchase_trade': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
},
package_data={
'trytond.modules.purchase_trade': (info.get('xml', [])
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
@@ -120,4 +120,4 @@ setup(name=name,
[trytond.modules]
purchase_trade = trytond.modules.purchase_trade
""",
)
)

View File

@@ -16,10 +16,53 @@ from itertools import chain, groupby
from operator import itemgetter
import datetime
from collections import defaultdict
from sql import Table
from trytond.modules.purchase_trade.service import ContractFactory
import requests
import io
import base64
import logging
import json
import re
import html
logger = logging.getLogger(__name__)
class Location(metaclass=PoolMeta):
__name__ = 'stock.location'
def get_places(self):
t = Table('places')
cursor = Transaction().connection.cursor()
cursor.execute(*t.select(
t.PLACE_KEY,
where=t.PLACE_NAME.ilike(f'%{self.name}%')
))
rows = cursor.fetchall()
if rows:
return int(rows[0][0])
@classmethod
def getLocationByName(cls, location, type):
location = location.upper()
loc = cls.search([('name', '=', location),('type', '=', type)], limit=1)
if loc:
return loc[0].id
else:
loc = cls()
loc.name = location
loc.type = type
cls.save([loc])
return loc
@classmethod
def get_transit_id(cls):
return cls.getLocationByName('TRANSIT','storage')
def is_transit(self):
if self.name == 'Transit':
return True
class Move(metaclass=PoolMeta):
__name__ = 'stock.move'
@@ -28,17 +71,12 @@ class Move(metaclass=PoolMeta):
lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
lot = fields.Many2One('lot.lot',"Lot")
# @fields.depends('lotqt','unit')
# def on_change_with_quantity(self):
# if self.lotqt:
# pool = Pool()
# Uom = pool.get('product.uom')
# if self.unit:
# return round(sum([(e.lot_quantity if e.lot_quantity else 0) for e in self.lotqt]),2)
# else:
# return 0
# else:
# return 0
def get_linked_transit_move(self):
if self.from_location.is_transit():
Move = Pool().get('stock.move')
Location = Pool().get('stock.location')
moves = Move.search([('lot','=',self.lot),('to_location','=',Location.get_transit_id())],order=[('id', 'DESC')],limit=1)
return moves[0] if moves else None
@classmethod
def validate(cls, moves):
@@ -343,6 +381,12 @@ class ShipmentContainer(ModelSQL, ModelView):
seal_no = fields.Char('Seal Number')
is_reefer = fields.Boolean('Reefer')
class ShipmentWR(ModelSQL,ModelView):
"Shipment WR"
__name__ = "shipment.wr"
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
wr = fields.Many2One('weight.report',"WR")
class ShipmentIn(metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
@@ -395,12 +439,24 @@ class ShipmentIn(metaclass=PoolMeta):
'shipment',
'Container'
)
shipment_wr = fields.One2Many('shipment.wr','shipment_in',"WR")
controller = fields.Many2One('party.party',"Controller")
controller_target = fields.Char("Targeted controller")
send_instruction = fields.Boolean("Send instruction")
instructions = fields.Text("Instructions")
add_bl = fields.Boolean("Add BL")
add_invoice = fields.Boolean("Add invoice")
returned_id = fields.Char("Returned ID")
result = fields.Char("Result",readonly=True)
agent = fields.Many2One('party.party',"Booking Agent")
service_order_key = fields.Integer("Service Order Key")
@classmethod
def __setup__(cls):
super().__setup__()
cls._buttons.update({
'compute': {},
'send': {},
})
def get_vessel_type(self,name=None):
@@ -413,6 +469,356 @@ class ShipmentIn(metaclass=PoolMeta):
else:
return str(self.id)
def create_fee(self,controller):
Fee = Pool().get('fee.fee')
Product = Pool().get('product.product')
fee = Fee()
fee.shipment_in = self.id
fee.supplier = controller
fee.type = 'budgeted'
fee.p_r = 'pay'
price,mode,curr,unit = controller.get_sla_cost(self.to_location)
if price and mode and curr and unit:
fee.mode = mode
fee.currency = curr
fee.unit = unit
fee.quantity = self.get_bales() or 1
fee.product = Product.get_by_name('Reweighing')
fee.price = price
Fee.save([fee])
def get_controller(self):
ControllerCategory = Pool().get('party.category')
PartyCategory = Pool().get('party.party-party.category')
cc = ControllerCategory.search(['name','=','CONTROLLER'])
if cc:
cc = cc[0]
controllers = PartyCategory.search(['category','=',cc.id])
for c in controllers:
if c.party.IsAvailableForControl(self):
return c.party
def get_instructions_html(self,inv_date,inv_nb):
vessel = self.vessel.vessel_name if self.vessel else ""
lines = [
"<p>Hi,</p>",
"<p>Please find details below for the requested control</p>",
]
lines.append(
"<p>"
f"<strong>BL number:</strong> {self.bl_number} | "
f"<strong>Vessel:</strong> {vessel} | "
f"<strong>ETA:</strong> {self.etad}"
"</p>"
)
if self.incoming_moves:
tot_net = sum([m.lot.get_current_quantity() for m in self.incoming_moves])
tot_gross = sum([m.lot.get_current_gross_quantity() for m in self.incoming_moves])
tot_bale = sum([m.lot.lot_qt for m in self.incoming_moves])
customer = self.incoming_moves[0].lot.sale_line.sale.party.name if self.incoming_moves[0].lot.sale_line else ""
unit = self.incoming_moves[0].lot.lot_unit_line.symbol
lines.append("<p>"
f"<strong>Customer:</strong> {customer} | "
f"<strong>Invoice Nb:</strong> {inv_nb} | "
f"<strong>Invoice Date:</strong> {inv_date}"
"</p>"
)
lines.append(
"<p>"
f"<strong>Nb Bales:</strong> {tot_bale} | "
f"<strong>Net Qt:</strong> {tot_net} {unit} | "
f"<strong>Gross Qt:</strong> {tot_gross} {unit}"
"</p>"
)
return "".join(lines)
# def get_instructions(self):
# lines = [
# "Hi,",
# "",
# "Please find details below for the requested control",
# f"BL number: {self.bl_number}",
# ""
# ]
# if self.incoming_moves:
# for m in self.incoming_moves:
# if m.lot:
# lines.append(
# f"Lot nb: {m.lot.lot_name} | "
# f"Net Qt: {m.lot.get_current_quantity()} {m.lot.lot_unit.symbol} | "
# f"Gross Qt: {m.lot.get_current_gross_quantity()} {m.lot.lot_unit.symbol}"
# )
# return "\n".join(lines)
def _create_lots_from_fintrade(self):
t = Table('freight_booking_lots')
cursor = Transaction().connection.cursor()
query = t.select(
t.BOOKING_NUMBER,
t.LOT_NUMBER,
t.LOT_NBR_BALES,
t.LOT_GROSS_WEIGHT,
t.LOT_NET_WEIGHT,
t.LOT_UOM,
t.LOT_QUALITY,
t.CUSTOMER,
t.SELL_PRICE_CURRENCY,
t.SELL_PRICE_UNIT,
t.SELL_PRICE,
t.SALE_INVOICE,
t.SELL_INV_AMOUNT,
t.SALE_INVOICE_DATE,
t.SELL_PREMIUM,
t.SALE_CONTRACT_NUMBER,
t.SALE_DECLARATION_KEY,
t.SHIPMENT_CHUNK_KEY,
where=(t.BOOKING_NUMBER == int(self.reference))
)
cursor.execute(*query)
rows = cursor.fetchall()
logger.info("ROWS:%s",rows)
inv_date = None
inv_nb = None
if rows:
sale_line = None
for row in rows:
logger.info("ROW:%s",row)
#Purchase & Sale creation
LotQt = Pool().get('lot.qt')
Lot = Pool().get('lot.lot')
LotAdd = Pool().get('lot.add.line')
Currency = Pool().get('currency.currency')
Product = Pool().get('product.product')
Party = Pool().get('party.party')
Uom = Pool().get('product.uom')
Sale = Pool().get('sale.sale')
SaleLine = Pool().get('sale.line')
dec_key = str(row[16]).strip()
chunk_key = str(row[17]).strip()
lot_unit = str(row[5]).strip().lower()
product = str(row[6]).strip().upper()
lot_net_weight = Decimal(row[4])
logger.info("LOT_NET_WEIGHT:%s",lot_net_weight)
lot_gross_weight = Decimal(row[3])
lot_bales = Decimal(row[2])
lot_number = row[1]
customer = str(row[7]).strip().upper()
sell_price_currency = str(row[8]).strip().upper()
sell_price_unit = str(row[9]).strip().lower()
inv_date = str(row[13]).strip()
inv_nb = str(row[11]).strip()
sell_price = Decimal(row[10])
premium = Decimal(row[14])
reference = Decimal(row[15])
logger.info("DECLARATION_KEY:%s",dec_key)
declaration = SaleLine.search(['note','=',dec_key])
if declaration:
sale_line = declaration[0]
logger.info("WITH_DEC:%s",sale_line)
vlot = sale_line.lots[0]
lqt = LotQt.search([('lot_s','=',vlot.id)])
if lqt:
for lq in lqt:
if lq.lot_p:
logger.info("VLOT_P:%s",lq.lot_p)
sale_line.quantity_theorical += round(lot_net_weight,2)
SaleLine.save([sale_line])
lq.lot_p.updateVirtualPart(round(lot_net_weight,2),self,lq.lot_s)
vlot.set_current_quantity(round(lot_net_weight,2),round(lot_gross_weight,2),1)
Lot.save([vlot])
else:
sale = Sale()
sale_line = SaleLine()
sale.party = Party.getPartyByName(customer,'CLIENT')
logger.info("SALE_PARTY:%s",sale.party)
sale.reference = reference
sale.from_location = self.from_location
sale.to_location = self.to_location
sale.company = 6
sale.payment_term = 2
if sale.party.addresses:
sale.invoice_address = sale.party.addresses[0]
sale.shipment_address = sale.party.addresses[0]
if sell_price_currency == 'USC':
sale.currency = Currency.get_by_name('USD')
sale_line.enable_linked_currency = True
sale_line.linked_currency = 1
sale_line.linked_unit = Uom.get_by_name(sell_price_unit)
sale_line.linked_price = round(sell_price,4)
sale_line.unit_price = sale_line.get_price_linked_currency()
else:
sale.currency = Currency.get_by_name(sell_price_currency)
sale_line.unit_price = round(sell_price,4)
sale_line.unit = Uom.get_by_name(sell_price_unit)
sale_line.premium = premium
Sale.save([sale])
sale_line.sale = sale.id
sale_line.quantity = round(lot_net_weight,2)
sale_line.quantity_theorical = round(lot_net_weight,2)
sale_line.product = Product.get_by_name('BRAZIL COTTON')
logger.info("PRODUCT:%s",sale_line.product)
sale_line.unit = Uom.get_by_name(lot_unit)
sale_line.price_type = 'priced'
sale_line.created_by_code = False
sale_line.note = dec_key
SaleLine.save([sale_line])
#need to link the virtual part to the shipment
lqt = LotQt.search([('lot_s','=',sale_line.lots[0])])
if lqt:
lqt[0].lot_shipment_in = self
LotQt.save(lqt)
logger.info("SALE_LINKED_TO_SHIPMENT:%s",self)
ContractStart = Pool().get('contracts.start')
ContractDetail = Pool().get('contract.detail')
ct = ContractStart()
d = ContractDetail()
ct.type = 'Purchase'
ct.matched = True
ct.shipment_in = self
ct.lot = sale_line.lots[0]
ct.product = sale_line.product
ct.unit = sale_line.unit
d.party = Party.getPartyByName('FAIRCOT')
if sale_line.enable_linked_currency:
d.currency_unit = str(sale_line.linked_currency.id) + '_' + str(sale_line.linked_unit.id)
else:
d.currency_unit = str(sale.currency.id) + '_' + str(sale_line.unit.id)
d.quantity = sale_line.quantity
d.unit = sale_line.unit
d.price = sale_line.unit_price
d.price_type = 'priced'
d.crop = None
d.tol_min = 0
d.tol_max = 0
d.incoterm = None
d.reference = str(sale.id)
d.from_location = sale.from_location
d.to_location = sale.to_location
d.del_period = None
d.from_del = None
d.to_del = None
d.payment_term = sale.payment_term
ct.contracts = [d]
ContractFactory.create_contracts(
ct.contracts,
type_=ct.type,
ct=ct,
)
#Lots creation
vlot = sale_line.lots[0]
lqt = LotQt.search([('lot_s','=',vlot.id),('lot_p','>',0)])
if lqt and vlot.lot_quantity > 0:
lqt = lqt[0]
l = LotAdd()
l.lot_qt = lot_bales
l.lot_unit = Uom.get_by_name('bale')
l.lot_unit_line = Uom.get_by_name(lot_unit)
l.lot_quantity = round(lot_net_weight,2)
l.lot_gross_quantity = round(lot_gross_weight,2)
l.lot_premium = premium
l.lot_chunk_key = int(chunk_key)
logger.info("ADD_LOT:%s",int(chunk_key))
LotQt.add_physical_lots(lqt,[l])
return inv_date,inv_nb
def html_to_text(self,html_content):
text = re.sub(r"<br\s*/?>", "\n", html_content, flags=re.IGNORECASE)
text = re.sub(r"</p\s*>", "\n\n", text, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
return html.unescape(text).strip()
def create_service_order(self,so_payload):
response = requests.post(
"http://automation-service:8006/service-order",
json=so_payload,
timeout=10
)
response.raise_for_status()
return response.json()
@classmethod
@ModelView.button
def send(cls, shipments):
Date = Pool().get('ir.date')
Attachment = Pool().get('ir.attachment')
for sh in shipments:
sh.result = "Email not sent"
attachment = []
if sh.add_bl:
attachments = Attachment.search([
('resource', '=', 'stock.shipment.in,' + str(sh.id)),
])
if attachments:
content_b64 = base64.b64encode(attachments[0].data).decode('ascii')
attachment = [
{
"filename": attachments[0].name,
"content": content_b64,
"content_type": "application/pdf"
}
]
if sh.controller:
Contact = Pool().get('party.contact_mechanism')
contact = Contact.search(['party','=',sh.controller.id])
if contact:
payload = {
"to": [contact[0].value],
"subject": "Request for control",
"body": sh.html_to_text(sh.instructions),
"attachments": attachment,
"meta": {
"shipment": sh.bl_number,
"controller": sh.controller.id
}
}
response = requests.post(
"http://automation-service:8006/mail",
json=payload,
timeout=10
)
response.raise_for_status()
data = response.json()
logger.info("SEND_FROM_SHIPMENT:%s",data)
now = datetime.datetime.now()
sh.result = f"Email sent on {now.strftime('%d/%m/%Y %H:%M')}"
sh.save()
if sh.fees:
fee = sh.fees[0]
so_payload = {
"ControllerAlfCode": sh.controller.get_alf(),
"CurrKey": '3',
"Point1PlaceKey": sh.from_location.get_places(),
"Point2PlaceKey": sh.to_location.get_places(),
"OrderReference": sh.reference,
"FeeTotalCost": float(fee.amount),
"FeeUnitPrice": float(fee.price),
"ContractNumbers": sh.number,
"OrderQuantityGW": float(sh.get_quantity()) if sh.get_quantity() else float(1),
"NumberOfPackingBales": int(fee.quantity) if fee.quantity else int(1),
"ChunkKeyList": sh.get_chunk_key()
}
logger.info("PAYLOAD:%s",so_payload)
data = sh.create_service_order(so_payload)
logger.info("SO_NUMBER:%s",data.get('service_order_number'))
sh.result += f" / SO Nb {data.get('service_order_number')}"
sh.service_order_key = int(data.get('service_order_key'))
sh.save()
@classmethod
@ModelView.button
def compute(cls, shipments):
@@ -457,9 +863,19 @@ class ShipmentIn(metaclass=PoolMeta):
def default_dashboard(cls):
return 1
def get_chunk_key(self):
keys = [m.lot.lot_chunk_key for m in self.incoming_moves if m.lot]
return ",".join(map(str, keys)) if keys else None
def get_quantity(self,name=None):
if self.incoming_moves:
return sum([(e.quantity if e.quantity else 0) for e in self.incoming_moves])
def get_bales(self,name=None):
Lot = Pool().get('lot.lot')
lots = Lot.search(['lot_shipment_in','=',self.id])
if lots:
return sum([l.lot_qt for l in lots])
def get_unit(self,name=None):
if self.incoming_moves:
@@ -574,13 +990,7 @@ class ShipmentIn(metaclass=PoolMeta):
#update line valuation
Pnl = Pool().get('valuation.valuation')
for lot in lots:
pnl = Pnl.search([('line','=',lot.line.id)])
if pnl:
Pnl.delete(pnl)
pnl_lines = []
pnl_lines.extend(lot.line.get_pnl_fee_lines())
pnl_lines.extend(lot.line.get_pnl_price_lines())
Pnl.save(pnl_lines)
Pnl.generate(lot.line if lot.line else lot.sale_line)
if sh.sof:
for sof in sh.sof:
if sof.chart:

View File

@@ -38,6 +38,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="model">stock.shipment.container</field>
<field name="type">tree</field>
<field name="name">shipment_container_tree</field>
</record>
<record model="ir.ui.view" id="shipment_wr_view_tree">
<field name="model">shipment.wr</field>
<field name="type">tree</field>
<field name="name">shipment_wr_tree</field>
</record>
<record model="ir.action.wizard" id="act_vf">

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

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