Compare commits
692 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c8abc7c1e | ||
|
|
37bf6ba23b | ||
|
|
2e649aa61b | ||
|
|
735a72d23e | ||
|
|
ebf9b6c495 | ||
|
|
13d26ac41b | ||
|
|
b829b11791 | ||
|
|
4a056ef402 | ||
|
|
da65da79c0 | ||
|
|
aa6b3fb9ad | ||
|
|
b1dd118628 | ||
|
|
f1f9d157cc | ||
|
|
283b71fda9 | ||
|
|
f27dd5620e | ||
|
|
143f59c62e | ||
|
|
be6b6517a5 | ||
|
|
d96973310b | ||
|
|
39278c4483 | ||
|
|
4534ad86f1 | ||
|
|
10e8e5be9b | ||
|
|
2fa541e962 | ||
|
|
ad2f7e6f78 | ||
| 43c62607a8 | |||
| 5cff728d79 | |||
|
|
d6382f624b | ||
| 08febb904f | |||
| 20d733e787 | |||
| 984b2ba56f | |||
| f67e5d8ccc | |||
| 2bf02e687c | |||
| 3c45ebd50f | |||
| 22d186f0ac | |||
| 0979021f41 | |||
| af4ae99dc0 | |||
| 11c489f79d | |||
| 620d6bb604 | |||
| 97cfd13da2 | |||
| 126455bf0f | |||
| 97cc447e0a | |||
| e2bbd0e522 | |||
| 51778bda9e | |||
| c06ea3bd99 | |||
| f0b0666773 | |||
| edca5fed55 | |||
| 8b6b93171f | |||
| 841f7a1c20 | |||
| 91acaba3dc | |||
| 3e5320cf9e | |||
| f9010ddefd | |||
| d722b58f4e | |||
| 3e646ea035 | |||
| 90f97daa7f | |||
| e1488e9677 | |||
| 0bcec5d3c4 | |||
| 10848ed533 | |||
| b90d65d245 | |||
| 357478e74c | |||
| 1f49c00cff | |||
| d38d0324d5 | |||
| b8ec7a3d2d | |||
| 0ac261b670 | |||
| 51411faff2 | |||
| a94906bb53 | |||
| 2262e79361 | |||
| 5327ca4f21 | |||
| 03997db3e4 | |||
| 74e4ac0c1b | |||
| 353cf17af7 | |||
| 412f8f00e0 | |||
| 6b841db4ce | |||
| 4e3818d46a | |||
| 8bb34aa4fb | |||
| 0392b191c5 | |||
| f98b0fd010 | |||
| 97dd672e6a | |||
| bde600e86a | |||
| 0e7cabdc00 | |||
| 15f9f4b724 | |||
| c24de18fcd | |||
| cb9c868e88 | |||
| 0e76aa6312 | |||
| 062ca10340 | |||
| 175c7a5e30 | |||
| e6519d30ee | |||
| f9ee416d99 | |||
| 80deac62f8 | |||
| b5d771341d | |||
| afdbd05375 | |||
| 36b94b98af | |||
| c655eb4170 | |||
| ed11b23f79 | |||
| 140eae06a0 | |||
| 234084f073 | |||
| 2fc6fbbd76 | |||
| 0b9c85f5ad | |||
| 717b51ad19 | |||
| dfdaf7a1cd | |||
| 7dba25c6d7 | |||
| 765d90512d | |||
| 090b4ea5c6 | |||
| 1326c8df4c | |||
| d10d805753 | |||
| 85bb272edf | |||
| 5f475d5714 | |||
| 8cc091ef57 | |||
| 7ba170e072 | |||
| 931b5b9ea2 | |||
| eb98ef87bb | |||
| 834ae46a14 | |||
| 7c746ad931 | |||
| 95f5c4af57 | |||
| f83c62ffa8 | |||
| 1f4b36633b | |||
| 36dc48bee0 | |||
| fc52aa8bbb | |||
| 5c516465e2 | |||
| d827fa6140 | |||
| 93d27059af | |||
| d2a13b3c01 | |||
| 22c4766e66 | |||
| 73eeee0f72 | |||
| 260300681f | |||
| a2e0c36a3c | |||
| 4f6641ceeb | |||
| cabd032bc6 | |||
| b2e095aad5 | |||
| 8c3ba0a23d | |||
| 13072de28f | |||
| a63a46cc40 | |||
| bf6e9f6a78 | |||
| 6c7947c7c1 | |||
| 6954044f7e | |||
| d6207855a0 | |||
| a41ec4412d | |||
| 8c3c224c07 | |||
| 3581b7ea80 | |||
| f7d87d3a78 | |||
| 5a52f09f97 | |||
| 420577cd72 | |||
| 01777f1536 | |||
| 24c8cd8075 | |||
| 7ace327d3f | |||
| a8d37bd766 | |||
| 6b96cdbc88 | |||
| 5bb1618b30 | |||
| 3ef819bf86 | |||
| 7ae54f9977 | |||
| 410604d890 | |||
| c8bbf9a12e | |||
| f32cce6177 | |||
| 32b48cf6d1 | |||
| eebcf936fe | |||
| 127a38f857 | |||
| 1cedd11304 | |||
| 8881caccd0 | |||
| 59a6554fa3 | |||
| 3d80a6ba5e | |||
| 07816b9cfe | |||
| 5bbd68448a | |||
| 185f26f31b | |||
| bbb88dbd22 | |||
| 3ad2ae2624 | |||
| e78eb80d08 | |||
| 498da9a728 | |||
| eac35b849f | |||
| 13770cc299 | |||
| 72b71675ae | |||
| 5cf5ca520a | |||
| 5fe0be5c59 | |||
| c504b4b2d7 | |||
| bbd3d30b37 | |||
| b4d09d3a69 | |||
| 8e7f9648fc | |||
| 2183d8d5a4 | |||
| 8398a8c212 | |||
| 8859ccad1a | |||
| c2adc96fbf | |||
| 9330662b30 | |||
| eadbbbbbc8 | |||
| 5b12e5f8db | |||
| 1d10591cf6 | |||
| fd3ce2188a | |||
| 4695a93e44 | |||
| 93f0e6b0af | |||
| 319d8a4afb | |||
| b6e85ee710 | |||
| c349441874 | |||
| 73fdbe9eba | |||
| b20b0c8df6 | |||
| cc5f4da38c | |||
| 016112a355 | |||
| 0cf03a75b0 | |||
| 7f6b93094f | |||
| ad58d40da7 | |||
| f0c1b8909e | |||
| 8e8afe39d0 | |||
| 8da50c72c7 | |||
| 683d3600ac | |||
| e2cb840844 | |||
| 19a4363d10 | |||
| 23c4edfec5 | |||
| b07ad57a36 | |||
| 6d06125360 | |||
| 4a14a78f78 | |||
| 2fba795b11 | |||
| b23dba865f | |||
| 0b1bb2ffa5 | |||
| 6890d0de07 | |||
| c180e8926f | |||
| 0fe16df326 | |||
| 3e15052520 | |||
| dee1896a6c | |||
| faf5fa605e | |||
| a7846b359c | |||
| c0cffde079 | |||
| 814071e4c1 | |||
| 30ae86a987 | |||
| 1de11a846b | |||
| 93acacb955 | |||
| 6d053dfe03 | |||
| 5f49e01495 | |||
| d609864822 | |||
| aff4459942 | |||
| 840f03e5d8 | |||
| 039b278757 | |||
| 265b41b206 | |||
| 5235b381c6 | |||
| 52c58df547 | |||
| cc8540ee91 | |||
| d70a784db8 | |||
| e6984e3393 | |||
| f4ae7ebd7c | |||
| 5f744ec25c | |||
| 1dc69008d8 | |||
| 96aab15a7a | |||
| dd06fc523c | |||
| 7cafd8381f | |||
| c009b15ed0 | |||
| 6717feb22d | |||
| 7998467adf | |||
| 83ef3c6ae9 | |||
| 372d04b30f | |||
| 4e6ddc39c1 | |||
| a3f15c30e6 | |||
| cdb0bf7254 | |||
| 843ade6f3d | |||
| 62f731530b | |||
| 14c2548c16 | |||
| 13000f110d | |||
| 915fd17d83 | |||
| 12de27d105 | |||
| 1a1f675cf6 | |||
| 3b67f3c251 | |||
| 551a040df0 | |||
| b6c3279917 | |||
| 2b956e6142 | |||
| ae0817e0ac | |||
| 7451e19125 | |||
| 52418b6244 | |||
| a577595ee6 | |||
| 8db31ddaf0 | |||
| e371b116f7 | |||
| 3212d551d2 | |||
| 8d557a63a0 | |||
| ee9866c28e | |||
| 8d9dd6c275 | |||
| 0841fb4609 | |||
| 5b059b70e6 | |||
| 3319563b22 | |||
| ddc2884f61 | |||
| 5dfd24b5de | |||
| 7ae145b6c1 | |||
| 7909b10636 | |||
| bab6c14499 | |||
| c816ecdbb7 | |||
| 024f8f78bd | |||
| 94595346b7 | |||
| 8a4d730c5a | |||
| 3226f55a7d | |||
| 8eba77b26c | |||
| a0c1408f8e | |||
| 7e726010b8 | |||
| 4abd9b7e85 | |||
| e984c026b9 | |||
| 4affffaf0a | |||
| c55338e879 | |||
| 1c288d9646 | |||
| d37bad643d | |||
| f493901e8d | |||
| 934f3511f7 | |||
| 62a1bc69f7 | |||
| 741389b71c | |||
| 9dd4543fc1 | |||
| af89743b40 | |||
| 7f0400400c | |||
| d9cf6c9a49 | |||
| 1a9114da7e | |||
| b5bc559b11 | |||
| 8e67c7aecb | |||
| 6b884af9cc | |||
| e768fe71a3 | |||
| 5f8d081b0e | |||
| 20c3dacded | |||
| 6fdf5e1994 | |||
| 92210a381a | |||
| cd2a82f61c | |||
| 4eae444c93 | |||
| 02993336de | |||
| 2c38d2742b | |||
| a724e0a086 | |||
| 399d9da9e5 | |||
| 6f0e0d65ee | |||
| 20f2b87d99 | |||
| 56f2c3a4a6 | |||
| a1a13a6846 | |||
| c244cf658c | |||
| 6fc0e5982a | |||
| 84fa03bf77 | |||
| 11cabcee32 | |||
| 1cdc54e59e | |||
| d2288e009f | |||
| d81b19bfc6 | |||
| b2fb1e9c4c | |||
| cac84abbef | |||
| 191abf022f | |||
| 0011a7f943 | |||
| 697b53e68a | |||
| 0e5c40e9a2 | |||
| bb313d29bf | |||
| 3f54cbef6a | |||
| 00cae2f82b | |||
| d1d37ee4b3 | |||
| 08a66bc219 | |||
| 35c9731a85 | |||
| d458e9849d | |||
| b94d6e911d | |||
| 4fb39d564e | |||
| 10b370f11b | |||
| 479e0d4d5a | |||
| 4059cd591e | |||
| 9565e82850 | |||
| 29ada1899e | |||
| 67d3ce545e | |||
| 9b8d372fa6 | |||
| dda6a63e74 | |||
| 2fc89282b9 | |||
| cca82dd9f7 | |||
| 1e51061ddc | |||
| 267e5a3509 | |||
| e040a4fc11 | |||
| 6f4649c778 | |||
| 177f862263 | |||
| 93456e9530 | |||
| 8ee9354327 | |||
| 6f4ad3723f | |||
| f3370e89ea | |||
| 330fc8d320 | |||
| f0b979ac8c | |||
| f3ebeb7cd3 | |||
| 981a97ed05 | |||
| 9ed4d5f6bb | |||
| e43189f052 | |||
| 0fcaca2b5a | |||
| 3db2f8c96e | |||
| b0ebc02434 | |||
| 0be0194ab5 | |||
| 7d99f56fa5 | |||
| 6eec004781 | |||
| 70603d95d4 | |||
| c78f8b1079 | |||
| 9ec76410f7 | |||
| e47023c4ba | |||
| 133805eab5 | |||
| 878588d567 | |||
| d88a9f204c | |||
| 1ab2b56f32 | |||
| b41fe0fa54 | |||
| 1326669f01 | |||
| e37cd4a1cc | |||
| 1eb86892de | |||
| 91095f28e9 | |||
| e41ad51634 | |||
| 2eabfe2587 | |||
| a71d666619 | |||
| 38254fee48 | |||
| 5cc66b3a3a | |||
| d007db7a1b | |||
| 935862a66c | |||
| 0c83fcf35e | |||
| bc5aa57319 | |||
| 231b733222 | |||
| c02d7212bb | |||
| df5ef074ad | |||
| 04179fde77 | |||
| 0cd9d63b15 | |||
| b5b2f1c362 | |||
| 14dd557e58 | |||
| 1e7cd1c7be | |||
| 0a04774fac | |||
| 720ad804a4 | |||
| 175b10e59d | |||
| 03d923441e | |||
| 3da61070eb | |||
| 6a0acb1c20 | |||
| 3195994444 | |||
| 7ab361c2b8 | |||
| 894375c143 | |||
| 225df5c6da | |||
| 1a2d595303 | |||
| cccff5726c | |||
| 9a354181b0 | |||
| c7dce8bedc | |||
| adecdbdb4f | |||
| 06d40ddbae | |||
| e009a9e1e3 | |||
| d22610abb3 | |||
| 8c033f4eab | |||
| cb6c1819f4 | |||
| 762cfc66c1 | |||
| 1d653b2756 | |||
| b7927b787d | |||
| e57019b39c | |||
| a09a88ff5f | |||
| c1a4b442a3 | |||
| 34166dedc6 | |||
| 9095f3b470 | |||
| 5255959614 | |||
| 1b92b1d207 | |||
| 67eca35a9a | |||
| a35e304561 | |||
| f93c81624d | |||
| d47c2cb5fb | |||
| 45ab8e8904 | |||
| 0f3497e867 | |||
| b0e19226eb | |||
| 95589bcf73 | |||
| 03c7f41457 | |||
| cd0c068b3f | |||
| 05d5d85bee | |||
| d213328fd9 | |||
| 9235c22e04 | |||
| c1ca8c707c | |||
| 6c4ded50d0 | |||
| 86b8d3e250 | |||
| 7b94c56e7c | |||
| c6f963b6be | |||
| 181217e755 | |||
| 00a6a5debe | |||
| 91f85ea7c1 | |||
| aaf4a264a1 | |||
| b03a97d02d | |||
| a69a9dcb57 | |||
| 0a88b26160 | |||
| 3a79652c59 | |||
| a0ed097b6c | |||
| a9d6f8fa58 | |||
| 2ebad01847 | |||
| 71716c1ad5 | |||
| 240f35af26 | |||
| 1ebbb874ef | |||
| eef972c679 | |||
| 011af03e8a | |||
| 24843ab72d | |||
| 65c0053df4 | |||
| f391e17fb6 | |||
| 44fc7dc855 | |||
| 99d895c951 | |||
| 39e59b5b8b | |||
| 7bc3e350ba | |||
| fe7c10d527 | |||
| 272875eb69 | |||
| 16df1c99a6 | |||
| 403db5133e | |||
| 43312ba412 | |||
| f4a53f2705 | |||
| 0a41765551 | |||
| 28789200c3 | |||
| 350e41714f | |||
| 84b70b73c5 | |||
| 68e0000afc | |||
| 33eeabf5e1 | |||
| a4f0a08469 | |||
| 7f7b103945 | |||
| 8fddb599ef | |||
| 313d574a08 | |||
| f7d6ed00fd | |||
| 6c570acfe6 | |||
| 2ba836c284 | |||
| 5b32a402a6 | |||
| 10aaff4cc8 | |||
| 40025169d4 | |||
| 38a903469c | |||
| 70a0a41787 | |||
| 318f9e5564 | |||
| d73816285e | |||
| 86ac172cfd | |||
| 703c7e46fa | |||
| 747c2e4a0e | |||
| 7d89418874 | |||
| 64806b143b | |||
| 7b669a27a8 | |||
| 62c2080fee | |||
| f5d6d4ef55 | |||
| 4513ce8ac7 | |||
| 88eb890c46 | |||
| 9a66952f50 | |||
| d0db0abdd4 | |||
| d9cb2b7961 | |||
| 3eedc0b30e | |||
| b51ee12331 | |||
| 744e147a1a | |||
| 8cc0bffcae | |||
| 2b3cf5c095 | |||
| 94f15d66da | |||
| 8edd5ea901 | |||
| 1e23528258 | |||
| 9447a685f2 | |||
| 7155708d4b | |||
| a76b8798ae | |||
| 016b92e5aa | |||
| 6085879d6f | |||
| 42652d0fcb | |||
| 5e8917430b | |||
| cdcefe6fbc | |||
| 0678263f3e | |||
| aa69b8157e | |||
| 47ce23f4e8 | |||
| f6fe3fb2bd | |||
| fabc380784 | |||
| 7c845d0f9a | |||
| 50da3d7a68 | |||
| da1b268862 | |||
| 73e46b399b | |||
| 431062f123 | |||
| 3846932517 | |||
| aa10a58b60 | |||
| 9db6d2cb45 | |||
| 509dcac22e | |||
| f3102ee835 | |||
| 4c3056326b | |||
| 1339f51256 | |||
| 408c5d82e4 | |||
| 71a58814b4 | |||
| 870f2e93f4 | |||
| 842a2b1da6 | |||
| bf7de1e5d1 | |||
| ba0dd0e0fb | |||
| e0c7480dd2 | |||
| 9b324ca7cd | |||
| 0cb824076b | |||
| 02320ce9c8 | |||
| 6d82621a34 | |||
| 0dae857364 | |||
| 9a974365a7 | |||
| e9e57803ae | |||
| a3b7d11ae3 | |||
| 2248f49f9d | |||
| f96e73d7b9 | |||
| ece0e55f98 | |||
| db207f4995 | |||
| f94f8d3abe | |||
| 017eb92c6a | |||
| 765d526c16 | |||
| e71c6b14d0 | |||
| 210adad3f6 | |||
| 72b1d4ffcd | |||
| e2fc7178c4 | |||
| 58b360ed6c | |||
| 985f4dc19d | |||
| 757b9f724d | |||
| fbfc943301 | |||
| 4c70f0bc5f | |||
| ef27c020e0 | |||
| 23d9d88492 | |||
| fffbb9708a | |||
| e7ff1fd56c | |||
| 5b962eeb1d | |||
| 08581f24ea | |||
| 5e9141f03a | |||
| 2269fc7709 | |||
| b966366bc4 | |||
| e2b0695cb8 | |||
| 000547ae21 | |||
| 8cc19b67f9 | |||
| fcb6377b21 | |||
| 8a54b6fcbc | |||
| ecdc4b283d | |||
| d9ab3cfe47 | |||
| 72fc0c5431 | |||
| 044096c8d7 | |||
| 96b6be3398 | |||
| 42fb2e0d22 | |||
| 227eb3cf46 | |||
| 0e007afed3 | |||
| ddf023f078 | |||
| a9d1fe7baf | |||
| aeadca6f60 | |||
| 17233d8ded | |||
| 797783b59e | |||
| e5e76e2dcb | |||
| 0fc80958af | |||
| b43ab082b4 | |||
| fcf61605d6 | |||
| c5053638df | |||
| c5fbd3e528 | |||
| 117c33a2ea | |||
| 327de18f1a | |||
| 437904bcd8 | |||
| 6d75b4660e | |||
| a110019345 | |||
| da530a2211 | |||
| 51a512e568 | |||
| 5df6378e80 | |||
| a479dc718b | |||
| c39bb1d6d1 | |||
| 5c1ca6a895 | |||
| 24c1186061 | |||
| 8ae4aeb3e8 | |||
| 9daa701a28 | |||
| 336c2415b3 | |||
| 49731501d6 | |||
| 661d38404a | |||
| e869338703 | |||
| b55c1e3c8f | |||
| 3820f00182 | |||
| 178fb25a2f | |||
| 6b2d5aebf8 | |||
| 0d3bfda08c | |||
| 11f33236cf | |||
| db7288c709 | |||
| a31e51152b | |||
| 97b8632b74 | |||
| 2ebbe334bf | |||
| 7571a1cb80 | |||
| b4f794c275 | |||
| 2f1adffba3 | |||
| 0047e6e879 | |||
| e8453c76a7 | |||
| 5d7d0ffe5b | |||
| 39c9d83f1b | |||
| b38f7553a6 | |||
| 9b887a6b4c | |||
| b43db20bc4 | |||
| 8e38ceb13e | |||
| 18c3cf21c6 | |||
| a73564a24d | |||
| 9c6029d152 | |||
| 7a35413da5 | |||
| 660e714983 | |||
| 9edfcd5058 | |||
| cd8f785881 | |||
| 24895173d6 | |||
| 6a1c26ce70 | |||
| ba06fb60c2 | |||
| 3204eee7ac | |||
| 815b8696d2 | |||
| 2e80c19e0e | |||
| 170cc09627 | |||
| 8fd8de607f | |||
| ebdfd7f499 | |||
| 19669281d5 | |||
| ccbf545fe2 | |||
| 550ad57354 | |||
| dbb488ba52 | |||
| 49d2438ea0 | |||
| 92ba297bb7 | |||
| cad3d04d08 | |||
| f1e002998e | |||
| c42edc4efd | |||
| e5056c6ea1 | |||
| 56a4bd9e82 | |||
| 9449f4f0ec | |||
| 5111265019 | |||
| 750db2fe4d | |||
| 36f9fb48a8 | |||
| b857ab3eed | |||
| dfc2ea3a4e | |||
| dfc2ae4896 | |||
| 272b91609c | |||
| f47036b6df | |||
| bed9c9deac | |||
| 837c75f8f0 | |||
| dee3d2ff90 | |||
| 02b99b4622 | |||
| 37d73b6962 | |||
| 35c56538dd | |||
| 13b7553641 | |||
| bb117d4ee6 | |||
| b0b2741422 | |||
| ca83f0ec7b | |||
| 1ab5e57017 | |||
| a34f02db00 |
17
.claude/settings.local.json
Normal file
17
.claude/settings.local.json
Normal 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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
deployment/vps-TradonDev_Instructions.md
|
||||||
|
deployment/vps/46.202.173.47-credentials.md
|
||||||
107
AGENTS.md
Normal file
107
AGENTS.md
Normal 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 `"` et `'` 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
5
deployment/README.md
Normal 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`
|
||||||
57
deployment/runbooks/vps-onboarding.md
Normal file
57
deployment/runbooks/vps-onboarding.md
Normal 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.
|
||||||
39
deployment/vps/46.202.173.47-quickstart.md
Normal file
39
deployment/vps/46.202.173.47-quickstart.md
Normal 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`).
|
||||||
3
deployment/vps/vps-TradonDev.md
Normal file
3
deployment/vps/vps-TradonDev.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Serveur: 'VPS-62.72.36.116'
|
||||||
|
Alias Name: 'VPS DEV'
|
||||||
|
IP Address:'62.72.36.116'
|
||||||
@@ -93,6 +93,7 @@ class Model(
|
|||||||
cursor.execute(*ir_model.select(ir_model.id,
|
cursor.execute(*ir_model.select(ir_model.id,
|
||||||
where=ir_model.model == model.__name__))
|
where=ir_model.model == model.__name__))
|
||||||
model_id = None
|
model_id = None
|
||||||
|
logger.info("MODEL_NAME:%s",model.__name__)
|
||||||
if cursor.rowcount == -1 or cursor.rowcount is None:
|
if cursor.rowcount == -1 or cursor.rowcount is None:
|
||||||
data = cursor.fetchone()
|
data = cursor.fetchone()
|
||||||
if data:
|
if data:
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class Binary(Field):
|
|||||||
on_change_with=None, depends=None, context=None, loading='lazy',
|
on_change_with=None, depends=None, context=None, loading='lazy',
|
||||||
filename=None, file_id=None, store_prefix=None):
|
filename=None, file_id=None, store_prefix=None):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.file_id = file_id
|
self.file_id = None #file_id
|
||||||
self.store_prefix = store_prefix
|
self.store_prefix = None #store_prefix
|
||||||
super(Binary, self).__init__(string=string, help=help,
|
super(Binary, self).__init__(string=string, help=help,
|
||||||
required=required, readonly=readonly, domain=domain, states=states,
|
required=required, readonly=readonly, domain=domain, states=states,
|
||||||
on_change=on_change, on_change_with=on_change_with,
|
on_change=on_change, on_change_with=on_change_with,
|
||||||
|
|||||||
@@ -103,15 +103,26 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_name(cls):
|
def _get_name(cls):
|
||||||
'''
|
if cls.__doc__ is None:
|
||||||
Returns the first non-empty line of the model docstring.
|
print("\n💥 MODELE SANS DOCSTRING :", cls.__name__, " (module:", cls.__module__, ")")
|
||||||
'''
|
raise Exception("MODELE SANS DOCSTRING")
|
||||||
assert cls.__doc__, '%s has no docstring' % cls
|
|
||||||
lines = cls.__doc__.splitlines()
|
lines = cls.__doc__.splitlines()
|
||||||
for line in lines:
|
if lines:
|
||||||
line = line.strip()
|
return lines[0]
|
||||||
if line:
|
return cls.__name__
|
||||||
return line
|
|
||||||
|
# @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
|
@classmethod
|
||||||
def __register__(cls, module_name):
|
def __register__(cls, module_name):
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
|
|||||||
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
|
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
|
||||||
post_date = fields.Date('Post Date', readonly=True)
|
post_date = fields.Date('Post Date', readonly=True)
|
||||||
description = fields.Char('Description', states=_MOVE_STATES)
|
description = fields.Char('Description', states=_MOVE_STATES)
|
||||||
|
ext_ref = fields.Char('Ext. Ref')
|
||||||
origin = fields.Reference('Origin', selection='get_origin',
|
origin = fields.Reference('Origin', selection='get_origin',
|
||||||
states=_MOVE_STATES)
|
states=_MOVE_STATES)
|
||||||
state = fields.Selection([
|
state = fields.Selection([
|
||||||
@@ -921,6 +922,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
|
|||||||
fields.Reference("Move Origin", selection='get_move_origin'),
|
fields.Reference("Move Origin", selection='get_move_origin'),
|
||||||
'get_move_field', searcher='search_move_field')
|
'get_move_field', searcher='search_move_field')
|
||||||
description = fields.Char('Description', states=_states)
|
description = fields.Char('Description', states=_states)
|
||||||
|
ext_ref = fields.Char('Ext. Ref')
|
||||||
move_description_used = fields.Function(
|
move_description_used = fields.Function(
|
||||||
fields.Char("Move Description", states=_states),
|
fields.Char("Move Description", states=_states),
|
||||||
'get_move_field',
|
'get_move_field',
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="origin" colspan="3"/>
|
<field name="origin" colspan="3"/>
|
||||||
<label name="description_used"/>
|
<label name="description_used"/>
|
||||||
<field name="description_used" colspan="3"/>
|
<field name="description_used" colspan="3"/>
|
||||||
|
<label name="ext_ref"/>
|
||||||
|
<field name="ext_ref" colspan="3"/>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page name="lines">
|
<page name="lines">
|
||||||
<field name="lines" colspan="4"
|
<field name="lines" colspan="4"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="origin"/>
|
<field name="origin"/>
|
||||||
<label name="description_used"/>
|
<label name="description_used"/>
|
||||||
<field name="description_used" colspan="3"/>
|
<field name="description_used" colspan="3"/>
|
||||||
|
<label name="ext_ref"/>
|
||||||
|
<field name="ext_ref" colspan="3"/>
|
||||||
<notebook colspan="4">
|
<notebook colspan="4">
|
||||||
<page string="Other Info" id="info">
|
<page string="Other Info" id="info">
|
||||||
<label name="date"/>
|
<label name="date"/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<tree editable="1">
|
<tree editable="1">
|
||||||
<field name="move"/>
|
<field name="move"/>
|
||||||
<field name="account" expand="1"/>
|
<field name="account" expand="1"/>
|
||||||
|
<field name="ext_ref" expand="1" optional="1"/>
|
||||||
<field name="party" expand="1"/>
|
<field name="party" expand="1"/>
|
||||||
<field name="debit" sum="1"/>
|
<field name="debit" sum="1"/>
|
||||||
<field name="credit" sum="1"/>
|
<field name="credit" sum="1"/>
|
||||||
|
|||||||
@@ -1286,9 +1286,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
remainder = sum(l.debit - l.credit for l in move_lines)
|
remainder = sum(l.debit - l.credit for l in move_lines)
|
||||||
if self.payment_term:
|
if self.payment_term:
|
||||||
payment_date = self.payment_term_date or self.invoice_date or today
|
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
|
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
|
||||||
term_lines = self.payment_term.compute(
|
logger.info("MODEL:%s",model)
|
||||||
self.total_amount, self.currency, payment_date, purchase_line)
|
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:
|
else:
|
||||||
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
||||||
past_payment_term_dates = []
|
past_payment_term_dates = []
|
||||||
@@ -1960,14 +1965,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
if amount < 0:
|
if amount < 0:
|
||||||
move_line.debit = Decimal(0)
|
move_line.debit = Decimal(0)
|
||||||
move_line.credit = -amount
|
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_.credit = Decimal(0)
|
||||||
move_line_.debit = -amount
|
move_line_.debit = -amount
|
||||||
move_line_.account = gl.product.account_stock_in_used
|
move_line_.account = gl.product.account_stock_in_used
|
||||||
else:
|
else:
|
||||||
move_line.debit = amount
|
move_line.debit = amount
|
||||||
move_line.credit = Decimal(0)
|
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_.debit = Decimal(0)
|
||||||
move_line_.credit = amount
|
move_line_.credit = amount
|
||||||
move_line_.account = gl.product.account_stock_in_used
|
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])
|
var_qt = sum([i.quantity for i in gl])
|
||||||
logger.info("LOT_TO_PROCESS:%s",lot)
|
logger.info("LOT_TO_PROCESS:%s",lot)
|
||||||
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
|
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 = []
|
adjust_move_lines = []
|
||||||
mov = None
|
mov = None
|
||||||
if self.type == 'in':
|
if self.type == 'in':
|
||||||
@@ -3684,13 +3695,19 @@ class InvoiceReport(Report):
|
|||||||
Invoice = pool.get('account.invoice')
|
Invoice = pool.get('account.invoice')
|
||||||
# Re-instantiate because records are TranslateModel
|
# Re-instantiate because records are TranslateModel
|
||||||
invoice, = Invoice.browse(records)
|
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 (
|
return (
|
||||||
invoice.invoice_report_format,
|
invoice.invoice_report_format,
|
||||||
invoice.invoice_report_cache)
|
invoice.invoice_report_cache)
|
||||||
else:
|
else:
|
||||||
result = super()._execute(records, header, data, action)
|
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
|
format_, data = result
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = bytes(data, 'utf-8')
|
data = bytes(data, 'utf-8')
|
||||||
@@ -3707,6 +3724,12 @@ class InvoiceReport(Report):
|
|||||||
with Transaction().set_context(language=False):
|
with Transaction().set_context(language=False):
|
||||||
return super().render(*args, **kwargs)
|
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
|
@classmethod
|
||||||
def execute(cls, ids, data):
|
def execute(cls, ids, data):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
|
|||||||
@@ -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="wiz_name">account.invoice.refresh_invoice_report</field>
|
||||||
<field name="model">account.invoice</field>
|
<field name="model">account.invoice</field>
|
||||||
</record>
|
</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">
|
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
|
||||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||||
<field name="group" ref="account.group_account_admin"/>
|
<field name="group" ref="account.group_account_admin"/>
|
||||||
@@ -293,7 +288,7 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.action.report" id="report_invoice">
|
<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="model">account.invoice</field>
|
||||||
<field name="report_name">account.invoice</field>
|
<field name="report_name">account.invoice</field>
|
||||||
<field name="report">account_invoice/invoice.fodt</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"/>
|
<field name="action" ref="report_prepayment"/>
|
||||||
</record>
|
</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">
|
<record model="ir.sequence.type" id="sequence_type_account_invoice">
|
||||||
<field name="name">Invoice</field>
|
<field name="name">Invoice</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
4103
modules/account_invoice/invoice_ict.fodt
Normal file
4103
modules/account_invoice/invoice_ict.fodt
Normal file
File diff suppressed because it is too large
Load Diff
4097
modules/account_invoice/invoice_ict_final.fodt
Normal file
4097
modules/account_invoice/invoice_ict_final.fodt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,10 @@ from trytond.pool import Pool
|
|||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.wizard import Button, StateView, Wizard
|
from trytond.wizard import Button, StateView, Wizard
|
||||||
|
|
||||||
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
|
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
||||||
@@ -46,7 +48,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
|||||||
'.msg_payment_term_missing_last_remainder',
|
'.msg_payment_term_missing_last_remainder',
|
||||||
payment_term=term.rec_name))
|
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
|
"""Calculate payment terms and return a list of tuples
|
||||||
with (date, amount) for each payment term line.
|
with (date, amount) for each payment term line.
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
|||||||
remainder = amount
|
remainder = amount
|
||||||
for line in self.lines:
|
for line in self.lines:
|
||||||
value = line.get_value(remainder, amount, currency)
|
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:
|
if value is None or not value_date:
|
||||||
continue
|
continue
|
||||||
if ((remainder - value) * sign) < Decimal(0):
|
if ((remainder - value) * sign) < Decimal(0):
|
||||||
@@ -155,12 +157,11 @@ class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
self.ratio = self.round(1 / self.divisor,
|
self.ratio = self.round(1 / self.divisor,
|
||||||
self.__class__.ratio.digits[1])
|
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:
|
#find date based on trigger:
|
||||||
if purchase_line and self.trigger_event:
|
if line and self.trigger_event:
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
trigger_date = line.get_date(self.trigger_event)
|
||||||
purchase_line = PurchaseLine(purchase_line)
|
logger.info("DATE_FROM_LINE:%s",trigger_date)
|
||||||
trigger_date = purchase_line.get_date(self.trigger_event)
|
|
||||||
if trigger_date:
|
if trigger_date:
|
||||||
date = trigger_date
|
date = trigger_date
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="invoice_report_revisions" colspan="4"/>
|
<field name="invoice_report_revisions" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Rate management" id="rate">
|
<page string="Rate management" id="rate">
|
||||||
<label name="warning"/>
|
<!-- <label name="warning"/>
|
||||||
<field name="warning"/>
|
<field name="warning"/> -->
|
||||||
<newline/>
|
<newline/>
|
||||||
<label name="rate"/>
|
<label name="rate"/>
|
||||||
<field name="rate"/>
|
<field name="rate"/>
|
||||||
|
|||||||
14
modules/account_itsa/__init__.py
Normal file
14
modules/account_itsa/__init__.py
Normal 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')
|
||||||
40
modules/account_itsa/account.py
Normal file
40
modules/account_itsa/account.py
Normal 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
|
||||||
|
|
||||||
3336
modules/account_itsa/account_itsa.xml
Normal file
3336
modules/account_itsa/account_itsa.xml
Normal file
File diff suppressed because it is too large
Load Diff
10
modules/account_itsa/tryton.cfg
Normal file
10
modules/account_itsa/tryton.cfg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[tryton]
|
||||||
|
version=7.2.3
|
||||||
|
depends:
|
||||||
|
account
|
||||||
|
extras_depend:
|
||||||
|
account_invoice
|
||||||
|
xml:
|
||||||
|
account_itsa.xml
|
||||||
|
#tax_ict.xml
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from decimal import Decimal
|
|||||||
from trytond.i18n import gettext
|
from trytond.i18n import gettext
|
||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
|
from trytond.exceptions import UserWarning, UserError
|
||||||
from .exceptions import COGSWarning
|
from .exceptions import COGSWarning
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ class InvoiceLine(metaclass=PoolMeta):
|
|||||||
if move_line.second_currency:
|
if move_line.second_currency:
|
||||||
move_line.amount_second_currency = amount
|
move_line.amount_second_currency = amount
|
||||||
else:
|
else:
|
||||||
move_line.debit = Decimal(0)
|
move_line.debit = -amount_converted
|
||||||
move_line.credit = -amount_converted
|
move_line.credit = Decimal(0)
|
||||||
move_line.account = self.product.account_stock_out_used
|
move_line.account = self.product.account_stock_out_used
|
||||||
if move_line.second_currency:
|
if move_line.second_currency:
|
||||||
move_line.amount_second_currency = amount
|
move_line.amount_second_currency = amount
|
||||||
@@ -171,10 +171,28 @@ class InvoiceLine(metaclass=PoolMeta):
|
|||||||
cost = self.amount
|
cost = self.amount
|
||||||
else:
|
else:
|
||||||
cost = self.lot.get_cog()
|
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)
|
logger.info("GETMOVELINES_COST:%s",cost)
|
||||||
|
anglo_saxon_move_lines_ = []
|
||||||
with Transaction().set_context(
|
with Transaction().set_context(
|
||||||
company=self.invoice.company.id, date=accounting_date):
|
company=self.invoice.company.id, date=accounting_date):
|
||||||
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
|
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
|
||||||
cost, type_)
|
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)
|
||||||
|
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
|
return result
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ account_names = [
|
|||||||
class Category(metaclass=PoolMeta):
|
class Category(metaclass=PoolMeta):
|
||||||
__name__ = 'product.category'
|
__name__ = 'product.category'
|
||||||
account_stock = fields.MultiValue(fields.Many2One(
|
account_stock = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock",
|
'account.account', "Account Stock/Cost Income",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
# ('type.stock', '=', True),
|
||||||
('type.statement', '=', 'balance'),
|
# ('type.statement', '=', 'balance'),
|
||||||
('company', '=', Eval('context', {}).get('company', -1)),
|
('company', '=', Eval('context', {}).get('company', -1)),
|
||||||
],
|
],
|
||||||
states={
|
states={
|
||||||
@@ -29,7 +29,7 @@ class Category(metaclass=PoolMeta):
|
|||||||
| ~Eval('accounting', False)),
|
| ~Eval('accounting', False)),
|
||||||
}))
|
}))
|
||||||
account_stock_in = fields.MultiValue(fields.Many2One(
|
account_stock_in = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock IN",
|
'account.account', "Account Stock IN/Cost liability",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
('type.stock', '=', True),
|
||||||
@@ -41,7 +41,7 @@ class Category(metaclass=PoolMeta):
|
|||||||
| ~Eval('accounting', False)),
|
| ~Eval('accounting', False)),
|
||||||
}))
|
}))
|
||||||
account_stock_out = fields.MultiValue(fields.Many2One(
|
account_stock_out = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock OUT",
|
'account.account', "Account Stock OUT/Cost liability",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
('type.stock', '=', True),
|
||||||
@@ -103,8 +103,8 @@ class CategoryAccount(metaclass=PoolMeta):
|
|||||||
'account.account', "Account Stock",
|
'account.account', "Account Stock",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
# ('type.stock', '=', True),
|
||||||
('type.statement', '=', 'balance'),
|
# ('type.statement', '=', 'balance'),
|
||||||
('company', '=', Eval('company', -1)),
|
('company', '=', Eval('company', -1)),
|
||||||
])
|
])
|
||||||
account_stock_in = fields.Many2One(
|
account_stock_in = fields.Many2One(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
from . import automation,rules #, document
|
from . import automation,rules,freight_booking,cron #, document
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
automation.AutomationDocument,
|
automation.AutomationDocument,
|
||||||
rules.AutomationRuleSet,
|
rules.AutomationRuleSet,
|
||||||
|
freight_booking.FreightBookingInfo,
|
||||||
|
cron.Cron,
|
||||||
|
cron.AutomationCron,
|
||||||
module='automation', type_='model')
|
module='automation', type_='model')
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
from trytond.model import ModelSQL, ModelView, fields, Workflow
|
from trytond.model import ModelSQL, ModelView, fields, Workflow
|
||||||
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.wizard import Button
|
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 requests
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,6 +22,7 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
('invoice', 'Invoice'),
|
('invoice', 'Invoice'),
|
||||||
('statement_of_facts', 'Statement of Facts'),
|
('statement_of_facts', 'Statement of Facts'),
|
||||||
('weight_report', 'Weight Report'),
|
('weight_report', 'Weight Report'),
|
||||||
|
('controller', 'Controller'),
|
||||||
('bol', 'Bill of Lading'),
|
('bol', 'Bill of Lading'),
|
||||||
('controller_invoice', 'Controller Invoice'),
|
('controller_invoice', 'Controller Invoice'),
|
||||||
], 'Type')
|
], 'Type')
|
||||||
@@ -57,25 +63,53 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
def run_ocr(cls, docs):
|
def run_ocr(cls, docs):
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
try:
|
try:
|
||||||
# Décoder le fichier depuis le champ Binary
|
if doc.type == 'weight_report':
|
||||||
file_data = doc.document.data or b""
|
# Décoder le fichier depuis le champ Binary
|
||||||
logger.info(f"File size: {len(file_data)} bytes")
|
file_data = doc.document.data or b""
|
||||||
logger.info(f"First 20 bytes: {file_data[:20]}")
|
logger.info(f"File size: {len(file_data)} bytes")
|
||||||
logger.info(f"Last 20 bytes: {file_data[-20:]}")
|
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
|
# Envoyer le fichier au service OCR
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"http://automation-service:8006/ocr",
|
"http://automation-service:8006/ocr",
|
||||||
files={"file": (file_name, io.BytesIO(file_data))}
|
files={"file": (file_name, io.BytesIO(file_data))}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
logger.info("RUN_OCR_RESPONSE:%s",data)
|
logger.info("RUN_OCR_RESPONSE:%s",data)
|
||||||
doc.ocr_text = data.get("ocr_text", "")
|
doc.ocr_text = data.get("ocr_text", "")
|
||||||
doc.state = "ocr_done"
|
doc.state = "ocr_done"
|
||||||
doc.notes = (doc.notes or "") + "OCR done\n"
|
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:
|
except Exception as e:
|
||||||
doc.state = "error"
|
doc.state = "error"
|
||||||
@@ -154,7 +188,8 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
|
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"http://automation-service:8006/metadata",
|
#"http://automation-service:8006/metadata",
|
||||||
|
"http://automation-service:8006/parse",
|
||||||
json={"text": doc.ocr_text or ""}
|
json={"text": doc.ocr_text or ""}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -176,6 +211,18 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
logger.error("Metadata processing error: %s", e)
|
logger.error("Metadata processing error: %s", e)
|
||||||
|
|
||||||
doc.save()
|
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
|
# FULL PIPELINE
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
@@ -184,18 +231,66 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
def run_pipeline(cls, docs):
|
def run_pipeline(cls, docs):
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
try:
|
try:
|
||||||
if cls.rule_set.ocr_required:
|
logger.info("DATA_TYPE:%s",type(doc.metadata_json))
|
||||||
cls.run_ocr([doc])
|
metadata = json.loads(str(doc.metadata_json))
|
||||||
if cls.rule_set.structure_required and doc.state != "error":
|
logger.info("JSON STRUCTURE:%s",metadata)
|
||||||
cls.run_structure([doc])
|
|
||||||
if cls.rule_set.table_required and doc.state != "error":
|
WeightReport = Pool().get('weight.report')
|
||||||
cls.run_tables([doc])
|
wr = WeightReport.create_from_json(metadata)
|
||||||
if cls.rule_set.metadata_required and doc.state != "error":
|
|
||||||
cls.run_metadata([doc])
|
ShipmentIn = Pool().get('stock.shipment.in')
|
||||||
if doc.state != "error":
|
ShipmentWR = Pool().get('shipment.wr')
|
||||||
doc.state = "validated"
|
sh = ShipmentIn.search([('bl_number','ilike',wr.bl_no)])
|
||||||
doc.notes = (doc.notes or "") + "Pipeline completed\n"
|
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:
|
except Exception as e:
|
||||||
|
logger.exception("PIPELINE FAILED") # 👈 TRACE COMPLETE
|
||||||
doc.state = "error"
|
doc.state = "error"
|
||||||
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
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()
|
doc.save()
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<record model="ir.model.button" id="auto_button1">
|
<record model="ir.model.button" id="auto_button1">
|
||||||
<field name="model">automation.document</field>
|
<field name="model">automation.document</field>
|
||||||
<field name="name">run_pipeline</field>
|
<field name="name">run_pipeline</field>
|
||||||
<field name="string">Run Full Pipeline</field>
|
<field name="string">Create Weight Report</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.model.button" id="auto_button2">
|
<record model="ir.model.button" id="auto_button2">
|
||||||
<field name="model">automation.document</field>
|
<field name="model">automation.document</field>
|
||||||
|
|||||||
377
modules/automation/cron.py
Normal file
377
modules/automation/cron.py
Normal 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)
|
||||||
37
modules/automation/cron.xml
Normal file
37
modules/automation/cron.xml
Normal 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>
|
||||||
72
modules/automation/execution_automation.py
Normal file
72
modules/automation/execution_automation.py
Normal 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"
|
||||||
|
)
|
||||||
9
modules/automation/execution_automation.xml
Normal file
9
modules/automation/execution_automation.xml
Normal 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>
|
||||||
56
modules/automation/freight_booking.py
Normal file
56
modules/automation/freight_booking.py
Normal 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'),
|
||||||
|
]
|
||||||
25
modules/automation/freight_booking.xml
Normal file
25
modules/automation/freight_booking.xml
Normal 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>
|
||||||
@@ -5,4 +5,6 @@ depends:
|
|||||||
res
|
res
|
||||||
document_incoming
|
document_incoming
|
||||||
xml:
|
xml:
|
||||||
automation.xml
|
automation.xml
|
||||||
|
freight_booking.xml
|
||||||
|
cron.xml
|
||||||
60
modules/automation/view/execution_automation_tree.xml
Normal file
60
modules/automation/view/execution_automation_tree.xml
Normal 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>
|
||||||
13
modules/automation/view/freight_booking_info_tree.xml
Normal file
13
modules/automation/view/freight_booking_info_tree.xml
Normal 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>
|
||||||
@@ -27,6 +27,14 @@ class Sale(metaclass=PoolMeta):
|
|||||||
invoice.save()
|
invoice.save()
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_agent(self):
|
||||||
|
if self.agent:
|
||||||
|
return (self.agent.party.address_get(
|
||||||
|
type='delivery')).full_address
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
@Workflow.transition('quotation')
|
@Workflow.transition('quotation')
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class Company(ModelSQL, ModelView):
|
|||||||
help="Used to compute the today date.")
|
help="Used to compute the today date.")
|
||||||
employees = fields.One2Many('company.employee', 'company', 'Employees',
|
employees = fields.One2Many('company.employee', 'company', 'Employees',
|
||||||
help="Add employees to the company.")
|
help="Add employees to the company.")
|
||||||
|
|
||||||
|
logo = fields.Binary("Logo")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def header_used(self):
|
def header_used(self):
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="header"/>
|
<field name="header"/>
|
||||||
<separator name="footer"/>
|
<separator name="footer"/>
|
||||||
<field name="footer"/>
|
<field name="footer"/>
|
||||||
|
<separator name="logo"/>
|
||||||
|
<field name="logo" widget="image" stretch="true"/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ class Currency(
|
|||||||
closer = date
|
closer = date
|
||||||
return res
|
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
|
@staticmethod
|
||||||
def _get_rate(currencies, tdate=None):
|
def _get_rate(currencies, tdate=None):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from trytond.transaction import Transaction
|
|||||||
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||||
|
|
||||||
from .exceptions import DocumentIncomingSplitError
|
from .exceptions import DocumentIncomingSplitError
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if config.getboolean('document_incoming', 'filestore', default=True):
|
if config.getboolean('document_incoming', 'filestore', default=True):
|
||||||
file_id = 'file_id'
|
file_id = 'file_id'
|
||||||
@@ -179,30 +181,112 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
|||||||
def _split_mime_types(cls):
|
def _split_mime_types(cls):
|
||||||
return ['application/pdf']
|
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
|
@classmethod
|
||||||
def from_inbound_email(cls, email_, rule):
|
def from_inbound_email(cls, email_, rule):
|
||||||
message = email_.as_dict()
|
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'))
|
data = message.get('text', message.get('html'))
|
||||||
|
logger.info("DATA_FROM_INBOUND_MAIL:%s", data)
|
||||||
|
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode()
|
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(
|
document = cls(
|
||||||
active=active,
|
active=active,
|
||||||
name=message.get('subject', 'No Subject'),
|
name=subject,
|
||||||
company=rule.document_incoming_company,
|
company=rule.document_incoming_company,
|
||||||
data=data,
|
data=data,
|
||||||
type=rule.document_incoming_type if active else None,
|
type=rule.document_incoming_type if active else None,
|
||||||
source='inbound_email',
|
source='inbound_email',
|
||||||
)
|
)
|
||||||
|
|
||||||
children = []
|
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(
|
child = cls(
|
||||||
name=attachment['filename'] or 'data.bin',
|
name='mail_' + subject + '.txt',
|
||||||
company=rule.document_incoming_company,
|
company=rule.document_incoming_company,
|
||||||
data=attachment['data'],
|
data=body_bytes,
|
||||||
type=rule.document_incoming_type,
|
type=rule.document_incoming_type,
|
||||||
source='inbound_email')
|
source='inbound_email',
|
||||||
|
)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
|
|
||||||
document.children = children
|
document.children = children
|
||||||
document.save()
|
document.save()
|
||||||
return document
|
return document
|
||||||
@@ -265,7 +349,6 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
|||||||
default.setdefault('children')
|
default.setdefault('children')
|
||||||
return super().copy(documents, default=default)
|
return super().copy(documents, default=default)
|
||||||
|
|
||||||
|
|
||||||
def iter_pages(expression, size):
|
def iter_pages(expression, size):
|
||||||
ranges = set()
|
ranges = set()
|
||||||
for pages in expression.split(','):
|
for pages in expression.split(','):
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
from trytond.model import fields
|
from trytond.model import fields
|
||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Rule(metaclass=PoolMeta):
|
class Rule(metaclass=PoolMeta):
|
||||||
__name__ = 'inbound.email.rule'
|
__name__ = 'inbound.email.rule'
|
||||||
@@ -53,4 +54,4 @@ class Rule(metaclass=PoolMeta):
|
|||||||
if (self.action == 'document.incoming|from_inbound_email'
|
if (self.action == 'document.incoming|from_inbound_email'
|
||||||
and self.document_incoming_process):
|
and self.document_incoming_process):
|
||||||
document = email_.result
|
document = email_.result
|
||||||
DocumentIncoming.process([document], with_children=True)
|
DocumentIncoming.process([document], with_children=True)
|
||||||
15
modules/document_incoming_wr/__init__.py
Normal file
15
modules/document_incoming_wr/__init__.py
Normal 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')
|
||||||
63
modules/document_incoming_wr/document.py
Normal file
63
modules/document_incoming_wr/document.py
Normal 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
|
||||||
12
modules/document_incoming_wr/document.xml
Normal file
12
modules/document_incoming_wr/document.xml
Normal 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>
|
||||||
8
modules/document_incoming_wr/tryton.cfg
Normal file
8
modules/document_incoming_wr/tryton.cfg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[tryton]
|
||||||
|
version=7.2.0
|
||||||
|
depends:
|
||||||
|
document_incoming
|
||||||
|
ir
|
||||||
|
party
|
||||||
|
xml:
|
||||||
|
document.xml
|
||||||
@@ -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>
|
||||||
@@ -17,6 +17,9 @@ from trytond.pyson import Eval
|
|||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.url import http_host
|
from trytond.url import http_host
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if config.getboolean('inbound_email', 'filestore', default=True):
|
if config.getboolean('inbound_email', 'filestore', default=True):
|
||||||
file_id = 'data_id'
|
file_id = 'data_id'
|
||||||
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
|
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
|
||||||
@@ -74,6 +77,7 @@ class Inbox(ModelSQL, ModelView):
|
|||||||
assert email_.inbox == self
|
assert email_.inbox == self
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
if rule.match(email_.as_dict()):
|
if rule.match(email_.as_dict()):
|
||||||
|
logger.info("RULE_MATCHED:%s",rule)
|
||||||
email_.rule = rule
|
email_.rule = rule
|
||||||
rule.run(email_)
|
rule.run(email_)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ __all__ = ['IncotermMixin', 'IncotermAvailableMixin']
|
|||||||
class IncotermMixin(Model):
|
class IncotermMixin(Model):
|
||||||
|
|
||||||
incoterm = fields.Many2One(
|
incoterm = fields.Many2One(
|
||||||
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'),
|
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'), required=False,
|
||||||
ondelete='RESTRICT')
|
ondelete='RESTRICT')
|
||||||
incoterm_location = fields.Many2One(
|
incoterm_location = fields.Many2One(
|
||||||
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),
|
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),
|
||||||
|
|||||||
@@ -477,15 +477,16 @@ class Lot(ModelSQL, ModelView):
|
|||||||
else:
|
else:
|
||||||
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
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)
|
qt = Decimal(0)
|
||||||
gross_qt = Decimal(0)
|
gross_qt = Decimal(0)
|
||||||
if self.lot_state:
|
if self.lot_state:
|
||||||
if self.lot_hist:
|
if self.lot_hist:
|
||||||
if seq != 0:
|
if state_id != 0:
|
||||||
st = seq
|
st = state_id
|
||||||
else:
|
else:
|
||||||
st = self.lot_state.id
|
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]
|
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
|
||||||
qt = round(lot.quantity,5)
|
qt = round(lot.quantity,5)
|
||||||
gross_qt = round(lot.gross_quantity,5)
|
gross_qt = round(lot.gross_quantity,5)
|
||||||
@@ -499,24 +500,48 @@ class Lot(ModelSQL, ModelView):
|
|||||||
physic_sum = Decimal(0)
|
physic_sum = Decimal(0)
|
||||||
for l in line.lots:
|
for l in line.lots:
|
||||||
if l.lot_type == 'physic' :
|
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
|
return line.quantity_theorical - physic_sum
|
||||||
|
|
||||||
def get_current_quantity(self,name=None):
|
def get_current_quantity(self,name=None):
|
||||||
# if self.lot_type == 'physic':
|
# if self.lot_type == 'physic':
|
||||||
qt, gross_qt = self.get_hist_quantity(0)
|
qt, gross_qt = self.get_hist_quantity()
|
||||||
return qt
|
return qt
|
||||||
# else:
|
# else:
|
||||||
# return self.get_virtual_diff()
|
# 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')
|
Uom = Pool().get('product.uom')
|
||||||
unit = self.line.unit if self.line else self.sale_line.unit
|
if not unit:
|
||||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
|
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):
|
def get_current_gross_quantity(self,name=None):
|
||||||
if self.lot_type == 'physic':
|
if self.lot_type == 'physic':
|
||||||
qt, gross_qt = self.get_hist_quantity(0)
|
qt, gross_qt = self.get_hist_quantity()
|
||||||
return gross_qt
|
return gross_qt
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@@ -526,6 +551,7 @@ class Lot(ModelSQL, ModelView):
|
|||||||
lqh = LotQtHist()
|
lqh = LotQtHist()
|
||||||
lqh.quantity_type = qt_type
|
lqh.quantity_type = qt_type
|
||||||
lqh.quantity = net
|
lqh.quantity = net
|
||||||
|
logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
|
||||||
lqh.gross_quantity = gross
|
lqh.gross_quantity = gross
|
||||||
lqh.lot = self
|
lqh.lot = self
|
||||||
return lqh
|
return lqh
|
||||||
@@ -542,6 +568,7 @@ class Lot(ModelSQL, ModelView):
|
|||||||
if existing:
|
if existing:
|
||||||
hist = existing[0]
|
hist = existing[0]
|
||||||
hist.quantity = net
|
hist.quantity = net
|
||||||
|
logger.info("SET_CURRENT_HIST:%s",gross)
|
||||||
hist.gross_quantity = gross
|
hist.gross_quantity = gross
|
||||||
else:
|
else:
|
||||||
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
|
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))
|
weight = fields.Numeric('Weight', digits=(16,5))
|
||||||
|
|
||||||
class SplitWizardStart(ModelView):
|
class SplitWizardStart(ModelView):
|
||||||
|
"Split Line Start"
|
||||||
__name__ = 'lot.split.wizard.start'
|
__name__ = 'lot.split.wizard.start'
|
||||||
|
|
||||||
mode = fields.Selection([
|
mode = fields.Selection([
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Price(
|
|||||||
price_composite = fields.One2Many('price.composite','price',"Composites")
|
price_composite = fields.One2Many('price.composite','price',"Composites")
|
||||||
price_product = fields.One2Many('price.product', 'price', "Product")
|
price_product = fields.One2Many('price.product', 'price', "Product")
|
||||||
price_ct_size = fields.Numeric("Ct size")
|
price_ct_size = fields.Numeric("Ct size")
|
||||||
|
|
||||||
def get_qt(self,nb_ct,unit):
|
def get_qt(self,nb_ct,unit):
|
||||||
Uom = Pool().get('product.uom')
|
Uom = Pool().get('product.uom')
|
||||||
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
|
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):
|
def get_price(self,dt,unit,currency,last=False):
|
||||||
price = float(0)
|
price = float(0)
|
||||||
PV = Pool().get('price.price_value')
|
PV = Pool().get('price.price_value')
|
||||||
logger.info("ASKED_PRICE_FOR:%s",dt)
|
|
||||||
if self.price_values:
|
if self.price_values:
|
||||||
dt = dt.strftime("%Y-%m-%d")
|
dt = dt.strftime("%Y-%m-%d")
|
||||||
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
|
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")
|
dt = dt.strftime("%Y-%m-%d")
|
||||||
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
|
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
|
||||||
if cl:
|
if cl:
|
||||||
#logger.info("ISQUOTE:%s",cl)
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@@ -136,3 +134,20 @@ class Product(ModelSQL,ModelView):
|
|||||||
__name__ = 'price.product'
|
__name__ = 'price.product'
|
||||||
price = fields.Many2One('price.price',"Price index")
|
price = fields.Many2One('price.price',"Price index")
|
||||||
product = fields.Many2One('product.product',"Product")
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<form>
|
<form>
|
||||||
<label name="price"/>
|
|
||||||
<field name="price"/>
|
|
||||||
<label name="product"/>
|
<label name="product"/>
|
||||||
<field name="product"/>
|
<field name="product"/>
|
||||||
|
<label name="attributes"/>
|
||||||
|
<field name="attributes"/>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<tree>
|
<tree>
|
||||||
<field name="price"/>
|
<field name="price"/>
|
||||||
<field name="product"/>
|
<field name="product"/>
|
||||||
|
<field name="attributes"/>
|
||||||
</tree>
|
</tree>
|
||||||
|
|||||||
@@ -609,6 +609,26 @@ class Product(
|
|||||||
('template.code', operator, code_value, *extra),
|
('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
|
@staticmethod
|
||||||
def get_price_uom(products, name):
|
def get_price_uom(products, name):
|
||||||
Uom = Pool().get('product.uom')
|
Uom = Pool().get('product.uom')
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
|
|||||||
def default_digits():
|
def default_digits():
|
||||||
return 2
|
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')
|
@fields.depends('factor')
|
||||||
def on_change_factor(self):
|
def on_change_factor(self):
|
||||||
if (self.factor or 0.0) == 0.0:
|
if (self.factor or 0.0) == 0.0:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Month(ModelView, ModelSQL):
|
|||||||
is_cotation = fields.Boolean("Cotation month")
|
is_cotation = fields.Boolean("Cotation month")
|
||||||
beg_date = fields.Date("Date from")
|
beg_date = fields.Date("Date from")
|
||||||
end_date = fields.Date("Date end")
|
end_date = fields.Date("Date end")
|
||||||
|
description = fields.Char("Description")
|
||||||
|
|
||||||
class ProductMonth(ModelView, ModelSQL):
|
class ProductMonth(ModelView, ModelSQL):
|
||||||
"Product month"
|
"Product month"
|
||||||
|
|||||||
105
modules/purchase/AGENTS.md
Normal file
105
modules/purchase/AGENTS.md
Normal 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.
|
||||||
122
modules/purchase/docs/business-rules.template.md
Normal file
122
modules/purchase/docs/business-rules.template.md
Normal 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`
|
||||||
|
|
||||||
@@ -89,14 +89,14 @@ class Purchase(
|
|||||||
number = fields.Char("Number", readonly=True)
|
number = fields.Char("Number", readonly=True)
|
||||||
reference = fields.Char("Reference")
|
reference = fields.Char("Reference")
|
||||||
description = fields.Char('Description', size=None, states=_states)
|
description = fields.Char('Description', size=None, states=_states)
|
||||||
purchase_date = fields.Date('Purchase Date',
|
purchase_date = fields.Date('Purchase Date', required=True,
|
||||||
states={
|
states={
|
||||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||||
'required': ~Eval('state').in_(
|
'required': ~Eval('state').in_(
|
||||||
['draft', 'quotation', 'cancelled']),
|
['draft', 'quotation', 'cancelled']),
|
||||||
})
|
})
|
||||||
payment_term = fields.Many2One(
|
payment_term = fields.Many2One(
|
||||||
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
|
'account.invoice.payment_term', "Payment Term", required=True, ondelete='RESTRICT',
|
||||||
states={
|
states={
|
||||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||||
})
|
})
|
||||||
@@ -389,6 +389,11 @@ class Purchase(
|
|||||||
def default_state():
|
def default_state():
|
||||||
return 'draft'
|
return 'draft'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_purchase_date(cls):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
return Date.today()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_currency(cls, **pattern):
|
def default_currency(cls, **pattern):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
@@ -462,6 +467,8 @@ class Purchase(
|
|||||||
self.tol_min = self.party.tol_min
|
self.tol_min = self.party.tol_min
|
||||||
if self.party.tol_max:
|
if self.party.tol_max:
|
||||||
self.tol_max = 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:
|
if self.party.wb:
|
||||||
self.wb = self.party.wb
|
self.wb = self.party.wb
|
||||||
if self.party.association:
|
if self.party.association:
|
||||||
@@ -734,6 +741,7 @@ class Purchase(
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, purchases, default=None):
|
def copy(cls, purchases, default=None):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
if default is None:
|
if default is None:
|
||||||
default = {}
|
default = {}
|
||||||
else:
|
else:
|
||||||
@@ -742,7 +750,7 @@ class Purchase(
|
|||||||
default.setdefault('invoice_state', 'none')
|
default.setdefault('invoice_state', 'none')
|
||||||
default.setdefault('invoices_ignored', None)
|
default.setdefault('invoices_ignored', None)
|
||||||
default.setdefault('shipment_state', 'none')
|
default.setdefault('shipment_state', 'none')
|
||||||
default.setdefault('purchase_date', None)
|
default.setdefault('purchase_date', Date.today())
|
||||||
default.setdefault('quoted_by')
|
default.setdefault('quoted_by')
|
||||||
default.setdefault('confirmed_by')
|
default.setdefault('confirmed_by')
|
||||||
default.setdefault('untaxed_amount_cache')
|
default.setdefault('untaxed_amount_cache')
|
||||||
@@ -1021,9 +1029,15 @@ class Purchase(
|
|||||||
pool = Pool()
|
pool = Pool()
|
||||||
Invoice = pool.get('account.invoice')
|
Invoice = pool.get('account.invoice')
|
||||||
|
|
||||||
Invoice.save(invoices.values())
|
Invoice.save(invoices.values())
|
||||||
|
|
||||||
for purchase, invoice in invoices.items():
|
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)
|
purchase.copy_resources_to(invoice)
|
||||||
if len(invoices)==1:
|
if len(invoices)==1:
|
||||||
if prepayment:
|
if prepayment:
|
||||||
@@ -1215,7 +1229,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
()),
|
()),
|
||||||
If(Eval('type') != 'line',
|
If(Eval('type') != 'line',
|
||||||
('id', '=', None),
|
('id', '=', None),
|
||||||
()),
|
())
|
||||||
],
|
],
|
||||||
states={
|
states={
|
||||||
'invisible': Eval('type') != 'line',
|
'invisible': Eval('type') != 'line',
|
||||||
@@ -1685,7 +1699,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
|
|
||||||
@fields.depends(
|
@fields.depends(
|
||||||
'type', 'quantity', 'unit_price',
|
'type', 'quantity', 'unit_price',
|
||||||
'purchase', '_parent_purchase.currency')
|
'purchase', '_parent_purchase.currency','premium')
|
||||||
def on_change_with_amount(self):
|
def on_change_with_amount(self):
|
||||||
if (self.type == 'line'
|
if (self.type == 'line'
|
||||||
and self.quantity is not None
|
and self.quantity is not None
|
||||||
@@ -1857,77 +1871,93 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
else:
|
else:
|
||||||
lots_to_invoice = self.lots
|
lots_to_invoice = self.lots
|
||||||
for l in lots_to_invoice:
|
for l in lots_to_invoice:
|
||||||
#if l.lot_type == 'physic':
|
if l.lot_type == 'physic':
|
||||||
invoice_line = InvoiceLine()
|
invoice_line = InvoiceLine()
|
||||||
invoice_line.type = self.type
|
invoice_line.type = self.type
|
||||||
invoice_line.currency = self.currency
|
invoice_line.currency = self.currency
|
||||||
invoice_line.company = self.company
|
invoice_line.company = self.company
|
||||||
invoice_line.description = self.description
|
invoice_line.description = self.description
|
||||||
invoice_line.note = self.note
|
invoice_line.note = self.note
|
||||||
invoice_line.origin = self
|
invoice_line.origin = self
|
||||||
qt, gross_qt = l.get_hist_quantity(0)
|
qt, gross_qt = l.get_hist_quantity(0)
|
||||||
quantity = float(qt)
|
quantity = float(qt)
|
||||||
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
||||||
if self.unit:
|
if self.unit:
|
||||||
quantity = self.unit.round(quantity)
|
quantity = self.unit.round(quantity)
|
||||||
invoice_line.unit_price = l.get_lot_price()
|
invoice_line.unit_price = l.get_lot_price()
|
||||||
invoice_line.product = l.lot_product
|
invoice_line.product = l.lot_product
|
||||||
invoice_line.quantity = quantity
|
invoice_line.quantity = quantity
|
||||||
if not invoice_line.quantity:
|
logger.info("GETINVOICELINE_QT:%s",quantity)
|
||||||
return []
|
if not invoice_line.quantity:
|
||||||
invoice_line.unit = self.unit
|
return []
|
||||||
invoice_line.taxes = self.taxes
|
invoice_line.unit = self.unit
|
||||||
if self.company.purchase_taxes_expense:
|
invoice_line.taxes = self.taxes
|
||||||
invoice_line.taxes_deductible_rate = 0
|
if self.company.purchase_taxes_expense:
|
||||||
elif self.product:
|
invoice_line.taxes_deductible_rate = 0
|
||||||
invoice_line.taxes_deductible_rate = (
|
elif self.product:
|
||||||
self.product.supplier_taxes_deductible_rate_used)
|
invoice_line.taxes_deductible_rate = (
|
||||||
invoice_line.invoice_type = 'in'
|
self.product.supplier_taxes_deductible_rate_used)
|
||||||
if self.product:
|
invoice_line.invoice_type = 'in'
|
||||||
invoice_line.account = self.product.account_stock_in_used
|
if self.product:
|
||||||
if not invoice_line.account:
|
if self.product.type == 'service' and not self.product.landed_cost:
|
||||||
raise AccountError(
|
invoice_line.account = self.product.account_stock_in_used
|
||||||
gettext('purchase'
|
else:
|
||||||
'.msg_purchase_product_missing_account_expense',
|
invoice_line.account = self.product.account_stock_in_used
|
||||||
purchase=self.purchase.rec_name,
|
if not invoice_line.account:
|
||||||
product=self.product.rec_name))
|
raise AccountError(
|
||||||
else:
|
gettext('purchase'
|
||||||
invoice_line.account = account_config.get_multivalue(
|
'.msg_purchase_product_missing_account_expense',
|
||||||
'default_category_account_expense', company=self.company.id)
|
purchase=self.purchase.rec_name,
|
||||||
if not invoice_line.account:
|
product=self.product.rec_name))
|
||||||
raise AccountError(
|
else:
|
||||||
gettext('purchase'
|
invoice_line.account = account_config.get_multivalue(
|
||||||
'.msg_purchase_missing_account_expense',
|
'default_category_account_expense', company=self.company.id)
|
||||||
purchase=self.purchase.rec_name))
|
if not invoice_line.account:
|
||||||
if action == 'prov':
|
raise AccountError(
|
||||||
invoice_line.description = 'Pro forma'
|
gettext('purchase'
|
||||||
elif action == 'final':
|
'.msg_purchase_missing_account_expense',
|
||||||
invoice_line.description = 'Final'
|
purchase=self.purchase.rec_name))
|
||||||
elif action == 'service':
|
if action == 'prov':
|
||||||
invoice_line.description = 'Service'
|
invoice_line.description = 'Pro forma'
|
||||||
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
elif action == 'final':
|
||||||
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
invoice_line.description = 'Final'
|
||||||
invoice_line.lot = l.id
|
elif action == 'service':
|
||||||
if self.product.type == 'service':
|
invoice_line.description = 'Service'
|
||||||
invoice_line.unit_price = self.unit_price
|
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
||||||
invoice_line.product = self.product
|
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
||||||
invoice_line.stock_moves = []
|
invoice_line.lot = l.id
|
||||||
Fee = Pool().get('fee.fee')
|
if self.product.type == 'service':
|
||||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
invoice_line.unit_price = self.unit_price
|
||||||
if fee:
|
invoice_line.product = self.product
|
||||||
invoice_line.fee = fee[0]
|
invoice_line.stock_moves = []
|
||||||
lines.append(invoice_line)
|
Fee = Pool().get('fee.fee')
|
||||||
logger.info("GETINVLINE:%s",self.product.type)
|
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||||
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
|
if fee:
|
||||||
if l.invoice_line_prov and self.product.type != 'service':
|
invoice_line.fee = fee[0]
|
||||||
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
|
if fee[0].mode == 'lumpsum':
|
||||||
'invoice': None,
|
invoice_line.quantity = 1
|
||||||
'quantity': -l.invoice_line_prov.quantity,
|
elif fee[0].mode == 'ppack':
|
||||||
'unit_price': l.invoice_line_prov.unit_price,
|
invoice_line.quantity = fee[0].quantity
|
||||||
'party': l.invoice_line_prov.invoice.party,
|
else:
|
||||||
'origin': str(self),
|
state_id = 0
|
||||||
})
|
LotQtType = Pool().get('lot.qt.type')
|
||||||
lines.append(invoice_line_)
|
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
|
return lines
|
||||||
|
|
||||||
def _get_invoice_line_quantity(self):
|
def _get_invoice_line_quantity(self):
|
||||||
@@ -1990,8 +2020,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
'''
|
'''
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
Move = pool.get('stock.move')
|
Move = pool.get('stock.move')
|
||||||
InvoiceLine = pool.get('account.invoice.line')
|
Location = pool.get('stock.location')
|
||||||
Uom = pool.get('product.uom')
|
|
||||||
if self.type != 'line':
|
if self.type != 'line':
|
||||||
return
|
return
|
||||||
if not self.product:
|
if not self.product:
|
||||||
@@ -2047,26 +2076,28 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
to_location = self.purchase.to_location
|
to_location = self.purchase.to_location
|
||||||
|
|
||||||
move.from_location = from_location
|
move.from_location = from_location
|
||||||
|
logger.info("FROM_LOCATION:%s",self.purchase.from_location)
|
||||||
if to_location.type != 'customer':
|
logger.info("TO_LOCATION:%s",self.purchase.to_location)
|
||||||
move.to_location = 8
|
if to_location:
|
||||||
else:
|
if to_location.type != 'customer':
|
||||||
move.to_location = to_location
|
move.to_location = Location.get_transit_id()
|
||||||
|
else:
|
||||||
|
move.to_location = to_location
|
||||||
|
|
||||||
unit_price = l.get_lot_price()
|
unit_price = l.get_lot_price()
|
||||||
if l.invoice_line_prov != None :
|
# if l.invoice_line_prov != None :
|
||||||
prov_inv = InvoiceLine(l.invoice_line_prov)
|
# prov_inv = InvoiceLine(l.invoice_line_prov)
|
||||||
quantity -= prov_inv.quantity
|
# quantity -= prov_inv.quantity
|
||||||
if quantity < 0 :
|
# if quantity < 0 :
|
||||||
move.from_location = self.purchase.to_location
|
# move.from_location = self.purchase.to_location
|
||||||
move.to_location = 16
|
# move.to_location = 16
|
||||||
elif quantity > 0 :
|
# elif quantity > 0 :
|
||||||
move.from_location = 16
|
# move.from_location = 16
|
||||||
move.to_location = self.purchase.to_location
|
# move.to_location = self.purchase.to_location
|
||||||
quantity = abs(quantity)
|
# quantity = abs(quantity)
|
||||||
unit_price = prov_inv.unit_price
|
# unit_price = prov_inv.unit_price
|
||||||
if quantity == 0:
|
# if quantity == 0:
|
||||||
continue
|
# continue
|
||||||
move.quantity = quantity
|
move.quantity = quantity
|
||||||
move.unit = self.unit
|
move.unit = self.unit
|
||||||
move.product = l.lot_product
|
move.product = l.lot_product
|
||||||
@@ -2086,7 +2117,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
moves.append(move)
|
moves.append(move)
|
||||||
if move.to_location.type != 'customer':
|
if move.to_location.type != 'customer':
|
||||||
move_to, = Move.copy([move.id], default={
|
move_to, = Move.copy([move.id], default={
|
||||||
'from_location': 8,
|
'from_location': Location.get_transit_id(),
|
||||||
'to_location': to_location,
|
'to_location': to_location,
|
||||||
})
|
})
|
||||||
moves.append(move_to)
|
moves.append(move_to)
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="payment_term"/>
|
<field name="payment_term"/>
|
||||||
<label name="currency"/>
|
<label name="currency"/>
|
||||||
<field name="currency"/>
|
<field name="currency"/>
|
||||||
<newline/>
|
|
||||||
<label name="certif"/>
|
|
||||||
<field name="certif"/>
|
|
||||||
</group>
|
</group>
|
||||||
<group col="2" colspan="2" id="hd" yfill="1">
|
<group col="2" colspan="2" id="hd" yfill="1">
|
||||||
<field name="viewer" widget="html_viewer" height="300" width="600"/>
|
<field name="viewer" widget="html_viewer" height="300" width="600"/>
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<label name="product_supplier"/>
|
<label name="product_supplier"/>
|
||||||
<field name="product_supplier"/>
|
<field name="product_supplier"/>
|
||||||
<newline/>
|
<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:"/>
|
<label id="delivery_date" string="Delivery Date:"/>
|
||||||
<group id="delivery_date" col="-1">
|
<group id="delivery_date" col="-1">
|
||||||
<field name="delivery_date" xexpand="0"/>
|
<field name="delivery_date" xexpand="0"/>
|
||||||
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
|
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
|
||||||
</group>
|
</group>
|
||||||
<label name="del_period"/>
|
|
||||||
<field name="del_period"/>
|
|
||||||
<newline/>
|
<newline/>
|
||||||
<label name="from_del"/>
|
<label name="from_del"/>
|
||||||
<field 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"/>
|
<separator name="description" colspan="4"/>
|
||||||
<field name="description" colspan="4"/>
|
<field name="description" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Attributes" id="att">
|
||||||
|
<label name="attributes"/>
|
||||||
|
<field name="attributes"/>
|
||||||
|
</page>
|
||||||
<page string="Taxes" id="taxes">
|
<page string="Taxes" id="taxes">
|
||||||
<field name="taxes" colspan="4"/>
|
<field name="taxes" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
|
|||||||
@@ -3,7 +3,38 @@
|
|||||||
|
|
||||||
from trytond.pool import Pool
|
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():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
@@ -47,6 +78,9 @@ def register():
|
|||||||
dashboard.News,
|
dashboard.News,
|
||||||
dashboard.Demos,
|
dashboard.Demos,
|
||||||
party.Party,
|
party.Party,
|
||||||
|
party.PartyExecution,
|
||||||
|
party.PartyExecutionSla,
|
||||||
|
party.PartyExecutionPlace,
|
||||||
payment_term.PaymentTerm,
|
payment_term.PaymentTerm,
|
||||||
payment_term.PaymentTermLine,
|
payment_term.PaymentTermLine,
|
||||||
purchase.Purchase,
|
purchase.Purchase,
|
||||||
@@ -69,8 +103,11 @@ def register():
|
|||||||
fee.Fee,
|
fee.Fee,
|
||||||
fee.FeeLots,
|
fee.FeeLots,
|
||||||
purchase.FeeLots,
|
purchase.FeeLots,
|
||||||
fee.Valuation,
|
valuation.Valuation,
|
||||||
fee.ValuationDyn,
|
valuation.ValuationLine,
|
||||||
|
valuation.ValuationDyn,
|
||||||
|
valuation.ValuationReport,
|
||||||
|
valuation.ValuationReportContext,
|
||||||
derivative.Derivative,
|
derivative.Derivative,
|
||||||
derivative.DerivativeMatch,
|
derivative.DerivativeMatch,
|
||||||
derivative.MatchWizardStart,
|
derivative.MatchWizardStart,
|
||||||
@@ -82,9 +119,12 @@ def register():
|
|||||||
forex.PForex,
|
forex.PForex,
|
||||||
forex.ForexBI,
|
forex.ForexBI,
|
||||||
purchase.PnlBI,
|
purchase.PnlBI,
|
||||||
|
purchase.PositionBI,
|
||||||
stock.Move,
|
stock.Move,
|
||||||
|
stock.Location,
|
||||||
stock.InvoiceLine,
|
stock.InvoiceLine,
|
||||||
stock.ShipmentIn,
|
stock.ShipmentIn,
|
||||||
|
stock.ShipmentWR,
|
||||||
stock.ShipmentInternal,
|
stock.ShipmentInternal,
|
||||||
stock.ShipmentOut,
|
stock.ShipmentOut,
|
||||||
stock.StatementOfFacts,
|
stock.StatementOfFacts,
|
||||||
@@ -128,14 +168,41 @@ def register():
|
|||||||
purchase.ContractDocumentType,
|
purchase.ContractDocumentType,
|
||||||
purchase.DocTemplate,
|
purchase.DocTemplate,
|
||||||
purchase.DocTypeTemplate,
|
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')
|
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(
|
Pool.register(
|
||||||
forex.Forex,
|
forex.Forex,
|
||||||
forex.ForexCoverFees,
|
forex.ForexCoverFees,
|
||||||
forex.ForexCategory,
|
forex.ForexCategory,
|
||||||
pricing.Component,
|
pricing.Component,
|
||||||
pricing.Mtm,
|
pricing.Mtm,
|
||||||
|
pricing.MtmStrategy,
|
||||||
|
pricing.MtmScenario,
|
||||||
|
pricing.MtmSnapshot,
|
||||||
|
pricing.PriceMatrix,
|
||||||
|
pricing.PriceMatrixLine,
|
||||||
pricing.Estimated,
|
pricing.Estimated,
|
||||||
pricing.Pricing,
|
pricing.Pricing,
|
||||||
pricing.Period,
|
pricing.Period,
|
||||||
@@ -151,6 +218,9 @@ def register():
|
|||||||
sale.SaleCreatePurchaseInput,
|
sale.SaleCreatePurchaseInput,
|
||||||
sale.Derivative,
|
sale.Derivative,
|
||||||
sale.Valuation,
|
sale.Valuation,
|
||||||
|
sale.ValuationLine,
|
||||||
|
sale.ValuationDyn,
|
||||||
|
sale.ValuationReport,
|
||||||
sale.Fee,
|
sale.Fee,
|
||||||
sale.Lot,
|
sale.Lot,
|
||||||
sale.FeeLots,
|
sale.FeeLots,
|
||||||
@@ -161,8 +231,11 @@ def register():
|
|||||||
forex.SForex,
|
forex.SForex,
|
||||||
forex.ForexCoverPhysicalSale,
|
forex.ForexCoverPhysicalSale,
|
||||||
sale.ContractDocumentType,
|
sale.ContractDocumentType,
|
||||||
sale.Mtm,
|
sale.SaleStrategy,
|
||||||
sale.OpenPosition,
|
sale.OpenPosition,
|
||||||
|
sale.Backtoback,
|
||||||
|
sale.AnalyticDimensionAssignment,
|
||||||
|
sale.PriceComposition,
|
||||||
module='sale', type_='model')
|
module='sale', type_='model')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
lot.LotShipping,
|
lot.LotShipping,
|
||||||
@@ -188,6 +261,7 @@ def register():
|
|||||||
dashboard.DashboardLoader,
|
dashboard.DashboardLoader,
|
||||||
forex.ForexReport,
|
forex.ForexReport,
|
||||||
purchase.PnlReport,
|
purchase.PnlReport,
|
||||||
|
purchase.PositionReport,
|
||||||
derivative.DerivativeMatchWizard,
|
derivative.DerivativeMatchWizard,
|
||||||
module='purchase', type_='wizard')
|
module='purchase', type_='wizard')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
|
|||||||
30
modules/purchase_trade/account.py
Normal file
30
modules/purchase_trade/account.py
Normal 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
|
||||||
69
modules/purchase_trade/account.xml
Normal file
69
modules/purchase_trade/account.xml
Normal 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>
|
||||||
22
modules/purchase_trade/backtoback.py
Normal file
22
modules/purchase_trade/backtoback.py
Normal 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")
|
||||||
|
|
||||||
65
modules/purchase_trade/backtoback.xml
Normal file
65
modules/purchase_trade/backtoback.xml
Normal 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>
|
||||||
@@ -155,7 +155,7 @@ class Party(metaclass=PoolMeta):
|
|||||||
if overdue > 0:
|
if overdue > 0:
|
||||||
# scale by overdue relative to limit
|
# scale by overdue relative to limit
|
||||||
limit = self.credit_limit or 1
|
limit = self.credit_limit or 1
|
||||||
score += int(min(40, (overdue / float(limit)) * 100))
|
score += int(min(40, (float(overdue) / float(limit)) * 100))
|
||||||
|
|
||||||
# cap
|
# cap
|
||||||
if score > 100:
|
if score > 100:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
# 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.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
@@ -193,7 +193,7 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
|
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
|
||||||
logger.info("EXITONCHANGE",self.chatbot)
|
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.
|
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
|
||||||
"""
|
"""
|
||||||
@@ -208,29 +208,93 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
rates = CurrencyRate.search(
|
rates = CurrencyRate.search(
|
||||||
[('currency', '=', to_currency.id)],
|
[('currency', '=', to_currency.id)],
|
||||||
order=[('date', 'DESC')],
|
order=[('date', 'DESC')],
|
||||||
limit=2,
|
limit=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not rates:
|
if not rates:
|
||||||
return None, None
|
return None, None, None, None, None
|
||||||
|
|
||||||
# Calcul du taux EUR/USD
|
# Calcul du taux EUR/USD
|
||||||
# Si la devise principale de la société est EUR, et que le taux stocké est
|
# 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
|
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
|
||||||
last_rate = rates[0].rate
|
f1 = rates[0].rate
|
||||||
prev_rate = rates[1].rate if len(rates) > 1 else None
|
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:
|
# if from_currency != to_currency:
|
||||||
# last_rate = 1 / last_rate if last_rate else None
|
# last_rate = 1 / last_rate if last_rate else None
|
||||||
# prev_rate = 1 / prev_rate if prev_rate else None
|
# prev_rate = 1 / prev_rate if prev_rate else None
|
||||||
|
|
||||||
if last_rate and prev_rate:
|
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
|
||||||
return round(1/last_rate,6), round(1/prev_rate,6)
|
|
||||||
|
|
||||||
def get_tremor(self,name):
|
def get_tremor(self,name):
|
||||||
Pnl = Pool().get('valuation.valuation')
|
Date = Pool().get('ir.date')
|
||||||
pnls = Pnl.search(['id','>',0])
|
Configuration = Pool().get('gr.configuration')
|
||||||
pnl_amount = "{:,.0f}".format(round(sum([e.amount for e in pnls]),0))
|
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')
|
Open = Pool().get('open.position')
|
||||||
opens = Open.search(['id','>',0])
|
opens = Open.search(['id','>',0])
|
||||||
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),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)
|
val_s = len(val)
|
||||||
conf = Sale.search(['state','=','confirmed'])
|
conf = Sale.search(['state','=','confirmed'])
|
||||||
conf_s = len(conf)
|
conf_s = len(conf)
|
||||||
Shipment = Pool().get('stock.shipment.in')
|
|
||||||
draft = Shipment.search(['state','=','draft'])
|
draft = Shipment.search(['state','=','draft'])
|
||||||
shipment_d = len(draft)
|
shipment_d = len(draft)
|
||||||
val = Purchase.search(['state','=','started'])
|
val = Shipment.search(['state','=','started'])
|
||||||
shipment_s = len(val)
|
shipment_s = len(val)
|
||||||
conf = Purchase.search(['state','=','received'])
|
conf = Shipment.search(['state','=','received'])
|
||||||
shipment_r = len(conf)
|
shipment_r = len(conf)
|
||||||
Lot = Pool().get('lot.lot')
|
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)
|
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)
|
lot_a = len(val)
|
||||||
conf = Lot.search(['lot_type','=','physic'])
|
|
||||||
lot_al = len(conf)
|
|
||||||
Invoice = Pool().get('account.invoice')
|
Invoice = Pool().get('account.invoice')
|
||||||
invs = Invoice.search(['type','=','in'])
|
invs = Invoice.search(['type','=','in'])
|
||||||
inv_p = len(invs)
|
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'])
|
invs = Invoice.search(['type','=','out'])
|
||||||
inv_s = len(invs)
|
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')
|
AccountMove = Pool().get('account.move')
|
||||||
accs = AccountMove.search(['id','>',0])
|
accs = AccountMove.search([('journal','=',3),('state','!=','posted')])
|
||||||
move_cash = len(accs)
|
pay_val = len(accs)
|
||||||
|
accs = AccountMove.search([('journal','=',3),('state','=','posted')])
|
||||||
|
pay_posted = len(accs)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"https://srv413259.hstgr.cloud/dashboard/index.html?pnl_amount="
|
config.dashboard +
|
||||||
|
"/dashboard/index.html?pnl_amount="
|
||||||
+ str(pnl_amount)
|
+ str(pnl_amount)
|
||||||
|
+ "&pnl_variation="
|
||||||
|
+ str(pnl_variation)
|
||||||
+ "&exposure="
|
+ "&exposure="
|
||||||
+ str(exposure)
|
+ str(exposure)
|
||||||
+ "&topay="
|
+ "&topay="
|
||||||
+ str(topay)
|
+ str(topay)
|
||||||
+ "&toreceive="
|
+ "&toreceive="
|
||||||
+ str(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="
|
+ "&draft_p="
|
||||||
+ str(draft_p)
|
+ str(draft_p)
|
||||||
+ "&val_p="
|
+ "&val_p="
|
||||||
@@ -312,14 +407,22 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
+ str(lot_m)
|
+ str(lot_m)
|
||||||
+ "&lot_a="
|
+ "&lot_a="
|
||||||
+ str(lot_a)
|
+ str(lot_a)
|
||||||
+ "&lot_al="
|
|
||||||
+ str(lot_al)
|
|
||||||
+ "&inv_p="
|
+ "&inv_p="
|
||||||
+ str(inv_p)
|
+ str(inv_p)
|
||||||
|
+ "&inv_p_p="
|
||||||
|
+ str(inv_p_p)
|
||||||
|
+ "&inv_p_np="
|
||||||
|
+ str(inv_p_np)
|
||||||
+ "&inv_s="
|
+ "&inv_s="
|
||||||
+ str(inv_s)
|
+ str(inv_s)
|
||||||
+ "&move_cash="
|
+ "&inv_s_p="
|
||||||
+ str(move_cash)
|
+ 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')
|
News = Pool().get('news.news')
|
||||||
Date = Pool().get('ir.date')
|
Date = Pool().get('ir.date')
|
||||||
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
|
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:
|
if last_rate and prev_rate:
|
||||||
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
|
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
|
||||||
direction = "📈" if variation > 0 else "📉"
|
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>'
|
' <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:
|
for n in demos:
|
||||||
icon = n.icon or "📰"
|
icon = n.icon or "📰"
|
||||||
category = n.category or "General"
|
category = n.category or "General"
|
||||||
@@ -499,12 +602,14 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
return pu
|
return pu
|
||||||
|
|
||||||
def gen_url(self,name=None):
|
def gen_url(self,name=None):
|
||||||
|
Configuration = Pool().get('gr.configuration')
|
||||||
|
config = Configuration.search(['id','>',0])[0]
|
||||||
payload = {
|
payload = {
|
||||||
"resource": {"dashboard": self.bi_id},
|
"resource": {"dashboard": self.bi_id},
|
||||||
"params": {},
|
"params": {},
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
"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)
|
logger.info("TOKEN:%s",token)
|
||||||
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
|
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_quantity = l.lot_qt
|
||||||
l.lot_gross_quantity = l.lot_qt
|
l.lot_gross_quantity = l.lot_qt
|
||||||
l.lot_premium = Decimal(0)
|
l.lot_premium = Decimal(0)
|
||||||
|
l.lot_chunk_key = None
|
||||||
lot_id = LotQt.add_physical_lots(lqt,[l])
|
lot_id = LotQt.add_physical_lots(lqt,[l])
|
||||||
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
|
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
|
||||||
Dashboard.save([d])
|
Dashboard.save([d])
|
||||||
|
|||||||
79
modules/purchase_trade/dimension.py
Normal file
79
modules/purchase_trade/dimension.py
Normal 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'
|
||||||
|
)
|
||||||
34
modules/purchase_trade/dimension.xml
Normal file
34
modules/purchase_trade/dimension.xml
Normal 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>
|
||||||
157
modules/purchase_trade/docs/business-rules.md
Normal file
157
modules/purchase_trade/docs/business-rules.md
Normal 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
|
||||||
149
modules/purchase_trade/docs/template-rules.md
Normal file
149
modules/purchase_trade/docs/template-rules.md
Normal 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:
|
||||||
|
- `"..."` pour les guillemets doubles
|
||||||
|
- `'...'` pour les apostrophes
|
||||||
|
- Eviter les formes avec antislashs:
|
||||||
|
- interdit: `\'\'`
|
||||||
|
- interdit: `\'value\'`
|
||||||
|
- Exemples corrects:
|
||||||
|
- `<replace text:p="set_lang(invoice.party.lang)">`
|
||||||
|
- `<if test="invoice.report_payment_description">`
|
||||||
|
- `<tax.description or ''>`
|
||||||
|
|
||||||
|
### 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 `"` / `'`.
|
||||||
|
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`
|
||||||
@@ -6,7 +6,7 @@ from trytond.pyson import Bool, Eval, Id, If
|
|||||||
from trytond.model import (ModelSQL, ModelView)
|
from trytond.model import (ModelSQL, ModelView)
|
||||||
from trytond.tools import is_full_text, lstrip_wildcard
|
from trytond.tools import is_full_text, lstrip_wildcard
|
||||||
from trytond.transaction import Transaction, inactive_records
|
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.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||||
from sql.conditionals import Case
|
from sql.conditionals import Case
|
||||||
from sql import Column, Literal
|
from sql import Column, Literal
|
||||||
@@ -18,120 +18,11 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from trytond.exceptions import UserWarning, UserError
|
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__)
|
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_state(state):
|
||||||
def filter(func):
|
def filter(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -166,16 +57,28 @@ class Fee(ModelSQL,ModelView):
|
|||||||
('lumpsum', 'Lump sum'),
|
('lumpsum', 'Lump sum'),
|
||||||
('perqt', 'Per qt'),
|
('perqt', 'Per qt'),
|
||||||
('pprice', '% price'),
|
('pprice', '% price'),
|
||||||
|
('rate', '% rate'),
|
||||||
('pcost', '% cost price'),
|
('pcost', '% cost price'),
|
||||||
|
('ppack', 'Per packing'),
|
||||||
], 'Mode', required=True)
|
], 'Mode', required=True)
|
||||||
inherit_qt = fields.Boolean("Inh Qt")
|
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
|
||||||
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
|
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
||||||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
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={
|
inherit_shipment = fields.Boolean("Inh Sh",states={
|
||||||
'invisible': (Eval('shipment_in')),
|
'invisible': (Eval('shipment_in')),
|
||||||
})
|
})
|
||||||
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
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')
|
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')
|
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))] )
|
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([
|
weight_type = fields.Selection([
|
||||||
('net', 'Net'),
|
('net', 'Net'),
|
||||||
('brut', 'Brut'),
|
('brut', 'Gross'),
|
||||||
], string='W. type')
|
], 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):
|
def get_lots(self, name):
|
||||||
logger.info("GET_LOTS_LINE:%s",self.line)
|
logger.info("GET_LOTS_LINE:%s",self.line)
|
||||||
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
||||||
@@ -230,6 +218,23 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if ml:
|
if ml:
|
||||||
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
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
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super().__setup__()
|
super().__setup__()
|
||||||
@@ -248,13 +253,14 @@ class Fee(ModelSQL,ModelView):
|
|||||||
def default_p_r(cls):
|
def default_p_r(cls):
|
||||||
return 'pay'
|
return 'pay'
|
||||||
|
|
||||||
def get_unit(self, name):
|
def get_unit(self, name=None):
|
||||||
Lot = Pool().get('lot.lot')
|
FeeLots = Pool().get('fee.lots')
|
||||||
if self.lots:
|
fl = FeeLots.search(['fee','=',self.id])
|
||||||
if self.lots[0].line:
|
if fl:
|
||||||
return self.lots[0].line.unit
|
if fl[0].lot.line:
|
||||||
if self.lots[0].sale_line:
|
return fl[0].lot.line.unit
|
||||||
return self.lots[0].sale_line.unit
|
if fl[0].lot.sale_line:
|
||||||
|
return fl[0].lot.sale_line.unit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
@@ -285,7 +291,11 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return round(self.price / self.quantity,4)
|
return round(self.price / self.quantity,4)
|
||||||
elif self.mode == 'perqt':
|
elif self.mode == 'perqt':
|
||||||
return self.price
|
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:
|
if self.line and self.price:
|
||||||
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
||||||
if self.sale_line and self.price:
|
if self.sale_line and self.price:
|
||||||
@@ -305,8 +315,8 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
def get_landed_status(self,name):
|
def get_landed_status(self,name):
|
||||||
if self.product:
|
if self.product:
|
||||||
return self.product.landed_cost
|
return self.product.template.landed_cost
|
||||||
|
|
||||||
def get_quantity(self,name=None):
|
def get_quantity(self,name=None):
|
||||||
qt = self.get_fee_lots_qt()
|
qt = self.get_fee_lots_qt()
|
||||||
if qt:
|
if qt:
|
||||||
@@ -317,6 +327,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return Decimal(lqts[0].lot_quantity)
|
return Decimal(lqts[0].lot_quantity)
|
||||||
|
|
||||||
def get_amount(self,name=None):
|
def get_amount(self,name=None):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
sign = Decimal(1)
|
sign = Decimal(1)
|
||||||
if self.price:
|
if self.price:
|
||||||
# if self.p_r:
|
# if self.p_r:
|
||||||
@@ -324,13 +335,54 @@ class Fee(ModelSQL,ModelView):
|
|||||||
# sign = -1
|
# sign = -1
|
||||||
if self.mode == 'lumpsum':
|
if self.mode == 'lumpsum':
|
||||||
return self.price * sign
|
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':
|
elif self.mode == 'perqt':
|
||||||
if self.shipment_in:
|
if self.shipment_in:
|
||||||
StockMove = Pool().get('stock.move')
|
StockMove = Pool().get('stock.move')
|
||||||
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
||||||
if sm:
|
if sm:
|
||||||
unique_lots = {e.lot for e in sm if e.lot}
|
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')
|
LotQt = Pool().get('lot.qt')
|
||||||
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
||||||
if lqts:
|
if lqts:
|
||||||
@@ -373,12 +425,12 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
return super().copy(fees, default=default)
|
return super().copy(fees, default=default)
|
||||||
|
|
||||||
def get_fee_lots_qt(self):
|
def get_fee_lots_qt(self,state_id=0):
|
||||||
qt = Decimal(0)
|
qt = Decimal(0)
|
||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||||
if fee_lots:
|
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)
|
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||||
return qt
|
return qt
|
||||||
|
|
||||||
@@ -388,10 +440,18 @@ class Fee(ModelSQL,ModelView):
|
|||||||
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
||||||
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
|
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
|
||||||
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
||||||
if self.price != self.purchase.lines[0].unit_price:
|
if self.mode == 'lumpsum':
|
||||||
self.purchase.lines[0].unit_price = self.price
|
if self.amount != self.purchase.lines[0].unit_price:
|
||||||
if self.quantity != self.purchase.lines[0].quantity:
|
self.purchase.lines[0].unit_price = self.amount
|
||||||
self.purchase.lines[0].quantity = self.quantity
|
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:
|
if self.product != self.purchase.lines[0].product:
|
||||||
self.purchase.lines[0].product = self.product
|
self.purchase.lines[0].product = self.product
|
||||||
PurchaseLine.save([self.purchase.lines[0]])
|
PurchaseLine.save([self.purchase.lines[0]])
|
||||||
@@ -408,46 +468,47 @@ class Fee(ModelSQL,ModelView):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, vlist):
|
def create(cls, vlist):
|
||||||
vlist = [x.copy() for x in 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_sh = Decimal(0)
|
||||||
qt_line = Decimal(0)
|
qt_line = Decimal(0)
|
||||||
unit = None
|
unit = None
|
||||||
for record in records:
|
for fee in fees:
|
||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
Lots = Pool().get('lot.lot')
|
Lots = Pool().get('lot.lot')
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
if record.line:
|
if fee.line:
|
||||||
for l in record.line.lots:
|
for l in fee.line.lots:
|
||||||
#if l.lot_type == 'physic':
|
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 = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
fl.line = l.line.id
|
fl.line = l.line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
qt_line += l.get_current_quantity_converted()
|
qt_line += l.get_current_quantity_converted()
|
||||||
unit = l.line.unit
|
unit = l.line.unit
|
||||||
if record.sale_line:
|
if fee.sale_line:
|
||||||
for l in record.sale_line.lots:
|
for l in fee.sale_line.lots:
|
||||||
#if l.lot_type == 'physic':
|
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 = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
fl.sale_line = l.sale_line.id
|
fl.sale_line = l.sale_line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
if record.shipment_in:
|
qt_line += l.get_current_quantity_converted()
|
||||||
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
|
unit = l.sale_line.unit
|
||||||
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
|
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:
|
if lots:
|
||||||
for l in lots:
|
for l in lots:
|
||||||
#if l.lot_type == 'physic':
|
|
||||||
fl = FeeLots()
|
fl = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
qt_sh += l.get_current_quantity_converted()
|
qt_sh += l.get_current_quantity_converted()
|
||||||
unit = l.line.unit
|
unit = l.line.unit
|
||||||
else:
|
else:
|
||||||
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
|
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||||
if lqts:
|
if lqts:
|
||||||
for l in lqts:
|
for l in lqts:
|
||||||
qt_sh += l.lot_p.get_current_quantity_converted()
|
qt_sh += l.lot_p.get_current_quantity_converted()
|
||||||
@@ -455,32 +516,154 @@ class Fee(ModelSQL,ModelView):
|
|||||||
else:
|
else:
|
||||||
raise UserError("You cannot add fee on received shipment!")
|
raise UserError("You cannot add fee on received shipment!")
|
||||||
|
|
||||||
type = record.type
|
type = fee.type
|
||||||
if type == 'ordered':
|
if type == 'ordered':
|
||||||
Purchase = Pool().get('purchase.purchase')
|
Purchase = Pool().get('purchase.purchase')
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
PurchaseLine = Pool().get('purchase.line')
|
||||||
pl = PurchaseLine()
|
pl = PurchaseLine()
|
||||||
pl.product = record.product
|
pl.product = fee.product
|
||||||
if record.line:
|
if fee.line or fee.sale_line:
|
||||||
pl.quantity = round(qt_line,5)
|
pl.quantity = round(qt_line,5)
|
||||||
if record.shipment_in:
|
if fee.shipment_in:
|
||||||
pl.quantity = round(qt_sh,5)
|
pl.quantity = round(qt_sh,5)
|
||||||
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
|
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
|
||||||
pl.unit = unit
|
pl.unit = unit
|
||||||
pl.fee_ = record.id
|
pl.fee_ = fee.id
|
||||||
if record.price:
|
if fee.price:
|
||||||
pl.unit_price = round(Decimal(record.price),4)
|
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 = Purchase()
|
||||||
p.lines = [pl]
|
p.lines = [pl]
|
||||||
p.party = record.supplier
|
p.party = fee.supplier
|
||||||
if p.party.addresses:
|
if p.party.addresses:
|
||||||
p.invoice_address = p.party.addresses[0]
|
p.invoice_address = p.party.addresses[0]
|
||||||
p.currency = record.currency
|
p.currency = fee.currency
|
||||||
p.line_type = 'service'
|
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])
|
Purchase.save([p])
|
||||||
|
#if reception of moves done we need to generate accrual for fee
|
||||||
return records
|
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):
|
class FeeLots(ModelSQL,ModelView):
|
||||||
|
|
||||||
"Fee lots"
|
"Fee lots"
|
||||||
|
|||||||
@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="name">fee_tree_sequence</field>
|
<field name="name">fee_tree_sequence</field>
|
||||||
</record>
|
</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">
|
<record model="ir.ui.view" id="fee_view_tree_sequence2">
|
||||||
<field name="model">fee.fee</field>
|
<field name="model">fee.fee</field>
|
||||||
<field name="type">tree</field>
|
<field name="type">tree</field>
|
||||||
|
|||||||
141
modules/purchase_trade/finance_tools.py
Normal file
141
modules/purchase_trade/finance_tools.py
Normal 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")
|
||||||
259
modules/purchase_trade/financing_tools.py
Normal file
259
modules/purchase_trade/financing_tools.py
Normal 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
|
||||||
|
}
|
||||||
@@ -385,11 +385,11 @@ class ForexBI(ModelSingleton,ModelSQL, ModelView):
|
|||||||
config = Configuration.search(['id','>',0])[0]
|
config = Configuration.search(['id','>',0])[0]
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"resource": {"dashboard": 3},
|
"resource": {"dashboard": config.forex_id},
|
||||||
"params": {},
|
"params": {},
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
"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)
|
logger.info("TOKEN:%s",token)
|
||||||
if config.dark:
|
if config.dark:
|
||||||
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
||||||
|
|||||||
@@ -12,4 +12,10 @@ class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
|
|||||||
__name__ = 'gr.configuration'
|
__name__ = 'gr.configuration'
|
||||||
|
|
||||||
bi = fields.Char("BI connexion")
|
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")
|
||||||
14
modules/purchase_trade/icons/tradon-btb.svg
Normal file
14
modules/purchase_trade/icons/tradon-btb.svg
Normal 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 |
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal file
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal 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 |
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal file
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal 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 |
430
modules/purchase_trade/invoice.py
Normal file
430
modules/purchase_trade/invoice.py
Normal 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)
|
||||||
@@ -20,6 +20,7 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from trytond.exceptions import UserWarning, UserError
|
from trytond.exceptions import UserWarning, UserError
|
||||||
|
from trytond.modules.purchase_trade.service import ContractFactory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ class LotMove(ModelSQL,ModelView):
|
|||||||
class Lot(metaclass=PoolMeta):
|
class Lot(metaclass=PoolMeta):
|
||||||
__name__ = 'lot.lot'
|
__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')
|
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
||||||
lot_move = fields.One2Many('lot.move','lot',"Move")
|
lot_move = fields.One2Many('lot.move','lot',"Move")
|
||||||
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
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_pr = fields.Numeric("Delta Pr")
|
||||||
delta_amt = fields.Numeric("Delta Amt")
|
delta_amt = fields.Numeric("Delta Amt")
|
||||||
warrant_nb = fields.Char("Warrant Nb")
|
warrant_nb = fields.Char("Warrant Nb")
|
||||||
|
lot_chunk_key = fields.Integer("Chunk key")
|
||||||
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
||||||
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
||||||
pivot = fields.Function(
|
pivot = fields.Function(
|
||||||
@@ -201,7 +203,7 @@ class Lot(metaclass=PoolMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pivot_data['options'] = {
|
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"],
|
"cols": ["account"],
|
||||||
"aggregatorName": "Sum",
|
"aggregatorName": "Sum",
|
||||||
"vals": ["amount"]
|
"vals": ["amount"]
|
||||||
@@ -578,6 +580,14 @@ class Lot(metaclass=PoolMeta):
|
|||||||
return True
|
return True
|
||||||
return False
|
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):
|
def GetShipment(self,type):
|
||||||
if type == 'in':
|
if type == 'in':
|
||||||
m = self.get_current_supplier_move()
|
m = self.get_current_supplier_move()
|
||||||
@@ -1087,6 +1097,7 @@ class LotQt(
|
|||||||
newlot.lot_shipment_internal = self.lot_shipment_internal
|
newlot.lot_shipment_internal = self.lot_shipment_internal
|
||||||
newlot.lot_shipment_out = self.lot_shipment_out
|
newlot.lot_shipment_out = self.lot_shipment_out
|
||||||
newlot.lot_product = self.lot_p.line.product
|
newlot.lot_product = self.lot_p.line.product
|
||||||
|
newlot.lot_chunk_key = l.lot_chunk_key
|
||||||
if self.lot_s:
|
if self.lot_s:
|
||||||
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
||||||
newlot.lot_type = 'physic'
|
newlot.lot_type = 'physic'
|
||||||
@@ -1168,6 +1179,7 @@ class LotQt(
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, lotqts):
|
def validate(cls, lotqts):
|
||||||
super(LotQt, cls).validate(lotqts)
|
super(LotQt, cls).validate(lotqts)
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
#Update Move
|
#Update Move
|
||||||
for lqt in lotqts:
|
for lqt in lotqts:
|
||||||
cls.updateMove(lqt.lot_move)
|
cls.updateMove(lqt.lot_move)
|
||||||
@@ -1177,23 +1189,23 @@ class LotQt(
|
|||||||
if lqt.lot_p and lqt.lot_quantity > 0:
|
if lqt.lot_p and lqt.lot_quantity > 0:
|
||||||
pl = lqt.lot_p.line
|
pl = lqt.lot_p.line
|
||||||
logger.info("VALIDATE_LQT_PL:%s",pl)
|
logger.info("VALIDATE_LQT_PL:%s",pl)
|
||||||
Pnl = Pool().get('valuation.valuation')
|
# Pnl = Pool().get('valuation.valuation')
|
||||||
pnl = Pnl.search([('line','=',pl.id)])
|
# pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
|
||||||
if pnl:
|
# if pnl:
|
||||||
Pnl.delete(pnl)
|
# Pnl.delete(pnl)
|
||||||
pnl_lines = []
|
# pnl_lines = []
|
||||||
pnl_lines.extend(pl.get_pnl_fee_lines())
|
# pnl_lines.extend(pl.get_pnl_fee_lines())
|
||||||
pnl_lines.extend(pl.get_pnl_price_lines())
|
# pnl_lines.extend(pl.get_pnl_price_lines())
|
||||||
pnl_lines.extend(pl.get_pnl_der_lines())
|
# pnl_lines.extend(pl.get_pnl_der_lines())
|
||||||
Pnl.save(pnl_lines)
|
# Pnl.save(pnl_lines)
|
||||||
|
|
||||||
#Open position update
|
#Open position update
|
||||||
if pl.quantity_theorical:
|
# if pl.quantity_theorical:
|
||||||
OpenPosition = Pool().get('open.position')
|
# OpenPosition = Pool().get('open.position')
|
||||||
OpenPosition.create_from_purchase_line(pl)
|
# OpenPosition.create_from_purchase_line(pl)
|
||||||
|
|
||||||
@classmethod
|
@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()
|
pool = Pool()
|
||||||
LotQt = pool.get('lot.qt')
|
LotQt = pool.get('lot.qt')
|
||||||
lqt = LotQt.__table__()
|
lqt = LotQt.__table__()
|
||||||
@@ -1241,8 +1253,12 @@ class LotQt(
|
|||||||
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
||||||
if ps == 'P':
|
if ps == 'P':
|
||||||
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
|
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
|
||||||
|
if not finished:
|
||||||
|
wh &= (pl.finished == False)
|
||||||
elif ps == 'S':
|
elif ps == 'S':
|
||||||
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
|
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:
|
if purchase:
|
||||||
wh &= (pu.id == purchase)
|
wh &= (pu.id == purchase)
|
||||||
if sale:
|
if sale:
|
||||||
@@ -1832,7 +1848,8 @@ class LotReport(
|
|||||||
supplier = context.get('supplier')
|
supplier = context.get('supplier')
|
||||||
#asof = context.get('asof')
|
#asof = context.get('asof')
|
||||||
#todate = context.get('todate')
|
#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
|
return query
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1922,6 +1939,12 @@ class LotContext(ModelView):
|
|||||||
('pnl', 'Pnl'),
|
('pnl', 'Pnl'),
|
||||||
],'Mode')
|
],'Mode')
|
||||||
|
|
||||||
|
finished = fields.Boolean("Display finished")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_finished(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_asof(cls):
|
def default_asof(cls):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
@@ -2002,17 +2025,24 @@ class LotShipping(Wizard):
|
|||||||
if r.r_lot_shipment_in:
|
if r.r_lot_shipment_in:
|
||||||
raise UserError("Please unlink before linking to a new shipment !")
|
raise UserError("Please unlink before linking to a new shipment !")
|
||||||
else:
|
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
|
shipment_origin = None
|
||||||
if self.ship.quantity:
|
if self.ship.quantity:
|
||||||
shipped_quantity = self.ship.quantity
|
shipped_quantity = self.ship.quantity
|
||||||
if shipped_quantity == 0:
|
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 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)
|
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
|
||||||
elif self.ship.shipment == 'out':
|
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)
|
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
|
||||||
elif self.ship.shipment == 'int':
|
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)
|
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
|
||||||
if r.id < 10000000 :
|
if r.id < 10000000 :
|
||||||
l = Lot(r.id)
|
l = Lot(r.id)
|
||||||
@@ -2030,15 +2060,22 @@ class LotShipping(Wizard):
|
|||||||
move = Move(l.move)
|
move = Move(l.move)
|
||||||
move.shipment = shipment_origin
|
move.shipment = shipment_origin
|
||||||
Move.save([move])
|
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
|
#Decrease forecasted virtual part shipped
|
||||||
vlot_p = l.getVlot_p()
|
vlot_p = l.getVlot_p()
|
||||||
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
||||||
l.lot_av = 'reserved'
|
l.lot_av = 'reserved'
|
||||||
Lot.save([l])
|
Lot.save([l])
|
||||||
|
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
||||||
|
Lot.save([l])
|
||||||
else:
|
else:
|
||||||
lqt = LotQt(r.id - 10000000)
|
lqt = LotQt(r.id - 10000000)
|
||||||
#Increase forecasted virtual part shipped
|
#Increase forecasted virtual part shipped
|
||||||
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
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)
|
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
||||||
#Decrease forecasted virtual part non shipped
|
#Decrease forecasted virtual part non shipped
|
||||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
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_gross_quantity = fields.Numeric("Gross weight")
|
||||||
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
||||||
lot_premium = fields.Numeric("Premium")
|
lot_premium = fields.Numeric("Premium")
|
||||||
|
lot_chunk_key = fields.Integer("Chunk key")
|
||||||
|
|
||||||
# @fields.depends('lot_qt')
|
# @fields.depends('lot_qt')
|
||||||
# def on_change_with_lot_quantity(self):
|
# def on_change_with_lot_quantity(self):
|
||||||
@@ -2604,6 +2642,17 @@ class LotInvoice(Wizard):
|
|||||||
|
|
||||||
invoicing = StateTransition()
|
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):
|
def transition_start(self):
|
||||||
return 'inv'
|
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_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_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_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']
|
unit = val['lot_unit']
|
||||||
val['lot_currency'] = lot.lot_price_ct_symbol
|
val['lot_currency'] = lot.lot_price_ct_symbol
|
||||||
lot_p.append(val)
|
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_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_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_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)
|
lot_s.append(val_s)
|
||||||
if line:
|
if line:
|
||||||
if line.fees:
|
if line.fees:
|
||||||
@@ -2747,12 +2797,27 @@ class LotInvoice(Wizard):
|
|||||||
continue
|
continue
|
||||||
lots.append(lot)
|
lots.append(lot)
|
||||||
|
|
||||||
|
invoice_line = None
|
||||||
if self.inv.type == 'purchase':
|
if self.inv.type == 'purchase':
|
||||||
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
|
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:
|
else:
|
||||||
if sale:
|
if sale:
|
||||||
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
|
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
|
||||||
return 'end'
|
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):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
@@ -3129,136 +3194,13 @@ class CreateContracts(Wizard):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def transition_creating(self):
|
def transition_creating(self):
|
||||||
SaleLine = Pool().get('sale.line')
|
ContractFactory.create_contracts(
|
||||||
Sale = Pool().get('sale.sale')
|
self.ct.contracts,
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
type_=self.ct.type,
|
||||||
Purchase = Pool().get('purchase.purchase')
|
ct=self.ct,
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
return 'end'
|
return 'end'
|
||||||
|
|
||||||
# def do_matching(self, action):
|
|
||||||
# return action, {
|
|
||||||
# 'ids': self.sale_lines,
|
|
||||||
# 'model': str(self.ct.lot.id),
|
|
||||||
# }
|
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
|
|
||||||
@@ -3288,7 +3230,6 @@ class ContractsStart(ModelView):
|
|||||||
def default_matched(cls):
|
def default_matched(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ContractDetail(ModelView):
|
class ContractDetail(ModelView):
|
||||||
|
|
||||||
"Contract Detail"
|
"Contract Detail"
|
||||||
@@ -3296,26 +3237,29 @@ class ContractDetail(ModelView):
|
|||||||
|
|
||||||
category = fields.Integer("Category")
|
category = fields.Integer("Category")
|
||||||
cd = fields.Many2One('contracts.start',"Contracts")
|
cd = fields.Many2One('contracts.start',"Contracts")
|
||||||
party = fields.Many2One('party.party',"Party",domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
||||||
currency = fields.Many2One('currency.currency',"Currency")
|
currency = fields.Many2One('currency.currency',"Currency", required=True)
|
||||||
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm")
|
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
|
||||||
quantity = fields.Numeric("Quantity",digits=(1,5))
|
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
|
||||||
unit = fields.Many2One('product.uom',"Unit")
|
unit = fields.Many2One('product.uom',"Unit", required=True)
|
||||||
qt_unit = fields.Many2One('product.uom',"Unit")
|
qt_unit = fields.Many2One('product.uom',"Unit")
|
||||||
tol_min = fields.Numeric("Tol - in %")
|
tol_min = fields.Numeric("Tol - in %", required=True)
|
||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %", required=True)
|
||||||
crop = fields.Many2One('purchase.crop',"Crop")
|
crop = fields.Many2One('purchase.crop',"Crop")
|
||||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||||
from_del = fields.Date("From")
|
from_del = fields.Date("From")
|
||||||
to_del = fields.Date("To")
|
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([
|
price_type = price_type = fields.Selection([
|
||||||
('cash', 'Cash Price'),
|
('cash', 'Cash Price'),
|
||||||
('priced', 'Priced'),
|
('priced', 'Priced'),
|
||||||
('basis', 'Basis'),
|
('basis', 'Basis'),
|
||||||
], 'Price type')
|
], 'Price type', required=True)
|
||||||
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
||||||
reference = fields.Char("Reference")
|
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
|
@classmethod
|
||||||
def default_category(cls):
|
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):
|
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
|
||||||
return lqt.lot_p.line.purchase.crop.id
|
return lqt.lot_p.line.purchase.crop.id
|
||||||
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
|
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
|
@classmethod
|
||||||
def default_currency(cls):
|
def default_currency(cls):
|
||||||
|
|||||||
152
modules/purchase_trade/numbers_to_words.py
Normal file
152
modules/purchase_trade/numbers_to_words.py
Normal 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}"
|
||||||
@@ -1,11 +1,60 @@
|
|||||||
from trytond.model import ModelSQL, ModelView, fields
|
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.exceptions import UserError
|
||||||
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
|
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']
|
class PartyExecution(ModelSQL,ModelView):
|
||||||
__metaclass__ = PoolMeta
|
"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):
|
class Party(metaclass=PoolMeta):
|
||||||
__name__ = 'party.party'
|
__name__ = 'party.party'
|
||||||
|
|
||||||
@@ -13,5 +62,53 @@ class Party(metaclass=PoolMeta):
|
|||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %")
|
||||||
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
|
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
|
||||||
association = fields.Many2One('purchase.association',"Association")
|
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
|
||||||
|
|
||||||
@@ -1,5 +1,29 @@
|
|||||||
<record model="ir.ui.view" id="party_view_form">
|
<tryton>
|
||||||
<field name="model">party.party</field>
|
<data>
|
||||||
<field name="inherit" ref="party.party_view_form"/>
|
<record model="ir.ui.view" id="party_view_form">
|
||||||
<field name="name">party_form</field>
|
<field name="model">party.party</field>
|
||||||
</record>
|
<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>
|
||||||
@@ -21,6 +21,7 @@ class PaymentTermLine(metaclass=PoolMeta):
|
|||||||
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
|
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
|
||||||
|
|
||||||
term_type = fields.Selection([
|
term_type = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('advance', 'Advance'),
|
('advance', 'Advance'),
|
||||||
('cad', 'CAD'),
|
('cad', 'CAD'),
|
||||||
('open', 'Open'),
|
('open', 'Open'),
|
||||||
@@ -30,18 +31,21 @@ class PaymentTermLine(metaclass=PoolMeta):
|
|||||||
|
|
||||||
trigger_offset = fields.Integer('Trigger Offset')
|
trigger_offset = fields.Integer('Trigger Offset')
|
||||||
offset_unit = fields.Selection([
|
offset_unit = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('calendar', 'Calendar Days'),
|
('calendar', 'Calendar Days'),
|
||||||
('business', 'Business Days'),
|
('business', 'Business Days'),
|
||||||
], 'Offset Unit')
|
], 'Offset Unit')
|
||||||
|
|
||||||
eom_flag = fields.Boolean('EOM Flag')
|
eom_flag = fields.Boolean('EOM Flag')
|
||||||
eom_mode = fields.Selection([
|
eom_mode = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('standard', 'Standard'),
|
('standard', 'Standard'),
|
||||||
('before', 'Before EOM'),
|
('before', 'Before EOM'),
|
||||||
('after', 'After EOM'),
|
('after', 'After EOM'),
|
||||||
], 'EOM Mode')
|
], 'EOM Mode')
|
||||||
|
|
||||||
risk_classification = fields.Selection([
|
risk_classification = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('fully_secured', 'Fully Secured'),
|
('fully_secured', 'Fully Secured'),
|
||||||
('partially_secured', 'Partially Secured'),
|
('partially_secured', 'Partially Secured'),
|
||||||
('unsecured', 'Unsecured'),
|
('unsecured', 'Unsecured'),
|
||||||
|
|||||||
@@ -50,37 +50,243 @@ DAYS = [
|
|||||||
('sunday', 'Sunday'),
|
('sunday', 'Sunday'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Estimated(ModelSQL, ModelView):
|
class Estimated(ModelSQL, ModelView):
|
||||||
"Estimated date"
|
"Estimated date"
|
||||||
__name__ = 'pricing.estimated'
|
__name__ = 'pricing.estimated'
|
||||||
|
|
||||||
trigger = fields.Selection(TRIGGERS,"Trigger")
|
trigger = fields.Selection(TRIGGERS,"Trigger")
|
||||||
estimated_date = fields.Date("Estimated date")
|
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):
|
class Mtm(ModelSQL, ModelView):
|
||||||
"Mtm"
|
"MtM Component"
|
||||||
__name__ = 'mtm.component'
|
__name__ = 'mtm.component'
|
||||||
|
|
||||||
fix_type = fields.Many2One('price.fixtype',"Fixation type")
|
strategy = fields.Many2One(
|
||||||
ratio = fields.Numeric("%")
|
'mtm.strategy', "Strategy",
|
||||||
price_index = fields.Many2One('price.price',"Curve")
|
required=True, ondelete='CASCADE'
|
||||||
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
|
)
|
||||||
|
|
||||||
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:
|
if self.price_index:
|
||||||
PI = Pool().get('price.price')
|
return self.price_index.price_currency
|
||||||
pi = PI(self.price_index)
|
if self.price_matrix:
|
||||||
return pi.price_currency
|
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):
|
class Component(ModelSQL, ModelView):
|
||||||
"Component"
|
"Component"
|
||||||
__name__ = 'pricing.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")
|
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_index = fields.Many2One('price.price',"Curve")
|
||||||
|
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
|
||||||
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
|
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
|
||||||
auto = fields.Boolean("Auto")
|
auto = fields.Boolean("Auto")
|
||||||
fallback = fields.Boolean("Fallback")
|
fallback = fields.Boolean("Fallback")
|
||||||
@@ -194,6 +400,7 @@ class Trigger(ModelSQL,ModelView):
|
|||||||
'readonly': Eval('pricing_period') != None,
|
'readonly': Eval('pricing_period') != None,
|
||||||
})
|
})
|
||||||
average = fields.Boolean("Avg")
|
average = fields.Boolean("Avg")
|
||||||
|
last = fields.Boolean("Last")
|
||||||
application_period = fields.Many2One('pricing.period',"Application period")
|
application_period = fields.Many2One('pricing.period',"Application period")
|
||||||
from_a = fields.Date("From",
|
from_a = fields.Date("From",
|
||||||
states={
|
states={
|
||||||
@@ -217,14 +424,11 @@ class Trigger(ModelSQL,ModelView):
|
|||||||
pp = PP(self.application_period)
|
pp = PP(self.application_period)
|
||||||
CO = Pool().get('pricing.component')
|
CO = Pool().get('pricing.component')
|
||||||
co = CO(self.component)
|
co = CO(self.component)
|
||||||
logger.info("DELDATEEST_:%s",co)
|
|
||||||
if co.line:
|
if co.line:
|
||||||
d = co.getEstimatedTriggerPurchase(pp.trigger)
|
d = co.getEstimatedTriggerPurchase(pp.trigger)
|
||||||
else:
|
else:
|
||||||
d = co.getEstimatedTriggerSale(pp.trigger)
|
d = co.getEstimatedTriggerSale(pp.trigger)
|
||||||
logger.info("DELDATEEST:%s",d)
|
|
||||||
date_from,date_to,dates = pp.getDates(d)
|
date_from,date_to,dates = pp.getDates(d)
|
||||||
logger.info("DELDATEEST2:%s",dates)
|
|
||||||
return date_from,date_to,d,pp.include,dates
|
return date_from,date_to,d,pp.include,dates
|
||||||
|
|
||||||
def getApplicationListDates(self, cal):
|
def getApplicationListDates(self, cal):
|
||||||
@@ -288,7 +492,7 @@ class Trigger(ModelSQL,ModelView):
|
|||||||
pi = PI(pc.price_index)
|
pi = PI(pc.price_index)
|
||||||
val = {}
|
val = {}
|
||||||
val['date'] = current_date
|
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'] = val['price']
|
||||||
val['avg_minus_1'] = val['price']
|
val['avg_minus_1'] = val['price']
|
||||||
val['isAvg'] = self.average
|
val['isAvg'] = self.average
|
||||||
@@ -330,8 +534,6 @@ class Period(ModelSQL,ModelView):
|
|||||||
date_from = None
|
date_from = None
|
||||||
date_to = None
|
date_to = None
|
||||||
dates = []
|
dates = []
|
||||||
logger.info("GETDATES:%s",t)
|
|
||||||
logger.info("GETDATES:%s",self.every)
|
|
||||||
if t:
|
if t:
|
||||||
if self.every:
|
if self.every:
|
||||||
if t:
|
if t:
|
||||||
@@ -348,21 +550,18 @@ class Period(ModelSQL,ModelView):
|
|||||||
while current.month == t.month:
|
while current.month == t.month:
|
||||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||||
current += datetime.timedelta(days=7)
|
current += datetime.timedelta(days=7)
|
||||||
logger.info("GETDATES:%s",dates)
|
|
||||||
elif self.nb_quotation > 0:
|
elif self.nb_quotation > 0:
|
||||||
days_to_add = (weekday_target - t.weekday()) % 7
|
days_to_add = (weekday_target - t.weekday()) % 7
|
||||||
current = t + datetime.timedelta(days=days_to_add)
|
current = t + datetime.timedelta(days=days_to_add)
|
||||||
while len(dates) < self.nb_quotation:
|
while len(dates) < self.nb_quotation:
|
||||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||||
current += datetime.timedelta(days=7)
|
current += datetime.timedelta(days=7)
|
||||||
logger.info("GETDATES:%s",dates)
|
|
||||||
elif self.nb_quotation < 0:
|
elif self.nb_quotation < 0:
|
||||||
days_to_sub = (t.weekday() - weekday_target) % 7
|
days_to_sub = (t.weekday() - weekday_target) % 7
|
||||||
current = t - datetime.timedelta(days=days_to_sub)
|
current = t - datetime.timedelta(days=days_to_sub)
|
||||||
while len(dates) < -self.nb_quotation:
|
while len(dates) < -self.nb_quotation:
|
||||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||||
current -= datetime.timedelta(days=7)
|
current -= datetime.timedelta(days=7)
|
||||||
logger.info("GETDATES:%s",dates)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if self.startday == 'before':
|
if self.startday == 'before':
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
this repository contains the full copyright notices and license terms. -->
|
this repository contains the full copyright notices and license terms. -->
|
||||||
<tryton>
|
<tryton>
|
||||||
<data>
|
<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">
|
<record model="ir.ui.view" id="summary_view_tree_sequence">
|
||||||
<field name="model">sale.pricing.summary</field>
|
<field name="model">sale.pricing.summary</field>
|
||||||
<field name="type">tree</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="type">form</field>
|
||||||
<field name="name">period_form</field>
|
<field name="name">period_form</field>
|
||||||
</record>
|
</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>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -126,6 +126,25 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="wiz_name">pnl.report</field>
|
<field name="wiz_name">pnl.report</field>
|
||||||
</record>
|
</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">
|
<record model="ir.ui.view" id="mtm_view_form">
|
||||||
<field name="model">mtm.component</field>
|
<field name="model">mtm.component</field>
|
||||||
<field name="type">form</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>
|
<field name="name">mtm_tree</field>
|
||||||
</record>
|
</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
|
<menuitem
|
||||||
name="Pnl Report"
|
name="Pnl Report"
|
||||||
parent="purchase_trade.menu_global_reporting"
|
parent="purchase_trade.menu_global_reporting"
|
||||||
@@ -144,6 +234,13 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
sequence="110"
|
sequence="110"
|
||||||
id="menu_pnl_bi"/>
|
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
|
<menuitem
|
||||||
parent="purchase_trade.menu_global_reporting"
|
parent="purchase_trade.menu_global_reporting"
|
||||||
sequence="100"
|
sequence="100"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from trytond.model import fields
|
|||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
|
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
|
||||||
from trytond.model import (ModelSQL, ModelView)
|
from trytond.model import (ModelSQL, ModelView)
|
||||||
|
from trytond.i18n import gettext
|
||||||
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
||||||
from trytond.transaction import Transaction, inactive_records
|
from trytond.transaction import Transaction, inactive_records
|
||||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||||
@@ -15,6 +16,7 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from trytond.exceptions import UserWarning, UserError
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,6 +34,11 @@ class ContractDocumentType(metaclass=PoolMeta):
|
|||||||
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
|
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
|
||||||
sale = fields.Many2One('sale.sale', "Sale")
|
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):
|
class Estimated(metaclass=PoolMeta):
|
||||||
"Estimated date"
|
"Estimated date"
|
||||||
__name__ = 'pricing.estimated'
|
__name__ = 'pricing.estimated'
|
||||||
@@ -45,6 +52,12 @@ class FeeLots(metaclass=PoolMeta):
|
|||||||
|
|
||||||
sale_line = fields.Many2One('sale.line',"Line")
|
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):
|
class OpenPosition(metaclass=PoolMeta):
|
||||||
"Open position"
|
"Open position"
|
||||||
__name__ = 'open.position'
|
__name__ = 'open.position'
|
||||||
@@ -52,10 +65,11 @@ class OpenPosition(metaclass=PoolMeta):
|
|||||||
sale_line = fields.Many2One('sale.line',"Sale Line")
|
sale_line = fields.Many2One('sale.line',"Sale Line")
|
||||||
client = fields.Many2One('party.party',"Client")
|
client = fields.Many2One('party.party',"Client")
|
||||||
|
|
||||||
class Mtm(metaclass=PoolMeta):
|
class SaleStrategy(ModelSQL):
|
||||||
"Mtm"
|
"Sale - Document Type"
|
||||||
__name__ = 'mtm.component'
|
__name__ = 'sale.strategy'
|
||||||
sale_line = fields.Many2One('sale.line',"Line")
|
sale_line = fields.Many2One('sale.line', 'Sale Line')
|
||||||
|
strategy = fields.Many2One('mtm.strategy', "Strategy")
|
||||||
|
|
||||||
class Component(metaclass=PoolMeta):
|
class Component(metaclass=PoolMeta):
|
||||||
"Component"
|
"Component"
|
||||||
@@ -172,7 +186,7 @@ class Summary(ModelSQL,ModelView):
|
|||||||
class Lot(metaclass=PoolMeta):
|
class Lot(metaclass=PoolMeta):
|
||||||
__name__ = 'lot.lot'
|
__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_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')
|
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):
|
class Sale(metaclass=PoolMeta):
|
||||||
__name__ = 'sale.sale'
|
__name__ = 'sale.sale'
|
||||||
|
|
||||||
from_location = fields.Many2One('stock.location', 'From location',domain=[('type', "!=", 'customer')])
|
btb = fields.Many2One('back.to.back',"Back to back")
|
||||||
to_location = fields.Many2One('stock.location', 'To location',domain=[('type', "!=", 'supplier')])
|
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')
|
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')
|
derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
|
||||||
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
|
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
|
||||||
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
|
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
|
||||||
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
|
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
|
||||||
tol_min = fields.Numeric("Tol - in %")
|
tol_min = fields.Numeric("Tol - in %", required=True)
|
||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %", required=True)
|
||||||
# certification = fields.Selection([
|
tol_min_qt = fields.Numeric("Tol -")
|
||||||
# (None, ''),
|
tol_max_qt = fields.Numeric("Tol +")
|
||||||
# ('bci', 'BCI'),
|
certif = fields.Many2One('purchase.certification',"Certification", required=True,states={'invisible': Eval('company_visible'),})
|
||||||
# ],"Certification")
|
wb = fields.Many2One('purchase.weight.basis',"Weight basis", required=True)
|
||||||
# weight_basis = fields.Selection([
|
association = fields.Many2One('purchase.association',"Association", required=True,states={'invisible': Eval('company_visible'),})
|
||||||
# (None, ''),
|
crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
|
||||||
# ('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")
|
|
||||||
viewer = fields.Function(fields.Text(""),'get_viewer')
|
viewer = fields.Function(fields.Text(""),'get_viewer')
|
||||||
doc_template = fields.Many2One('doc.template',"Template")
|
doc_template = fields.Many2One('doc.template',"Template")
|
||||||
required_documents = fields.Many2Many(
|
required_documents = fields.Many2Many(
|
||||||
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
|
'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
|
@classmethod
|
||||||
def default_viewer(cls):
|
def default_viewer(cls):
|
||||||
country_start = "Zobiland"
|
country_start = "Zobiland"
|
||||||
@@ -316,26 +532,20 @@ class Sale(metaclass=PoolMeta):
|
|||||||
for sale in sales:
|
for sale in sales:
|
||||||
for line in sale.lines:
|
for line in sale.lines:
|
||||||
if not line.quantity_theorical and line.quantity > 0:
|
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])
|
Line.save([line])
|
||||||
|
|
||||||
if line.lots:
|
if line.lots:
|
||||||
line_p = line.lots[0].line
|
line_p = line.get_matched_lines()#line.lots[0].line
|
||||||
if line_p:
|
if line_p:
|
||||||
#compute pnl
|
for l in line_p:
|
||||||
Pnl = Pool().get('valuation.valuation')
|
#compute pnl
|
||||||
pnl = Pnl.search([('line','=',line_p.id)])
|
Pnl = Pool().get('valuation.valuation')
|
||||||
if pnl:
|
Pnl.generate(l.lot_p.line)
|
||||||
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)
|
|
||||||
|
|
||||||
if line.quantity_theorical:
|
# if line.quantity_theorical:
|
||||||
OpenPosition = Pool().get('open.position')
|
# OpenPosition = Pool().get('open.position')
|
||||||
OpenPosition.create_from_sale_line(line)
|
# OpenPosition.create_from_sale_line(line)
|
||||||
|
|
||||||
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
|
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
|
||||||
unit_price = line.get_basis_price()
|
unit_price = line.get_basis_price()
|
||||||
@@ -348,6 +558,11 @@ class Sale(metaclass=PoolMeta):
|
|||||||
for d in line.derivatives:
|
for d in line.derivatives:
|
||||||
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
|
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
|
||||||
Line.save([line])
|
Line.save([line])
|
||||||
|
|
||||||
|
class PriceComposition(metaclass=PoolMeta):
|
||||||
|
__name__ = 'price.composition'
|
||||||
|
|
||||||
|
sale_line = fields.Many2One('sale.line',"Sale line")
|
||||||
|
|
||||||
class SaleLine(metaclass=PoolMeta):
|
class SaleLine(metaclass=PoolMeta):
|
||||||
__name__ = 'sale.line'
|
__name__ = 'sale.line'
|
||||||
@@ -369,8 +584,18 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
}),'get_progress')
|
}),'get_progress')
|
||||||
from_del = fields.Date("From")
|
from_del = fields.Date("From")
|
||||||
to_del = fields.Date("To")
|
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")
|
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")
|
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
|
||||||
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
|
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
|
||||||
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
|
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={
|
tol_max = fields.Numeric("Tol + in %",states={
|
||||||
'readonly': (Eval('inherit_tol')),
|
'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")
|
inherit_tol = fields.Boolean("Inherit tolerance")
|
||||||
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
|
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
|
||||||
tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
|
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')
|
premium = fields.Numeric("Premium/Discount",digits='unit')
|
||||||
fee_ = fields.Many2One('fee.fee',"Fee")
|
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
|
@classmethod
|
||||||
def default_price_type(cls):
|
def default_price_type(cls):
|
||||||
return 'priced'
|
return 'priced'
|
||||||
@@ -419,10 +720,20 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
def default_inherit_cer(cls):
|
def default_inherit_cer(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# @fields.depends('quantity')
|
def get_matched_lines(self):
|
||||||
# def on_change_with_quantity_theorical(self):
|
if self.lots:
|
||||||
# if not self.quantity_theorical:
|
LotQt = Pool().get('lot.qt')
|
||||||
# return self.quantity
|
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):
|
def get_tol_min(self,name):
|
||||||
if self.inherit_tol:
|
if self.inherit_tol:
|
||||||
@@ -659,9 +970,9 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
valuations = Valuation.search([('lot','in',line.lots)])
|
valuations = Valuation.search([('lot','in',line.lots)])
|
||||||
if valuations:
|
if valuations:
|
||||||
Valuation.delete(valuations)
|
Valuation.delete(valuations)
|
||||||
op = OpenPosition.search(['sale_line','=',line.id])
|
# op = OpenPosition.search(['sale_line','=',line.id])
|
||||||
if op:
|
# if op:
|
||||||
OpenPosition.delete(op)
|
# OpenPosition.delete(op)
|
||||||
|
|
||||||
super(SaleLine, cls).delete(lines)
|
super(SaleLine, cls).delete(lines)
|
||||||
|
|
||||||
@@ -698,7 +1009,7 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
lot.sale_line = line.id
|
lot.sale_line = line.id
|
||||||
lot.lot_qt = line.quantity
|
lot.lot_qt = line.quantity
|
||||||
lot.lot_unit_line = line.unit
|
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_status = 'forecast'
|
||||||
lot.lot_type = 'virtual'
|
lot.lot_type = 'virtual'
|
||||||
lot.lot_product = line.product
|
lot.lot_product = line.product
|
||||||
@@ -733,14 +1044,7 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
if purchase_lines:
|
if purchase_lines:
|
||||||
for pl in purchase_lines:
|
for pl in purchase_lines:
|
||||||
Pnl = Pool().get('valuation.valuation')
|
Pnl = Pool().get('valuation.valuation')
|
||||||
pnl = Pnl.search([('line','=',pl.id)])
|
Pnl.generate(pl)
|
||||||
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)
|
|
||||||
|
|
||||||
class SaleCreatePurchase(Wizard):
|
class SaleCreatePurchase(Wizard):
|
||||||
"Create mirror purchase"
|
"Create mirror purchase"
|
||||||
@@ -831,6 +1135,20 @@ class Valuation(metaclass=PoolMeta):
|
|||||||
sale = fields.Many2One('sale.sale',"Sale")
|
sale = fields.Many2One('sale.sale',"Sale")
|
||||||
sale_line = fields.Many2One('sale.line',"Line")
|
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):
|
class ValuationDyn(metaclass=PoolMeta):
|
||||||
"Valuation"
|
"Valuation"
|
||||||
__name__ = 'valuation.valuation.dyn'
|
__name__ = 'valuation.valuation.dyn'
|
||||||
@@ -855,18 +1173,22 @@ class ValuationDyn(metaclass=PoolMeta):
|
|||||||
Max(val.sale).as_('r_sale'),
|
Max(val.sale).as_('r_sale'),
|
||||||
Max(val.line).as_('r_line'),
|
Max(val.line).as_('r_line'),
|
||||||
Max(val.date).as_('r_date'),
|
Max(val.date).as_('r_date'),
|
||||||
val.type.as_('r_type'),
|
Literal(None).as_('r_type'),
|
||||||
Max(val.reference).as_('r_reference'),
|
Max(val.reference).as_('r_reference'),
|
||||||
val.counterparty.as_('r_counterparty'),
|
Literal(None).as_('r_counterparty'),
|
||||||
Max(val.product).as_('r_product'),
|
Max(val.product).as_('r_product'),
|
||||||
val.state.as_('r_state'),
|
Literal(None).as_('r_state'),
|
||||||
Avg(val.price).as_('r_price'),
|
Avg(val.price).as_('r_price'),
|
||||||
Max(val.currency).as_('r_currency'),
|
Max(val.currency).as_('r_currency'),
|
||||||
Sum(val.quantity).as_('r_quantity'),
|
Sum(val.quantity).as_('r_quantity'),
|
||||||
Max(val.unit).as_('r_unit'),
|
Max(val.unit).as_('r_unit'),
|
||||||
Sum(val.amount).as_('r_amount'),
|
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'),
|
Sum(val.mtm).as_('r_mtm'),
|
||||||
|
Max(val.strategy).as_('r_strategy'),
|
||||||
Max(val.lot).as_('r_lot'),
|
Max(val.lot).as_('r_lot'),
|
||||||
|
Max(val.sale_line).as_('r_sale_line'),
|
||||||
where=wh,
|
where=wh,
|
||||||
group_by=[val.purchase,val.sale])
|
group_by=[val.purchase,val.sale])
|
||||||
|
|
||||||
|
|||||||
@@ -52,5 +52,11 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="model">sale.sale,-1</field>
|
<field name="model">sale.sale,-1</field>
|
||||||
<field name="action" ref="act_sale_allocations_wizard"/>
|
<field name="action" ref="act_sale_allocations_wizard"/>
|
||||||
</record>
|
</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>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
221
modules/purchase_trade/service.py
Normal file
221
modules/purchase_trade/service.py
Normal 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
|
||||||
@@ -67,10 +67,10 @@ setup(name=name,
|
|||||||
+ ['trytond.modules.purchase_trade.%s' % p
|
+ ['trytond.modules.purchase_trade.%s' % p
|
||||||
for p in find_packages()]
|
for p in find_packages()]
|
||||||
),
|
),
|
||||||
package_data={
|
package_data={
|
||||||
'trytond.modules.purchase_trade': (info.get('xml', [])
|
'trytond.modules.purchase_trade': (info.get('xml', [])
|
||||||
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
|
+ ['tryton.cfg', 'view/*.xml', 'locale/*.po']),
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Plugins',
|
'Environment :: Plugins',
|
||||||
@@ -120,4 +120,4 @@ setup(name=name,
|
|||||||
[trytond.modules]
|
[trytond.modules]
|
||||||
purchase_trade = trytond.modules.purchase_trade
|
purchase_trade = trytond.modules.purchase_trade
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,53 @@ from itertools import chain, groupby
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
import datetime
|
import datetime
|
||||||
from collections import defaultdict
|
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 logging
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import html
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class Move(metaclass=PoolMeta):
|
||||||
__name__ = 'stock.move'
|
__name__ = 'stock.move'
|
||||||
@@ -28,17 +71,12 @@ class Move(metaclass=PoolMeta):
|
|||||||
lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
|
lotqt = fields.One2Many('lot.qt','lot_move',"Lots")
|
||||||
lot = fields.Many2One('lot.lot',"Lot")
|
lot = fields.Many2One('lot.lot',"Lot")
|
||||||
|
|
||||||
# @fields.depends('lotqt','unit')
|
def get_linked_transit_move(self):
|
||||||
# def on_change_with_quantity(self):
|
if self.from_location.is_transit():
|
||||||
# if self.lotqt:
|
Move = Pool().get('stock.move')
|
||||||
# pool = Pool()
|
Location = Pool().get('stock.location')
|
||||||
# Uom = pool.get('product.uom')
|
moves = Move.search([('lot','=',self.lot),('to_location','=',Location.get_transit_id())],order=[('id', 'DESC')],limit=1)
|
||||||
# if self.unit:
|
return moves[0] if moves else None
|
||||||
# return round(sum([(e.lot_quantity if e.lot_quantity else 0) for e in self.lotqt]),2)
|
|
||||||
# else:
|
|
||||||
# return 0
|
|
||||||
# else:
|
|
||||||
# return 0
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, moves):
|
def validate(cls, moves):
|
||||||
@@ -343,6 +381,12 @@ class ShipmentContainer(ModelSQL, ModelView):
|
|||||||
seal_no = fields.Char('Seal Number')
|
seal_no = fields.Char('Seal Number')
|
||||||
is_reefer = fields.Boolean('Reefer')
|
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):
|
class ShipmentIn(metaclass=PoolMeta):
|
||||||
__name__ = 'stock.shipment.in'
|
__name__ = 'stock.shipment.in'
|
||||||
|
|
||||||
@@ -395,12 +439,24 @@ class ShipmentIn(metaclass=PoolMeta):
|
|||||||
'shipment',
|
'shipment',
|
||||||
'Container'
|
'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
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super().__setup__()
|
super().__setup__()
|
||||||
cls._buttons.update({
|
cls._buttons.update({
|
||||||
'compute': {},
|
'compute': {},
|
||||||
|
'send': {},
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_vessel_type(self,name=None):
|
def get_vessel_type(self,name=None):
|
||||||
@@ -413,6 +469,356 @@ class ShipmentIn(metaclass=PoolMeta):
|
|||||||
else:
|
else:
|
||||||
return str(self.id)
|
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
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
def compute(cls, shipments):
|
def compute(cls, shipments):
|
||||||
@@ -457,9 +863,19 @@ class ShipmentIn(metaclass=PoolMeta):
|
|||||||
def default_dashboard(cls):
|
def default_dashboard(cls):
|
||||||
return 1
|
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):
|
def get_quantity(self,name=None):
|
||||||
if self.incoming_moves:
|
if self.incoming_moves:
|
||||||
return sum([(e.quantity if e.quantity else 0) for e in 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):
|
def get_unit(self,name=None):
|
||||||
if self.incoming_moves:
|
if self.incoming_moves:
|
||||||
@@ -574,13 +990,7 @@ class ShipmentIn(metaclass=PoolMeta):
|
|||||||
#update line valuation
|
#update line valuation
|
||||||
Pnl = Pool().get('valuation.valuation')
|
Pnl = Pool().get('valuation.valuation')
|
||||||
for lot in lots:
|
for lot in lots:
|
||||||
pnl = Pnl.search([('line','=',lot.line.id)])
|
Pnl.generate(lot.line if lot.line else lot.sale_line)
|
||||||
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)
|
|
||||||
if sh.sof:
|
if sh.sof:
|
||||||
for sof in sh.sof:
|
for sof in sh.sof:
|
||||||
if sof.chart:
|
if sof.chart:
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="model">stock.shipment.container</field>
|
<field name="model">stock.shipment.container</field>
|
||||||
<field name="type">tree</field>
|
<field name="type">tree</field>
|
||||||
<field name="name">shipment_container_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>
|
||||||
|
|
||||||
<record model="ir.action.wizard" id="act_vf">
|
<record model="ir.action.wizard" id="act_vf">
|
||||||
|
|||||||
2
modules/purchase_trade/tests/__init__.py
Normal file
2
modules/purchase_trade/tests/__init__.py
Normal 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
Reference in New Issue
Block a user