Compare commits
738 Commits
v1.0.0
...
4d94aa78ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d94aa78ed | |||
| da01249f66 | |||
|
|
5cbb57c657 | ||
| 50a8c6328f | |||
| eaa5c8b544 | |||
| 78e9e06a8b | |||
| 9f06398b2c | |||
| 51a84f1f2e | |||
| 00330008d1 | |||
| 3480eb8a7a | |||
| 5179d98289 | |||
| 2109d7a3e4 | |||
| 1f350e6207 | |||
| 7722292482 | |||
| ec359f6b8a | |||
| 845b9cf749 | |||
| 48b941b109 | |||
| 18ece66cdb | |||
| acfc2fe88a | |||
| 888b880bd6 | |||
| 1f62ae91dd | |||
| 05e68636ad | |||
| bfb9bb3188 | |||
| b78e64f9f1 | |||
| 199b8aec12 | |||
| 1757075f2b | |||
| 172d38479d | |||
| 4902368b15 | |||
| 7b4f757cb5 | |||
| b37f132cdf | |||
| 15f791bd92 | |||
| 58cd66e543 | |||
| cc6ce82ec1 | |||
| 11526ef3ee | |||
| 6d52317804 | |||
| a99efcfc5b | |||
| 0d5cf7dffc | |||
| 613b679908 | |||
| 51ced23ab8 | |||
| 2958e1fb9e | |||
| 346a34951d | |||
| b644aea007 | |||
| 5dbaba5f32 | |||
| d133665fc7 | |||
| c2cb2a874c | |||
| 408970c339 | |||
| ea2627c9ae | |||
| ac988a714a | |||
| 97eae6e4a6 | |||
| 3976b387d7 | |||
| 9b8e8127a1 | |||
| f53a9bce27 | |||
| a7753b974f | |||
| c687828ba5 | |||
| 5054b64cd0 | |||
| 06922973b7 | |||
| 18ebf7f06c | |||
| 44c4560f24 | |||
| 7643bf21fb | |||
| 97677025d7 | |||
| 02fe5b3e5d | |||
| 6e529deca0 | |||
| efee365fc6 | |||
| 6bf245ac64 | |||
| 238869989a | |||
|
|
2fa541e962 | ||
|
|
ad2f7e6f78 | ||
| 43c62607a8 | |||
| 5cff728d79 | |||
|
|
d6382f624b | ||
|
|
806e374ceb | ||
| 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 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.pyc
|
||||
112
AGENTS.md
Normal file
112
AGENTS.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 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.
|
||||
- Conventions de champs dates:
|
||||
- Dans ce projet, ne pas introduire de `fields.DateTime`.
|
||||
- Utiliser `fields.Date` pour les dates metier et les champs de suivi UI, sauf demande explicite deja existante dans le module cible.
|
||||
- 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.
|
||||
- Pour les templates `stock.shipment.in`, preferer aussi des proprietes `report_*` sur le shipment plutot que des contextes ad hoc (`si_*`) quand le document devient metier ou client-specifique.
|
||||
- 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.
|
||||
- Pour les templates shipment, ne pas supposer qu'une variable locale comme `shipment` sera definie partout dans Genshi, surtout dans les headers/footers; preferer `records[0]....` ou des placeholders alignes sur le scope reel du report.
|
||||
- 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).
|
||||
1
debug.log
Normal file
1
debug.log
Normal file
@@ -0,0 +1 @@
|
||||
[0407/143111.471:ERROR:third_party\crashpad\crashpad\util\win\registration_protocol_win.cc:108] CreateFile: Accès refusé. (0x5)
|
||||
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.
|
||||
36
deployment/vps/46.202.173.47-credentials.md
Normal file
36
deployment/vps/46.202.173.47-credentials.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Fiche VPS - 46.202.173.47
|
||||
|
||||
Date de reference: 2026-04-07
|
||||
|
||||
## Identite serveur
|
||||
|
||||
- IP: `46.202.173.47`
|
||||
- Hostname alias conseille: `vps3`
|
||||
|
||||
## Acces
|
||||
|
||||
- Cle publique Laurent Barontini (vps-deploy):
|
||||
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEm8JMCYsk6I1IoYhIHXNrdyERHdh+eeDCJagOHaRAEK vps-deploy`
|
||||
|
||||
- Cle publique Sylvain Duvernay (s.duvernay@singa-associates.com):
|
||||
`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6Xsp/v6q6JO04ETv1880qoSPptUMxlWQvgcBz67o63 s.duvernay@singa-associates.com`
|
||||
- Fichier local: `$env:USERPROFILE\.ssh\id_ed25519`
|
||||
|
||||
- Mot de passe fourni:
|
||||
`!!OpenSquared!!`
|
||||
|
||||
- Utilisateur SSH:
|
||||
'root'
|
||||
- Port SSH:
|
||||
'22'
|
||||
|
||||
## Commande de connexion type
|
||||
|
||||
- Laurent Barontini (cle vps-deploy):
|
||||
`ssh -i $env:USERPROFILE\.ssh\vps_deploy_key <user>@46.202.173.47`
|
||||
|
||||
- Sylvain Duvernay (cle id_ed25519):
|
||||
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 <user>@46.202.173.47`
|
||||
|
||||
- Avec port custom:
|
||||
`ssh -i $env:USERPROFILE\.ssh\id_ed25519 -p <port> <user>@46.202.173.47`
|
||||
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`).
|
||||
@@ -93,6 +93,7 @@ class Model(
|
||||
cursor.execute(*ir_model.select(ir_model.id,
|
||||
where=ir_model.model == model.__name__))
|
||||
model_id = None
|
||||
logger.info("MODEL_NAME:%s",model.__name__)
|
||||
if cursor.rowcount == -1 or cursor.rowcount is None:
|
||||
data = cursor.fetchone()
|
||||
if data:
|
||||
|
||||
@@ -30,8 +30,8 @@ class Binary(Field):
|
||||
on_change_with=None, depends=None, context=None, loading='lazy',
|
||||
filename=None, file_id=None, store_prefix=None):
|
||||
self.filename = filename
|
||||
self.file_id = file_id
|
||||
self.store_prefix = store_prefix
|
||||
self.file_id = None #file_id
|
||||
self.store_prefix = None #store_prefix
|
||||
super(Binary, self).__init__(string=string, help=help,
|
||||
required=required, readonly=readonly, domain=domain, states=states,
|
||||
on_change=on_change, on_change_with=on_change_with,
|
||||
|
||||
@@ -103,15 +103,26 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
|
||||
|
||||
@classmethod
|
||||
def _get_name(cls):
|
||||
'''
|
||||
Returns the first non-empty line of the model docstring.
|
||||
'''
|
||||
assert cls.__doc__, '%s has no docstring' % cls
|
||||
if cls.__doc__ is None:
|
||||
print("\n💥 MODELE SANS DOCSTRING :", cls.__name__, " (module:", cls.__module__, ")")
|
||||
raise Exception("MODELE SANS DOCSTRING")
|
||||
|
||||
lines = cls.__doc__.splitlines()
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line:
|
||||
return line
|
||||
if lines:
|
||||
return lines[0]
|
||||
return cls.__name__
|
||||
|
||||
# @classmethod
|
||||
# def _get_name(cls):
|
||||
# '''
|
||||
# Returns the first non-empty line of the model docstring.
|
||||
# '''
|
||||
# assert cls.__doc__, '%s has no docstring' % cls
|
||||
# lines = cls.__doc__.splitlines()
|
||||
# for line in lines:
|
||||
# line = line.strip()
|
||||
# if line:
|
||||
# return line
|
||||
|
||||
@classmethod
|
||||
def __register__(cls, module_name):
|
||||
|
||||
@@ -117,6 +117,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
|
||||
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
|
||||
post_date = fields.Date('Post Date', readonly=True)
|
||||
description = fields.Char('Description', states=_MOVE_STATES)
|
||||
ext_ref = fields.Char('Ext. Ref')
|
||||
origin = fields.Reference('Origin', selection='get_origin',
|
||||
states=_MOVE_STATES)
|
||||
state = fields.Selection([
|
||||
@@ -921,6 +922,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
|
||||
fields.Reference("Move Origin", selection='get_move_origin'),
|
||||
'get_move_field', searcher='search_move_field')
|
||||
description = fields.Char('Description', states=_states)
|
||||
ext_ref = fields.Char('Ext. Ref')
|
||||
move_description_used = fields.Function(
|
||||
fields.Char("Move Description", states=_states),
|
||||
'get_move_field',
|
||||
|
||||
@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="origin" colspan="3"/>
|
||||
<label name="description_used"/>
|
||||
<field name="description_used" colspan="3"/>
|
||||
<label name="ext_ref"/>
|
||||
<field name="ext_ref" colspan="3"/>
|
||||
<notebook>
|
||||
<page name="lines">
|
||||
<field name="lines" colspan="4"
|
||||
|
||||
@@ -26,6 +26,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="origin"/>
|
||||
<label name="description_used"/>
|
||||
<field name="description_used" colspan="3"/>
|
||||
<label name="ext_ref"/>
|
||||
<field name="ext_ref" colspan="3"/>
|
||||
<notebook colspan="4">
|
||||
<page string="Other Info" id="info">
|
||||
<label name="date"/>
|
||||
|
||||
@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<tree editable="1">
|
||||
<field name="move"/>
|
||||
<field name="account" expand="1"/>
|
||||
<field name="ext_ref" expand="1" optional="1"/>
|
||||
<field name="party" expand="1"/>
|
||||
<field name="debit" sum="1"/>
|
||||
<field name="credit" sum="1"/>
|
||||
|
||||
@@ -1286,9 +1286,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
remainder = sum(l.debit - l.credit for l in move_lines)
|
||||
if self.payment_term:
|
||||
payment_date = self.payment_term_date or self.invoice_date or today
|
||||
purchase_line = int(str(self.lines[0].origin).split(",")[1]) if self.lines[0].origin else None
|
||||
term_lines = self.payment_term.compute(
|
||||
self.total_amount, self.currency, payment_date, purchase_line)
|
||||
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
|
||||
logger.info("MODEL:%s",model)
|
||||
if model:
|
||||
Line = Pool().get(model)
|
||||
line = Line(int(str(self.lines[0].origin).split(",")[1]))
|
||||
logger.info("LINE:%s",line)
|
||||
term_lines = self.payment_term.compute(
|
||||
self.total_amount, self.currency, payment_date, line)
|
||||
else:
|
||||
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
||||
past_payment_term_dates = []
|
||||
@@ -1960,14 +1965,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
if amount < 0:
|
||||
move_line.debit = Decimal(0)
|
||||
move_line.credit = -amount
|
||||
move_line.account = gl.product.account_stock_used
|
||||
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
|
||||
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
|
||||
move_line_.credit = Decimal(0)
|
||||
move_line_.debit = -amount
|
||||
move_line_.account = gl.product.account_stock_in_used
|
||||
else:
|
||||
move_line.debit = amount
|
||||
move_line.credit = Decimal(0)
|
||||
move_line.account = gl.product.account_stock_used
|
||||
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
|
||||
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
|
||||
move_line_.debit = Decimal(0)
|
||||
move_line_.credit = amount
|
||||
move_line_.account = gl.product.account_stock_in_used
|
||||
@@ -2031,7 +2038,11 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
var_qt = sum([i.quantity for i in gl])
|
||||
logger.info("LOT_TO_PROCESS:%s",lot)
|
||||
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
|
||||
if lot:
|
||||
if (gl[0].fee and not gl[0].product.landed_cost):
|
||||
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
|
||||
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
|
||||
Move.save([account_move])
|
||||
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
|
||||
adjust_move_lines = []
|
||||
mov = None
|
||||
if self.type == 'in':
|
||||
@@ -3684,13 +3695,19 @@ class InvoiceReport(Report):
|
||||
Invoice = pool.get('account.invoice')
|
||||
# Re-instantiate because records are TranslateModel
|
||||
invoice, = Invoice.browse(records)
|
||||
if invoice.invoice_report_cache:
|
||||
report_path = cls._get_action_report_path(action)
|
||||
use_cache = (
|
||||
report_path in (None, 'account_invoice/invoice.fodt')
|
||||
and invoice.invoice_report_cache
|
||||
)
|
||||
if use_cache:
|
||||
return (
|
||||
invoice.invoice_report_format,
|
||||
invoice.invoice_report_cache)
|
||||
else:
|
||||
result = super()._execute(records, header, data, action)
|
||||
if invoice.invoice_report_versioned:
|
||||
if (invoice.invoice_report_versioned
|
||||
and report_path in (None, 'account_invoice/invoice.fodt')):
|
||||
format_, data = result
|
||||
if isinstance(data, str):
|
||||
data = bytes(data, 'utf-8')
|
||||
@@ -3707,6 +3724,12 @@ class InvoiceReport(Report):
|
||||
with Transaction().set_context(language=False):
|
||||
return super().render(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _get_action_report_path(action):
|
||||
if isinstance(action, dict):
|
||||
return action.get('report')
|
||||
return getattr(action, 'report', None)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, ids, data):
|
||||
pool = Pool()
|
||||
|
||||
@@ -264,11 +264,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
|
||||
<field name="model">account.invoice</field>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="refresh_invoice_report_keyword">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">account.invoice,-1</field>
|
||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||
</record>
|
||||
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
|
||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||
<field name="group" ref="account.group_account_admin"/>
|
||||
@@ -318,6 +313,19 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="action" ref="report_prepayment"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.report" id="report_invoice_ict_final">
|
||||
<field name="name">CN/DN</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="report_name">account.invoice</field>
|
||||
<field name="report">account_invoice/invoice_ict_final.fodt</field>
|
||||
<field name="single" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="report_invoice_ict_final_keyword">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">account.invoice,-1</field>
|
||||
<field name="action" ref="report_invoice_ict_final"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.sequence.type" id="sequence_type_account_invoice">
|
||||
<field name="name">Invoice</field>
|
||||
</record>
|
||||
|
||||
4110
modules/account_invoice/invoice_ict.fodt
Normal file
4110
modules/account_invoice/invoice_ict.fodt
Normal file
File diff suppressed because it is too large
Load Diff
4095
modules/account_invoice/invoice_ict_final.fodt
Normal file
4095
modules/account_invoice/invoice_ict_final.fodt
Normal file
File diff suppressed because it is too large
Load Diff
1708
modules/account_invoice/invoice_melya.fodt
Normal file
1708
modules/account_invoice/invoice_melya.fodt
Normal file
File diff suppressed because it is too large
Load Diff
1872
modules/account_invoice/payment_order.fodt
Normal file
1872
modules/account_invoice/payment_order.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.transaction import Transaction
|
||||
from trytond.wizard import Button, StateView, Wizard
|
||||
|
||||
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
||||
@@ -46,7 +48,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
||||
'.msg_payment_term_missing_last_remainder',
|
||||
payment_term=term.rec_name))
|
||||
|
||||
def compute(self, amount, currency, date, purchase_line = None):
|
||||
def compute(self, amount, currency, date, line_ = None):
|
||||
"""Calculate payment terms and return a list of tuples
|
||||
with (date, amount) for each payment term line.
|
||||
|
||||
@@ -59,7 +61,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
||||
remainder = amount
|
||||
for line in self.lines:
|
||||
value = line.get_value(remainder, amount, currency)
|
||||
value_date = line.get_date(date, purchase_line)
|
||||
value_date = line.get_date(date, line_)
|
||||
if value is None or not value_date:
|
||||
continue
|
||||
if ((remainder - value) * sign) < Decimal(0):
|
||||
@@ -155,12 +157,11 @@ class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView):
|
||||
self.ratio = self.round(1 / self.divisor,
|
||||
self.__class__.ratio.digits[1])
|
||||
|
||||
def get_date(self, date, purchase_line = None):
|
||||
def get_date(self, date, line = None):
|
||||
#find date based on trigger:
|
||||
if purchase_line and self.trigger_event:
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
purchase_line = PurchaseLine(purchase_line)
|
||||
trigger_date = purchase_line.get_date(self.trigger_event)
|
||||
if line and self.trigger_event:
|
||||
trigger_date = line.get_date(self.trigger_event)
|
||||
logger.info("DATE_FROM_LINE:%s",trigger_date)
|
||||
if trigger_date:
|
||||
date = trigger_date
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="invoice_report_revisions" colspan="4"/>
|
||||
</page>
|
||||
<page string="Rate management" id="rate">
|
||||
<label name="warning"/>
|
||||
<field name="warning"/>
|
||||
<!-- <label name="warning"/>
|
||||
<field name="warning"/> -->
|
||||
<newline/>
|
||||
<label name="rate"/>
|
||||
<field name="rate"/>
|
||||
|
||||
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.pool import Pool, PoolMeta
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
from trytond.exceptions import UserWarning, UserError
|
||||
from .exceptions import COGSWarning
|
||||
import logging
|
||||
|
||||
@@ -74,8 +74,8 @@ class InvoiceLine(metaclass=PoolMeta):
|
||||
if move_line.second_currency:
|
||||
move_line.amount_second_currency = amount
|
||||
else:
|
||||
move_line.debit = Decimal(0)
|
||||
move_line.credit = -amount_converted
|
||||
move_line.debit = -amount_converted
|
||||
move_line.credit = Decimal(0)
|
||||
move_line.account = self.product.account_stock_out_used
|
||||
if move_line.second_currency:
|
||||
move_line.amount_second_currency = amount
|
||||
@@ -171,10 +171,28 @@ class InvoiceLine(metaclass=PoolMeta):
|
||||
cost = self.amount
|
||||
else:
|
||||
cost = self.lot.get_cog()
|
||||
if not cost or cost == 0:
|
||||
raise UserError('No COG for this invoice, please generate the reception of the goods')
|
||||
if self.amount < 0 :
|
||||
cost *= -1
|
||||
logger.info("GETMOVELINES_COST:%s",cost)
|
||||
anglo_saxon_move_lines_ = []
|
||||
with Transaction().set_context(
|
||||
company=self.invoice.company.id, date=accounting_date):
|
||||
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
|
||||
cost, type_)
|
||||
if type_ == 'in_supplier' and (self.lot.sale_invoice_line_prov or self.lot.sale_invoice_line) and not self.fee:
|
||||
anglo_saxon_move_lines_ = self._get_anglo_saxon_move_lines(cost, 'out_customer')
|
||||
result.extend(anglo_saxon_move_lines)
|
||||
result.extend(anglo_saxon_move_lines_)
|
||||
#Fee inventoried delivery management
|
||||
if self.lot and type_ != 'in_supplier':
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
fees = FeeLots.search(['lot','=',self.lot.id])
|
||||
for fl in fees:
|
||||
if fl.fee.type == 'ordered' and fl.fee.product.template.landed_cost:
|
||||
AccountMove = Pool().get('account.move')
|
||||
account_move = fl.fee._get_account_move_fee(fl.lot,'out')
|
||||
AccountMove.save([account_move])
|
||||
|
||||
return result
|
||||
|
||||
@@ -16,11 +16,11 @@ account_names = [
|
||||
class Category(metaclass=PoolMeta):
|
||||
__name__ = 'product.category'
|
||||
account_stock = fields.MultiValue(fields.Many2One(
|
||||
'account.account', "Account Stock",
|
||||
'account.account', "Account Stock/Cost Income",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.stock', '=', True),
|
||||
('type.statement', '=', 'balance'),
|
||||
# ('type.stock', '=', True),
|
||||
# ('type.statement', '=', 'balance'),
|
||||
('company', '=', Eval('context', {}).get('company', -1)),
|
||||
],
|
||||
states={
|
||||
@@ -29,7 +29,7 @@ class Category(metaclass=PoolMeta):
|
||||
| ~Eval('accounting', False)),
|
||||
}))
|
||||
account_stock_in = fields.MultiValue(fields.Many2One(
|
||||
'account.account', "Account Stock IN",
|
||||
'account.account', "Account Stock IN/Cost liability",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.stock', '=', True),
|
||||
@@ -41,7 +41,7 @@ class Category(metaclass=PoolMeta):
|
||||
| ~Eval('accounting', False)),
|
||||
}))
|
||||
account_stock_out = fields.MultiValue(fields.Many2One(
|
||||
'account.account', "Account Stock OUT",
|
||||
'account.account', "Account Stock OUT/Cost liability",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.stock', '=', True),
|
||||
@@ -103,8 +103,8 @@ class CategoryAccount(metaclass=PoolMeta):
|
||||
'account.account', "Account Stock",
|
||||
domain=[
|
||||
('closed', '!=', True),
|
||||
('type.stock', '=', True),
|
||||
('type.statement', '=', 'balance'),
|
||||
# ('type.stock', '=', True),
|
||||
# ('type.statement', '=', 'balance'),
|
||||
('company', '=', Eval('company', -1)),
|
||||
])
|
||||
account_stock_in = fields.Many2One(
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from trytond.pool import Pool
|
||||
from . import automation,rules #, document
|
||||
from . import automation,rules,freight_booking,cron #, document
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
automation.AutomationDocument,
|
||||
rules.AutomationRuleSet,
|
||||
freight_booking.FreightBookingInfo,
|
||||
cron.Cron,
|
||||
cron.AutomationCron,
|
||||
module='automation', type_='model')
|
||||
@@ -1,10 +1,15 @@
|
||||
from trytond.model import ModelSQL, ModelView, fields, Workflow
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
from trytond.wizard import Button
|
||||
from trytond.transaction import Transaction
|
||||
from sql import Table
|
||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
import requests
|
||||
import io
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +22,7 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
||||
('invoice', 'Invoice'),
|
||||
('statement_of_facts', 'Statement of Facts'),
|
||||
('weight_report', 'Weight Report'),
|
||||
('controller', 'Controller'),
|
||||
('bol', 'Bill of Lading'),
|
||||
('controller_invoice', 'Controller Invoice'),
|
||||
], 'Type')
|
||||
@@ -57,25 +63,53 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
||||
def run_ocr(cls, docs):
|
||||
for doc in docs:
|
||||
try:
|
||||
# Décoder le fichier depuis le champ Binary
|
||||
file_data = doc.document.data or b""
|
||||
logger.info(f"File size: {len(file_data)} bytes")
|
||||
logger.info(f"First 20 bytes: {file_data[:20]}")
|
||||
logger.info(f"Last 20 bytes: {file_data[-20:]}")
|
||||
if doc.type == 'weight_report':
|
||||
# Décoder le fichier depuis le champ Binary
|
||||
file_data = doc.document.data or b""
|
||||
logger.info(f"File size: {len(file_data)} bytes")
|
||||
logger.info(f"First 20 bytes: {file_data[:20]}")
|
||||
logger.info(f"Last 20 bytes: {file_data[-20:]}")
|
||||
|
||||
file_name = doc.document.name or "document"
|
||||
file_name = doc.document.name or "document"
|
||||
|
||||
# Envoyer le fichier au service OCR
|
||||
response = requests.post(
|
||||
"http://automation-service:8006/ocr",
|
||||
files={"file": (file_name, io.BytesIO(file_data))}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
logger.info("RUN_OCR_RESPONSE:%s",data)
|
||||
doc.ocr_text = data.get("ocr_text", "")
|
||||
doc.state = "ocr_done"
|
||||
doc.notes = (doc.notes or "") + "OCR done\n"
|
||||
# Envoyer le fichier au service OCR
|
||||
response = requests.post(
|
||||
"http://automation-service:8006/ocr",
|
||||
files={"file": (file_name, io.BytesIO(file_data))}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
logger.info("RUN_OCR_RESPONSE:%s",data)
|
||||
doc.ocr_text = data.get("ocr_text", "")
|
||||
doc.state = "ocr_done"
|
||||
doc.notes = (doc.notes or "") + "OCR done\n"
|
||||
else:
|
||||
doc.ocr_text = (doc.document.data or b"").decode('utf-8', errors='replace')
|
||||
match = re.search(r"\bID\s*:\s*(\d+)", doc.ocr_text)
|
||||
if match:
|
||||
request_id = match.group(1)
|
||||
match = re.search(r"\bBL\s*number\s*:\s*([A-Za-z0-9_-]+)", doc.ocr_text, re.IGNORECASE)
|
||||
if match:
|
||||
bl_number = match.group(1)
|
||||
ShipmentIn = Pool().get('stock.shipment.in')
|
||||
sh = ShipmentIn.search(['bl_number','=',bl_number])
|
||||
if sh:
|
||||
sh[0].returned_id = request_id
|
||||
ShipmentIn.save(sh)
|
||||
doc.notes = (doc.notes or "") + "Id returned: " + request_id
|
||||
|
||||
so_payload = {
|
||||
"ServiceOrderKey": sh[0].service_order_key,
|
||||
"ID_Number": request_id
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
"http://automation-service:8006/service-order-update",
|
||||
json=so_payload,
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
doc.notes = (doc.notes or "") + " SO updated"
|
||||
|
||||
except Exception as e:
|
||||
doc.state = "error"
|
||||
@@ -154,7 +188,8 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
||||
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
|
||||
|
||||
response = requests.post(
|
||||
"http://automation-service:8006/metadata",
|
||||
#"http://automation-service:8006/metadata",
|
||||
"http://automation-service:8006/parse",
|
||||
json={"text": doc.ocr_text or ""}
|
||||
)
|
||||
response.raise_for_status()
|
||||
@@ -176,6 +211,18 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
||||
logger.error("Metadata processing error: %s", e)
|
||||
|
||||
doc.save()
|
||||
|
||||
def create_weight_report(self,wr_payload):
|
||||
|
||||
response = requests.post(
|
||||
"http://automation-service:8006/weight-report",
|
||||
json=wr_payload, # 👈 ICI la correction
|
||||
timeout=10
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# -------------------------------------------------------
|
||||
# FULL PIPELINE
|
||||
# -------------------------------------------------------
|
||||
@@ -184,18 +231,47 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
||||
def run_pipeline(cls, docs):
|
||||
for doc in docs:
|
||||
try:
|
||||
if cls.rule_set.ocr_required:
|
||||
cls.run_ocr([doc])
|
||||
if cls.rule_set.structure_required and doc.state != "error":
|
||||
cls.run_structure([doc])
|
||||
if cls.rule_set.table_required and doc.state != "error":
|
||||
cls.run_tables([doc])
|
||||
if cls.rule_set.metadata_required and doc.state != "error":
|
||||
cls.run_metadata([doc])
|
||||
if doc.state != "error":
|
||||
doc.state = "validated"
|
||||
doc.notes = (doc.notes or "") + "Pipeline completed\n"
|
||||
logger.info("DATA_TYPE:%s",type(doc.metadata_json))
|
||||
metadata = json.loads(str(doc.metadata_json))
|
||||
logger.info("JSON STRUCTURE:%s",metadata)
|
||||
|
||||
WeightReport = Pool().get('weight.report')
|
||||
wr = WeightReport.create_from_json(metadata)
|
||||
|
||||
ShipmentIn = Pool().get('stock.shipment.in')
|
||||
ShipmentWR = Pool().get('shipment.wr')
|
||||
sh = ShipmentIn.search([('bl_number','ilike',wr.bl_no)])
|
||||
if sh:
|
||||
swr = ShipmentWR()
|
||||
swr.shipment_in = sh[0]
|
||||
swr.wr = wr
|
||||
ShipmentWR.save([swr])
|
||||
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
|
||||
logger.info("BL_NUMBER:%s",sh[0].bl_number)
|
||||
doc.notes = (
|
||||
(doc.notes or "")
|
||||
+ "Global WR linked to shipment. "
|
||||
+ "Create remote lot WRs from the weight report form.\n")
|
||||
|
||||
# if cls.rule_set.ocr_required:[]
|
||||
# cls.run_ocr([doc])
|
||||
# if cls.rule_set.structure_required and doc.state != "error":
|
||||
# cls.run_structure([doc])
|
||||
# if cls.rule_set.table_required and doc.state != "error":
|
||||
# cls.run_tables([doc])
|
||||
# if cls.rule_set.metadata_required and doc.state != "error":
|
||||
# cls.run_metadata([doc])
|
||||
# if doc.state != "error":
|
||||
# doc.state = "validated"
|
||||
# doc.notes = (doc.notes or "") + "Pipeline completed\n"
|
||||
except Exception as e:
|
||||
logger.exception("PIPELINE FAILED") # 👈 TRACE COMPLETE
|
||||
doc.state = "error"
|
||||
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
||||
doc.save()
|
||||
doc.save()
|
||||
raise
|
||||
|
||||
# except Exception as e:
|
||||
# doc.state = "error"
|
||||
# doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
||||
doc.save()
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<record model="ir.model.button" id="auto_button1">
|
||||
<field name="model">automation.document</field>
|
||||
<field name="name">run_pipeline</field>
|
||||
<field name="string">Run Full Pipeline</field>
|
||||
<field name="string">Create Weight Report</field>
|
||||
</record>
|
||||
<record model="ir.model.button" id="auto_button2">
|
||||
<field name="model">automation.document</field>
|
||||
|
||||
377
modules/automation/cron.py
Normal file
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
|
||||
document_incoming
|
||||
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()
|
||||
return invoice
|
||||
|
||||
@property
|
||||
def report_agent(self):
|
||||
if self.agent:
|
||||
return (self.agent.party.address_get(
|
||||
type='delivery')).full_address
|
||||
else:
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('quotation')
|
||||
|
||||
@@ -47,6 +47,8 @@ class Company(ModelSQL, ModelView):
|
||||
help="Used to compute the today date.")
|
||||
employees = fields.One2Many('company.employee', 'company', 'Employees',
|
||||
help="Add employees to the company.")
|
||||
|
||||
logo = fields.Binary("Logo")
|
||||
|
||||
@property
|
||||
def header_used(self):
|
||||
|
||||
@@ -17,6 +17,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="header"/>
|
||||
<separator name="footer"/>
|
||||
<field name="footer"/>
|
||||
<separator name="logo"/>
|
||||
<field name="logo" widget="image" stretch="true"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
|
||||
@@ -137,6 +137,13 @@ class Currency(
|
||||
closer = date
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name):
|
||||
currencies = cls.search([('symbol', '=', name)], limit=1)
|
||||
if not currencies:
|
||||
return None
|
||||
return currencies[0]
|
||||
|
||||
@staticmethod
|
||||
def _get_rate(currencies, tdate=None):
|
||||
'''
|
||||
|
||||
@@ -17,6 +17,8 @@ from trytond.transaction import Transaction
|
||||
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||
|
||||
from .exceptions import DocumentIncomingSplitError
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if config.getboolean('document_incoming', 'filestore', default=True):
|
||||
file_id = 'file_id'
|
||||
@@ -179,30 +181,112 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
||||
def _split_mime_types(cls):
|
||||
return ['application/pdf']
|
||||
|
||||
# @classmethod
|
||||
# def from_inbound_email(cls, email_, rule):
|
||||
# message = email_.as_dict()
|
||||
# attachments = message.get('attachments')
|
||||
# active = False
|
||||
# data = message.get('text', message.get('html'))
|
||||
# logger.info("DATA_FROM_INBOUND_MAIL:%s",data)
|
||||
# if isinstance(data, str):
|
||||
# data = data.encode()
|
||||
# body = message.get('text') or message.get('html') or ''
|
||||
# if isinstance(body, str):
|
||||
# body_bytes = body.encode('utf-8')
|
||||
# else:
|
||||
# body_bytes = body
|
||||
# document = cls(
|
||||
# active=active,
|
||||
# name=message.get('subject', 'No Subject'),
|
||||
# company=rule.document_incoming_company,
|
||||
# data=data,
|
||||
# type=rule.document_incoming_type if active else None,
|
||||
# source='inbound_email',
|
||||
# )
|
||||
# children = []
|
||||
# if attachments:
|
||||
# for attachment in attachments:
|
||||
# child = cls(
|
||||
# name=attachment['filename'] or 'data.bin',
|
||||
# company=rule.document_incoming_company,
|
||||
# data=attachment['data'],
|
||||
# type=rule.document_incoming_type,
|
||||
# source='inbound_email')
|
||||
# children.append(child)
|
||||
# else:
|
||||
# child = cls(
|
||||
# name='mail_' + message.get('subject', 'No Subject') + '.txt',
|
||||
# company=rule.document_incoming_company,
|
||||
# data=body_bytes,
|
||||
# type=rule.document_incoming_type,
|
||||
# source='inbound_email',
|
||||
# )
|
||||
# children.append(child)
|
||||
# document.children = children
|
||||
# document.save()
|
||||
# return document
|
||||
@classmethod
|
||||
def from_inbound_email(cls, email_, rule):
|
||||
message = email_.as_dict()
|
||||
active = not message.get('attachments')
|
||||
|
||||
def clean(value):
|
||||
if not value:
|
||||
return value
|
||||
return (
|
||||
value
|
||||
.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.replace("'", '')
|
||||
.replace('"', '')
|
||||
.strip()
|
||||
)
|
||||
|
||||
subject = clean(message.get('subject', 'No Subject'))
|
||||
|
||||
attachments = message.get('attachments')
|
||||
active = False
|
||||
data = message.get('text', message.get('html'))
|
||||
logger.info("DATA_FROM_INBOUND_MAIL:%s", data)
|
||||
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
|
||||
body = message.get('text') or message.get('html') or ''
|
||||
if isinstance(body, str):
|
||||
body_bytes = body.encode('utf-8')
|
||||
else:
|
||||
body_bytes = body
|
||||
|
||||
document = cls(
|
||||
active=active,
|
||||
name=message.get('subject', 'No Subject'),
|
||||
name=subject,
|
||||
company=rule.document_incoming_company,
|
||||
data=data,
|
||||
type=rule.document_incoming_type if active else None,
|
||||
source='inbound_email',
|
||||
)
|
||||
)
|
||||
|
||||
children = []
|
||||
for attachment in message.get('attachments', []):
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
filename = clean(attachment['filename'] or 'data.bin')
|
||||
child = cls(
|
||||
name=filename,
|
||||
company=rule.document_incoming_company,
|
||||
data=attachment['data'],
|
||||
type=rule.document_incoming_type,
|
||||
source='inbound_email')
|
||||
children.append(child)
|
||||
else:
|
||||
child = cls(
|
||||
name=attachment['filename'] or 'data.bin',
|
||||
name='mail_' + subject + '.txt',
|
||||
company=rule.document_incoming_company,
|
||||
data=attachment['data'],
|
||||
data=body_bytes,
|
||||
type=rule.document_incoming_type,
|
||||
source='inbound_email')
|
||||
source='inbound_email',
|
||||
)
|
||||
children.append(child)
|
||||
|
||||
document.children = children
|
||||
document.save()
|
||||
return document
|
||||
@@ -265,7 +349,6 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
||||
default.setdefault('children')
|
||||
return super().copy(documents, default=default)
|
||||
|
||||
|
||||
def iter_pages(expression, size):
|
||||
ranges = set()
|
||||
for pages in expression.split(','):
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
from trytond.model import fields
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Rule(metaclass=PoolMeta):
|
||||
__name__ = 'inbound.email.rule'
|
||||
@@ -53,4 +54,4 @@ class Rule(metaclass=PoolMeta):
|
||||
if (self.action == 'document.incoming|from_inbound_email'
|
||||
and self.document_incoming_process):
|
||||
document = email_.result
|
||||
DocumentIncoming.process([document], with_children=True)
|
||||
DocumentIncoming.process([document], with_children=True)
|
||||
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.url import http_host
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if config.getboolean('inbound_email', 'filestore', default=True):
|
||||
file_id = 'data_id'
|
||||
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
|
||||
@@ -74,6 +77,7 @@ class Inbox(ModelSQL, ModelView):
|
||||
assert email_.inbox == self
|
||||
for rule in self.rules:
|
||||
if rule.match(email_.as_dict()):
|
||||
logger.info("RULE_MATCHED:%s",rule)
|
||||
email_.rule = rule
|
||||
rule.run(email_)
|
||||
return
|
||||
|
||||
@@ -12,7 +12,7 @@ __all__ = ['IncotermMixin', 'IncotermAvailableMixin']
|
||||
class IncotermMixin(Model):
|
||||
|
||||
incoterm = fields.Many2One(
|
||||
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'),
|
||||
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'), required=False,
|
||||
ondelete='RESTRICT')
|
||||
incoterm_location = fields.Many2One(
|
||||
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),
|
||||
|
||||
@@ -477,15 +477,16 @@ class Lot(ModelSQL, ModelView):
|
||||
else:
|
||||
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
||||
|
||||
def get_hist_quantity(self,seq):
|
||||
def get_hist_quantity(self,state_id=0):
|
||||
qt = Decimal(0)
|
||||
gross_qt = Decimal(0)
|
||||
if self.lot_state:
|
||||
if self.lot_hist:
|
||||
if seq != 0:
|
||||
st = seq
|
||||
if state_id != 0:
|
||||
st = state_id
|
||||
else:
|
||||
st = self.lot_state.id
|
||||
logger.info("GET_HIST_QT:%s",st)
|
||||
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
|
||||
qt = round(lot.quantity,5)
|
||||
gross_qt = round(lot.gross_quantity,5)
|
||||
@@ -499,24 +500,48 @@ class Lot(ModelSQL, ModelView):
|
||||
physic_sum = Decimal(0)
|
||||
for l in line.lots:
|
||||
if l.lot_type == 'physic' :
|
||||
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit)),5)
|
||||
factor = None
|
||||
rate = None
|
||||
if l.lot_unit_line.category.id != l.line.unit.category.id:
|
||||
factor = 1
|
||||
rate = 1
|
||||
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit, True, factor, rate)),5)
|
||||
return line.quantity_theorical - physic_sum
|
||||
|
||||
def get_current_quantity(self,name=None):
|
||||
# if self.lot_type == 'physic':
|
||||
qt, gross_qt = self.get_hist_quantity(0)
|
||||
qt, gross_qt = self.get_hist_quantity()
|
||||
return qt
|
||||
# else:
|
||||
# return self.get_virtual_diff()
|
||||
|
||||
def get_current_quantity_converted(self,name=None):
|
||||
def get_current_quantity_converted(self,state_id=0,unit=None):
|
||||
Uom = Pool().get('product.uom')
|
||||
unit = self.line.unit if self.line else self.sale_line.unit
|
||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
|
||||
if not unit:
|
||||
unit = self.line.unit if self.line else self.sale_line.unit
|
||||
qt, gross_qt = self.get_hist_quantity(state_id)
|
||||
factor = None
|
||||
rate = None
|
||||
if self.lot_unit_line.category.id != unit.category.id:
|
||||
factor = 1
|
||||
rate = 1
|
||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(qt), unit, True, factor, rate)),5)
|
||||
|
||||
def get_current_gross_quantity_converted(self,state_id=0,unit=None):
|
||||
Uom = Pool().get('product.uom')
|
||||
if not unit:
|
||||
unit = self.line.unit if self.line else self.sale_line.unit
|
||||
qt, gross_qt = self.get_hist_quantity(state_id)
|
||||
factor = None
|
||||
rate = None
|
||||
if self.lot_unit_line.category.id != unit.category.id:
|
||||
factor = 1
|
||||
rate = 1
|
||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(gross_qt), unit, True, factor, rate)),5)
|
||||
|
||||
def get_current_gross_quantity(self,name=None):
|
||||
if self.lot_type == 'physic':
|
||||
qt, gross_qt = self.get_hist_quantity(0)
|
||||
qt, gross_qt = self.get_hist_quantity()
|
||||
return gross_qt
|
||||
else:
|
||||
return None
|
||||
@@ -526,6 +551,7 @@ class Lot(ModelSQL, ModelView):
|
||||
lqh = LotQtHist()
|
||||
lqh.quantity_type = qt_type
|
||||
lqh.quantity = net
|
||||
logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
|
||||
lqh.gross_quantity = gross
|
||||
lqh.lot = self
|
||||
return lqh
|
||||
@@ -542,6 +568,7 @@ class Lot(ModelSQL, ModelView):
|
||||
if existing:
|
||||
hist = existing[0]
|
||||
hist.quantity = net
|
||||
logger.info("SET_CURRENT_HIST:%s",gross)
|
||||
hist.gross_quantity = gross
|
||||
else:
|
||||
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
|
||||
@@ -633,6 +660,7 @@ class SplitLine(ModelView):
|
||||
weight = fields.Numeric('Weight', digits=(16,5))
|
||||
|
||||
class SplitWizardStart(ModelView):
|
||||
"Split Line Start"
|
||||
__name__ = 'lot.split.wizard.start'
|
||||
|
||||
mode = fields.Selection([
|
||||
|
||||
@@ -44,7 +44,7 @@ class Price(
|
||||
price_composite = fields.One2Many('price.composite','price',"Composites")
|
||||
price_product = fields.One2Many('price.product', 'price', "Product")
|
||||
price_ct_size = fields.Numeric("Ct size")
|
||||
|
||||
|
||||
def get_qt(self,nb_ct,unit):
|
||||
Uom = Pool().get('product.uom')
|
||||
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
|
||||
@@ -71,7 +71,6 @@ class Price(
|
||||
def get_price(self,dt,unit,currency,last=False):
|
||||
price = float(0)
|
||||
PV = Pool().get('price.price_value')
|
||||
logger.info("ASKED_PRICE_FOR:%s",dt)
|
||||
if self.price_values:
|
||||
dt = dt.strftime("%Y-%m-%d")
|
||||
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
|
||||
@@ -115,7 +114,6 @@ class Calendar(DeactivableMixin,ModelSQL,ModelView,MultiValueMixin):
|
||||
dt = dt.strftime("%Y-%m-%d")
|
||||
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
|
||||
if cl:
|
||||
#logger.info("ISQUOTE:%s",cl)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -136,3 +134,20 @@ class Product(ModelSQL,ModelView):
|
||||
__name__ = 'price.product'
|
||||
price = fields.Many2One('price.price',"Price index")
|
||||
product = fields.Many2One('product.product',"Product")
|
||||
attributes = fields.Many2One('product.attribute',"Attribute",domain=[
|
||||
('sets', '=', Eval('attribute_set')),
|
||||
],
|
||||
states={
|
||||
'readonly': ~Eval('attribute_set'),
|
||||
},
|
||||
depends=['product', 'attribute_set'])
|
||||
attribute_set = fields.Function(
|
||||
fields.Many2One('product.attribute.set', "Attribute Set"),
|
||||
'on_change_with_attribute_set'
|
||||
)
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_with_attribute_set(self, name=None):
|
||||
if self.product and self.product.template and self.product.template.attribute_set:
|
||||
return self.product.template.attribute_set.id
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<form>
|
||||
<label name="price"/>
|
||||
<field name="price"/>
|
||||
<label name="product"/>
|
||||
<field name="product"/>
|
||||
<label name="attributes"/>
|
||||
<field name="attributes"/>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<tree>
|
||||
<field name="price"/>
|
||||
<field name="product"/>
|
||||
<field name="attributes"/>
|
||||
</tree>
|
||||
|
||||
@@ -609,6 +609,26 @@ class Product(
|
||||
('template.code', operator, code_value, *extra),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name, type_='goods'):
|
||||
pool = Pool()
|
||||
Template = pool.get('product.template')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
templates = Template.search([('name', '=', name)], limit=1)
|
||||
if templates:
|
||||
return templates[0].products[0]
|
||||
|
||||
unit_uom, = Uom.search([('name', '=', 'Mt')], limit=1)
|
||||
|
||||
template, = Template.create([{
|
||||
'name': name,
|
||||
'type': type_,
|
||||
'default_uom': unit_uom.id,
|
||||
'cost_price_method': 'fixed',
|
||||
}])
|
||||
return template.products[0]
|
||||
|
||||
@staticmethod
|
||||
def get_price_uom(products, name):
|
||||
Uom = Pool().get('product.uom')
|
||||
|
||||
@@ -92,6 +92,13 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
|
||||
def default_digits():
|
||||
return 2
|
||||
|
||||
@classmethod
|
||||
def get_by_name(cls, name):
|
||||
uom = cls.search([('symbol', '=', name)], limit=1)
|
||||
if not uom:
|
||||
return None
|
||||
return uom[0]
|
||||
|
||||
@fields.depends('factor')
|
||||
def on_change_factor(self):
|
||||
if (self.factor or 0.0) == 0.0:
|
||||
|
||||
@@ -22,6 +22,7 @@ class Month(ModelView, ModelSQL):
|
||||
is_cotation = fields.Boolean("Cotation month")
|
||||
beg_date = fields.Date("Date from")
|
||||
end_date = fields.Date("Date end")
|
||||
description = fields.Char("Description")
|
||||
|
||||
class ProductMonth(ModelView, ModelSQL):
|
||||
"Product month"
|
||||
|
||||
105
modules/purchase/AGENTS.md
Normal file
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)
|
||||
reference = fields.Char("Reference")
|
||||
description = fields.Char('Description', size=None, states=_states)
|
||||
purchase_date = fields.Date('Purchase Date',
|
||||
purchase_date = fields.Date('Purchase Date', required=True,
|
||||
states={
|
||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||
'required': ~Eval('state').in_(
|
||||
['draft', 'quotation', 'cancelled']),
|
||||
})
|
||||
payment_term = fields.Many2One(
|
||||
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
|
||||
'account.invoice.payment_term', "Payment Term", required=True, ondelete='RESTRICT',
|
||||
states={
|
||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||
})
|
||||
@@ -389,6 +389,11 @@ class Purchase(
|
||||
def default_state():
|
||||
return 'draft'
|
||||
|
||||
@classmethod
|
||||
def default_purchase_date(cls):
|
||||
Date = Pool().get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
@classmethod
|
||||
def default_currency(cls, **pattern):
|
||||
pool = Pool()
|
||||
@@ -462,6 +467,8 @@ class Purchase(
|
||||
self.tol_min = self.party.tol_min
|
||||
if self.party.tol_max:
|
||||
self.tol_max = self.party.tol_max
|
||||
if self.party.origin:
|
||||
self.product_origin = self.party.origin
|
||||
if self.party.wb:
|
||||
self.wb = self.party.wb
|
||||
if self.party.association:
|
||||
@@ -734,6 +741,7 @@ class Purchase(
|
||||
|
||||
@classmethod
|
||||
def copy(cls, purchases, default=None):
|
||||
Date = Pool().get('ir.date')
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
@@ -742,7 +750,7 @@ class Purchase(
|
||||
default.setdefault('invoice_state', 'none')
|
||||
default.setdefault('invoices_ignored', None)
|
||||
default.setdefault('shipment_state', 'none')
|
||||
default.setdefault('purchase_date', None)
|
||||
default.setdefault('purchase_date', Date.today())
|
||||
default.setdefault('quoted_by')
|
||||
default.setdefault('confirmed_by')
|
||||
default.setdefault('untaxed_amount_cache')
|
||||
@@ -1021,9 +1029,15 @@ class Purchase(
|
||||
pool = Pool()
|
||||
Invoice = pool.get('account.invoice')
|
||||
|
||||
Invoice.save(invoices.values())
|
||||
|
||||
Invoice.save(invoices.values())
|
||||
|
||||
for purchase, invoice in invoices.items():
|
||||
#check if forex
|
||||
forex_rate = invoice.get_forex()
|
||||
if forex_rate:
|
||||
invoice.selection_rate = 'forex'
|
||||
invoice.rate = invoice.on_change_with_rate()
|
||||
Invoice.save([invoice])
|
||||
purchase.copy_resources_to(invoice)
|
||||
if len(invoices)==1:
|
||||
if prepayment:
|
||||
@@ -1215,7 +1229,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
()),
|
||||
If(Eval('type') != 'line',
|
||||
('id', '=', None),
|
||||
()),
|
||||
())
|
||||
],
|
||||
states={
|
||||
'invisible': Eval('type') != 'line',
|
||||
@@ -1685,7 +1699,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
|
||||
@fields.depends(
|
||||
'type', 'quantity', 'unit_price',
|
||||
'purchase', '_parent_purchase.currency')
|
||||
'purchase', '_parent_purchase.currency','premium')
|
||||
def on_change_with_amount(self):
|
||||
if (self.type == 'line'
|
||||
and self.quantity is not None
|
||||
@@ -1857,77 +1871,93 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
else:
|
||||
lots_to_invoice = self.lots
|
||||
for l in lots_to_invoice:
|
||||
#if l.lot_type == 'physic':
|
||||
invoice_line = InvoiceLine()
|
||||
invoice_line.type = self.type
|
||||
invoice_line.currency = self.currency
|
||||
invoice_line.company = self.company
|
||||
invoice_line.description = self.description
|
||||
invoice_line.note = self.note
|
||||
invoice_line.origin = self
|
||||
qt, gross_qt = l.get_hist_quantity(0)
|
||||
quantity = float(qt)
|
||||
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
||||
if self.unit:
|
||||
quantity = self.unit.round(quantity)
|
||||
invoice_line.unit_price = l.get_lot_price()
|
||||
invoice_line.product = l.lot_product
|
||||
invoice_line.quantity = quantity
|
||||
if not invoice_line.quantity:
|
||||
return []
|
||||
invoice_line.unit = self.unit
|
||||
invoice_line.taxes = self.taxes
|
||||
if self.company.purchase_taxes_expense:
|
||||
invoice_line.taxes_deductible_rate = 0
|
||||
elif self.product:
|
||||
invoice_line.taxes_deductible_rate = (
|
||||
self.product.supplier_taxes_deductible_rate_used)
|
||||
invoice_line.invoice_type = 'in'
|
||||
if self.product:
|
||||
invoice_line.account = self.product.account_stock_in_used
|
||||
if not invoice_line.account:
|
||||
raise AccountError(
|
||||
gettext('purchase'
|
||||
'.msg_purchase_product_missing_account_expense',
|
||||
purchase=self.purchase.rec_name,
|
||||
product=self.product.rec_name))
|
||||
else:
|
||||
invoice_line.account = account_config.get_multivalue(
|
||||
'default_category_account_expense', company=self.company.id)
|
||||
if not invoice_line.account:
|
||||
raise AccountError(
|
||||
gettext('purchase'
|
||||
'.msg_purchase_missing_account_expense',
|
||||
purchase=self.purchase.rec_name))
|
||||
if action == 'prov':
|
||||
invoice_line.description = 'Pro forma'
|
||||
elif action == 'final':
|
||||
invoice_line.description = 'Final'
|
||||
elif action == 'service':
|
||||
invoice_line.description = 'Service'
|
||||
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
||||
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
||||
invoice_line.lot = l.id
|
||||
if self.product.type == 'service':
|
||||
invoice_line.unit_price = self.unit_price
|
||||
invoice_line.product = self.product
|
||||
invoice_line.stock_moves = []
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||
if fee:
|
||||
invoice_line.fee = fee[0]
|
||||
lines.append(invoice_line)
|
||||
logger.info("GETINVLINE:%s",self.product.type)
|
||||
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
|
||||
if l.invoice_line_prov and self.product.type != 'service':
|
||||
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
|
||||
'invoice': None,
|
||||
'quantity': -l.invoice_line_prov.quantity,
|
||||
'unit_price': l.invoice_line_prov.unit_price,
|
||||
'party': l.invoice_line_prov.invoice.party,
|
||||
'origin': str(self),
|
||||
})
|
||||
lines.append(invoice_line_)
|
||||
if l.lot_type == 'physic':
|
||||
invoice_line = InvoiceLine()
|
||||
invoice_line.type = self.type
|
||||
invoice_line.currency = self.currency
|
||||
invoice_line.company = self.company
|
||||
invoice_line.description = self.description
|
||||
invoice_line.note = self.note
|
||||
invoice_line.origin = self
|
||||
qt, gross_qt = l.get_hist_quantity(0)
|
||||
quantity = float(qt)
|
||||
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
||||
if self.unit:
|
||||
quantity = self.unit.round(quantity)
|
||||
invoice_line.unit_price = l.get_lot_price()
|
||||
invoice_line.product = l.lot_product
|
||||
invoice_line.quantity = quantity
|
||||
logger.info("GETINVOICELINE_QT:%s",quantity)
|
||||
if not invoice_line.quantity:
|
||||
return []
|
||||
invoice_line.unit = self.unit
|
||||
invoice_line.taxes = self.taxes
|
||||
if self.company.purchase_taxes_expense:
|
||||
invoice_line.taxes_deductible_rate = 0
|
||||
elif self.product:
|
||||
invoice_line.taxes_deductible_rate = (
|
||||
self.product.supplier_taxes_deductible_rate_used)
|
||||
invoice_line.invoice_type = 'in'
|
||||
if self.product:
|
||||
if self.product.type == 'service' and not self.product.landed_cost:
|
||||
invoice_line.account = self.product.account_stock_in_used
|
||||
else:
|
||||
invoice_line.account = self.product.account_stock_in_used
|
||||
if not invoice_line.account:
|
||||
raise AccountError(
|
||||
gettext('purchase'
|
||||
'.msg_purchase_product_missing_account_expense',
|
||||
purchase=self.purchase.rec_name,
|
||||
product=self.product.rec_name))
|
||||
else:
|
||||
invoice_line.account = account_config.get_multivalue(
|
||||
'default_category_account_expense', company=self.company.id)
|
||||
if not invoice_line.account:
|
||||
raise AccountError(
|
||||
gettext('purchase'
|
||||
'.msg_purchase_missing_account_expense',
|
||||
purchase=self.purchase.rec_name))
|
||||
if action == 'prov':
|
||||
invoice_line.description = 'Pro forma'
|
||||
elif action == 'final':
|
||||
invoice_line.description = 'Final'
|
||||
elif action == 'service':
|
||||
invoice_line.description = 'Service'
|
||||
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
||||
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
||||
invoice_line.lot = l.id
|
||||
if self.product.type == 'service':
|
||||
invoice_line.unit_price = self.unit_price
|
||||
invoice_line.product = self.product
|
||||
invoice_line.stock_moves = []
|
||||
Fee = Pool().get('fee.fee')
|
||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||
if fee:
|
||||
invoice_line.fee = fee[0]
|
||||
if fee[0].mode == 'lumpsum':
|
||||
invoice_line.quantity = 1
|
||||
elif fee[0].mode == 'ppack':
|
||||
invoice_line.quantity = fee[0].quantity
|
||||
else:
|
||||
state_id = 0
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('name','=','BL')])
|
||||
if lqt:
|
||||
state_id = lqt[0].id
|
||||
invoice_line.quantity = fee[0].get_fee_lots_qt(state_id)
|
||||
|
||||
lines.append(invoice_line)
|
||||
logger.info("GETINVLINE:%s",self.product.type)
|
||||
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
|
||||
if l.invoice_line_prov and self.product.type != 'service':
|
||||
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
|
||||
'invoice': None,
|
||||
'quantity': -l.invoice_line_prov.quantity,
|
||||
'unit_price': l.invoice_line_prov.unit_price,
|
||||
'party': l.invoice_line_prov.invoice.party,
|
||||
'origin': str(self),
|
||||
})
|
||||
lines.append(invoice_line_)
|
||||
return lines
|
||||
|
||||
def _get_invoice_line_quantity(self):
|
||||
@@ -1990,8 +2020,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
'''
|
||||
pool = Pool()
|
||||
Move = pool.get('stock.move')
|
||||
InvoiceLine = pool.get('account.invoice.line')
|
||||
Uom = pool.get('product.uom')
|
||||
Location = pool.get('stock.location')
|
||||
if self.type != 'line':
|
||||
return
|
||||
if not self.product:
|
||||
@@ -2047,26 +2076,28 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
to_location = self.purchase.to_location
|
||||
|
||||
move.from_location = from_location
|
||||
|
||||
if to_location.type != 'customer':
|
||||
move.to_location = 8
|
||||
else:
|
||||
move.to_location = to_location
|
||||
logger.info("FROM_LOCATION:%s",self.purchase.from_location)
|
||||
logger.info("TO_LOCATION:%s",self.purchase.to_location)
|
||||
if to_location:
|
||||
if to_location.type != 'customer':
|
||||
move.to_location = Location.get_transit_id()
|
||||
else:
|
||||
move.to_location = to_location
|
||||
|
||||
unit_price = l.get_lot_price()
|
||||
if l.invoice_line_prov != None :
|
||||
prov_inv = InvoiceLine(l.invoice_line_prov)
|
||||
quantity -= prov_inv.quantity
|
||||
if quantity < 0 :
|
||||
move.from_location = self.purchase.to_location
|
||||
move.to_location = 16
|
||||
elif quantity > 0 :
|
||||
move.from_location = 16
|
||||
move.to_location = self.purchase.to_location
|
||||
quantity = abs(quantity)
|
||||
unit_price = prov_inv.unit_price
|
||||
if quantity == 0:
|
||||
continue
|
||||
# if l.invoice_line_prov != None :
|
||||
# prov_inv = InvoiceLine(l.invoice_line_prov)
|
||||
# quantity -= prov_inv.quantity
|
||||
# if quantity < 0 :
|
||||
# move.from_location = self.purchase.to_location
|
||||
# move.to_location = 16
|
||||
# elif quantity > 0 :
|
||||
# move.from_location = 16
|
||||
# move.to_location = self.purchase.to_location
|
||||
# quantity = abs(quantity)
|
||||
# unit_price = prov_inv.unit_price
|
||||
# if quantity == 0:
|
||||
# continue
|
||||
move.quantity = quantity
|
||||
move.unit = self.unit
|
||||
move.product = l.lot_product
|
||||
@@ -2086,7 +2117,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
||||
moves.append(move)
|
||||
if move.to_location.type != 'customer':
|
||||
move_to, = Move.copy([move.id], default={
|
||||
'from_location': 8,
|
||||
'from_location': Location.get_transit_id(),
|
||||
'to_location': to_location,
|
||||
})
|
||||
moves.append(move_to)
|
||||
|
||||
@@ -17,9 +17,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="payment_term"/>
|
||||
<label name="currency"/>
|
||||
<field name="currency"/>
|
||||
<newline/>
|
||||
<label name="certif"/>
|
||||
<field name="certif"/>
|
||||
</group>
|
||||
<group col="2" colspan="2" id="hd" yfill="1">
|
||||
<field name="viewer" widget="html_viewer" height="300" width="600"/>
|
||||
|
||||
@@ -16,13 +16,21 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<label name="product_supplier"/>
|
||||
<field name="product_supplier"/>
|
||||
<newline/>
|
||||
<label name="attributes_name"/>
|
||||
<field name="attributes_name"/>
|
||||
<label name="concentration"/>
|
||||
<field name="concentration"/>
|
||||
<newline/>
|
||||
<label name="del_period"/>
|
||||
<field name="del_period"/>
|
||||
<label name="period_at"/>
|
||||
<field name="period_at"/>
|
||||
<newline/>
|
||||
<label id="delivery_date" string="Delivery Date:"/>
|
||||
<group id="delivery_date" col="-1">
|
||||
<field name="delivery_date" xexpand="0"/>
|
||||
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
|
||||
</group>
|
||||
<label name="del_period"/>
|
||||
<field name="del_period"/>
|
||||
<newline/>
|
||||
<label name="from_del"/>
|
||||
<field name="from_del"/>
|
||||
@@ -40,6 +48,10 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<separator name="description" colspan="4"/>
|
||||
<field name="description" colspan="4"/>
|
||||
</page>
|
||||
<page string="Attributes" id="att">
|
||||
<label name="attributes"/>
|
||||
<field name="attributes"/>
|
||||
</page>
|
||||
<page string="Taxes" id="taxes">
|
||||
<field name="taxes" colspan="4"/>
|
||||
</page>
|
||||
|
||||
168
modules/purchase_trade/AGENTS.md
Normal file
168
modules/purchase_trade/AGENTS.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# AGENTS.md - Module `purchase_trade`
|
||||
|
||||
Ce guide complete le `AGENTS.md` racine.
|
||||
Pour ce module, les regles locales ci-dessous priment.
|
||||
|
||||
## 1) Perimetre metier
|
||||
|
||||
Le module `purchase_trade` etend les flux achat/vente Tryton avec une logique
|
||||
de negoce physique:
|
||||
|
||||
- contrats d'achat (`purchase.purchase`, `purchase.line`)
|
||||
- contrats de vente (`sale.sale`, `sale.line`)
|
||||
- lots physiques et virtuels
|
||||
- matching achat/vente
|
||||
- shipments et execution logistique
|
||||
- frais (`fee.fee`)
|
||||
- templates de documents metier et facture
|
||||
|
||||
## 2) Fichiers pivots
|
||||
|
||||
- Contrats achat:
|
||||
- `modules/purchase_trade/purchase.py`
|
||||
- Contrats vente:
|
||||
- `modules/purchase_trade/sale.py`
|
||||
- Lots / matching / invoicing:
|
||||
- `modules/purchase_trade/lot.py`
|
||||
- Shipments / lien facture-lot:
|
||||
- `modules/purchase_trade/stock.py`
|
||||
- Fees:
|
||||
- `modules/purchase_trade/fee.py`
|
||||
- Bridge facture / templates:
|
||||
- `modules/purchase_trade/invoice.py`
|
||||
- Vues:
|
||||
- `modules/purchase_trade/view/*.xml`
|
||||
- Actions module:
|
||||
- `modules/purchase_trade/*.xml`
|
||||
- Manifest:
|
||||
- `modules/purchase_trade/tryton.cfg`
|
||||
|
||||
## 3) Documentation locale a lire en priorite
|
||||
|
||||
- Regles metier:
|
||||
- `modules/purchase_trade/docs/business-rules.md`
|
||||
- Regles templates:
|
||||
- `modules/purchase_trade/docs/template-rules.md`
|
||||
- Catalogue des proprietes templates:
|
||||
- `modules/purchase_trade/docs/template-properties.md`
|
||||
|
||||
## 4) Invariants metier a preserver
|
||||
|
||||
- Un lot `virtual` est la reference d'ouverture de quantite pour une `purchase.line`.
|
||||
- Une `sale.line` doit aussi avoir au minimum un lot `virtual`; une valuation
|
||||
cote sale ne doit donc pas disparaitre juste parce que le lot est `open`.
|
||||
- Le lot physique est le pont principal entre:
|
||||
- `purchase.line`
|
||||
- `sale.line`
|
||||
- shipment
|
||||
- facture
|
||||
- Pour remonter d'une facture vers shipment / BL / controller / fret:
|
||||
- privilegier le lot physique
|
||||
- ne pas multiplier des chemins d'acces concurrents
|
||||
- Pour les champs de colis (`NB BALES`) dans les templates facture:
|
||||
- la source de verite est `line.lot.lot_qt`
|
||||
- sur une facture, sommer les `lot_qt` des lignes de facture
|
||||
- tenir compte du signe de la ligne de facture pour les notes finales
|
||||
- ne pas proratiser depuis le poids (`net` / `gross`)
|
||||
- Le `FREIGHT VALUE` d'un template facture vient du `fee.fee` du shipment
|
||||
dont le produit est `Maritime freight`.
|
||||
- Pour `stock/insurance.fodt`, le `Amount insured` doit venir en priorite de
|
||||
`110%` du total des `incoming_moves` (fallback fee `Insurance` si aucun
|
||||
montant incoming calculable).
|
||||
- Pour le surveyor du certificat d'assurance shipment, la priorite est:
|
||||
`shipment.surveyor` -> `shipment.controller` -> fournisseur du fee
|
||||
`Insurance`.
|
||||
- Pour `payment_order.fodt`, utiliser des proprietes
|
||||
`invoice.report_payment_order_*` plutot que des tokens legacy `<...>`.
|
||||
- Ajouter un champ de template dans `Document Templates` ne rend pas le report
|
||||
visible dans la fiche: il faut aussi l'action `ir.action.report` +
|
||||
`ir.action.keyword` (`form_print`) cote `account.invoice`.
|
||||
- Le wizard `Create contracts` en mode `matched` peut maintenant partir de
|
||||
plusieurs `lot.qt`, mais doit conserver un matching par lot source et laisser
|
||||
`created_by_code = True` sur les lignes creees pour ne pas declencher les
|
||||
creations automatiques de lots dans les validations.
|
||||
- En valuation / PnL:
|
||||
- la valeur stockee dans `type` est la cle technique (`pur. priced`,
|
||||
`sale priced`, `pur. fee`, etc.), pas le label affiche dans l'UI
|
||||
- les references doivent rester coherentes avec le type de lot:
|
||||
`Purchase/Open`, `Purchase/Physic`, `Sale/Open`, `Sale/Physic`
|
||||
- pour une sale matchee, les lignes de valuation purchase generees sur un lot
|
||||
physique doivent aussi renseigner `sale` et `sale_line` afin de remonter
|
||||
dans l'onglet PnL de la sale
|
||||
- une sale non matchee doit etre valorisable "sale-first" et alimenter
|
||||
`valuation.valuation` / `valuation.valuation.line`
|
||||
- si une `sale.line` `basis` n'a ni `price_summary` ni `lot_price_sale`,
|
||||
creer quand meme une ligne `sale priced` avec `price = 0` et `amount = 0`
|
||||
plutot que de ne rien generer
|
||||
- le MTM ne doit etre renseigne que pour `pur. priced`, `sale priced` et
|
||||
`derivative`; jamais pour les fees
|
||||
- `mtm_price` doit afficher le prix brut de valorisation (sans ratio), alors
|
||||
que `mtm` reste le montant calcule selon la logique de strategie
|
||||
- En pricing:
|
||||
- le `unit_price` doit rester un prix de base, hors `premium`
|
||||
- le `premium` doit impacter le prix total economique et donc le `amount`,
|
||||
aussi bien en `priced` qu'en `basis`
|
||||
- pour les documents commerciaux / facture, une ligne `basis` affiche le
|
||||
`premium` comme prix visible, pas le prix economique total
|
||||
- si `linked currency` est active, le `premium` est saisi dans la devise /
|
||||
unite liee (ex: `USC/LB`) puis converti vers le repere de la ligne pour le
|
||||
calcul du `amount`
|
||||
- en `basis + linked currency`, le `linked_price` doit representer le prix
|
||||
basis brut (hors premium) dans la devise liee; le `unit_price` reste ce
|
||||
prix brut converti, et le `premium` converti est ajoute seulement dans
|
||||
l'`amount`
|
||||
- si `linked currency` est cochee, `linked_price`, `linked_currency` et
|
||||
`linked_unit` sont requis
|
||||
- dans les forms, presenter le bloc prix dans l'ordre:
|
||||
`price_type` -> linked fields -> `premium` -> `unit_price` -> `amount`
|
||||
- en valuation `basis`, le premium s'applique a chaque composant, pas
|
||||
uniquement a une ligne de resume
|
||||
- pour une ligne `basis` sans `price_summary`, la valuation fallback doit
|
||||
utiliser `unit_price + premium` (et pas `unit_price` seul)
|
||||
- a la validation d'une `sale.line`, si un lot virtuel est cree et qu'aucun
|
||||
matching purchase n'existe, il faut lancer `generate_from_sale_line()` pour
|
||||
alimenter le PnL sale-first
|
||||
|
||||
## 5) Conventions de modification
|
||||
|
||||
1. Modifier la logique metier dans le fichier pivot le plus proche.
|
||||
2. Si un template `.fodt` devient complexe, deplacer la logique dans une
|
||||
propriete Python `report_*`.
|
||||
3. Pour une facture trade, preferer enrichir `modules/purchase_trade/invoice.py`
|
||||
plutot que surcharger lourdement le `.fodt`.
|
||||
4. Si une regle metier durable change, mettre a jour
|
||||
`docs/business-rules.md`.
|
||||
5. Si une convention de template change, mettre a jour
|
||||
`docs/template-rules.md`.
|
||||
6. Pour les vues XML Tryton de ce module, utiliser `editable="1"` sur les
|
||||
`<tree>` editables; ne pas utiliser `editable="bottom"`.
|
||||
7. Si une regle de texte par defaut durable est demandee sur achat/vente,
|
||||
preferer un singleton de configuration expose dans un menu fonctionnel
|
||||
existant plutot qu'un menu technique `purchase_trade`.
|
||||
|
||||
## 6) Pieges connus
|
||||
|
||||
- Plusieurs actions de report `account.invoice` peuvent sembler rendre le meme
|
||||
document a cause du cache `invoice_report_cache`.
|
||||
- Les reports alternatifs (`Final Invoice`, `Prepayment`, etc.) ne doivent pas
|
||||
reutiliser le cache du report standard sans verification.
|
||||
- Pour les donnees achat/vente partagees, ne pas supposer qu'une facture de
|
||||
vente doit lire directement sur la `sale.line`: souvent, la verite metier
|
||||
passe par le lot physique et/ou la `account.invoice.line`.
|
||||
- Les templates `invoice_ict*` peuvent partager les memes proprietes Python;
|
||||
si une regle doit valoir pour provisional et final, la mettre dans
|
||||
`modules/purchase_trade/invoice.py` plutot que dupliquer dans les `.fodt`.
|
||||
- Dans les ecrans PnL, le label `Sale price` correspond au type stocke
|
||||
`sale priced`; idem pour `Pur. price` / `pur. priced`.
|
||||
- Une ligne `basis` sans resume de pricing peut sinon disparaitre de la
|
||||
valuation si aucun fallback explicite a `0` n'est prevu.
|
||||
- Le calcul du prix peut diverger entre `unit_price`, `linked_price`,
|
||||
`lot_price` et valuation si le premium n'est pas traite explicitement dans
|
||||
chaque maillon.
|
||||
|
||||
## 7) Definition of done (module `purchase_trade`)
|
||||
|
||||
- Le flux achat/vente/lot cible reste coherent.
|
||||
- Les impacts templates/facture ont ete verifies conceptuellement.
|
||||
- Les docs locales ont ete mises a jour si une nouvelle regle durable a emerge.
|
||||
- Le patch reste minimal et local au domaine demande.
|
||||
@@ -3,7 +3,39 @@
|
||||
|
||||
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,
|
||||
configuration,
|
||||
purchase,
|
||||
sale,
|
||||
global_reporting,
|
||||
stock,
|
||||
derivative,
|
||||
lot,
|
||||
pricing,
|
||||
workflow,
|
||||
lc,
|
||||
dashboard,
|
||||
fee,
|
||||
payment_term,
|
||||
purchase_prepayment,
|
||||
cron,
|
||||
party,
|
||||
forex,
|
||||
outgoing,
|
||||
incoming,
|
||||
optional,
|
||||
association_tables,
|
||||
document_tracking,
|
||||
open_position,
|
||||
credit_risk,
|
||||
valuation,
|
||||
dimension,
|
||||
weight_report,
|
||||
backtoback,
|
||||
service,
|
||||
invoice,
|
||||
)
|
||||
|
||||
def register():
|
||||
Pool.register(
|
||||
@@ -23,9 +55,10 @@ def register():
|
||||
incoming.ImportSwift,
|
||||
lc.LCMT700,
|
||||
lc.LCMessage,
|
||||
lc.CreateLCStart,
|
||||
global_reporting.GRConfiguration,
|
||||
module='purchase_trade', type_='model')
|
||||
lc.CreateLCStart,
|
||||
global_reporting.GRConfiguration,
|
||||
configuration.Configuration,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
incoming.ImportSwift,
|
||||
incoming.PrepareDocuments,
|
||||
@@ -47,6 +80,9 @@ def register():
|
||||
dashboard.News,
|
||||
dashboard.Demos,
|
||||
party.Party,
|
||||
party.PartyExecution,
|
||||
party.PartyExecutionSla,
|
||||
party.PartyExecutionPlace,
|
||||
payment_term.PaymentTerm,
|
||||
payment_term.PaymentTermLine,
|
||||
purchase.Purchase,
|
||||
@@ -69,9 +105,15 @@ def register():
|
||||
fee.Fee,
|
||||
fee.FeeLots,
|
||||
purchase.FeeLots,
|
||||
fee.Valuation,
|
||||
fee.ValuationDyn,
|
||||
derivative.Derivative,
|
||||
valuation.Valuation,
|
||||
valuation.ValuationLine,
|
||||
valuation.ValuationDyn,
|
||||
valuation.ValuationReport,
|
||||
valuation.ValuationReportContext,
|
||||
valuation.ValuationProcessDimension,
|
||||
valuation.ValuationProcessStart,
|
||||
valuation.ValuationProcessResult,
|
||||
derivative.Derivative,
|
||||
derivative.DerivativeMatch,
|
||||
derivative.MatchWizardStart,
|
||||
derivative.DerivativeReport,
|
||||
@@ -82,9 +124,12 @@ def register():
|
||||
forex.PForex,
|
||||
forex.ForexBI,
|
||||
purchase.PnlBI,
|
||||
purchase.PositionBI,
|
||||
stock.Move,
|
||||
stock.Location,
|
||||
stock.InvoiceLine,
|
||||
stock.ShipmentIn,
|
||||
stock.ShipmentWR,
|
||||
stock.ShipmentInternal,
|
||||
stock.ShipmentOut,
|
||||
stock.StatementOfFacts,
|
||||
@@ -128,14 +173,41 @@ def register():
|
||||
purchase.ContractDocumentType,
|
||||
purchase.DocTemplate,
|
||||
purchase.DocTypeTemplate,
|
||||
purchase.Mtm,
|
||||
purchase.PurchaseStrategy,
|
||||
purchase.PriceComposition,
|
||||
purchase.QualityAnalysis,
|
||||
purchase.Assay,
|
||||
purchase.AssayLine,
|
||||
purchase.AssayElement,
|
||||
purchase.AssayUnit,
|
||||
purchase.PayableRule,
|
||||
purchase.PenaltyRule,
|
||||
purchase.PenaltyRuleTier,
|
||||
purchase.ConcentrateTerm,
|
||||
backtoback.Backtoback,
|
||||
dimension.AnalyticDimension,
|
||||
dimension.AnalyticDimensionValue,
|
||||
dimension.AnalyticDimensionAssignment,
|
||||
weight_report.WeightReport,
|
||||
module='purchase', type_='model')
|
||||
Pool.register(
|
||||
account.PhysicalTradeIFRS,
|
||||
module='purchase_trade', type_='model')
|
||||
Pool.register(
|
||||
invoice.Invoice,
|
||||
invoice.InvoiceLine,
|
||||
module='account_invoice', type_='model')
|
||||
Pool.register(
|
||||
forex.Forex,
|
||||
forex.ForexCoverFees,
|
||||
forex.ForexCategory,
|
||||
pricing.Component,
|
||||
pricing.Mtm,
|
||||
pricing.MtmStrategy,
|
||||
pricing.MtmScenario,
|
||||
pricing.MtmSnapshot,
|
||||
pricing.PriceMatrix,
|
||||
pricing.PriceMatrixLine,
|
||||
pricing.Estimated,
|
||||
pricing.Pricing,
|
||||
pricing.Period,
|
||||
@@ -151,6 +223,9 @@ def register():
|
||||
sale.SaleCreatePurchaseInput,
|
||||
sale.Derivative,
|
||||
sale.Valuation,
|
||||
sale.ValuationLine,
|
||||
sale.ValuationDyn,
|
||||
sale.ValuationReport,
|
||||
sale.Fee,
|
||||
sale.Lot,
|
||||
sale.FeeLots,
|
||||
@@ -161,8 +236,11 @@ def register():
|
||||
forex.SForex,
|
||||
forex.ForexCoverPhysicalSale,
|
||||
sale.ContractDocumentType,
|
||||
sale.Mtm,
|
||||
sale.SaleStrategy,
|
||||
sale.OpenPosition,
|
||||
sale.Backtoback,
|
||||
sale.AnalyticDimensionAssignment,
|
||||
sale.PriceComposition,
|
||||
module='sale', type_='model')
|
||||
Pool.register(
|
||||
lot.LotShipping,
|
||||
@@ -185,13 +263,23 @@ def register():
|
||||
purchase.InvoicePayment,
|
||||
stock.ImportSoFWizard,
|
||||
dashboard.BotWizard,
|
||||
dashboard.DashboardLoader,
|
||||
forex.ForexReport,
|
||||
purchase.PnlReport,
|
||||
derivative.DerivativeMatchWizard,
|
||||
module='purchase', type_='wizard')
|
||||
Pool.register(
|
||||
sale.SaleCreatePurchase,
|
||||
sale.SaleAllocationsWizard,
|
||||
module='sale', type_='wizard')
|
||||
dashboard.DashboardLoader,
|
||||
forex.ForexReport,
|
||||
purchase.PnlReport,
|
||||
purchase.PositionReport,
|
||||
valuation.ValuationProcess,
|
||||
derivative.DerivativeMatchWizard,
|
||||
module='purchase', type_='wizard')
|
||||
Pool.register(
|
||||
sale.SaleCreatePurchase,
|
||||
sale.SaleAllocationsWizard,
|
||||
module='sale', type_='wizard')
|
||||
Pool.register(
|
||||
invoice.InvoiceReport,
|
||||
invoice.SaleReport,
|
||||
invoice.PurchaseReport,
|
||||
stock.ShipmentShippingReport,
|
||||
stock.ShipmentInsuranceReport,
|
||||
stock.ShipmentPackingListReport,
|
||||
module='purchase_trade', type_='report')
|
||||
|
||||
|
||||
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>
|
||||
19
modules/purchase_trade/configuration.py
Normal file
19
modules/purchase_trade/configuration.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
|
||||
|
||||
|
||||
class Configuration(ModelSingleton, ModelSQL, ModelView):
|
||||
"Purchase Trade Configuration"
|
||||
__name__ = 'purchase_trade.configuration'
|
||||
|
||||
pricing_rule = fields.Text("Pricing Rule")
|
||||
sale_report_template = fields.Char("Sale Template")
|
||||
sale_bill_report_template = fields.Char("Sale Bill Template")
|
||||
sale_final_report_template = fields.Char("Sale Final Template")
|
||||
invoice_report_template = fields.Char("Invoice Template")
|
||||
invoice_cndn_report_template = fields.Char("CN/DN Template")
|
||||
invoice_prepayment_report_template = fields.Char("Prepayment Template")
|
||||
invoice_payment_order_report_template = fields.Char("Payment Order Template")
|
||||
purchase_report_template = fields.Char("Purchase Template")
|
||||
shipment_shipping_report_template = fields.Char("Shipping Template")
|
||||
shipment_insurance_report_template = fields.Char("Insurance Template")
|
||||
shipment_packing_list_report_template = fields.Char("Packing List Template")
|
||||
48
modules/purchase_trade/configuration.xml
Normal file
48
modules/purchase_trade/configuration.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="purchase_trade_configuration_view_form">
|
||||
<field name="model">purchase_trade.configuration</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">configuration_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="purchase_trade_template_configuration_view_form">
|
||||
<field name="model">purchase_trade.configuration</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">template_configuration_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_purchase_trade_configuration_form">
|
||||
<field name="name">Pricing Configuration</field>
|
||||
<field name="res_model">purchase_trade.configuration</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_purchase_trade_configuration_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="purchase_trade_configuration_view_form"/>
|
||||
<field name="act_window" ref="act_purchase_trade_configuration_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_purchase_trade_template_configuration_form">
|
||||
<field name="name">Document Templates</field>
|
||||
<field name="res_model">purchase_trade.configuration</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_purchase_trade_template_configuration_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="purchase_trade_template_configuration_view_form"/>
|
||||
<field name="act_window" ref="act_purchase_trade_template_configuration_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Configuration"
|
||||
parent="price.menu_price"
|
||||
action="act_purchase_trade_configuration_form"
|
||||
sequence="10"
|
||||
id="menu_purchase_trade_configuration"
|
||||
icon="tryton-settings"/>
|
||||
<menuitem
|
||||
name="Document Templates"
|
||||
parent="document_incoming.menu_configuration"
|
||||
action="act_purchase_trade_template_configuration_form"
|
||||
sequence="20"
|
||||
id="menu_purchase_trade_template_configuration"
|
||||
icon="tryton-settings"/>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -155,7 +155,7 @@ class Party(metaclass=PoolMeta):
|
||||
if overdue > 0:
|
||||
# scale by overdue relative to limit
|
||||
limit = self.credit_limit or 1
|
||||
score += int(min(40, (overdue / float(limit)) * 100))
|
||||
score += int(min(40, (float(overdue) / float(limit)) * 100))
|
||||
|
||||
# cap
|
||||
if score > 100:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
|
||||
from trytond.pyson import Eval
|
||||
from trytond.transaction import Transaction
|
||||
@@ -193,7 +193,7 @@ class Dashboard(ModelSQL, ModelView):
|
||||
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
|
||||
logger.info("EXITONCHANGE",self.chatbot)
|
||||
|
||||
def get_last_two_fx_rates(self, from_code='USD', to_code='EUR'):
|
||||
def get_last_five_fx_rates(self, from_code='USD', to_code='EUR'):
|
||||
"""
|
||||
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
|
||||
"""
|
||||
@@ -208,29 +208,93 @@ class Dashboard(ModelSQL, ModelView):
|
||||
rates = CurrencyRate.search(
|
||||
[('currency', '=', to_currency.id)],
|
||||
order=[('date', 'DESC')],
|
||||
limit=2,
|
||||
limit=5,
|
||||
)
|
||||
|
||||
if not rates:
|
||||
return None, None
|
||||
return None, None, None, None, None
|
||||
|
||||
# Calcul du taux EUR/USD
|
||||
# Si la devise principale de la société est EUR, et que le taux stocké est
|
||||
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
|
||||
last_rate = rates[0].rate
|
||||
prev_rate = rates[1].rate if len(rates) > 1 else None
|
||||
f1 = rates[0].rate
|
||||
f2 = rates[1].rate if len(rates) > 1 else None
|
||||
f3 = rates[2].rate if len(rates) > 2 else None
|
||||
f4 = rates[3].rate if len(rates) > 3 else None
|
||||
f5 = rates[4].rate if len(rates) > 4 else None
|
||||
d1 = rates[0].date
|
||||
d2 = rates[1].date if len(rates) > 1 else None
|
||||
d3 = rates[2].date if len(rates) > 2 else None
|
||||
d4 = rates[3].date if len(rates) > 3 else None
|
||||
d5 = rates[4].date if len(rates) > 4 else None
|
||||
|
||||
# if from_currency != to_currency:
|
||||
# last_rate = 1 / last_rate if last_rate else None
|
||||
# prev_rate = 1 / prev_rate if prev_rate else None
|
||||
|
||||
if last_rate and prev_rate:
|
||||
return round(1/last_rate,6), round(1/prev_rate,6)
|
||||
return round(1/f1,6), round(1/f2,6) if f2 else None, round(1/f3,6) if f3 else None, round(1/f4,6) if f4 else None, round(1/f5,6) if f5 else None, d1, d2, d3, d4, d5
|
||||
|
||||
def get_tremor(self,name):
|
||||
Pnl = Pool().get('valuation.valuation')
|
||||
pnls = Pnl.search(['id','>',0])
|
||||
pnl_amount = "{:,.0f}".format(round(sum([e.amount for e in pnls]),0))
|
||||
Date = Pool().get('ir.date')
|
||||
Configuration = Pool().get('gr.configuration')
|
||||
config = Configuration.search(['id','>',0])[0]
|
||||
Shipment = Pool().get('stock.shipment.in')
|
||||
DocumentIncoming = Pool().get('document.incoming')
|
||||
Fee = Pool().get('fee.fee')
|
||||
WR = Pool().get('weight.report')
|
||||
if config.automation:
|
||||
shipment = Shipment.search([('state','!=','received')])
|
||||
shipment_trend = [sh for sh in shipment if sh.create_date == Date.today()]
|
||||
controller = Shipment.search([('controller','!=',None)])
|
||||
controller_trend = [co for co in controller if co.create_date == Date.today()]
|
||||
instruction = Shipment.search([('result','!=',None)])
|
||||
instruction_trend = [si for si in instruction if si.create_date == Date.today()]
|
||||
id_received = Shipment.search([('returned_id','!=',None)])
|
||||
id_received_trend = [i for i in id_received if i.create_date == Date.today()]
|
||||
wr = WR.search([('id','>',0)])
|
||||
wr_trend = [w for w in wr if w.create_date == Date.today()]
|
||||
so = Fee.search(['id','=',25])
|
||||
so_trend = [s for s in so if s.create_date == Date.today()]
|
||||
di = DocumentIncoming.search(['id','>',0])
|
||||
di_trend = [d for d in di if d.create_date == Date.today()]
|
||||
return (
|
||||
config.dashboard +
|
||||
"/dashboard/index.html?shipment="
|
||||
+ str(len(shipment))
|
||||
+ "&shipment_trend="
|
||||
+ str(len(shipment_trend))
|
||||
+ "&controller="
|
||||
+ str(len(controller))
|
||||
+ "&controller_trend="
|
||||
+ str(len(controller_trend))
|
||||
+ "&instruction="
|
||||
+ str(len(instruction))
|
||||
+ "&instruction_trend="
|
||||
+ str(len(instruction_trend))
|
||||
+ "&wr="
|
||||
+ str(len(wr))
|
||||
+ "&wr_trend="
|
||||
+ str(len(wr_trend))
|
||||
+ "&so="
|
||||
+ str(len(so))
|
||||
+ "&so_trend="
|
||||
+ str(len(so_trend))
|
||||
+ "&di="
|
||||
+ str(len(di))
|
||||
+ "&di_trend="
|
||||
+ str(len(di_trend))
|
||||
+ "&id_received="
|
||||
+ str(len(id_received))
|
||||
+ "&id_received_trend="
|
||||
+ str(len(id_received_trend)))
|
||||
|
||||
f1,f2,f3,f4,f5,d1,d2,d3,d4,d5 = self.get_last_five_fx_rates()
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
last_total,last_variation = Valuation.get_totals()
|
||||
pnl_amount = "{:,.0f}".format(round(last_total,0))
|
||||
pnl_variation = 0
|
||||
if last_total and last_variation:
|
||||
pnl_variation = "{:,.2f}".format(round((last_variation/last_total)*100,0))
|
||||
Open = Pool().get('open.position')
|
||||
opens = Open.search(['id','>',0])
|
||||
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
|
||||
@@ -258,38 +322,69 @@ class Dashboard(ModelSQL, ModelView):
|
||||
val_s = len(val)
|
||||
conf = Sale.search(['state','=','confirmed'])
|
||||
conf_s = len(conf)
|
||||
Shipment = Pool().get('stock.shipment.in')
|
||||
|
||||
draft = Shipment.search(['state','=','draft'])
|
||||
shipment_d = len(draft)
|
||||
val = Purchase.search(['state','=','started'])
|
||||
val = Shipment.search(['state','=','started'])
|
||||
shipment_s = len(val)
|
||||
conf = Purchase.search(['state','=','received'])
|
||||
conf = Shipment.search(['state','=','received'])
|
||||
shipment_r = len(conf)
|
||||
Lot = Pool().get('lot.lot')
|
||||
lots = Lot.search(['sale_line','!=',None])
|
||||
lots = Lot.search([('sale_line','!=',None),('line','!=',None),('lot_type','=','physic')])
|
||||
lot_m = len(lots)
|
||||
val = Lot.search(['sale_line','=',None])
|
||||
val = Lot.search([('sale_line','=',None),('line','!=',None),('lot_type','=','physic')])
|
||||
lot_a = len(val)
|
||||
conf = Lot.search(['lot_type','=','physic'])
|
||||
lot_al = len(conf)
|
||||
Invoice = Pool().get('account.invoice')
|
||||
invs = Invoice.search(['type','=','in'])
|
||||
inv_p = len(invs)
|
||||
invs = Invoice.search([('type','=','in'),('state','=','paid')])
|
||||
inv_p_p = len(invs)
|
||||
invs = Invoice.search([('type','=','in'),('state','!=','paid')])
|
||||
inv_p_np = len(invs)
|
||||
invs = Invoice.search(['type','=','out'])
|
||||
inv_s = len(invs)
|
||||
invs = Invoice.search([('type','=','out'),('state','=','paid')])
|
||||
inv_s_p = len(invs)
|
||||
invs = Invoice.search([('type','=','out'),('state','!=','paid')])
|
||||
inv_s_np = len(invs)
|
||||
AccountMove = Pool().get('account.move')
|
||||
accs = AccountMove.search(['id','>',0])
|
||||
move_cash = len(accs)
|
||||
accs = AccountMove.search([('journal','=',3),('state','!=','posted')])
|
||||
pay_val = len(accs)
|
||||
accs = AccountMove.search([('journal','=',3),('state','=','posted')])
|
||||
pay_posted = len(accs)
|
||||
|
||||
return (
|
||||
"https://srv413259.hstgr.cloud/dashboard/index.html?pnl_amount="
|
||||
config.dashboard +
|
||||
"/dashboard/index.html?pnl_amount="
|
||||
+ str(pnl_amount)
|
||||
+ "&pnl_variation="
|
||||
+ str(pnl_variation)
|
||||
+ "&exposure="
|
||||
+ str(exposure)
|
||||
+ "&topay="
|
||||
+ str(topay)
|
||||
+ "&toreceive="
|
||||
+ str(toreceive)
|
||||
+ "&eurusd="
|
||||
+ str(f1)
|
||||
+ "&eurusd="
|
||||
+ str(f2)
|
||||
+ "&eurusd="
|
||||
+ str(f3)
|
||||
+ "&eurusd="
|
||||
+ str(f4)
|
||||
+ "&eurusd="
|
||||
+ str(f5)
|
||||
+ "&eurusd_date="
|
||||
+ str(d1)
|
||||
+ "&eurusd_date="
|
||||
+ str(d2)
|
||||
+ "&eurusd_date="
|
||||
+ str(d3)
|
||||
+ "&eurusd_date="
|
||||
+ str(d4)
|
||||
+ "&eurusd_date="
|
||||
+ str(d5)
|
||||
+ "&draft_p="
|
||||
+ str(draft_p)
|
||||
+ "&val_p="
|
||||
@@ -312,14 +407,22 @@ class Dashboard(ModelSQL, ModelView):
|
||||
+ str(lot_m)
|
||||
+ "&lot_a="
|
||||
+ str(lot_a)
|
||||
+ "&lot_al="
|
||||
+ str(lot_al)
|
||||
+ "&inv_p="
|
||||
+ str(inv_p)
|
||||
+ "&inv_p_p="
|
||||
+ str(inv_p_p)
|
||||
+ "&inv_p_np="
|
||||
+ str(inv_p_np)
|
||||
+ "&inv_s="
|
||||
+ str(inv_s)
|
||||
+ "&move_cash="
|
||||
+ str(move_cash)
|
||||
+ "&inv_s_p="
|
||||
+ str(inv_s_p)
|
||||
+ "&inv_s_np="
|
||||
+ str(inv_s_np)
|
||||
+ "&pay_val="
|
||||
+ str(pay_val)
|
||||
+ "&pay_posted="
|
||||
+ str(pay_posted)
|
||||
)
|
||||
|
||||
|
||||
@@ -327,7 +430,7 @@ class Dashboard(ModelSQL, ModelView):
|
||||
News = Pool().get('news.news')
|
||||
Date = Pool().get('ir.date')
|
||||
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
|
||||
last_rate,prev_rate = self.get_last_two_fx_rates()
|
||||
last_rate,prev_rate, = self.get_last_five_fx_rates()
|
||||
if last_rate and prev_rate:
|
||||
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
|
||||
direction = "📈" if variation > 0 else "📉"
|
||||
@@ -412,7 +515,7 @@ class Dashboard(ModelSQL, ModelView):
|
||||
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
|
||||
]
|
||||
|
||||
demos = Demos.search([('active', '=', True)])
|
||||
demos = Demos.search([('active', '=', True)],order=[('id', 'DESC')])
|
||||
for n in demos:
|
||||
icon = n.icon or "📰"
|
||||
category = n.category or "General"
|
||||
@@ -499,12 +602,14 @@ class Dashboard(ModelSQL, ModelView):
|
||||
return pu
|
||||
|
||||
def gen_url(self,name=None):
|
||||
Configuration = Pool().get('gr.configuration')
|
||||
config = Configuration.search(['id','>',0])[0]
|
||||
payload = {
|
||||
"resource": {"dashboard": self.bi_id},
|
||||
"params": {},
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||||
}
|
||||
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
|
||||
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||||
logger.info("TOKEN:%s",token)
|
||||
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
|
||||
|
||||
@@ -663,6 +768,7 @@ class BotWizard(Wizard):
|
||||
l.lot_quantity = l.lot_qt
|
||||
l.lot_gross_quantity = l.lot_qt
|
||||
l.lot_premium = Decimal(0)
|
||||
l.lot_chunk_key = None
|
||||
lot_id = LotQt.add_physical_lots(lqt,[l])
|
||||
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
|
||||
Dashboard.save([d])
|
||||
|
||||
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>
|
||||
451
modules/purchase_trade/docs/business-rules.md
Normal file
451
modules/purchase_trade/docs/business-rules.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# Business Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.4`
|
||||
Derniere mise a jour: `2026-04-02`
|
||||
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`
|
||||
|
||||
### BR-PT-004 - La valuation doit couvrir les flux purchase et sale, y compris les sales non matchees
|
||||
|
||||
- Intent: obtenir un PnL coherent cote achat et cote vente, meme lorsqu'une
|
||||
sale n'est pas encore matchee a une purchase.
|
||||
- Description:
|
||||
- Le flux historique de valuation part de `purchase.line` puis remonte vers
|
||||
les ventes via les lots/lots matchants.
|
||||
- Le systeme doit egalement savoir valoriser directement une `sale.line`
|
||||
non matchee ("sale-first").
|
||||
- Une sale non matchee doit creer des lignes dans
|
||||
`valuation.valuation` et `valuation.valuation.line` afin d'apparaitre dans
|
||||
l'onglet PnL de la sale.
|
||||
- Resultat attendu:
|
||||
- pour une `sale.line` non matchee, generer au minimum les types:
|
||||
- `sale priced`
|
||||
- `sale fee`
|
||||
- `derivative` si la ligne porte des derives
|
||||
- si la sale est matchee via un lot physique, les lignes purchase portees par
|
||||
ce lot physique doivent aussi renseigner `sale` et `sale_line`
|
||||
- une sale matchee doit donc voir:
|
||||
- ses lignes `sale *`
|
||||
- les lignes purchase portees par le lot physique partage
|
||||
- Priorite:
|
||||
- `structurante`
|
||||
|
||||
### BR-PT-005 - Les references de valuation doivent decrire la nature du lot de la ligne
|
||||
|
||||
- Intent: eviter les ambiguïtes dans les ecrans PnL entre lots `open` et lots
|
||||
`physic`.
|
||||
- Description:
|
||||
- La reference affichee dans la valuation doit decrire la ligne elle-meme,
|
||||
pas son vis-a-vis.
|
||||
- Les references autorisees pour les lignes de prix sont:
|
||||
- `Purchase/Open`
|
||||
- `Purchase/Physic`
|
||||
- `Sale/Open`
|
||||
- `Sale/Physic`
|
||||
- Resultat attendu:
|
||||
- un lot `virtual` cote purchase ne doit jamais sortir avec la reference
|
||||
`Purchase/Physic`
|
||||
- un lot `virtual` cote sale ne doit jamais sortir avec la reference
|
||||
`Sale/Physic`
|
||||
- un lot physique matche peut produire:
|
||||
- une ligne purchase en `Purchase/Physic`
|
||||
- une ligne sale en `Sale/Physic`
|
||||
- un open sale matche a un open purchase peut produire des quantites egales
|
||||
tout en gardant des references differentes (`Purchase/Open` vs `Sale/Open`)
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-006 - Une sale basis sans prix detaille doit quand meme apparaitre en valuation
|
||||
|
||||
- Intent: ne pas perdre les lignes de PnL lorsque le detail de pricing n'est
|
||||
pas encore renseigne.
|
||||
- Description:
|
||||
- Une `sale.line` de type `basis` peut exister avec un lot `virtual`, sans
|
||||
`price_summary` et sans `lot_price_sale`.
|
||||
- Dans ce cas, la valuation doit quand meme creer une ligne `sale priced`.
|
||||
- Resultat attendu:
|
||||
- si `price_summary` est vide:
|
||||
- creer une ligne `sale priced`
|
||||
- avec `price = 0`
|
||||
- avec `amount = 0`
|
||||
- avec un `state` de type `unfixed`
|
||||
- si `lot_price_sale` est vide sur un lot sale, utiliser `sale_line.unit_price`
|
||||
comme fallback quand il existe
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-007 - Le MTM de valuation ne s'applique pas aux fees
|
||||
|
||||
- Intent: distinguer les lignes de prix marquables au marche des lignes de
|
||||
frais qui ne doivent pas etre mark-to-market.
|
||||
- Description:
|
||||
- Le systeme peut renseigner `mtm_price`, `mtm` et `strategy` uniquement pour:
|
||||
- `pur. priced`
|
||||
- `sale priced`
|
||||
- `derivative`
|
||||
- Les fees (`pur. fee`, `sale fee`, `shipment fee`, `line fee`) ne doivent
|
||||
jamais porter de valorisation MTM.
|
||||
- Resultat attendu:
|
||||
- les lignes de fee doivent conserver:
|
||||
- `mtm_price = NULL`
|
||||
- `mtm = NULL`
|
||||
- `strategy = NULL`
|
||||
- `mtm_price` doit representer le prix brut de valorisation sans appliquer le
|
||||
ratio de composant
|
||||
- `mtm` reste le montant calcule selon la logique de strategie
|
||||
- Priorite:
|
||||
- `structurante`
|
||||
|
||||
### BR-PT-008 - Le premium fait partie du prix contractuel en `priced` et en `basis`
|
||||
|
||||
- Intent: garantir que le montant total valorise et facture reflete toujours le
|
||||
premium/discount saisi sur la ligne.
|
||||
- Description:
|
||||
- Le `premium` d'une `purchase.line` ou `sale.line` doit impacter le prix
|
||||
total quelle que soit la `price_type`.
|
||||
- Cette regle vaut pour:
|
||||
- les calculs de `amount`
|
||||
- la valuation / PnL
|
||||
- Resultat attendu:
|
||||
- le `unit_price` reste le prix de base, hors premium
|
||||
- en `priced`, le montant economique = `unit_price + premium`
|
||||
- en `basis`, le premium s'ajoute aussi au prix total economique
|
||||
- en valuation `basis`, le premium s'applique a chaque composant valorise
|
||||
(ex: meme premium repete sur chaque bloc ICE)
|
||||
- Exemple metier:
|
||||
- `8.30 USC/LB 500 TONS ON ICE MCH'26`
|
||||
- `8.30 USC/LB 500 TONS ON ICE MAY 26`
|
||||
- le premium `8.30 USC/LB` s'applique a chaque composant
|
||||
- Priorite:
|
||||
- `structurante`
|
||||
|
||||
### BR-PT-009 - En linked currency, le premium est exprime dans la devise/unite liee
|
||||
|
||||
- Intent: respecter la facon dont les traders saisissent les prix sur certains
|
||||
produits (ex: coton en `USC/LB`).
|
||||
- Description:
|
||||
- Quand `enable_linked_currency` est coche, le `premium` est saisi dans la
|
||||
devise / unite liee, pas dans la devise / unite native de la ligne.
|
||||
- Le systeme doit convertir ce premium vers le repere de la ligne pour les
|
||||
calculs internes de montant et de valuation.
|
||||
- Resultat attendu:
|
||||
- `premium` est interprete dans le repere `linked_currency` / `linked_unit`
|
||||
- le `unit_price` ne doit pas absorber ce premium
|
||||
- les `amount` et valuations doivent refleter ce premium converti
|
||||
- si `linked currency` est cochee, `linked_price`, `linked_currency` et
|
||||
`linked_unit` sont obligatoires
|
||||
- Priorite:
|
||||
- `structurante`
|
||||
|
||||
### BR-PT-010 - En `basis + linked currency`, le linked price suit le basis brut
|
||||
|
||||
- Intent: rendre lisible la decomposition entre prix basis de marche et premium.
|
||||
- Description:
|
||||
- Quand une ligne est en `basis` et `linked currency`, le bloc
|
||||
`linked_price` doit etre recalcule automatiquement.
|
||||
- Ce `linked_price` doit representer le prix basis brut, hors premium.
|
||||
- Le `unit_price` de la ligne doit rester ce prix brut converti.
|
||||
- Le premium converti n'est ajoute qu'au niveau du `amount`.
|
||||
- Resultat attendu:
|
||||
- modification du basis -> mise a jour automatique du `linked_price`
|
||||
- `linked_price` = base market / basis
|
||||
- `unit_price` = `linked_price` converti
|
||||
- `amount` = quantite * (`unit_price` + premium converti)
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-011 - Une sale line non matchee avec lot virtuel doit generer une valuation sale-first des la validation
|
||||
|
||||
- Intent: ne pas attendre un matching purchase pour afficher le PnL d'une sale
|
||||
ouverte.
|
||||
- Description:
|
||||
- Lors de la validation d'une `sale.line`, le systeme peut creer un lot
|
||||
`virtual`.
|
||||
- Si aucun `lot.qt` ne relie ce lot a une `purchase.line`, il faut tout de
|
||||
meme generer la valuation cote sale.
|
||||
|
||||
### BR-PT-012 - Le wizard Create contracts peut creer un seul achat matche a plusieurs open sales
|
||||
|
||||
- Intent: permettre la creation d'un contrat achat unique a partir de plusieurs
|
||||
`lot.qt` de vente selectionnes.
|
||||
- Description:
|
||||
- En mode `matched`, le wizard `Create contracts` peut recevoir plusieurs
|
||||
`lot.qt` selectionnes.
|
||||
- Il doit creer un seul contrat, avec une ligne par lot source selectionne.
|
||||
- Chaque ligne doit conserver son lot d'origine pour le matching.
|
||||
- Resultat attendu:
|
||||
- le wizard agrege les quantites de la selection
|
||||
- il refuse une quantite saisie differente du total selectionne
|
||||
- il conserve `created_by_code = True` sur les lignes creees pour ne pas
|
||||
declencher les creations automatiques parasites lors des validations
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-013 - Le texte par defaut de pricing_rule est configure globalement
|
||||
|
||||
- Intent: centraliser un texte metier recurrent reutilise a la creation des
|
||||
lignes achat et vente.
|
||||
- Description:
|
||||
- Le module expose un singleton `purchase_trade.configuration` avec un champ
|
||||
texte `pricing_rule`.
|
||||
- Toute nouvelle `purchase.line` et `sale.line` doit prendre ce texte comme
|
||||
valeur par defaut de `pricing_rule`.
|
||||
- Resultat attendu:
|
||||
- la configuration est accessible depuis le menu `Prices`
|
||||
- la valeur sert de defaut a la creation des lignes
|
||||
- les lignes existantes ne sont pas modifiees retroactivement
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-014 - L'affectation d'un controller doit suivre l'ecart a l'objectif regional
|
||||
|
||||
- Intent: repartir les controllers selon les cibles definies dans l'onglet
|
||||
`Execution` des `party.party`.
|
||||
- Description:
|
||||
- chaque ligne `party.execution` fixe une cible `% targeted` pour un
|
||||
controller sur une `country.region`
|
||||
- le `% achieved` est calcule a partir des `stock.shipment.in` deja affectes
|
||||
a un controller dans cette zone
|
||||
- la zone d'un shipment est determinee par `shipment.to_location.country`
|
||||
- une region parente couvre aussi ses sous-regions
|
||||
- Resultat attendu:
|
||||
- pour une ligne `party.execution`, `achieved_percent` =
|
||||
`shipments de la zone avec ce controller / shipments controles de la zone`
|
||||
- le denominateur ne compte que les `stock.shipment.in` qui ont deja un
|
||||
`controller`; les shipments encore non affectes ne biaisent donc pas la
|
||||
statistique affichee
|
||||
- lors d'un choix automatique de controller, la priorite va a la regle dont
|
||||
l'ecart `targeted - achieved` est le plus eleve
|
||||
- un controller a `80%` cible et `40%` reel doit donc passer avant un
|
||||
controller a `50%` cible et `45%` reel sur la meme zone
|
||||
- l'appartenance a la zone se lit depuis `shipment.to_location.country`, et
|
||||
une region parente couvre aussi ses sous-regions
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-015 - Les weight reports distants par lot partent du weight report global attache au shipment
|
||||
|
||||
- Intent: separer la creation du `weight.report` global et l'export detaille
|
||||
par lot vers le systeme distant.
|
||||
- Description:
|
||||
- l'automation cree le `weight.report` global et l'attache au
|
||||
`stock.shipment.in`
|
||||
- l'export FastAPI par lot ne part plus directement de l'automation
|
||||
- l'utilisateur ouvre le `weight.report` voulu depuis le shipment et lance
|
||||
l'action d'export depuis ce rapport
|
||||
- Resultat attendu:
|
||||
- le rapport choisi sert de base unique pour calculer les payloads par lot
|
||||
- seuls les lots physiques des `incoming_moves` du shipment sont exportes
|
||||
- l'action exige au minimum un `controller` et un `returned_id` sur le
|
||||
shipment
|
||||
- les cles renvoyees par le systeme distant et la date d'envoi sont
|
||||
conservees sur le `weight.report` local
|
||||
- Priorite:
|
||||
- `importante`
|
||||
- Resultat attendu:
|
||||
- apres creation du lot virtuel, si aucun matching purchase n'existe:
|
||||
- appeler `Valuation.generate_from_sale_line(line)`
|
||||
- creer au moins la ligne `sale priced` fallback si la ligne porte un prix
|
||||
economique via le premium
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-012 - Fallback valuation basis sans summary: utiliser le prix economique de la ligne
|
||||
|
||||
- Intent: eviter qu'une valuation `basis` ouverte sorte a zero alors que la
|
||||
ligne a bien une valeur economique via le premium.
|
||||
- Description:
|
||||
- Une ligne `basis` peut ne pas avoir encore de `price_summary`.
|
||||
- Dans ce cas, la valuation fallback ne doit pas prendre `unit_price` seul si
|
||||
celui-ci est brut et hors premium.
|
||||
- Resultat attendu:
|
||||
- le fallback valuation `basis` doit utiliser:
|
||||
- `unit_price + premium converti`
|
||||
- cette regle vaut au minimum pour:
|
||||
- `sale.line` non matchee
|
||||
- `purchase.line` sans summary
|
||||
- Priorite:
|
||||
- `importante`
|
||||
|
||||
### BR-PT-013 - Create Contracts multi-lots doit conserver un matching par lot source
|
||||
|
||||
- Intent: permettre la creation d'un seul contrat mirror a partir de plusieurs
|
||||
open quantities sans perdre le lien lot-a-lot.
|
||||
- Description:
|
||||
- Le wizard `Create contracts` peut etre lance avec plusieurs `lot.qt`
|
||||
selectionnes.
|
||||
- En creation `matched`, le systeme doit creer un seul contrat avec une ligne
|
||||
par lot source selectionne, et chaque ligne doit etre matchee avec son lot
|
||||
d'origine.
|
||||
- Resultat attendu:
|
||||
- la quantite totale du wizard = somme des open quantities selectionnees
|
||||
- le contrat cree porte plusieurs lignes si plusieurs lots source sont
|
||||
selectionnes
|
||||
- chaque ligne creee reutilise le `shipment_origin` et le lot source qui lui
|
||||
correspondent
|
||||
- `created_by_code` doit rester positionne sur les lignes creees par wizard
|
||||
pour eviter la recreation automatique de lots virtuels dans les `validate`
|
||||
de `purchase.line`, `sale.line` et `lot.lot`
|
||||
- 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`
|
||||
- `modules/purchase_trade/valuation.py`
|
||||
- `modules/purchase_trade/sale.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
|
||||
- valuation purchase/sale sur lot physique matche
|
||||
- valuation sale-first sur sale non matchee avec lot virtual
|
||||
- valuation sale `basis` sans `price_summary`
|
||||
- absence de MTM sur les fees
|
||||
- premium en `priced`
|
||||
- premium en `basis`
|
||||
- premium en `linked currency`
|
||||
- synchro `basis` -> `linked_price` -> `unit_price`
|
||||
417
modules/purchase_trade/docs/template-properties.md
Normal file
417
modules/purchase_trade/docs/template-properties.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# Template Properties - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.2`
|
||||
Derniere mise a jour: `2026-04-07`
|
||||
|
||||
## 1) Objectif
|
||||
|
||||
- Lister les proprietes Python exposees pour alimenter les templates Relatorio.
|
||||
- Donner un point d'entree rapide aux createurs de templates.
|
||||
- Eviter de reparser tout `modules/purchase_trade/invoice.py`, `sale.py` ou `purchase.py`.
|
||||
|
||||
## 2) Fichiers sources
|
||||
|
||||
- Bridge facture:
|
||||
- `modules/purchase_trade/invoice.py`
|
||||
- Proprietes de vente reutilisables:
|
||||
- `modules/purchase_trade/sale.py`
|
||||
- Proprietes d'achat reutilisables:
|
||||
- `modules/purchase_trade/purchase.py`
|
||||
|
||||
## 3) Principes de lecture
|
||||
|
||||
- Pour une facture:
|
||||
- preferer les proprietes `report_*` exposees sur `account.invoice`
|
||||
- pour une facture finale detaillee, utiliser aussi les proprietes `report_*`
|
||||
exposees sur `account.invoice.line`
|
||||
- Pour une vente:
|
||||
- reutiliser si possible les proprietes `report_*` deja presentes sur `sale.sale`
|
||||
- Pour un achat:
|
||||
- reutiliser si possible les proprietes `report_*` deja presentes sur `purchase.purchase`
|
||||
- Pour un shipment entrant:
|
||||
- reutiliser si possible les proprietes `report_*` exposees sur `stock.shipment.in`
|
||||
|
||||
## 4) Propriete disponibles sur `account.invoice`
|
||||
|
||||
Source code: `modules/purchase_trade/invoice.py`
|
||||
|
||||
### Identite du document / parties
|
||||
|
||||
- `report_address`
|
||||
- Usage: adresse d'affichage de la facture
|
||||
- Source de verite: `sale.report_address` ou `purchase.report_address`, fallback `invoice.invoice_address.full_address`
|
||||
|
||||
- `report_contract_number`
|
||||
- Usage: numero de contrat
|
||||
- Source de verite: `sale.full_number` ou `purchase.full_number`
|
||||
|
||||
- `report_trader_initial`
|
||||
- Usage: initiales trader dans les templates
|
||||
- Source de verite: contrat lie
|
||||
|
||||
- `report_operator_initial`
|
||||
- Usage: initiales operator dans les templates
|
||||
- Source de verite: contrat lie
|
||||
|
||||
### Produit / contrat / quantites
|
||||
|
||||
- `report_origin`
|
||||
- Usage: origine produit
|
||||
- Source de verite: `sale.product_origin` ou `purchase.product_origin`
|
||||
|
||||
- `report_product_description`
|
||||
- Usage: description produit principale
|
||||
- Source de verite: premiere ligne metier liee a la facture
|
||||
|
||||
- `report_product_name`
|
||||
- Usage: nom produit principal
|
||||
- Source de verite: premiere ligne metier liee a la facture
|
||||
|
||||
- `report_description_upper`
|
||||
- Usage: description de ligne en majuscules
|
||||
- Source de verite: premiere `account.invoice.line`
|
||||
|
||||
- `report_crop_name`
|
||||
- Usage: campagne / crop
|
||||
- Source de verite: contrat lie
|
||||
|
||||
- `report_attributes_name`
|
||||
- Usage: attributs produit
|
||||
- Source de verite: premiere ligne metier liee a la facture
|
||||
|
||||
- `report_price`
|
||||
- Usage: prix en toutes lettres
|
||||
- Source de verite: `sale.report_price` ou `purchase.report_price`
|
||||
|
||||
- `report_nb_bale`
|
||||
- Usage: nombre de balles
|
||||
- Source de verite: `sale.report_nb_bale` ou recalcul sur les lots physiques
|
||||
|
||||
- `report_gross`
|
||||
- Usage: poids brut
|
||||
- Source de verite: `sale.report_gross` ou recalcul sur les lots physiques
|
||||
|
||||
- `report_net`
|
||||
- Usage: poids net
|
||||
- Source de verite: `sale.report_net` ou `purchase.report_net` ou recalcul sur les lots physiques
|
||||
|
||||
- `report_lbs`
|
||||
- Usage: poids net converti en LBS
|
||||
- Source de verite: conversion de `report_net`
|
||||
|
||||
- `report_quantity_lines`
|
||||
- Usage: detail quantite multi-lignes pour les templates facture
|
||||
- Source de verite: `sale.report_quantity_lines` si vente source, sinon aggregation des `account.invoice.line`
|
||||
|
||||
### Bloc prix type `sale_ict`
|
||||
|
||||
- `report_rate_currency_upper`
|
||||
- Usage: devise du bloc `At ... PER ...`
|
||||
- Source de verite: premiere `account.invoice.line` de type `line`
|
||||
|
||||
- `report_rate_value`
|
||||
- Usage: prix numerique du bloc `At ... PER ...`
|
||||
- Source de verite: premiere `account.invoice.line` de type `line`
|
||||
|
||||
- `report_rate_unit_upper`
|
||||
- Usage: unite du bloc `At ... PER ...`
|
||||
- Source de verite: premiere `account.invoice.line` de type `line`
|
||||
|
||||
- `report_rate_price_words`
|
||||
- Usage: prix en toutes lettres dans le bloc `At ... PER ...`
|
||||
- Source de verite: premiere `account.invoice.line` de type `line`, fallback `report_price`
|
||||
|
||||
- `report_rate_pricing_text`
|
||||
- Usage: texte de pricing additionnel
|
||||
- Source de verite: premiere `account.invoice.line` de type `line`
|
||||
|
||||
- `report_rate_lines`
|
||||
- Usage: detail multi-lignes du bloc `At ... PER ...`
|
||||
- Source de verite: `sale.report_price_lines` si vente source, sinon aggregation des `account.invoice.line`
|
||||
|
||||
### Logistique / shipment
|
||||
|
||||
- `report_shipment`
|
||||
- Usage: resume vessel / BL / shipment
|
||||
- Source de verite: contrat lie
|
||||
|
||||
- `report_bl_date`
|
||||
- Usage: date de BL
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_bl_nb`
|
||||
- Usage: numero de BL
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_vessel`
|
||||
- Usage: nom du vessel
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_loading_port`
|
||||
- Usage: port of loading
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_discharge_port`
|
||||
- Usage: port of discharge
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_controller_name`
|
||||
- Usage: nom du controller
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
- `report_si_number`
|
||||
- Usage: S/I number
|
||||
- Source de verite: shipment du lot physique
|
||||
|
||||
### Conditions commerciales
|
||||
|
||||
- `report_incoterm`
|
||||
- Usage: incoterm + location
|
||||
- Source de verite: contrat lie
|
||||
|
||||
- `report_payment_date`
|
||||
- Usage: date de paiement
|
||||
- Source de verite: contrat lie
|
||||
|
||||
- `report_payment_description`
|
||||
- Usage: description des conditions de paiement
|
||||
- Source de verite: payment term du contrat ou de la facture
|
||||
|
||||
### Pro forma / freight
|
||||
|
||||
- `report_proforma_invoice_number`
|
||||
- Usage: numero de facture provisoire
|
||||
- Source de verite: lot physique via `invoice_line_prov` ou `sale_invoice_line_prov`
|
||||
|
||||
- `report_proforma_invoice_date`
|
||||
- Usage: date de facture provisoire
|
||||
- Source de verite: lot physique via `invoice_line_prov` ou `sale_invoice_line_prov`
|
||||
|
||||
- `report_freight_amount`
|
||||
- Usage: `FREIGHT VALUE`
|
||||
- Source de verite:
|
||||
- lot physique
|
||||
- shipment du lot
|
||||
- `fee.fee` avec `product.name = 'Maritime freight'`
|
||||
- montant = `fee.get_amount()`
|
||||
|
||||
- `report_freight_currency_symbol`
|
||||
- Usage: devise du `FREIGHT VALUE`
|
||||
- Source de verite: devise du fee `Maritime freight`, fallback devise facture
|
||||
|
||||
### Payment order
|
||||
|
||||
- `report_payment_order_short_name`
|
||||
- Usage: nom court emetteur du payment order
|
||||
- Source de verite: `invoice.company.party.rec_name`
|
||||
|
||||
- `report_payment_order_document_reference`
|
||||
- Usage: reference du document payment order
|
||||
- Source de verite: `invoice.number`, fallback `invoice.reference`
|
||||
|
||||
- `report_payment_order_from_account_nb`
|
||||
- Usage: compte bancaire emetteur
|
||||
- Source de verite: premier `bank.account` de la societe
|
||||
|
||||
- `report_payment_order_to_bank_name`
|
||||
- Usage: banque destinataire
|
||||
- Source de verite: banque du premier compte bancaire du partenaire facture
|
||||
|
||||
- `report_payment_order_to_bank_city`
|
||||
- Usage: ville banque destinataire
|
||||
- Source de verite: adresse de la banque destinataire
|
||||
|
||||
- `report_payment_order_amount`
|
||||
- Usage: montant payment order
|
||||
- Source de verite: `invoice.total_amount`
|
||||
|
||||
- `report_payment_order_currency_code`
|
||||
- Usage: devise payment order
|
||||
- Source de verite: `invoice.currency` (`code`, fallback `rec_name/symbol`)
|
||||
|
||||
- `report_payment_order_amount_text`
|
||||
- Usage: montant en lettres
|
||||
- Source de verite: conversion `amount_to_currency_words(invoice.total_amount)`
|
||||
|
||||
- `report_payment_order_value_date`
|
||||
- Usage: date valeur
|
||||
- Source de verite: `invoice.payment_term_date`, fallback `invoice.invoice_date`
|
||||
|
||||
- `report_payment_order_company_address`
|
||||
- Usage: bloc beneficiaire
|
||||
- Source de verite: `invoice.invoice_address.full_address`, fallback
|
||||
`invoice.report_address`
|
||||
|
||||
- `report_payment_order_beneficiary_account_nb`
|
||||
- Usage: compte beneficiaire
|
||||
- Source de verite: premier compte bancaire du `invoice.party`
|
||||
|
||||
- `report_payment_order_beneficiary_bank_name`
|
||||
- Usage: banque beneficiaire
|
||||
- Source de verite: banque du compte beneficiaire
|
||||
|
||||
- `report_payment_order_beneficiary_bank_city`
|
||||
- Usage: ville banque beneficiaire
|
||||
- Source de verite: adresse banque beneficiaire
|
||||
|
||||
- `report_payment_order_swift_code`
|
||||
- Usage: swift/bic beneficiaire
|
||||
- Source de verite: `bank.bic`
|
||||
|
||||
- `report_payment_order_other_instructions`
|
||||
- Usage: instructions complementaires
|
||||
- Source de verite: `invoice.description`
|
||||
|
||||
- `report_payment_order_reference`
|
||||
- Usage: reference business de paiement
|
||||
- Source de verite: `invoice.reference`, fallback `invoice.number`
|
||||
|
||||
- `report_payment_order_current_user`
|
||||
- Usage: signataire payment order
|
||||
- Source de verite: utilisateur courant (`res.user`)
|
||||
|
||||
- `report_payment_order_current_user_email`
|
||||
- Usage: email retour swift
|
||||
- Source de verite: contact email du `party` utilisateur, fallback `user.email`
|
||||
|
||||
## 5) Proprietes disponibles sur `account.invoice.line`
|
||||
|
||||
Source code: `modules/purchase_trade/invoice.py`
|
||||
|
||||
- `report_product_description`
|
||||
- Usage: description produit de la ligne
|
||||
- Source de verite: `invoice_line.product` ou `origin.product`
|
||||
|
||||
- `report_description_upper`
|
||||
- Usage: description de ligne en uppercase
|
||||
- Source de verite: `invoice_line.description`
|
||||
|
||||
- `report_crop_name`
|
||||
- Usage: crop de la ligne
|
||||
- Source de verite: contrat relie via `origin`
|
||||
|
||||
- `report_attributes_name`
|
||||
- Usage: attributs de la ligne
|
||||
- Source de verite: `origin.attributes_name`
|
||||
|
||||
- `report_net`
|
||||
- Usage: quantite nette de la ligne
|
||||
- Source de verite: `invoice_line.quantity`
|
||||
|
||||
- `report_lbs`
|
||||
- Usage: quantite convertie en LBS
|
||||
- Source de verite: conversion de `report_net`
|
||||
|
||||
- `report_rate_currency_upper`
|
||||
- Usage: devise de prix de la ligne
|
||||
- Source de verite: `origin.linked_currency` ou `invoice_line.currency`
|
||||
|
||||
- `report_rate_value`
|
||||
- Usage: prix numerique de la ligne
|
||||
- Source de verite: `invoice_line.unit_price`
|
||||
|
||||
- `report_rate_unit_upper`
|
||||
- Usage: unite de prix de la ligne
|
||||
- Source de verite: `origin.linked_unit` ou `invoice_line.unit`
|
||||
|
||||
- `report_rate_price_words`
|
||||
- Usage: prix en toutes lettres de la ligne
|
||||
- Source de verite: contrat relie via `trade.report_price`
|
||||
|
||||
- `report_rate_pricing_text`
|
||||
- Usage: texte de pricing de la ligne
|
||||
- Source de verite: `origin.get_pricing_text`
|
||||
|
||||
## 6) Proprietes utiles deja presentes sur `sale.sale`
|
||||
|
||||
Source code: `modules/purchase_trade/sale.py`
|
||||
|
||||
- `report_terms`
|
||||
- `report_crop_name`
|
||||
- `report_gross`
|
||||
- `report_net`
|
||||
- `report_qt`
|
||||
- `report_total_quantity`
|
||||
- `report_quantity_unit_upper`
|
||||
- `report_quantity_lines`
|
||||
- `report_nb_bale`
|
||||
- `report_deal`
|
||||
- `report_packing`
|
||||
- `report_price`
|
||||
- `report_price_lines`
|
||||
- `report_delivery`
|
||||
- `report_payment_date`
|
||||
- `report_shipment`
|
||||
- `report_shipment_periods`
|
||||
- `report_product_name`
|
||||
- `report_product_description`
|
||||
|
||||
Usage typique:
|
||||
- base de travail pour les templates de type `sale_ict.fodt`
|
||||
- source de verite de plusieurs proprietes du bridge facture
|
||||
|
||||
## 7) Proprietes utiles deja presentes sur `purchase.purchase`
|
||||
|
||||
Source code: `modules/purchase_trade/purchase.py`
|
||||
|
||||
- `report_terms`
|
||||
- `report_qt`
|
||||
- `report_price`
|
||||
- `report_delivery`
|
||||
- `report_payment_date`
|
||||
- `report_shipment`
|
||||
|
||||
Usage typique:
|
||||
- templates et bridges pour facturation fournisseur
|
||||
- fallback achat quand une facture n'est pas liee a une vente
|
||||
|
||||
## 8) Templates connus qui utilisent ces proprietes
|
||||
|
||||
- `modules/account_invoice/invoice_ict.fodt`
|
||||
- `modules/account_invoice/invoice_ict_final.fodt`
|
||||
- `modules/sale/sale_ict.fodt`
|
||||
- `modules/stock/insurance.fodt`
|
||||
|
||||
## 9) Proprietes utiles deja presentes sur `stock.shipment.in`
|
||||
|
||||
Source code: `modules/purchase_trade/stock.py`
|
||||
|
||||
- `report_product_name`
|
||||
- `report_product_description`
|
||||
- `report_insurance_footer_ref`
|
||||
- `report_insurance_certificate_number`
|
||||
- `report_insurance_account_of`
|
||||
- `report_insurance_goods_description`
|
||||
- `report_insurance_loading_port`
|
||||
- `report_insurance_discharge_port`
|
||||
- `report_insurance_transport`
|
||||
- `report_insurance_amount`
|
||||
- `report_insurance_incoming_amount`
|
||||
- `report_insurance_amount_insured`
|
||||
- `report_insurance_surveyor`
|
||||
- `report_insurance_contact_surveyor`
|
||||
- `report_insurance_issue_place_and_date`
|
||||
|
||||
Usage typique:
|
||||
- templates shipment relies a l'assurance
|
||||
- `report_insurance_amount`: montant affiche dans `Amount insured` (priorite a
|
||||
`110%` du total incoming, fallback fee `Insurance`)
|
||||
- `report_insurance_incoming_amount`: somme `incoming_moves` de
|
||||
`quantity * unit_price`, avec fallback lot
|
||||
(`lot.line.unit_price * lot.get_current_quantity_converted()`)
|
||||
- `report_insurance_amount_insured`: `110%` de
|
||||
`report_insurance_incoming_amount`
|
||||
- `report_insurance_contact_surveyor`: surveyor affiche sous
|
||||
`Contact the following surveyor` (priorite au champ shipment `surveyor`,
|
||||
puis fallback controller / fee `Insurance`)
|
||||
- base de travail pour un certificat d'assurance lie a un shipment
|
||||
|
||||
## 10) Recommandations
|
||||
|
||||
- Avant d'ajouter une nouvelle expression dans un `.fodt`, verifier si une
|
||||
propriete `report_*` existe deja ici.
|
||||
- Si une nouvelle propriete est ajoutee pour un template, la documenter dans ce
|
||||
fichier.
|
||||
- Pour les donnees logistiques facture, privilegier toujours:
|
||||
- facture -> ligne metier -> lot physique -> shipment / fee
|
||||
322
modules/purchase_trade/docs/template-rules.md
Normal file
322
modules/purchase_trade/docs/template-rules.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Template Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.4`
|
||||
Derniere mise a jour: `2026-04-07`
|
||||
|
||||
## 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.
|
||||
- Centraliser les proprietes `report_*` dans une documentation reutilisable.
|
||||
|
||||
## 2.1) Index de reference
|
||||
|
||||
- Catalogue des proprietes templates:
|
||||
- `modules/purchase_trade/docs/template-properties.md`
|
||||
|
||||
## 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
|
||||
- pour les clients multi-templates, preferer une configuration metier qui
|
||||
stocke le nom du template par action (`Invoice`, `CN/DN`, `Prepayment`)
|
||||
plutot qu'une modification manuelle de `ir_action_report.report`
|
||||
|
||||
### TR-012 - Centraliser les templates client dans `Document Templates`
|
||||
|
||||
- Pour les templates client-specifiques, ne pas modifier `ir_action_report.report`
|
||||
en base a la main selon l'environnement ou le client.
|
||||
- Preferer la configuration singleton `purchase_trade.configuration`,
|
||||
exposee dans `Documents > Configuration > Document Templates`.
|
||||
- Sections actuellement attendues:
|
||||
- `Sale`
|
||||
- `Invoice`
|
||||
- `Payment`
|
||||
- `Purchase`
|
||||
- `Shipment`
|
||||
- Dans la section `Shipment`, les templates metier attendus sont:
|
||||
- `Shipping`
|
||||
- `Insurance`
|
||||
- `Packing List`
|
||||
- Regle:
|
||||
- si le champ de template correspondant est vide, le report doit echouer
|
||||
explicitement avec `No template found`
|
||||
- ne pas masquer dynamiquement l'action d'impression si ce n'est pas
|
||||
necessaire
|
||||
|
||||
### TR-013 - `sale_melya.fodt` et `invoice_melya.fodt` doivent afficher nom + description produit
|
||||
|
||||
- Dans les templates client Melya, le bloc produit doit prevoir:
|
||||
- une ligne pour le nom produit
|
||||
- une ligne pour la description produit
|
||||
- Ne pas dereferencer directement `line.product.name` / `line.product.description`
|
||||
dans les `.fodt`.
|
||||
- Preferer:
|
||||
- `sale.report_product_name`
|
||||
- `sale.report_product_description`
|
||||
- `invoice.report_product_name`
|
||||
- `invoice.report_product_description`
|
||||
|
||||
### TR-014 - `invoice_melya.fodt` doit afficher `Invoice` et `Reference` sur les bons champs
|
||||
|
||||
- Pour `modules/account_invoice/invoice_melya.fodt`:
|
||||
- `Invoice` doit afficher `invoice.number`
|
||||
- `Reference` doit afficher `invoice.report_contract_number`
|
||||
- Ne pas reutiliser `invoice.reference` pour ce label dans ce template client
|
||||
sans demande explicite
|
||||
|
||||
### TR-015 - Le template `stock/insurance.fodt` doit lire sur `stock.shipment.in`
|
||||
|
||||
- Le template `modules/stock/insurance.fodt` est pilote par le report
|
||||
`stock.shipment.in.insurance`.
|
||||
- Toutes les croix rouges / placeholders metier doivent etre remplacees par
|
||||
des proprietes `report_*` exposees sur `stock.shipment.in`.
|
||||
- Pour ce template, ne pas compter sur une variable Genshi locale `shipment`
|
||||
dans tout le document; preferer `records[0]....` dans le `.fodt`.
|
||||
- Source de verite du montant assure:
|
||||
- sommer les montants des `incoming_moves` du shipment
|
||||
- montant d'un move = `move.quantity * move.unit_price`
|
||||
- si `move.unit_price` est vide, fallback via lot:
|
||||
`lot.line.unit_price * lot.get_current_quantity_converted()`
|
||||
- exposer au moins:
|
||||
- le montant total des incoming moves
|
||||
- le montant assure a `110%` de ce total
|
||||
- pour le placeholder `Amount insured`, `report_insurance_amount` doit
|
||||
afficher ce `110%`, avec fallback fee `Insurance` si aucun montant
|
||||
incoming n'est calculable
|
||||
|
||||
### TR-016 - Hypotheses actuelles pour le certificat d'assurance shipment
|
||||
|
||||
- Tant qu'une source metier plus precise n'est pas fournie:
|
||||
- numero du certificat: `shipment.bl_number`, sinon `shipment.number`
|
||||
- `insured for account of`: client de la premiere ligne metier retrouvee via
|
||||
lot physique, sinon `shipment.supplier`
|
||||
- `surveyor`: `shipment.surveyor`, sinon `shipment.controller`, sinon
|
||||
fournisseur du fee `Insurance`
|
||||
- lieu/date d'emission: ville de la societe + date du jour
|
||||
- Si une source differente est decidee plus tard, corriger la propriete Python
|
||||
plutot que complexifier `insurance.fodt`
|
||||
|
||||
### TR-017 - `payment_order.fodt` doit utiliser des proprietes `report_payment_order_*`
|
||||
|
||||
- Pour `modules/account_invoice/payment_order.fodt`, ne pas utiliser des
|
||||
placeholders externes legacy (tokens metier entre `<...>` du systeme source).
|
||||
- Tous les placeholders du template doivent pointer vers des proprietes Python
|
||||
stables exposees sur `account.invoice`:
|
||||
- `report_payment_order_document_reference`
|
||||
- `report_payment_order_from_account_nb`
|
||||
- `report_payment_order_to_bank_name`
|
||||
- `report_payment_order_to_bank_city`
|
||||
- `report_payment_order_amount`
|
||||
- `report_payment_order_currency_code`
|
||||
- `report_payment_order_amount_text`
|
||||
- `report_payment_order_value_date`
|
||||
- `report_payment_order_company_address`
|
||||
- `report_payment_order_beneficiary_account_nb`
|
||||
- `report_payment_order_beneficiary_bank_name`
|
||||
- `report_payment_order_beneficiary_bank_city`
|
||||
- `report_payment_order_swift_code`
|
||||
- `report_payment_order_other_instructions`
|
||||
- `report_payment_order_reference`
|
||||
- `report_payment_order_current_user`
|
||||
- `report_payment_order_current_user_email`
|
||||
- Eviter les marqueurs conditionnels heredites de l'ancien moteur (`++...`):
|
||||
privilegier des placeholders simples avec fallback `or ''`.
|
||||
|
||||
### TR-018 - Un template configure n'apparait dans le form que si une action report existe
|
||||
|
||||
- Ajouter un champ dans `Document Templates` ne suffit pas a rendre un
|
||||
template imprimable depuis la fiche.
|
||||
- Pour afficher l'entree dans `account.invoice`, il faut aussi:
|
||||
- un `ir.action.report` sur `model = account.invoice`
|
||||
- un `ir.action.keyword` `form_print` lie a cette action
|
||||
- Appliquer cette regle pour `Payment Order` comme pour `Invoice`,
|
||||
`Prepayment` et `CN/DN`.
|
||||
|
||||
### TR-019 - Un placeholder Relatorio doit etre dans une balise `text:placeholder`
|
||||
|
||||
- Dans un `.fodt`, une expression du type `<records[0].report_* ...>`
|
||||
ecrite en texte brut peut s'afficher telle quelle a l'impression.
|
||||
- Regle stricte:
|
||||
- encapsuler les expressions dans
|
||||
`<text:placeholder text:placeholder-type="text">...</text:placeholder>`
|
||||
- ne pas laisser de token `<...>` directement dans un `text:span`,
|
||||
`text:p`, `text:h`, etc.
|
||||
- Exemple:
|
||||
- incorrect:
|
||||
`PAYMENT ORDER <records[0].report_payment_order_document_reference or ''>`
|
||||
- correct:
|
||||
`PAYMENT ORDER <text:placeholder text:placeholder-type="text"><records[0].report_payment_order_document_reference or ''></text:placeholder>`
|
||||
|
||||
### 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.
|
||||
|
||||
### TR-009 - Ne pas dereferencer directement `del_period.description` dans les templates
|
||||
|
||||
- Eviter les expressions du type:
|
||||
- `sale.lines[0].del_period.description`
|
||||
- `purchase.lines[0].del_period.description`
|
||||
- Meme avec un `if ... else`, ces acces sont fragiles dans un `.fodt` et
|
||||
rendent le debug plus difficile.
|
||||
- Preferer une propriete Python stable:
|
||||
- `sale.report_delivery_period_description`
|
||||
- `purchase.report_delivery_period_description`
|
||||
- `invoice.report_delivery_period_description`
|
||||
|
||||
### TR-010 - En template, un contrat `basis` affiche le premium comme prix
|
||||
|
||||
- Pour les templates commerciaux/facture (`sale_ict`, `invoice_ict`, etc.),
|
||||
le prix affiche d'une ligne `basis` ne doit pas etre le prix economique total
|
||||
(`unit_price`, `linked_price` ou prix basis brut).
|
||||
- La valeur a afficher est uniquement le `premium`:
|
||||
- en devise/unite liee si `linked currency` est active
|
||||
- sinon dans la devise/unite native de la ligne
|
||||
- Le texte de curve / pricing (`ON ICE ...`) reste affiche a cote, mais la
|
||||
valeur numerique et sa version en lettres doivent representer le premium.
|
||||
|
||||
### TR-011 - Pour `NB BALES` sur une facture, sommer les `lot_qt` des lignes facture
|
||||
|
||||
- Pour `invoice_ict.fodt` et `invoice_ict_final.fodt`, la source de verite du
|
||||
nombre de bales n'est pas le poids (`report_net`, `report_gross`) mais
|
||||
`line.lot.lot_qt`.
|
||||
- La regle attendue est:
|
||||
- lire les lignes de facture
|
||||
- recuperer leur `lot`
|
||||
- sommer `lot.lot_qt`
|
||||
- sur une note finale, tenir compte du signe de la ligne de facture pour que
|
||||
les lignes positives et negatives se compensent
|
||||
- Ne pas recalculer le nombre de bales a partir du poids:
|
||||
- les poids peuvent varier (humidite, poids net/gross)
|
||||
- le nombre de bales peut rester stable
|
||||
|
||||
## 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.tools import is_full_text, lstrip_wildcard
|
||||
from trytond.transaction import Transaction, inactive_records
|
||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
|
||||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||
from sql.conditionals import Case
|
||||
from sql import Column, Literal
|
||||
@@ -18,120 +18,11 @@ import datetime
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from trytond.exceptions import UserWarning, UserError
|
||||
from trytond.modules.account.exceptions import PeriodNotFoundError
|
||||
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VALTYPE = [
|
||||
('priced', 'Price'),
|
||||
('pur. priced', 'Pur. price'),
|
||||
('pur. efp', 'Pur. efp'),
|
||||
('sale priced', 'Sale price'),
|
||||
('sale efp', 'Sale efp'),
|
||||
('line fee', 'Line fee'),
|
||||
('pur. fee', 'Pur. fee'),
|
||||
('sale fee', 'Sale fee'),
|
||||
('shipment fee', 'Shipment fee'),
|
||||
('market', 'Market'),
|
||||
('derivative', 'Derivative'),
|
||||
]
|
||||
|
||||
class Valuation(ModelSQL,ModelView):
|
||||
"Valuation"
|
||||
__name__ = 'valuation.valuation'
|
||||
|
||||
purchase = fields.Many2One('purchase.purchase',"Purchase")
|
||||
line = fields.Many2One('purchase.line',"Purch. Line")
|
||||
date = fields.Date("Date")
|
||||
type = fields.Selection(VALTYPE, "Type")
|
||||
reference = fields.Char("Reference")
|
||||
counterparty = fields.Many2One('party.party',"Counterparty")
|
||||
product = fields.Many2One('product.product',"Product")
|
||||
state = fields.Char("State")
|
||||
price = fields.Numeric("Price",digits='unit')
|
||||
currency = fields.Many2One('currency.currency',"Cur")
|
||||
quantity = fields.Numeric("Quantity",digits='unit')
|
||||
unit = fields.Many2One('product.uom',"Unit")
|
||||
amount = fields.Numeric("Amount",digits='unit')
|
||||
mtm = fields.Numeric("Mtm",digits='unit')
|
||||
lot = fields.Many2One('lot.lot',"Lot")
|
||||
|
||||
class ValuationDyn(ModelSQL,ModelView):
|
||||
"Valuation"
|
||||
__name__ = 'valuation.valuation.dyn'
|
||||
|
||||
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
|
||||
r_line = fields.Many2One('purchase.line',"Line")
|
||||
r_date = fields.Date("Date")
|
||||
r_type = fields.Selection(VALTYPE, "Type")
|
||||
r_reference = fields.Char("Reference")
|
||||
r_counterparty = fields.Many2One('party.party',"Counterparty")
|
||||
r_product = fields.Many2One('product.product',"Product")
|
||||
r_state = fields.Char("State")
|
||||
r_price = fields.Numeric("Price",digits='r_unit')
|
||||
r_currency = fields.Many2One('currency.currency',"Cur")
|
||||
r_quantity = fields.Numeric("Quantity",digits='r_unit')
|
||||
r_unit = fields.Many2One('product.uom',"Unit")
|
||||
r_amount = fields.Numeric("Amount",digits='r_unit')
|
||||
r_mtm = fields.Numeric("Mtm",digits='r_unit')
|
||||
r_lot = fields.Many2One('lot.lot',"Lot")
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
val = Valuation.__table__()
|
||||
context = Transaction().context
|
||||
group_pnl = context.get('group_pnl')
|
||||
wh = (val.id > 0)
|
||||
# query = val.select(
|
||||
# Literal(0).as_('create_uid'),
|
||||
# CurrentTimestamp().as_('create_date'),
|
||||
# Literal(None).as_('write_uid'),
|
||||
# Literal(None).as_('write_date'),
|
||||
# val.id.as_('id'),
|
||||
# val.purchase.as_('r_purchase'),
|
||||
# val.line.as_('r_line'),
|
||||
# val.date.as_('r_date'),
|
||||
# val.type.as_('r_type'),
|
||||
# val.reference.as_('r_reference'),
|
||||
# val.counterparty.as_('r_counterparty'),
|
||||
# val.product.as_('r_product'),
|
||||
# val.state.as_('r_state'),
|
||||
# val.price.as_('r_price'),
|
||||
# val.currency.as_('r_currency'),
|
||||
# val.quantity.as_('r_quantity'),
|
||||
# val.unit.as_('r_unit'),
|
||||
# val.amount.as_('r_amount'),
|
||||
# val.mtm.as_('r_mtm'),
|
||||
# val.lot.as_('r_lot'),
|
||||
# where=wh)
|
||||
|
||||
#if group_pnl==True:
|
||||
query = val.select(
|
||||
Literal(0).as_('create_uid'),
|
||||
CurrentTimestamp().as_('create_date'),
|
||||
Literal(None).as_('write_uid'),
|
||||
Literal(None).as_('write_date'),
|
||||
Max(val.id).as_('id'),
|
||||
Max(val.purchase).as_('r_purchase'),
|
||||
Max(val.line).as_('r_line'),
|
||||
Max(val.date).as_('r_date'),
|
||||
val.type.as_('r_type'),
|
||||
Max(val.reference).as_('r_reference'),
|
||||
val.counterparty.as_('r_counterparty'),
|
||||
Max(val.product).as_('r_product'),
|
||||
val.state.as_('r_state'),
|
||||
Avg(val.price).as_('r_price'),
|
||||
Max(val.currency).as_('r_currency'),
|
||||
Sum(val.quantity).as_('r_quantity'),
|
||||
Max(val.unit).as_('r_unit'),
|
||||
Sum(val.amount).as_('r_amount'),
|
||||
Sum(val.mtm).as_('r_mtm'),
|
||||
Max(val.lot).as_('r_lot'),
|
||||
where=wh,
|
||||
group_by=[val.type,val.counterparty,val.state])
|
||||
|
||||
return query
|
||||
|
||||
def filter_state(state):
|
||||
def filter(func):
|
||||
@wraps(func)
|
||||
@@ -166,16 +57,28 @@ class Fee(ModelSQL,ModelView):
|
||||
('lumpsum', 'Lump sum'),
|
||||
('perqt', 'Per qt'),
|
||||
('pprice', '% price'),
|
||||
('rate', '% rate'),
|
||||
('pcost', '% cost price'),
|
||||
('ppack', 'Per packing'),
|
||||
], 'Mode', required=True)
|
||||
inherit_qt = fields.Boolean("Inh Qt")
|
||||
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
|
||||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
||||
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
|
||||
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
||||
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
|
||||
unit = fields.Many2One('product.uom',"Unit",domain=[
|
||||
If(Eval('mode') == 'ppack',
|
||||
('category', '=', Eval('packing_category')),
|
||||
()),
|
||||
],
|
||||
states={
|
||||
'readonly': (Bool(Eval('mode') != 'ppack') & Bool(Eval('mode') != 'perqt')),
|
||||
},
|
||||
depends=['mode', 'packing_category'])
|
||||
packing_category = fields.Function(fields.Many2One('product.uom.category',"Packing Category"),'on_change_with_packing_category')
|
||||
inherit_shipment = fields.Boolean("Inh Sh",states={
|
||||
'invisible': (Eval('shipment_in')),
|
||||
})
|
||||
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
||||
|
||||
qt_state = fields.Many2One('lot.qt.type',"Qt State")
|
||||
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
|
||||
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
|
||||
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
|
||||
@@ -191,9 +94,94 @@ class Fee(ModelSQL,ModelView):
|
||||
|
||||
weight_type = fields.Selection([
|
||||
('net', 'Net'),
|
||||
('brut', 'Brut'),
|
||||
('brut', 'Gross'),
|
||||
], string='W. type')
|
||||
|
||||
fee_date = fields.Date("Date")
|
||||
|
||||
@classmethod
|
||||
def default_fee_date(cls):
|
||||
Date = Pool().get('ir.date')
|
||||
return Date.today()
|
||||
|
||||
@classmethod
|
||||
def default_qt_state(cls):
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('name','=','BL')])
|
||||
if lqt:
|
||||
return lqt[0].id
|
||||
|
||||
@fields.depends('mode','unit')
|
||||
def on_change_with_packing_category(self, name=None):
|
||||
UnitCategory = Pool().get('product.uom.category')
|
||||
packing = UnitCategory.search(['name','=','Packing'])
|
||||
if packing:
|
||||
return packing[0]
|
||||
|
||||
@fields.depends('line','sale_line','shipment_in','lots','price','unit','auto_calculation','mode','_parent_line.unit','_parent_line.lots','_parent_sale_line.unit','_parent_sale_line.lots','_parent_shipment_in.id')
|
||||
def on_change_with_quantity(self, name=None):
|
||||
qt = None
|
||||
unit = None
|
||||
line = self.line
|
||||
logger.info("ON_CHANGE_WITH_LINE:%s",line)
|
||||
if not line:
|
||||
line = self.sale_line
|
||||
if line:
|
||||
if line.lots:
|
||||
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots])
|
||||
qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots])
|
||||
unit = line.lots[0].lot_unit
|
||||
logger.info("ON_CHANGE_WITH_QT0:%s",qt)
|
||||
logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in)
|
||||
if self.shipment_in:
|
||||
Lot = Pool().get('lot.lot')
|
||||
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
||||
logger.info("ON_CHANGE_WITH_LOTS:%s",lots)
|
||||
if lots:
|
||||
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots])
|
||||
qt_ = sum([e.get_current_quantity_converted(0) for e in lots])
|
||||
unit = lots[0].lot_unit
|
||||
if not qt:
|
||||
logger.info("ON_CHANGE_WITH_QT1:%s",qt)
|
||||
LotQt = Pool().get('lot.qt')
|
||||
if self.shipment_in:
|
||||
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
||||
if lqts:
|
||||
qt = Decimal(lqts[0].lot_quantity)
|
||||
qt_ = qt
|
||||
unit = lqts[0].lot_unit
|
||||
logger.info("ON_CHANGE_WITH_QT2:%s",qt)
|
||||
if self.mode != 'ppack':
|
||||
return qt
|
||||
else:
|
||||
if self.auto_calculation:
|
||||
logger.info("AUTOCALCULATION:%s",qt)
|
||||
logger.info("AUTOCALCULATION2:%s",qt_)
|
||||
logger.info("AUTOCALCULATION3:%s",Decimal(unit.factor))
|
||||
logger.info("AUTOCALCULATION4:%s",Decimal(self.unit.factor))
|
||||
return (qt_ * Decimal(unit.factor) / Decimal(self.unit.factor)).to_integral_value(rounding=ROUND_UP)
|
||||
|
||||
@fields.depends('price','mode','_parent_line.lots','_parent_sale_line.lots','shipment_in')
|
||||
def on_change_with_unit(self, name=None):
|
||||
if self.mode != 'ppack' and self.mode != 'perqt':
|
||||
line = self.line
|
||||
if not line:
|
||||
line = self.sale_line
|
||||
if line:
|
||||
if line.lots:
|
||||
if len(line.lots) == 1:
|
||||
return line.lots[0].lot_unit_line
|
||||
else:
|
||||
return line.lots[1].lot_unit_line
|
||||
if self.shipment_in:
|
||||
Lot = Pool().get('lot.lot')
|
||||
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
||||
logger.info("ON_CHANGE_WITH_UNIT:%s",lots)
|
||||
if lots:
|
||||
return lots[0].lot_unit_line
|
||||
else:
|
||||
return self.unit
|
||||
|
||||
def get_lots(self, name):
|
||||
logger.info("GET_LOTS_LINE:%s",self.line)
|
||||
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
||||
@@ -230,6 +218,23 @@ class Fee(ModelSQL,ModelView):
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
||||
|
||||
def get_non_cog(self,lot):
|
||||
MoveLine = Pool().get('account.move.line')
|
||||
Currency = Pool().get('currency.currency')
|
||||
Date = Pool().get('ir.date')
|
||||
AccountConfiguration = Pool().get('account.configuration')
|
||||
account_configuration = AccountConfiguration(1)
|
||||
Uom = Pool().get('product.uom')
|
||||
ml = MoveLine.search([
|
||||
('lot', '=', lot.id),
|
||||
('fee', '=', self.id),
|
||||
('account', '=', self.product.account_stock_in_used.id),
|
||||
])
|
||||
|
||||
logger.info("GET_NON_COG_FEE:%s",ml)
|
||||
if ml:
|
||||
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
@@ -248,13 +253,14 @@ class Fee(ModelSQL,ModelView):
|
||||
def default_p_r(cls):
|
||||
return 'pay'
|
||||
|
||||
def get_unit(self, name):
|
||||
Lot = Pool().get('lot.lot')
|
||||
if self.lots:
|
||||
if self.lots[0].line:
|
||||
return self.lots[0].line.unit
|
||||
if self.lots[0].sale_line:
|
||||
return self.lots[0].sale_line.unit
|
||||
def get_unit(self, name=None):
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
fl = FeeLots.search(['fee','=',self.id])
|
||||
if fl:
|
||||
if fl[0].lot.line:
|
||||
return fl[0].lot.line.unit
|
||||
if fl[0].lot.sale_line:
|
||||
return fl[0].lot.sale_line.unit
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@@ -285,7 +291,11 @@ class Fee(ModelSQL,ModelView):
|
||||
return round(self.price / self.quantity,4)
|
||||
elif self.mode == 'perqt':
|
||||
return self.price
|
||||
elif self.mode == 'pprice':
|
||||
elif self.mode == 'ppack':
|
||||
unit = self.get_unit()
|
||||
if unit and self.unit:
|
||||
return round(self.price / Decimal(self.unit.factor) * Decimal(unit.factor),4)
|
||||
elif self.mode == 'pprice' or self.mode == 'pcost':
|
||||
if self.line and self.price:
|
||||
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
||||
if self.sale_line and self.price:
|
||||
@@ -305,8 +315,8 @@ class Fee(ModelSQL,ModelView):
|
||||
|
||||
def get_landed_status(self,name):
|
||||
if self.product:
|
||||
return self.product.landed_cost
|
||||
|
||||
return self.product.template.landed_cost
|
||||
|
||||
def get_quantity(self,name=None):
|
||||
qt = self.get_fee_lots_qt()
|
||||
if qt:
|
||||
@@ -317,6 +327,7 @@ class Fee(ModelSQL,ModelView):
|
||||
return Decimal(lqts[0].lot_quantity)
|
||||
|
||||
def get_amount(self,name=None):
|
||||
Date = Pool().get('ir.date')
|
||||
sign = Decimal(1)
|
||||
if self.price:
|
||||
# if self.p_r:
|
||||
@@ -324,13 +335,54 @@ class Fee(ModelSQL,ModelView):
|
||||
# sign = -1
|
||||
if self.mode == 'lumpsum':
|
||||
return self.price * sign
|
||||
elif self.mode == 'ppack':
|
||||
return round(self.price * self.quantity,2)
|
||||
elif self.mode == 'rate':
|
||||
#take period with estimated trigger date
|
||||
if self.line:
|
||||
if self.line.estimated_date:
|
||||
beg_date = self.fee_date if self.fee_date else Date.today()
|
||||
est_lines = [dd for dd in self.line.estimated_date if dd.trigger == 'bldate']
|
||||
est_line = est_lines[0] if est_lines else None
|
||||
if est_line and est_line.estimated_date:
|
||||
est_date = est_line.estimated_date + datetime.timedelta(
|
||||
days=est_line.fin_int_delta or 0
|
||||
)
|
||||
if est_date and beg_date:
|
||||
factor = InterestCalculator.calculate(
|
||||
start_date=beg_date,
|
||||
end_date=est_date,
|
||||
rate=self.price/100,
|
||||
rate_type='annual',
|
||||
convention='ACT/360',
|
||||
compounding='simple'
|
||||
)
|
||||
|
||||
return round(factor * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
|
||||
if self.sale_line:
|
||||
if self.sale_line.sale.payment_term:
|
||||
beg_date = self.fee_date if self.fee_date else Date.today()
|
||||
est_date = self.sale_line.sale.payment_term.lines[0].get_date(beg_date,self.sale_line)
|
||||
logger.info("EST_DATE:%s",est_date)
|
||||
if est_date and beg_date:
|
||||
factor = InterestCalculator.calculate(
|
||||
start_date=beg_date,
|
||||
end_date=est_date,
|
||||
rate=self.price/100,
|
||||
rate_type='annual',
|
||||
convention='ACT/360',
|
||||
compounding='simple'
|
||||
)
|
||||
logger.info("FACTOR:%s",factor)
|
||||
return round(factor * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
|
||||
|
||||
elif self.mode == 'perqt':
|
||||
if self.shipment_in:
|
||||
StockMove = Pool().get('stock.move')
|
||||
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
||||
if sm:
|
||||
unique_lots = {e.lot for e in sm if e.lot}
|
||||
return round(self.price * Decimal(sum([e.get_current_quantity_converted() for e in unique_lots])) * sign,2)
|
||||
return round(self.price * Decimal(sum([e.get_current_quantity_converted(0,self.unit) for e in unique_lots])) * sign,2)
|
||||
LotQt = Pool().get('lot.qt')
|
||||
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
||||
if lqts:
|
||||
@@ -373,12 +425,12 @@ class Fee(ModelSQL,ModelView):
|
||||
|
||||
return super().copy(fees, default=default)
|
||||
|
||||
def get_fee_lots_qt(self):
|
||||
def get_fee_lots_qt(self,state_id=0):
|
||||
qt = Decimal(0)
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||
if fee_lots:
|
||||
qt = sum([e.lot.get_current_quantity_converted() for e in fee_lots])
|
||||
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
|
||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||
return qt
|
||||
|
||||
@@ -388,10 +440,18 @@ class Fee(ModelSQL,ModelView):
|
||||
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
||||
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
|
||||
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
||||
if self.price != self.purchase.lines[0].unit_price:
|
||||
self.purchase.lines[0].unit_price = self.price
|
||||
if self.quantity != self.purchase.lines[0].quantity:
|
||||
self.purchase.lines[0].quantity = self.quantity
|
||||
if self.mode == 'lumpsum':
|
||||
if self.amount != self.purchase.lines[0].unit_price:
|
||||
self.purchase.lines[0].unit_price = self.amount
|
||||
elif self.mode == 'ppack':
|
||||
if self.amount != self.purchase.lines[0].amount:
|
||||
self.purchase.lines[0].unit_price = self.price
|
||||
self.purchase.lines[0].quantity = self.quantity
|
||||
else:
|
||||
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
||||
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
||||
if self.quantity != self.purchase.lines[0].quantity:
|
||||
self.purchase.lines[0].quantity = self.quantity
|
||||
if self.product != self.purchase.lines[0].product:
|
||||
self.purchase.lines[0].product = self.product
|
||||
PurchaseLine.save([self.purchase.lines[0]])
|
||||
@@ -408,46 +468,47 @@ class Fee(ModelSQL,ModelView):
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
vlist = [x.copy() for x in vlist]
|
||||
records = super(Fee, cls).create(vlist)
|
||||
fees = super(Fee, cls).create(vlist)
|
||||
qt_sh = Decimal(0)
|
||||
qt_line = Decimal(0)
|
||||
unit = None
|
||||
for record in records:
|
||||
for fee in fees:
|
||||
FeeLots = Pool().get('fee.lots')
|
||||
Lots = Pool().get('lot.lot')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
if record.line:
|
||||
for l in record.line.lots:
|
||||
#if l.lot_type == 'physic':
|
||||
fl = FeeLots()
|
||||
fl.fee = record.id
|
||||
fl.lot = l.id
|
||||
fl.line = l.line.id
|
||||
FeeLots.save([fl])
|
||||
qt_line += l.get_current_quantity_converted()
|
||||
unit = l.line.unit
|
||||
if record.sale_line:
|
||||
for l in record.sale_line.lots:
|
||||
#if l.lot_type == 'physic':
|
||||
fl = FeeLots()
|
||||
fl.fee = record.id
|
||||
fl.lot = l.id
|
||||
fl.sale_line = l.sale_line.id
|
||||
FeeLots.save([fl])
|
||||
if record.shipment_in:
|
||||
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
|
||||
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
|
||||
if fee.line:
|
||||
for l in fee.line.lots:
|
||||
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
|
||||
fl = FeeLots()
|
||||
fl.fee = fee.id
|
||||
fl.lot = l.id
|
||||
fl.line = l.line.id
|
||||
FeeLots.save([fl])
|
||||
qt_line += l.get_current_quantity_converted()
|
||||
unit = l.line.unit
|
||||
if fee.sale_line:
|
||||
for l in fee.sale_line.lots:
|
||||
if (l.lot_type == 'virtual' and len(fee.sale_line.lots)==1) or (l.lot_type == 'physic' and len(fee.sale_line.lots)>1):
|
||||
fl = FeeLots()
|
||||
fl.fee = fee.id
|
||||
fl.lot = l.id
|
||||
fl.sale_line = l.sale_line.id
|
||||
FeeLots.save([fl])
|
||||
qt_line += l.get_current_quantity_converted()
|
||||
unit = l.sale_line.unit
|
||||
if fee.shipment_in:
|
||||
if fee.shipment_in.state == 'draft'or fee.shipment_in.state == 'started':
|
||||
lots = Lots.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||
if lots:
|
||||
for l in lots:
|
||||
#if l.lot_type == 'physic':
|
||||
fl = FeeLots()
|
||||
fl.fee = record.id
|
||||
fl.fee = fee.id
|
||||
fl.lot = l.id
|
||||
FeeLots.save([fl])
|
||||
qt_sh += l.get_current_quantity_converted()
|
||||
unit = l.line.unit
|
||||
else:
|
||||
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
|
||||
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||
if lqts:
|
||||
for l in lqts:
|
||||
qt_sh += l.lot_p.get_current_quantity_converted()
|
||||
@@ -455,32 +516,154 @@ class Fee(ModelSQL,ModelView):
|
||||
else:
|
||||
raise UserError("You cannot add fee on received shipment!")
|
||||
|
||||
type = record.type
|
||||
type = fee.type
|
||||
if type == 'ordered':
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
pl = PurchaseLine()
|
||||
pl.product = record.product
|
||||
if record.line:
|
||||
pl.product = fee.product
|
||||
if fee.line or fee.sale_line:
|
||||
pl.quantity = round(qt_line,5)
|
||||
if record.shipment_in:
|
||||
if fee.shipment_in:
|
||||
pl.quantity = round(qt_sh,5)
|
||||
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
|
||||
pl.unit = unit
|
||||
pl.fee_ = record.id
|
||||
if record.price:
|
||||
pl.unit_price = round(Decimal(record.price),4)
|
||||
pl.fee_ = fee.id
|
||||
if fee.price:
|
||||
fee_price = fee.get_price_per_qt()
|
||||
logger.info("GET_FEE_PRICE_PER_QT:%s",fee_price)
|
||||
pl.unit_price = round(Decimal(fee_price),4)
|
||||
if fee.mode == 'lumpsum':
|
||||
pl.quantity = 1
|
||||
pl.unit_price = round(Decimal(fee.amount),4)
|
||||
elif fee.mode == 'ppack':
|
||||
pl.unit_price = fee.price
|
||||
p = Purchase()
|
||||
p.lines = [pl]
|
||||
p.party = record.supplier
|
||||
p.party = fee.supplier
|
||||
if p.party.addresses:
|
||||
p.invoice_address = p.party.addresses[0]
|
||||
p.currency = record.currency
|
||||
p.currency = fee.currency
|
||||
p.line_type = 'service'
|
||||
p.from_location = fee.shipment_in.from_location if fee.shipment_in else (fee.line.purchase.from_location if fee.line else fee.sale_line.sale.from_location)
|
||||
p.to_location = fee.shipment_in.to_location if fee.shipment_in else (fee.line.purchase.to_location if fee.line else fee.sale_line.sale.to_location)
|
||||
if fee.shipment_in and fee.shipment_in.lotqt:
|
||||
p.payment_term = fee.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
|
||||
elif fee.line:
|
||||
p.payment_term = fee.line.purchase.payment_term
|
||||
elif fee.sale_line:
|
||||
p.payment_term = fee.sale_line.sale.payment_term
|
||||
Purchase.save([p])
|
||||
|
||||
return records
|
||||
#if reception of moves done we need to generate accrual for fee
|
||||
if not fee.sale_line:
|
||||
feelots = FeeLots.search(['fee','=',fee.id])
|
||||
for fl in feelots:
|
||||
if fee.product.template.landed_cost:
|
||||
move = fl.lot.get_received_move()
|
||||
if move:
|
||||
Warning = Pool().get('res.user.warning')
|
||||
warning_name = Warning.format("Lot ever received", [])
|
||||
if Warning.check(warning_name):
|
||||
raise UserWarning(warning_name,
|
||||
"By clicking yes, an accrual for this fee will be created")
|
||||
AccountMove = Pool().get('account.move')
|
||||
account_move = move._get_account_stock_move_fee(fee)
|
||||
AccountMove.save([account_move])
|
||||
else:
|
||||
AccountMove = Pool().get('account.move')
|
||||
account_move = fee._get_account_move_fee(fl.lot)
|
||||
AccountMove.save([account_move])
|
||||
|
||||
return fees
|
||||
|
||||
def _get_account_move_fee(self,lot,in_out='in',amt = None):
|
||||
pool = Pool()
|
||||
AccountMove = pool.get('account.move')
|
||||
Date = pool.get('ir.date')
|
||||
Period = pool.get('account.period')
|
||||
AccountConfiguration = pool.get('account.configuration')
|
||||
|
||||
if self.product.type != 'service':
|
||||
return
|
||||
|
||||
today = Date.today()
|
||||
company = lot.line.purchase.company if lot.line else lot.sale_line.sale.company
|
||||
for date in [today]:
|
||||
try:
|
||||
period = Period.find(company, date=date, test_state=False)
|
||||
except PeriodNotFoundError:
|
||||
if date < today:
|
||||
return
|
||||
continue
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if period.state != 'open':
|
||||
date = today
|
||||
period = Period.find(company, date=date)
|
||||
|
||||
AccountMoveLine = pool.get('account.move.line')
|
||||
Currency = pool.get('currency.currency')
|
||||
move_line = AccountMoveLine()
|
||||
move_line.lot = lot
|
||||
move_line.fee = self
|
||||
move_line.origin = None
|
||||
move_line_ = AccountMoveLine()
|
||||
move_line_.lot = lot
|
||||
move_line_.fee = self
|
||||
move_line_.origin = None
|
||||
amount = amt if amt else self.amount
|
||||
|
||||
if self.currency != company.currency:
|
||||
with Transaction().set_context(date=today):
|
||||
amount_converted = amount
|
||||
amount = Currency.compute(self.currency,
|
||||
amount, company.currency)
|
||||
move_line.second_currency = self.currency
|
||||
|
||||
if self.p_r == 'pay':
|
||||
move_line.debit = amount
|
||||
move_line.credit = Decimal(0)
|
||||
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
||||
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
||||
move_line.amount_second_currency = amount_converted
|
||||
move_line_.debit = Decimal(0)
|
||||
move_line_.credit = amount
|
||||
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
||||
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
||||
move_line_.amount_second_currency = -amount_converted
|
||||
else:
|
||||
move_line.debit = Decimal(0)
|
||||
move_line.credit = amount
|
||||
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
||||
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
||||
move_line.amount_second_currency = -amount_converted
|
||||
move_line_.debit = amount
|
||||
move_line_.credit = Decimal(0)
|
||||
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
||||
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
||||
move_line_.amount_second_currency = amount_converted
|
||||
|
||||
logger.info("FEE_MOVELINES_1:%s",move_line)
|
||||
logger.info("FEE_MOVELINES_2:%s",move_line_)
|
||||
|
||||
AccountJournal = Pool().get('account.journal')
|
||||
journal = AccountJournal.search(['type','=','expense'])
|
||||
if journal:
|
||||
journal = journal[0]
|
||||
|
||||
description = None
|
||||
description = 'Fee'
|
||||
return AccountMove(
|
||||
journal=journal,
|
||||
period=period,
|
||||
date=date,
|
||||
origin=None,
|
||||
description=description,
|
||||
lines=[move_line,move_line_],
|
||||
)
|
||||
|
||||
class FeeLots(ModelSQL,ModelView):
|
||||
|
||||
"Fee lots"
|
||||
|
||||
@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">fee_tree_sequence</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
|
||||
<field name="model">valuation.valuation</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">valuation_tree_sequence3</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="valuation_view_graph">
|
||||
<field name="model">valuation.valuation</field>
|
||||
<field name="type">graph</field>
|
||||
<field name="name">valuation_graph</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="valuation_view_graph2">
|
||||
<field name="model">valuation.valuation</field>
|
||||
<field name="type">graph</field>
|
||||
<field name="name">valuation_graph2</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
|
||||
<field name="model">valuation.valuation.dyn</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">valuation_tree_sequence4</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="fee_view_tree_sequence2">
|
||||
<field name="model">fee.fee</field>
|
||||
<field name="type">tree</field>
|
||||
|
||||
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]
|
||||
|
||||
payload = {
|
||||
"resource": {"dashboard": 3},
|
||||
"resource": {"dashboard": config.forex_id},
|
||||
"params": {},
|
||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||||
}
|
||||
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
|
||||
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||||
logger.info("TOKEN:%s",token)
|
||||
if config.dark:
|
||||
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
||||
|
||||
@@ -12,4 +12,10 @@ class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
|
||||
__name__ = 'gr.configuration'
|
||||
|
||||
bi = fields.Char("BI connexion")
|
||||
dark = fields.Boolean("Dark mode")
|
||||
dashboard = fields.Char("Dashboard connexion")
|
||||
dark = fields.Boolean("Dark mode")
|
||||
pnl_id = fields.Integer("Pnl ID")
|
||||
position_id = fields.Integer("Position ID")
|
||||
forex_id = fields.Integer("Forex ID")
|
||||
payload = fields.Char("Metabase payload")
|
||||
automation = fields.Boolean("Automation")
|
||||
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 |
936
modules/purchase_trade/invoice.py
Normal file
936
modules/purchase_trade/invoice.py
Normal file
@@ -0,0 +1,936 @@
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from datetime import date as dt_date
|
||||
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.modules.purchase_trade.numbers_to_words import amount_to_currency_words
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.modules.account_invoice.invoice import (
|
||||
InvoiceReport as BaseInvoiceReport)
|
||||
from trytond.modules.sale.sale import SaleReport as BaseSaleReport
|
||||
from trytond.modules.purchase.purchase import (
|
||||
PurchaseReport as BasePurchaseReport)
|
||||
|
||||
|
||||
class Invoice(metaclass=PoolMeta):
|
||||
__name__ = 'account.invoice'
|
||||
|
||||
@staticmethod
|
||||
def _format_report_number(value, digits='0.0000', keep_trailing_decimal=False,
|
||||
strip_trailing_zeros=True):
|
||||
value = Decimal(str(value or 0)).quantize(Decimal(digits))
|
||||
text = format(value, 'f')
|
||||
if strip_trailing_zeros:
|
||||
text = text.rstrip('0').rstrip('.')
|
||||
if keep_trailing_decimal and '.' not in text:
|
||||
text += '.0'
|
||||
return text or '0'
|
||||
|
||||
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_invoice_lines(self):
|
||||
lines = [
|
||||
line for line in (self.lines or [])
|
||||
if getattr(line, 'type', None) == 'line'
|
||||
]
|
||||
return lines or list(self.lines or [])
|
||||
|
||||
@staticmethod
|
||||
def _clean_report_description(value):
|
||||
text = (value or '').strip()
|
||||
normalized = text.replace(' ', '').upper()
|
||||
if normalized == 'PROFORMA':
|
||||
return ''
|
||||
return text.upper() if text else ''
|
||||
|
||||
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_invoice_lots(self):
|
||||
invoice_lines = self._get_report_invoice_lines()
|
||||
if not invoice_lines:
|
||||
return []
|
||||
|
||||
def _same_invoice_line(left, right):
|
||||
if not left or not right:
|
||||
return False
|
||||
left_id = getattr(left, 'id', None)
|
||||
right_id = getattr(right, 'id', None)
|
||||
if left_id is not None and right_id is not None:
|
||||
return left_id == right_id
|
||||
return left is right
|
||||
|
||||
trade = self._get_report_trade()
|
||||
trade_lines = getattr(trade, 'lines', []) if trade else []
|
||||
lots = []
|
||||
for line in trade_lines or []:
|
||||
for lot in getattr(line, 'lots', []) or []:
|
||||
if getattr(lot, 'lot_type', None) != 'physic':
|
||||
continue
|
||||
refs = [
|
||||
getattr(lot, 'sale_invoice_line', None),
|
||||
getattr(lot, 'sale_invoice_line_prov', None),
|
||||
getattr(lot, 'invoice_line', None),
|
||||
getattr(lot, 'invoice_line_prov', None),
|
||||
]
|
||||
if any(
|
||||
_same_invoice_line(ref, invoice_line)
|
||||
for ref in refs for invoice_line in invoice_lines):
|
||||
lots.append(lot)
|
||||
return lots
|
||||
|
||||
@staticmethod
|
||||
def _format_report_package_label(unit):
|
||||
label = (
|
||||
getattr(unit, 'symbol', None)
|
||||
or getattr(unit, 'rec_name', None)
|
||||
or getattr(unit, 'name', None)
|
||||
or 'BALE'
|
||||
)
|
||||
label = label.upper()
|
||||
if not label.endswith('S'):
|
||||
label += 'S'
|
||||
return label
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_report_bank_account(party):
|
||||
accounts = list(getattr(party, 'bank_accounts', []) or [])
|
||||
return accounts[0] if accounts else None
|
||||
|
||||
@staticmethod
|
||||
def _get_report_bank_account_number(account):
|
||||
if not account:
|
||||
return ''
|
||||
numbers = list(getattr(account, 'numbers', []) or [])
|
||||
for number in numbers:
|
||||
if getattr(number, 'type', None) == 'iban' and getattr(number, 'number', None):
|
||||
return number.number or ''
|
||||
for number in numbers:
|
||||
if getattr(number, 'number', None):
|
||||
return number.number or ''
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _get_report_bank_name(account):
|
||||
bank = getattr(account, 'bank', None) if account else None
|
||||
party = getattr(bank, 'party', None) if bank else None
|
||||
return getattr(party, 'rec_name', None) or getattr(bank, 'rec_name', None) or ''
|
||||
|
||||
@staticmethod
|
||||
def _get_report_bank_city(account):
|
||||
bank = getattr(account, 'bank', None) if account else None
|
||||
party = getattr(bank, 'party', None) if bank else None
|
||||
address = party.address_get() if party and hasattr(party, 'address_get') else None
|
||||
return getattr(address, 'city', None) or ''
|
||||
|
||||
@staticmethod
|
||||
def _get_report_bank_swift(account):
|
||||
bank = getattr(account, 'bank', None) if account else None
|
||||
return getattr(bank, 'bic', None) or ''
|
||||
|
||||
@staticmethod
|
||||
def _format_report_payment_amount(value):
|
||||
amount = Decimal(str(value or 0)).quantize(Decimal('0.01'))
|
||||
return format(amount, 'f')
|
||||
|
||||
@property
|
||||
def _report_payment_order_company_account(self):
|
||||
return self._get_report_bank_account(getattr(self.company, 'party', None))
|
||||
|
||||
@property
|
||||
def _report_payment_order_beneficiary_account(self):
|
||||
return self._get_report_bank_account(self.party)
|
||||
|
||||
@property
|
||||
def report_payment_order_short_name(self):
|
||||
company_party = getattr(self.company, 'party', None)
|
||||
return getattr(company_party, 'rec_name', '') or ''
|
||||
|
||||
@property
|
||||
def report_payment_order_document_reference(self):
|
||||
return self.number or self.reference or ''
|
||||
|
||||
@property
|
||||
def report_payment_order_from_account_nb(self):
|
||||
return self._get_report_bank_account_number(
|
||||
self._report_payment_order_company_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_to_bank_name(self):
|
||||
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_to_bank_city(self):
|
||||
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_amount(self):
|
||||
return self._format_report_payment_amount(self.total_amount)
|
||||
|
||||
@property
|
||||
def report_payment_order_currency_code(self):
|
||||
currency = self.currency
|
||||
code = getattr(currency, 'code', None) or ''
|
||||
rec_name = getattr(currency, 'rec_name', None) or ''
|
||||
symbol = getattr(currency, 'symbol', None) or ''
|
||||
if code and any(ch.isalpha() for ch in code):
|
||||
return code
|
||||
if rec_name and any(ch.isalpha() for ch in rec_name):
|
||||
return rec_name
|
||||
if symbol and any(ch.isalpha() for ch in symbol):
|
||||
return symbol
|
||||
return code or rec_name or symbol or ''
|
||||
|
||||
@property
|
||||
def report_payment_order_amount_text(self):
|
||||
return amount_to_currency_words(self.total_amount)
|
||||
|
||||
@property
|
||||
def report_payment_order_value_date(self):
|
||||
value_date = self.payment_term_date or self.invoice_date
|
||||
if isinstance(value_date, dt_date):
|
||||
return value_date.strftime('%d-%m-%Y')
|
||||
return ''
|
||||
|
||||
@property
|
||||
def report_payment_order_company_address(self):
|
||||
if self.invoice_address and getattr(self.invoice_address, 'full_address', None):
|
||||
return self.invoice_address.full_address
|
||||
return self.report_address
|
||||
|
||||
@property
|
||||
def report_payment_order_beneficiary_account_nb(self):
|
||||
return self._get_report_bank_account_number(
|
||||
self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_beneficiary_bank_name(self):
|
||||
return self._get_report_bank_name(self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_beneficiary_bank_city(self):
|
||||
return self._get_report_bank_city(self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_swift_code(self):
|
||||
return self._get_report_bank_swift(self._report_payment_order_beneficiary_account)
|
||||
|
||||
@property
|
||||
def report_payment_order_other_instructions(self):
|
||||
return self.description or ''
|
||||
|
||||
@property
|
||||
def report_payment_order_reference(self):
|
||||
return self.reference or self.number or ''
|
||||
|
||||
@staticmethod
|
||||
def _get_report_current_user():
|
||||
user_id = Transaction().user
|
||||
if not user_id:
|
||||
return None
|
||||
User = Pool().get('res.user')
|
||||
return User(user_id)
|
||||
|
||||
@property
|
||||
def report_payment_order_current_user(self):
|
||||
user = self._get_report_current_user()
|
||||
return getattr(user, 'rec_name', None) or ''
|
||||
|
||||
@property
|
||||
def report_payment_order_current_user_email(self):
|
||||
user = self._get_report_current_user()
|
||||
party = getattr(user, 'party', None) if user else None
|
||||
if party and hasattr(party, 'contact_mechanism_get'):
|
||||
return party.contact_mechanism_get('email') or ''
|
||||
return getattr(user, 'email', None) or ''
|
||||
|
||||
@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_product_name(self):
|
||||
line = self._get_report_trade_line()
|
||||
if line and line.product:
|
||||
return line.product.name or ''
|
||||
return ''
|
||||
|
||||
@property
|
||||
def report_description_upper(self):
|
||||
if self.lines:
|
||||
return self._clean_report_description(self.lines[0].description)
|
||||
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_quantity_lines(self):
|
||||
details = []
|
||||
for line in self._get_report_invoice_lines():
|
||||
quantity = getattr(line, 'report_net', '')
|
||||
if quantity == '':
|
||||
quantity = getattr(line, 'quantity', '')
|
||||
if quantity == '':
|
||||
continue
|
||||
quantity_text = self._format_report_number(
|
||||
quantity, keep_trailing_decimal=True)
|
||||
unit = getattr(line, 'unit', None)
|
||||
unit_name = unit.rec_name.upper() if unit and unit.rec_name else ''
|
||||
lbs = getattr(line, 'report_lbs', '')
|
||||
parts = [quantity_text, unit_name]
|
||||
if lbs != '':
|
||||
parts.append(
|
||||
f"({self._format_report_number(lbs, digits='0.01')} LBS)")
|
||||
detail = ' '.join(part for part in parts if part)
|
||||
if detail:
|
||||
details.append(detail)
|
||||
return '\n'.join(details)
|
||||
|
||||
@property
|
||||
def report_trade_blocks(self):
|
||||
blocks = []
|
||||
quantity_lines = self.report_quantity_lines.splitlines()
|
||||
rate_lines = self.report_rate_lines.splitlines()
|
||||
for index, quantity_line in enumerate(quantity_lines):
|
||||
price_line = rate_lines[index] if index < len(rate_lines) else ''
|
||||
blocks.append((quantity_line, price_line))
|
||||
return blocks
|
||||
|
||||
@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_rate_lines(self):
|
||||
details = []
|
||||
for line in self._get_report_invoice_lines():
|
||||
currency = getattr(line, 'report_rate_currency_upper', '') or ''
|
||||
value = getattr(line, 'report_rate_value', '')
|
||||
value_text = ''
|
||||
if value != '':
|
||||
value_text = self._format_report_number(
|
||||
value, strip_trailing_zeros=False)
|
||||
unit = getattr(line, 'report_rate_unit_upper', '') or ''
|
||||
words = getattr(line, 'report_rate_price_words', '') or ''
|
||||
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
|
||||
detail = ' '.join(
|
||||
part for part in [
|
||||
currency,
|
||||
value_text,
|
||||
'PER' if unit else '',
|
||||
unit,
|
||||
f"({words})" if words else '',
|
||||
pricing_text,
|
||||
] if part)
|
||||
if detail:
|
||||
details.append(detail)
|
||||
return '\n'.join(details)
|
||||
|
||||
@property
|
||||
def report_positive_rate_lines(self):
|
||||
sale = self._get_report_sale()
|
||||
if sale and getattr(sale, 'report_price_lines', None):
|
||||
return sale.report_price_lines
|
||||
details = []
|
||||
for line in self._get_report_invoice_lines():
|
||||
quantity = getattr(line, 'report_net', '')
|
||||
if quantity == '':
|
||||
quantity = getattr(line, 'quantity', '')
|
||||
if Decimal(str(quantity or 0)) <= 0:
|
||||
continue
|
||||
currency = getattr(line, 'report_rate_currency_upper', '') or ''
|
||||
value = getattr(line, 'report_rate_value', '')
|
||||
value_text = ''
|
||||
if value != '':
|
||||
value_text = self._format_report_number(
|
||||
value, strip_trailing_zeros=False)
|
||||
unit = getattr(line, 'report_rate_unit_upper', '') or ''
|
||||
words = getattr(line, 'report_rate_price_words', '') or ''
|
||||
pricing_text = getattr(line, 'report_rate_pricing_text', '') or ''
|
||||
detail = ' '.join(
|
||||
part for part in [
|
||||
currency,
|
||||
value_text,
|
||||
'PER' if unit else '',
|
||||
unit,
|
||||
f"({words})" if words else '',
|
||||
pricing_text,
|
||||
] if part)
|
||||
if detail:
|
||||
details.append(detail)
|
||||
return '\n'.join(details)
|
||||
|
||||
@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_delivery_period_description(self):
|
||||
trade = self._get_report_trade()
|
||||
if trade and getattr(trade, 'report_delivery_period_description', None):
|
||||
return trade.report_delivery_period_description
|
||||
line = self._get_report_trade_line()
|
||||
if line and getattr(line, 'del_period', None):
|
||||
return line.del_period.description or ''
|
||||
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):
|
||||
total_packages = Decimal(0)
|
||||
package_unit = None
|
||||
has_invoice_line_packages = False
|
||||
for line in self._get_report_invoice_lines():
|
||||
lot = getattr(line, 'lot', None)
|
||||
if not lot or getattr(lot, 'lot_qt', None) in (None, ''):
|
||||
continue
|
||||
has_invoice_line_packages = True
|
||||
if not package_unit and getattr(lot, 'lot_unit', None):
|
||||
package_unit = lot.lot_unit
|
||||
sign = Decimal(1)
|
||||
if Decimal(str(getattr(line, 'quantity', 0) or 0)) < 0:
|
||||
sign = Decimal(-1)
|
||||
total_packages += (
|
||||
Decimal(str(lot.lot_qt or 0)).quantize(
|
||||
Decimal('1'), rounding=ROUND_HALF_UP) * sign)
|
||||
if has_invoice_line_packages:
|
||||
label = self._format_report_package_label(package_unit)
|
||||
return f"NB {label}: {int(total_packages)}"
|
||||
|
||||
lots = self._get_report_invoice_lots()
|
||||
if lots:
|
||||
total_packages = Decimal(0)
|
||||
package_unit = None
|
||||
for lot in lots:
|
||||
if getattr(lot, 'lot_qt', None):
|
||||
total_packages += Decimal(str(lot.lot_qt or 0))
|
||||
if not package_unit and getattr(lot, 'lot_unit', None):
|
||||
package_unit = lot.lot_unit
|
||||
package_qty = total_packages.quantize(
|
||||
Decimal('1'), rounding=ROUND_HALF_UP)
|
||||
label = self._format_report_package_label(package_unit)
|
||||
return f"NB {label}: {int(package_qty)}"
|
||||
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):
|
||||
if self.lines:
|
||||
return sum(
|
||||
Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||||
for line in self._get_report_invoice_lines())
|
||||
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):
|
||||
if self.lines:
|
||||
return sum(
|
||||
Decimal(str(getattr(line, 'quantity', 0) or 0))
|
||||
for line in self._get_report_invoice_lines())
|
||||
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_weight_unit_upper(self):
|
||||
line = self._get_report_trade_line() or self._get_report_invoice_line()
|
||||
unit = getattr(line, 'unit', None) if line else None
|
||||
if unit and unit.rec_name:
|
||||
return unit.rec_name.upper()
|
||||
return 'KGS'
|
||||
|
||||
@property
|
||||
def report_note_title(self):
|
||||
total = Decimal(str(self.total_amount or 0))
|
||||
if total < 0:
|
||||
return 'Debit Note'
|
||||
return 'Credit Note'
|
||||
|
||||
@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_product_name(self):
|
||||
if self.product:
|
||||
return self.product.name or ''
|
||||
origin = getattr(self, 'origin', None)
|
||||
if origin and getattr(origin, 'product', None):
|
||||
return origin.product.name or ''
|
||||
return ''
|
||||
|
||||
@property
|
||||
def report_description_upper(self):
|
||||
return Invoice._clean_report_description(self.description)
|
||||
|
||||
@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):
|
||||
origin = self._get_report_trade_line()
|
||||
if origin and getattr(origin, 'price_type', None) == 'basis':
|
||||
if getattr(origin, 'enable_linked_currency', False) and getattr(origin, 'linked_currency', None):
|
||||
return Decimal(str(origin.premium or 0))
|
||||
return Decimal(str(origin._get_premium_price() or 0))
|
||||
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):
|
||||
origin = self._get_report_trade_line()
|
||||
if origin and getattr(origin, 'price_type', None) == 'basis':
|
||||
value = self.report_rate_value
|
||||
if self.report_rate_currency_upper == 'USC':
|
||||
return amount_to_currency_words(value, 'USC', 'USC')
|
||||
return amount_to_currency_words(value)
|
||||
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)
|
||||
|
||||
|
||||
class ReportTemplateMixin:
|
||||
@classmethod
|
||||
def _get_purchase_trade_configuration(cls):
|
||||
Configuration = Pool().get('purchase_trade.configuration')
|
||||
configurations = Configuration.search([], limit=1)
|
||||
return configurations[0] if configurations else None
|
||||
|
||||
@classmethod
|
||||
def _get_action_name(cls, action):
|
||||
if isinstance(action, dict):
|
||||
return action.get('name') or ''
|
||||
return getattr(action, 'name', '') or ''
|
||||
|
||||
@classmethod
|
||||
def _get_action_report_path(cls, action):
|
||||
if isinstance(action, dict):
|
||||
return action.get('report') or ''
|
||||
return getattr(action, 'report', '') or ''
|
||||
|
||||
@classmethod
|
||||
def _resolve_template_path(cls, action, field_name, default_prefix):
|
||||
config = cls._get_purchase_trade_configuration()
|
||||
template = getattr(config, field_name, '') if config else ''
|
||||
template = (template or '').strip()
|
||||
if not template:
|
||||
raise UserError('No template found')
|
||||
if '/' not in template:
|
||||
return f'{default_prefix}/{template}'
|
||||
return template
|
||||
|
||||
@classmethod
|
||||
def _get_resolved_action(cls, action):
|
||||
report_path = cls._resolve_configured_report_path(action)
|
||||
if isinstance(action, dict):
|
||||
resolved = dict(action)
|
||||
resolved['report'] = report_path
|
||||
return resolved
|
||||
setattr(action, 'report', report_path)
|
||||
return action
|
||||
|
||||
@classmethod
|
||||
def _execute(cls, records, header, data, action):
|
||||
resolved_action = cls._get_resolved_action(action)
|
||||
return super()._execute(records, header, data, resolved_action)
|
||||
|
||||
|
||||
class InvoiceReport(ReportTemplateMixin, BaseInvoiceReport):
|
||||
__name__ = 'account.invoice'
|
||||
|
||||
@classmethod
|
||||
def _resolve_configured_report_path(cls, action):
|
||||
report_path = cls._get_action_report_path(action) or ''
|
||||
action_name = cls._get_action_name(action)
|
||||
|
||||
if (report_path.endswith('/prepayment.fodt')
|
||||
or action_name == 'Prepayment'):
|
||||
field_name = 'invoice_prepayment_report_template'
|
||||
elif (report_path.endswith('/payment_order.fodt')
|
||||
or action_name == 'Payment Order'):
|
||||
field_name = 'invoice_payment_order_report_template'
|
||||
elif (report_path.endswith('/invoice_ict_final.fodt')
|
||||
or action_name == 'CN/DN'):
|
||||
field_name = 'invoice_cndn_report_template'
|
||||
else:
|
||||
field_name = 'invoice_report_template'
|
||||
return cls._resolve_template_path(action, field_name, 'account_invoice')
|
||||
|
||||
|
||||
class SaleReport(ReportTemplateMixin, BaseSaleReport):
|
||||
__name__ = 'sale.sale'
|
||||
|
||||
@classmethod
|
||||
def _resolve_configured_report_path(cls, action):
|
||||
report_path = cls._get_action_report_path(action)
|
||||
action_name = cls._get_action_name(action)
|
||||
if report_path.endswith('/bill.fodt') or action_name == 'Bill':
|
||||
field_name = 'sale_bill_report_template'
|
||||
elif report_path.endswith('/sale_final.fodt') or action_name == 'Sale (final)':
|
||||
field_name = 'sale_final_report_template'
|
||||
else:
|
||||
field_name = 'sale_report_template'
|
||||
return cls._resolve_template_path(action, field_name, 'sale')
|
||||
|
||||
|
||||
class PurchaseReport(ReportTemplateMixin, BasePurchaseReport):
|
||||
__name__ = 'purchase.purchase'
|
||||
|
||||
@classmethod
|
||||
def _resolve_configured_report_path(cls, action):
|
||||
return cls._resolve_template_path(
|
||||
action, 'purchase_report_template', 'purchase')
|
||||
16
modules/purchase_trade/invoice.xml
Normal file
16
modules/purchase_trade/invoice.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.action.report" id="report_payment_order">
|
||||
<field name="name">Payment Order</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="report_name">account.invoice</field>
|
||||
<field name="report">account_invoice/payment_order.fodt</field>
|
||||
<field name="single" eval="True"/>
|
||||
</record>
|
||||
<record model="ir.action.keyword" id="report_payment_order_keyword">
|
||||
<field name="keyword">form_print</field>
|
||||
<field name="model">account.invoice,-1</field>
|
||||
<field name="action" ref="report_payment_order"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -20,6 +20,7 @@ import datetime
|
||||
import json
|
||||
import logging
|
||||
from trytond.exceptions import UserWarning, UserError
|
||||
from trytond.modules.purchase_trade.service import ContractFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,7 +48,7 @@ class LotMove(ModelSQL,ModelView):
|
||||
class Lot(metaclass=PoolMeta):
|
||||
__name__ = 'lot.lot'
|
||||
|
||||
line = fields.Many2One('purchase.line',"Purchase")
|
||||
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
|
||||
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
||||
lot_move = fields.One2Many('lot.move','lot',"Move")
|
||||
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
||||
@@ -58,6 +59,7 @@ class Lot(metaclass=PoolMeta):
|
||||
delta_pr = fields.Numeric("Delta Pr")
|
||||
delta_amt = fields.Numeric("Delta Amt")
|
||||
warrant_nb = fields.Char("Warrant Nb")
|
||||
lot_chunk_key = fields.Integer("Chunk key")
|
||||
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
||||
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
||||
pivot = fields.Function(
|
||||
@@ -201,7 +203,7 @@ class Lot(metaclass=PoolMeta):
|
||||
)
|
||||
|
||||
pivot_data['options'] = {
|
||||
"rows": ["lot","ct type","event_date","event","move","curr","rate"],
|
||||
"rows": ["lot","ct type","event_date","event","move","Curr","rate"],
|
||||
"cols": ["account"],
|
||||
"aggregatorName": "Sum",
|
||||
"vals": ["amount"]
|
||||
@@ -578,6 +580,14 @@ class Lot(metaclass=PoolMeta):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_received_move(self):
|
||||
if self.lot_move:
|
||||
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
||||
for m in lm:
|
||||
if m.move.from_location.type == 'supplier' and m.move.state == 'done':
|
||||
return m.move
|
||||
return None
|
||||
|
||||
def GetShipment(self,type):
|
||||
if type == 'in':
|
||||
m = self.get_current_supplier_move()
|
||||
@@ -1087,6 +1097,7 @@ class LotQt(
|
||||
newlot.lot_shipment_internal = self.lot_shipment_internal
|
||||
newlot.lot_shipment_out = self.lot_shipment_out
|
||||
newlot.lot_product = self.lot_p.line.product
|
||||
newlot.lot_chunk_key = l.lot_chunk_key
|
||||
if self.lot_s:
|
||||
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
||||
newlot.lot_type = 'physic'
|
||||
@@ -1168,6 +1179,7 @@ class LotQt(
|
||||
@classmethod
|
||||
def validate(cls, lotqts):
|
||||
super(LotQt, cls).validate(lotqts)
|
||||
Date = Pool().get('ir.date')
|
||||
#Update Move
|
||||
for lqt in lotqts:
|
||||
cls.updateMove(lqt.lot_move)
|
||||
@@ -1177,23 +1189,23 @@ class LotQt(
|
||||
if lqt.lot_p and lqt.lot_quantity > 0:
|
||||
pl = lqt.lot_p.line
|
||||
logger.info("VALIDATE_LQT_PL:%s",pl)
|
||||
Pnl = Pool().get('valuation.valuation')
|
||||
pnl = Pnl.search([('line','=',pl.id)])
|
||||
if pnl:
|
||||
Pnl.delete(pnl)
|
||||
pnl_lines = []
|
||||
pnl_lines.extend(pl.get_pnl_fee_lines())
|
||||
pnl_lines.extend(pl.get_pnl_price_lines())
|
||||
pnl_lines.extend(pl.get_pnl_der_lines())
|
||||
Pnl.save(pnl_lines)
|
||||
# Pnl = Pool().get('valuation.valuation')
|
||||
# pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
|
||||
# if pnl:
|
||||
# Pnl.delete(pnl)
|
||||
# pnl_lines = []
|
||||
# pnl_lines.extend(pl.get_pnl_fee_lines())
|
||||
# pnl_lines.extend(pl.get_pnl_price_lines())
|
||||
# pnl_lines.extend(pl.get_pnl_der_lines())
|
||||
# Pnl.save(pnl_lines)
|
||||
|
||||
#Open position update
|
||||
if pl.quantity_theorical:
|
||||
OpenPosition = Pool().get('open.position')
|
||||
OpenPosition.create_from_purchase_line(pl)
|
||||
# if pl.quantity_theorical:
|
||||
# OpenPosition = Pool().get('open.position')
|
||||
# OpenPosition.create_from_purchase_line(pl)
|
||||
|
||||
@classmethod
|
||||
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None):
|
||||
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None,finished=False):
|
||||
pool = Pool()
|
||||
LotQt = pool.get('lot.qt')
|
||||
lqt = LotQt.__table__()
|
||||
@@ -1241,8 +1253,12 @@ class LotQt(
|
||||
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
||||
if ps == 'P':
|
||||
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
|
||||
if not finished:
|
||||
wh &= (pl.finished == False)
|
||||
elif ps == 'S':
|
||||
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
|
||||
if not finished:
|
||||
wh &= (sl.finished == False)
|
||||
if purchase:
|
||||
wh &= (pu.id == purchase)
|
||||
if sale:
|
||||
@@ -1832,7 +1848,8 @@ class LotReport(
|
||||
supplier = context.get('supplier')
|
||||
#asof = context.get('asof')
|
||||
#todate = context.get('todate')
|
||||
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin)
|
||||
finished = context.get('finished')
|
||||
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin,finished)
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
@@ -1922,6 +1939,12 @@ class LotContext(ModelView):
|
||||
('pnl', 'Pnl'),
|
||||
],'Mode')
|
||||
|
||||
finished = fields.Boolean("Display finished")
|
||||
|
||||
@classmethod
|
||||
def default_finished(cls):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def default_asof(cls):
|
||||
pool = Pool()
|
||||
@@ -2002,17 +2025,24 @@ class LotShipping(Wizard):
|
||||
if r.r_lot_shipment_in:
|
||||
raise UserError("Please unlink before linking to a new shipment !")
|
||||
else:
|
||||
shipped_quantity = Decimal(r.r_lot_quantity)
|
||||
shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
|
||||
logger.info("LotShipping:%s",shipped_quantity)
|
||||
shipment_origin = None
|
||||
if self.ship.quantity:
|
||||
shipped_quantity = self.ship.quantity
|
||||
if shipped_quantity == 0:
|
||||
shipped_quantity = Decimal(r.r_lot_matched)
|
||||
shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
|
||||
if self.ship.shipment == 'in':
|
||||
if not self.ship.shipment_in:
|
||||
UserError("Shipment not known!")
|
||||
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
|
||||
elif self.ship.shipment == 'out':
|
||||
if not self.ship.shipment_out:
|
||||
UserError("Shipment not known!")
|
||||
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
|
||||
elif self.ship.shipment == 'int':
|
||||
if not self.ship.shipment_internal:
|
||||
UserError("Shipment not known!")
|
||||
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
|
||||
if r.id < 10000000 :
|
||||
l = Lot(r.id)
|
||||
@@ -2030,15 +2060,22 @@ class LotShipping(Wizard):
|
||||
move = Move(l.move)
|
||||
move.shipment = shipment_origin
|
||||
Move.save([move])
|
||||
linked_transit_move = move.get_linked_transit_move()
|
||||
if linked_transit_move:
|
||||
linked_transit_move.shipment = shipment_origin
|
||||
Move.save([linked_transit_move])
|
||||
#Decrease forecasted virtual part shipped
|
||||
vlot_p = l.getVlot_p()
|
||||
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
||||
l.lot_av = 'reserved'
|
||||
Lot.save([l])
|
||||
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
||||
Lot.save([l])
|
||||
else:
|
||||
lqt = LotQt(r.id - 10000000)
|
||||
#Increase forecasted virtual part shipped
|
||||
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
||||
logger.info("LotShipping2:%s",shipped_quantity)
|
||||
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
||||
#Decrease forecasted virtual part non shipped
|
||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
||||
@@ -2442,6 +2479,7 @@ class LotAddLine(ModelView):
|
||||
lot_gross_quantity = fields.Numeric("Gross weight")
|
||||
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
||||
lot_premium = fields.Numeric("Premium")
|
||||
lot_chunk_key = fields.Integer("Chunk key")
|
||||
|
||||
# @fields.depends('lot_qt')
|
||||
# def on_change_with_lot_quantity(self):
|
||||
@@ -2604,6 +2642,17 @@ class LotInvoice(Wizard):
|
||||
|
||||
invoicing = StateTransition()
|
||||
|
||||
message = StateView(
|
||||
'purchase.create_prepayment.message',
|
||||
'purchase_trade.create_prepayment_message_form',
|
||||
[
|
||||
Button('OK', 'end', 'tryton-ok'),
|
||||
Button('See Invoice', 'see_invoice', 'tryton-go-next'),
|
||||
]
|
||||
)
|
||||
|
||||
see_invoice = StateAction('account_invoice.act_invoice_form')
|
||||
|
||||
def transition_start(self):
|
||||
return 'inv'
|
||||
|
||||
@@ -2650,7 +2699,7 @@ class LotInvoice(Wizard):
|
||||
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
|
||||
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
|
||||
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
|
||||
val['lot_unit'] = lot.lot_unit_line.id
|
||||
val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
|
||||
unit = val['lot_unit']
|
||||
val['lot_currency'] = lot.lot_price_ct_symbol
|
||||
lot_p.append(val)
|
||||
@@ -2666,6 +2715,7 @@ class LotInvoice(Wizard):
|
||||
val_s['lot_diff_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
|
||||
val_s['lot_diff_amount'] = val_s['lot_amount'] - Decimal(lot.sale_invoice_line_prov.amount)
|
||||
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
|
||||
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
|
||||
lot_s.append(val_s)
|
||||
if line:
|
||||
if line.fees:
|
||||
@@ -2724,35 +2774,64 @@ class LotInvoice(Wizard):
|
||||
'action': act
|
||||
}
|
||||
|
||||
def transition_invoicing(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
Sale = Pool().get('sale.sale')
|
||||
lots = []
|
||||
action = self.inv.action
|
||||
for r in self.records:
|
||||
purchase = r.r_line.purchase
|
||||
sale = None
|
||||
if r.r_sale_line:
|
||||
sale = r.r_sale_line.sale
|
||||
lot = Lot(r.r_lot_p)
|
||||
# if lot.move == None:
|
||||
# Warning = Pool().get('res.user.warning')
|
||||
# warning_name = Warning.format("Lot not confirmed", [])
|
||||
# if Warning.check(warning_name):
|
||||
def transition_invoicing(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
Sale = Pool().get('sale.sale')
|
||||
lots = []
|
||||
purchases = []
|
||||
sales = []
|
||||
action = self.inv.action
|
||||
for r in self.records:
|
||||
purchase = r.r_line.purchase if r.r_line else None
|
||||
sale = r.r_sale_line.sale if r.r_sale_line else None
|
||||
if purchase and purchase not in purchases:
|
||||
purchases.append(purchase)
|
||||
if sale and sale not in sales:
|
||||
sales.append(sale)
|
||||
lot = Lot(r.r_lot_p)
|
||||
# if lot.move == None:
|
||||
# Warning = Pool().get('res.user.warning')
|
||||
# warning_name = Warning.format("Lot not confirmed", [])
|
||||
# if Warning.check(warning_name):
|
||||
# raise QtWarning(warning_name,
|
||||
# "Lot not confirmed, click yes to confirm and invoice")
|
||||
# continue
|
||||
if lot.invoice_line:
|
||||
continue
|
||||
lots.append(lot)
|
||||
if lot.invoice_line:
|
||||
continue
|
||||
lots.append(lot)
|
||||
|
||||
invoice_line = None
|
||||
if self.inv.type == 'purchase':
|
||||
Purchase._process_invoice(purchases, lots, action, self.inv.pp_pur)
|
||||
for lot in lots:
|
||||
lot = Lot(lot.id)
|
||||
invoice_line = lot.invoice_line or lot.invoice_line_prov
|
||||
if invoice_line:
|
||||
break
|
||||
else:
|
||||
if sales:
|
||||
Sale._process_invoice(sales, lots, action, self.inv.pp_sale)
|
||||
for lot in lots:
|
||||
lot = Lot(lot.id)
|
||||
invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
|
||||
if invoice_line:
|
||||
break
|
||||
if not invoice_line:
|
||||
raise UserError("No invoice line was generated from the selected lots.")
|
||||
self.message.invoice = invoice_line.invoice
|
||||
|
||||
return 'message'
|
||||
|
||||
if self.inv.type == 'purchase':
|
||||
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
|
||||
else:
|
||||
if sale:
|
||||
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
|
||||
return 'end'
|
||||
def default_message(self, fields):
|
||||
return {
|
||||
'message': 'The invoice has been successfully created.',
|
||||
}
|
||||
|
||||
def do_see_invoice(self, action):
|
||||
action['views'].reverse() # pour ouvrir en form directement
|
||||
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
|
||||
return action, {'res_id':self.message.invoice.id}
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
@@ -3089,37 +3168,55 @@ class CreateContracts(Wizard):
|
||||
def transition_start(self):
|
||||
return 'ct'
|
||||
|
||||
def default_ct(self, fields):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Lot = Pool().get('lot.lot')
|
||||
context = Transaction().context
|
||||
ids = context.get('active_ids')
|
||||
def default_ct(self, fields):
|
||||
LotQt = Pool().get('lot.qt')
|
||||
Lot = Pool().get('lot.lot')
|
||||
context = Transaction().context
|
||||
ids = context.get('active_ids')
|
||||
unit = None
|
||||
product = None
|
||||
sh_in = None
|
||||
sh_int = None
|
||||
sh_out = None
|
||||
lot = None
|
||||
qt = None
|
||||
type = None
|
||||
for i in ids:
|
||||
val = {}
|
||||
if i < 10000000:
|
||||
raise UserError("You must create contract from an open quantity !")
|
||||
l = LotQt(i - 10000000)
|
||||
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
||||
type = "Sale" if l.lot_p else "Purchase"
|
||||
unit = l.lot_unit.id
|
||||
qt = l.lot_quantity
|
||||
product = ll.lot_product.id
|
||||
sh_in = l.lot_shipment_in.id if l.lot_shipment_in else None
|
||||
sh_int = l.lot_shipment_internal.id if l.lot_shipment_internal else None
|
||||
sh_out = l.lot_shipment_out.id if l.lot_shipment_out else None
|
||||
lot = ll.id
|
||||
|
||||
return {
|
||||
'quantity': qt,
|
||||
'unit': unit,
|
||||
sh_out = None
|
||||
lot = None
|
||||
qt = Decimal(0)
|
||||
type = None
|
||||
shipment_in_values = set()
|
||||
shipment_internal_values = set()
|
||||
shipment_out_values = set()
|
||||
for i in ids:
|
||||
if i < 10000000:
|
||||
raise UserError("You must create contract from an open quantity !")
|
||||
l = LotQt(i - 10000000)
|
||||
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
||||
current_type = "Sale" if l.lot_p else "Purchase"
|
||||
if type and current_type != type:
|
||||
raise UserError("You must select open quantities from the same side.")
|
||||
type = current_type
|
||||
if product and ll.lot_product.id != product:
|
||||
raise UserError("You must select open quantities with the same product.")
|
||||
if unit and l.lot_unit.id != unit:
|
||||
raise UserError("You must select open quantities with the same unit.")
|
||||
unit = l.lot_unit.id
|
||||
qt += abs(Decimal(str(l.lot_quantity or 0)))
|
||||
product = ll.lot_product.id
|
||||
shipment_in_values.add(l.lot_shipment_in.id if l.lot_shipment_in else None)
|
||||
shipment_internal_values.add(
|
||||
l.lot_shipment_internal.id if l.lot_shipment_internal else None)
|
||||
shipment_out_values.add(l.lot_shipment_out.id if l.lot_shipment_out else None)
|
||||
if lot is None:
|
||||
lot = ll.id
|
||||
|
||||
if len(shipment_in_values) == 1:
|
||||
sh_in = next(iter(shipment_in_values))
|
||||
if len(shipment_internal_values) == 1:
|
||||
sh_int = next(iter(shipment_internal_values))
|
||||
if len(shipment_out_values) == 1:
|
||||
sh_out = next(iter(shipment_out_values))
|
||||
|
||||
return {
|
||||
'quantity': qt,
|
||||
'unit': unit,
|
||||
'product': product,
|
||||
'shipment_in': sh_in,
|
||||
'shipment_internal': sh_int,
|
||||
@@ -3129,136 +3226,13 @@ class CreateContracts(Wizard):
|
||||
}
|
||||
|
||||
def transition_creating(self):
|
||||
SaleLine = Pool().get('sale.line')
|
||||
Sale = Pool().get('sale.sale')
|
||||
PurchaseLine = Pool().get('purchase.line')
|
||||
Purchase = Pool().get('purchase.purchase')
|
||||
LotQt = Pool().get('lot.qt')
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
Lot = Pool().get('lot.lot')
|
||||
Date = Pool().get('ir.date')
|
||||
self.sale_lines = []
|
||||
type = self.ct.type
|
||||
base_contract = self.ct.lot.sale_line.sale if type == 'Purchase' else self.ct.lot.line.purchase
|
||||
for c in self.ct.contracts:
|
||||
contract = Purchase() if type == 'Purchase' else Sale()
|
||||
contract_line = PurchaseLine() if type == 'Purchase' else SaleLine()
|
||||
parts = c.currency_unit.split("_")
|
||||
if int(parts[0]) != 0:
|
||||
contract.currency = int(parts[0])
|
||||
else:
|
||||
contract.currency = 1
|
||||
contract.party = c.party
|
||||
contract.crop = c.crop
|
||||
contract.tol_min = c.tol_min
|
||||
contract.tol_max = c.tol_max
|
||||
if type == 'Purchase':
|
||||
contract.purchase_date = Date.today()
|
||||
else:
|
||||
contract.sale_date = Date.today()
|
||||
contract.reference = c.reference
|
||||
if base_contract.from_location and base_contract.to_location:
|
||||
if type == 'Purchase':
|
||||
contract.to_location = base_contract.from_location
|
||||
else:
|
||||
contract.from_location = base_contract.to_location
|
||||
if base_contract.from_location.type == 'supplier' and base_contract.to_location.type == 'customer':
|
||||
contract.from_location = base_contract.from_location
|
||||
contract.to_location = base_contract.to_location
|
||||
if c.party.wb:
|
||||
contract.wb = c.party.wb
|
||||
if c.party.association:
|
||||
contract.association = c.party.association
|
||||
if type == 'Purchase':
|
||||
if c.party.supplier_payment_term:
|
||||
contract.payment_term = c.party.supplier_payment_term
|
||||
else:
|
||||
if c.party.customer_payment_term:
|
||||
contract.payment_term = c.party.customer_payment_term
|
||||
contract.incoterm = c.incoterm
|
||||
if c.party.addresses:
|
||||
contract.invoice_address = c.party.addresses[0]
|
||||
if type == 'Sale':
|
||||
contract.shipment_address = c.party.addresses[0]
|
||||
contract.__class__.save([contract])
|
||||
contract_line.quantity = c.quantity
|
||||
contract_line.quantity_theorical = c.quantity
|
||||
contract_line.product = self.ct.product
|
||||
contract_line.price_type = c.price_type
|
||||
contract_line.unit = self.ct.unit
|
||||
if type == 'Purchase':
|
||||
contract_line.purchase = contract.id
|
||||
else:
|
||||
contract_line.sale = contract.id
|
||||
contract_line.created_by_code = self.ct.matched
|
||||
contract_line.premium = Decimal(0)
|
||||
if int(parts[0]) == 0:
|
||||
contract_line.enable_linked_currency = True
|
||||
contract_line.linked_currency = 1
|
||||
contract_line.linked_unit = int(parts[1])
|
||||
contract_line.linked_price = c.price
|
||||
contract_line.unit_price = contract_line.get_price_linked_currency()
|
||||
else:
|
||||
contract_line.unit_price = c.price if c.price else Decimal(0)
|
||||
contract_line.del_period = c.del_period
|
||||
contract_line.from_del = c.from_del
|
||||
contract_line.to_del = c.to_del
|
||||
contract_line.__class__.save([contract_line])
|
||||
logger.info("CREATE_ID:%s",contract.id)
|
||||
logger.info("CREATE_LINE_ID:%s",contract_line.id)
|
||||
if self.ct.matched:
|
||||
lot = Lot()
|
||||
if type == 'Purchase':
|
||||
lot.line = contract_line.id
|
||||
else:
|
||||
lot.sale_line = contract_line.id
|
||||
lot.lot_qt = None
|
||||
lot.lot_unit = None
|
||||
lot.lot_unit_line = contract_line.unit
|
||||
lot.lot_quantity = round(contract_line.quantity,5)
|
||||
lot.lot_gross_quantity = None
|
||||
lot.lot_status = 'forecast'
|
||||
lot.lot_type = 'virtual'
|
||||
lot.lot_product = contract_line.product
|
||||
lqtt = LotQtType.search([('sequence','=',1)])
|
||||
if lqtt:
|
||||
lqh = LotQtHist()
|
||||
lqh.quantity_type = lqtt[0]
|
||||
lqh.quantity = round(lot.lot_quantity,5)
|
||||
lqh.gross_quantity = round(lot.lot_quantity,5)
|
||||
lot.lot_hist = [lqh]
|
||||
Lot.save([lot])
|
||||
vlot = self.ct.lot
|
||||
shipment_origin = None
|
||||
if self.ct.shipment_in:
|
||||
shipment_origin = 'stock.shipment.in,' + str(self.ct.shipment_in.id)
|
||||
elif self.ct.shipment_internal:
|
||||
shipment_origin = 'stock.shipment.internal,' + str(self.ct.shipment_internal.id)
|
||||
elif self.ct.shipment_out:
|
||||
shipment_origin = 'stock.shipment.out,' + str(self.ct.shipment_out.id)
|
||||
|
||||
qt = c.quantity
|
||||
if type == 'Purchase':
|
||||
if not lot.updateVirtualPart(qt,shipment_origin,vlot):
|
||||
lot.createVirtualPart(qt,shipment_origin,vlot)
|
||||
#Decrease forecasted virtual part non matched
|
||||
lot.updateVirtualPart(-qt,shipment_origin,vlot,'only sale')
|
||||
else:
|
||||
if not vlot.updateVirtualPart(qt,shipment_origin,lot):
|
||||
vlot.createVirtualPart(qt,shipment_origin,lot)
|
||||
#Decrease forecasted virtual part non matched
|
||||
vlot.updateVirtualPart(-qt,shipment_origin,None)
|
||||
|
||||
|
||||
ContractFactory.create_contracts(
|
||||
self.ct.contracts,
|
||||
type_=self.ct.type,
|
||||
ct=self.ct,
|
||||
)
|
||||
return 'end'
|
||||
|
||||
# def do_matching(self, action):
|
||||
# return action, {
|
||||
# 'ids': self.sale_lines,
|
||||
# 'model': str(self.ct.lot.id),
|
||||
# }
|
||||
|
||||
def end(self):
|
||||
return 'reload'
|
||||
|
||||
@@ -3288,7 +3262,6 @@ class ContractsStart(ModelView):
|
||||
def default_matched(cls):
|
||||
return True
|
||||
|
||||
|
||||
class ContractDetail(ModelView):
|
||||
|
||||
"Contract Detail"
|
||||
@@ -3296,26 +3269,29 @@ class ContractDetail(ModelView):
|
||||
|
||||
category = fields.Integer("Category")
|
||||
cd = fields.Many2One('contracts.start',"Contracts")
|
||||
party = fields.Many2One('party.party',"Party",domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
||||
currency = fields.Many2One('currency.currency',"Currency")
|
||||
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm")
|
||||
quantity = fields.Numeric("Quantity",digits=(1,5))
|
||||
unit = fields.Many2One('product.uom',"Unit")
|
||||
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
||||
currency = fields.Many2One('currency.currency',"Currency", required=True)
|
||||
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
|
||||
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
|
||||
unit = fields.Many2One('product.uom',"Unit", required=True)
|
||||
qt_unit = fields.Many2One('product.uom',"Unit")
|
||||
tol_min = fields.Numeric("Tol - in %")
|
||||
tol_max = fields.Numeric("Tol + in %")
|
||||
tol_min = fields.Numeric("Tol - in %", required=True)
|
||||
tol_max = fields.Numeric("Tol + in %", required=True)
|
||||
crop = fields.Many2One('purchase.crop',"Crop")
|
||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||
from_del = fields.Date("From")
|
||||
to_del = fields.Date("To")
|
||||
price = fields.Numeric("Price",digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
|
||||
price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
|
||||
price_type = price_type = fields.Selection([
|
||||
('cash', 'Cash Price'),
|
||||
('priced', 'Priced'),
|
||||
('basis', 'Basis'),
|
||||
], 'Price type')
|
||||
], 'Price type', required=True)
|
||||
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
||||
reference = fields.Char("Reference")
|
||||
from_location = fields.Many2One('stock.location',"From location")
|
||||
to_location = fields.Many2One('stock.location',"To location")
|
||||
payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
|
||||
|
||||
@classmethod
|
||||
def default_category(cls):
|
||||
@@ -3372,7 +3348,7 @@ class ContractDetail(ModelView):
|
||||
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
|
||||
return lqt.lot_p.line.purchase.crop.id
|
||||
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
|
||||
return lqt.lot_s.line.sale.crop.id
|
||||
return lqt.lot_s.sale_line.sale.crop.id
|
||||
|
||||
@classmethod
|
||||
def default_currency(cls):
|
||||
@@ -3415,4 +3391,4 @@ class ContractDetail(ModelView):
|
||||
if self.del_period:
|
||||
self.from_del = self.del_period.beg_date
|
||||
self.to_del = self.del_period.end_date
|
||||
|
||||
|
||||
|
||||
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,17 +1,183 @@
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from trytond.pool import PoolMeta
|
||||
from trytond.pool import PoolMeta, Pool
|
||||
from trytond.exceptions import UserError
|
||||
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
|
||||
from trytond.transaction import Transaction
|
||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
from sql import Table
|
||||
from trytond.pyson import Bool, Eval, Id, If
|
||||
|
||||
__all__ = ['Party']
|
||||
__metaclass__ = PoolMeta
|
||||
class PartyExecution(ModelSQL,ModelView):
|
||||
"Party Execution"
|
||||
__name__ = 'party.execution'
|
||||
|
||||
class Party(metaclass=PoolMeta):
|
||||
__name__ = 'party.party'
|
||||
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')
|
||||
|
||||
@staticmethod
|
||||
def _to_decimal(value):
|
||||
if value is None:
|
||||
return Decimal('0')
|
||||
if isinstance(value, Decimal):
|
||||
return value
|
||||
return Decimal(str(value))
|
||||
|
||||
@classmethod
|
||||
def _round_percent(cls, value):
|
||||
return cls._to_decimal(value).quantize(
|
||||
Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
def matches_country(self, country):
|
||||
if not self.area or not country or not getattr(country, 'region', None):
|
||||
return False
|
||||
region = country.region
|
||||
while region:
|
||||
if region.id == self.area.id:
|
||||
return True
|
||||
region = getattr(region, 'parent', None)
|
||||
return False
|
||||
|
||||
def matches_shipment(self, shipment):
|
||||
location = getattr(shipment, 'to_location', None)
|
||||
country = getattr(location, 'country', None)
|
||||
return self.matches_country(country)
|
||||
|
||||
@classmethod
|
||||
def compute_achieved_percent_for(cls, party, area):
|
||||
if not party or not area:
|
||||
return Decimal('0')
|
||||
Shipment = Pool().get('stock.shipment.in')
|
||||
shipments = Shipment.search([
|
||||
('controller', '!=', None),
|
||||
])
|
||||
execution = cls()
|
||||
execution.area = area
|
||||
shipments = [
|
||||
shipment for shipment in shipments
|
||||
if execution.matches_shipment(shipment)]
|
||||
total = len(shipments)
|
||||
if not total:
|
||||
return Decimal('0')
|
||||
achieved = sum(
|
||||
1 for shipment in shipments
|
||||
if shipment.controller and shipment.controller.id == party.id)
|
||||
return cls._round_percent(
|
||||
(Decimal(achieved) * Decimal('100')) / Decimal(total))
|
||||
|
||||
def compute_achieved_percent(self):
|
||||
return self.__class__.compute_achieved_percent_for(
|
||||
self.party, self.area)
|
||||
|
||||
def get_target_gap(self):
|
||||
return self._to_decimal(self.percent) - self.compute_achieved_percent()
|
||||
|
||||
def get_percent(self,name):
|
||||
return self.compute_achieved_percent()
|
||||
|
||||
class PartyExecutionSla(ModelSQL,ModelView):
|
||||
"Party Execution Sla"
|
||||
__name__ = 'party.execution.sla'
|
||||
|
||||
party = fields.Many2One('party.party',"Party")
|
||||
reference = fields.Char("Reference")
|
||||
product = fields.Many2One('product.product',"Product")
|
||||
date_from = fields.Date("From")
|
||||
date_to = fields.Date("To")
|
||||
places = fields.One2Many('party.execution.place','pes',"")
|
||||
|
||||
class PartyExecutionPlace(ModelSQL,ModelView):
|
||||
"Party Sla Place"
|
||||
__name__ = 'party.execution.place'
|
||||
|
||||
pes = fields.Many2One('party.execution.sla',"Sla")
|
||||
location = fields.Many2One('stock.location',"Location")
|
||||
cost = fields.Numeric("Cost",digits=(16,4))
|
||||
mode = fields.Selection([
|
||||
('lumpsum', 'Lump sum'),
|
||||
('perqt', 'Per qt'),
|
||||
('pprice', '% price'),
|
||||
('rate', '% rate'),
|
||||
('pcost', '% cost price'),
|
||||
('ppack', 'Per packing'),
|
||||
], 'Mode', required=True)
|
||||
currency = fields.Many2One('currency.currency',"Currency")
|
||||
unit = fields.Many2One('product.uom',"Unit",domain=[
|
||||
If(Eval('mode') == 'ppack',
|
||||
('category', '=', 8),
|
||||
()),
|
||||
],
|
||||
states={
|
||||
'readonly': Eval('mode') != 'ppack',
|
||||
})
|
||||
|
||||
class Party(metaclass=PoolMeta):
|
||||
__name__ = 'party.party'
|
||||
|
||||
tol_min = fields.Numeric("Tol - in %")
|
||||
tol_max = fields.Numeric("Tol + in %")
|
||||
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
|
||||
association = fields.Many2One('purchase.association',"Association")
|
||||
|
||||
|
||||
origin =fields.Char("Origin")
|
||||
execution = fields.One2Many('party.execution','party',"")
|
||||
sla = fields.One2Many('party.execution.sla','party', "Sla")
|
||||
initial = fields.Char("Initials")
|
||||
|
||||
def IsAvailableForControl(self,sh):
|
||||
return True
|
||||
|
||||
def get_controller_execution_priority(self, shipment):
|
||||
best_rule = None
|
||||
best_gap = None
|
||||
for execution in self.execution or []:
|
||||
if not execution.matches_shipment(shipment):
|
||||
continue
|
||||
gap = execution.get_target_gap()
|
||||
if best_gap is None or gap > best_gap:
|
||||
best_gap = gap
|
||||
best_rule = execution
|
||||
return best_gap, best_rule
|
||||
|
||||
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">
|
||||
<field name="model">party.party</field>
|
||||
<field name="inherit" ref="party.party_view_form"/>
|
||||
<field name="name">party_form</field>
|
||||
</record>
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="party_view_form">
|
||||
<field name="model">party.party</field>
|
||||
<field name="inherit" ref="party.party_view_form"/>
|
||||
<field name="name">party_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="party_exec_view_list">
|
||||
<field name="model">party.execution</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">party_exec_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="party_exec_sla_view_form">
|
||||
<field name="model">party.execution.sla</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">party_exec_sla_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="party_exec_sla_view_list">
|
||||
<field name="model">party.execution.sla</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">party_exec_sla_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="party_exec_place_view_form">
|
||||
<field name="model">party.execution.place</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">party_exec_place_tree</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -21,6 +21,7 @@ class PaymentTermLine(metaclass=PoolMeta):
|
||||
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
|
||||
|
||||
term_type = fields.Selection([
|
||||
(None, ''),
|
||||
('advance', 'Advance'),
|
||||
('cad', 'CAD'),
|
||||
('open', 'Open'),
|
||||
@@ -30,18 +31,21 @@ class PaymentTermLine(metaclass=PoolMeta):
|
||||
|
||||
trigger_offset = fields.Integer('Trigger Offset')
|
||||
offset_unit = fields.Selection([
|
||||
(None, ''),
|
||||
('calendar', 'Calendar Days'),
|
||||
('business', 'Business Days'),
|
||||
], 'Offset Unit')
|
||||
|
||||
eom_flag = fields.Boolean('EOM Flag')
|
||||
eom_mode = fields.Selection([
|
||||
(None, ''),
|
||||
('standard', 'Standard'),
|
||||
('before', 'Before EOM'),
|
||||
('after', 'After EOM'),
|
||||
], 'EOM Mode')
|
||||
|
||||
risk_classification = fields.Selection([
|
||||
(None, ''),
|
||||
('fully_secured', 'Fully Secured'),
|
||||
('partially_secured', 'Partially Secured'),
|
||||
('unsecured', 'Unsecured'),
|
||||
|
||||
@@ -50,37 +50,243 @@ DAYS = [
|
||||
('sunday', 'Sunday'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
class Estimated(ModelSQL, ModelView):
|
||||
"Estimated date"
|
||||
__name__ = 'pricing.estimated'
|
||||
|
||||
trigger = fields.Selection(TRIGGERS,"Trigger")
|
||||
estimated_date = fields.Date("Estimated date")
|
||||
fin_int_delta = fields.Integer("Financing interests delta")
|
||||
|
||||
class MtmScenario(ModelSQL, ModelView):
|
||||
"MtM Scenario"
|
||||
__name__ = 'mtm.scenario'
|
||||
|
||||
name = fields.Char("Scenario", required=True)
|
||||
valuation_date = fields.Date("Valuation Date", required=True)
|
||||
use_last_price = fields.Boolean("Use Last Available Price")
|
||||
calendar = fields.Many2One(
|
||||
'price.calendar', "Calendar"
|
||||
)
|
||||
|
||||
class MtmStrategy(ModelSQL, ModelView):
|
||||
"Mark to Market Strategy"
|
||||
__name__ = 'mtm.strategy'
|
||||
|
||||
name = fields.Char("Name", required=True)
|
||||
active = fields.Boolean("Active")
|
||||
|
||||
scenario = fields.Many2One(
|
||||
'mtm.scenario', "Scenario", required=True
|
||||
)
|
||||
|
||||
currency = fields.Many2One(
|
||||
'currency.currency', "Valuation Currency"
|
||||
)
|
||||
|
||||
components = fields.One2Many(
|
||||
'pricing.component', 'strategy', "Components"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def default_active(cls):
|
||||
return True
|
||||
|
||||
def get_mtm(self,line,qty):
|
||||
pool = Pool()
|
||||
Currency = pool.get('currency.currency')
|
||||
total = Decimal(0)
|
||||
|
||||
scenario = self.scenario
|
||||
dt = scenario.valuation_date
|
||||
|
||||
for comp in self.components:
|
||||
value = Decimal(0)
|
||||
|
||||
if comp.price_source_type == 'curve' and comp.price_index:
|
||||
value = Decimal(
|
||||
comp.price_index.get_price(
|
||||
dt,
|
||||
line.unit,
|
||||
self.currency,
|
||||
last=scenario.use_last_price
|
||||
)
|
||||
)
|
||||
|
||||
elif comp.price_source_type == 'matrix' and comp.price_matrix:
|
||||
value = self._get_matrix_price(comp, line, dt)
|
||||
|
||||
if comp.ratio:
|
||||
value *= Decimal(comp.ratio) / Decimal(100)
|
||||
|
||||
total += value * qty
|
||||
|
||||
return Decimal(str(total)).quantize(Decimal("0.01"))
|
||||
|
||||
def _get_matrix_price(self, comp, line, dt):
|
||||
MatrixLine = Pool().get('price.matrix.line')
|
||||
|
||||
domain = [
|
||||
('matrix', '=', comp.price_matrix.id),
|
||||
]
|
||||
|
||||
if line:
|
||||
domain += [
|
||||
('origin', '=', line.purchase.from_location),
|
||||
('destination', '=', line.purchase.to_location),
|
||||
]
|
||||
|
||||
lines = MatrixLine.search(domain)
|
||||
if lines:
|
||||
return Decimal(lines[0].price_value)
|
||||
|
||||
return Decimal(0)
|
||||
|
||||
def run_daily_mtm():
|
||||
Strategy = Pool().get('mtm.strategy')
|
||||
Snapshot = Pool().get('mtm.snapshot')
|
||||
|
||||
for strat in Strategy.search([('active', '=', True)]):
|
||||
amount = strat.compute_mtm()
|
||||
Snapshot.create([{
|
||||
'strategy': strat.id,
|
||||
'valuation_date': strat.scenario.valuation_date,
|
||||
'amount': amount,
|
||||
'currency': strat.currency.id,
|
||||
}])
|
||||
|
||||
class Mtm(ModelSQL, ModelView):
|
||||
"Mtm"
|
||||
"MtM Component"
|
||||
__name__ = 'mtm.component'
|
||||
|
||||
fix_type = fields.Many2One('price.fixtype',"Fixation type")
|
||||
ratio = fields.Numeric("%")
|
||||
price_index = fields.Many2One('price.price',"Curve")
|
||||
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
|
||||
strategy = fields.Many2One(
|
||||
'mtm.strategy', "Strategy",
|
||||
required=True, ondelete='CASCADE'
|
||||
)
|
||||
|
||||
def get_cur(self,name):
|
||||
name = fields.Char("Component", required=True)
|
||||
|
||||
component_type = fields.Selection([
|
||||
('commodity', 'Commodity'),
|
||||
('freight', 'Freight'),
|
||||
('quality', 'Quality'),
|
||||
('fx', 'FX'),
|
||||
('storage', 'Storage'),
|
||||
('other', 'Other'),
|
||||
], "Type", required=True)
|
||||
|
||||
fix_type = fields.Many2One('price.fixtype', "Fixation Type")
|
||||
|
||||
price_source_type = fields.Selection([
|
||||
('curve', 'Curve'),
|
||||
('matrix', 'Matrix'),
|
||||
('manual', 'Manual'),
|
||||
], "Price Source", required=True)
|
||||
|
||||
price_index = fields.Many2One('price.price', "Price Curve")
|
||||
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
|
||||
|
||||
ratio = fields.Numeric("Ratio / %", digits=(16, 6))
|
||||
|
||||
manual_price = fields.Numeric(
|
||||
"Manual Price",
|
||||
digits=(16, 6),
|
||||
help="Price set manually if price_source_type is 'manual'"
|
||||
)
|
||||
|
||||
currency = fields.Many2One('currency.currency', "Currency")
|
||||
|
||||
def get_cur(self, name=None):
|
||||
if self.price_index:
|
||||
PI = Pool().get('price.price')
|
||||
pi = PI(self.price_index)
|
||||
return pi.price_currency
|
||||
|
||||
return self.price_index.price_currency
|
||||
if self.price_matrix:
|
||||
return self.price_matrix.currency
|
||||
return None
|
||||
|
||||
@fields.depends('price_index','price_matrix')
|
||||
def on_change_with_currency(self):
|
||||
return self.get_cur()
|
||||
|
||||
class PriceMatrix(ModelSQL, ModelView):
|
||||
"Price Matrix"
|
||||
__name__ = 'price.matrix'
|
||||
|
||||
name = fields.Char("Name", required=True)
|
||||
|
||||
matrix_type = fields.Selection([
|
||||
('freight', 'Freight'),
|
||||
('location', 'Location Spread'),
|
||||
('quality', 'Quality'),
|
||||
('storage', 'Storage'),
|
||||
('other', 'Other'),
|
||||
], "Matrix Type", required=True)
|
||||
|
||||
unit = fields.Many2One('product.uom', "Unit")
|
||||
currency = fields.Many2One('currency.currency', "Currency")
|
||||
|
||||
calendar = fields.Many2One(
|
||||
'price.calendar', "Calendar"
|
||||
)
|
||||
|
||||
valid_from = fields.Date("Valid From")
|
||||
valid_to = fields.Date("Valid To")
|
||||
|
||||
lines = fields.One2Many(
|
||||
'price.matrix.line', 'matrix', "Lines"
|
||||
)
|
||||
|
||||
class PriceMatrixLine(ModelSQL, ModelView):
|
||||
"Price Matrix Line"
|
||||
__name__ = 'price.matrix.line'
|
||||
|
||||
matrix = fields.Many2One(
|
||||
'price.matrix', "Matrix",
|
||||
required=True, ondelete='CASCADE'
|
||||
)
|
||||
|
||||
origin = fields.Many2One('stock.location', "Origin")
|
||||
destination = fields.Many2One('stock.location', "Destination")
|
||||
|
||||
product = fields.Many2One('product.product', "Product")
|
||||
quality = fields.Many2One('product.category', "Quality")
|
||||
|
||||
price_value = fields.Numeric("Price", digits=(16, 6))
|
||||
|
||||
class MtmSnapshot(ModelSQL, ModelView):
|
||||
"MtM Snapshot"
|
||||
__name__ = 'mtm.snapshot'
|
||||
|
||||
strategy = fields.Many2One(
|
||||
'mtm.strategy', "Strategy",
|
||||
required=True, ondelete='CASCADE'
|
||||
)
|
||||
|
||||
valuation_date = fields.Date("Valuation Date", required=True)
|
||||
|
||||
amount = fields.Numeric("MtM Amount", digits=(16, 6))
|
||||
currency = fields.Many2One('currency.currency', "Currency")
|
||||
|
||||
created_at = fields.DateTime("Created At")
|
||||
|
||||
class Component(ModelSQL, ModelView):
|
||||
"Component"
|
||||
__name__ = 'pricing.component'
|
||||
|
||||
strategy = fields.Many2One(
|
||||
'mtm.strategy', "Strategy",
|
||||
required=False, ondelete='CASCADE'
|
||||
)
|
||||
|
||||
price_source_type = fields.Selection([
|
||||
('curve', 'Curve'),
|
||||
('matrix', 'Matrix'),
|
||||
# ('manual', 'Manual'),
|
||||
], "Price Source", required=True)
|
||||
|
||||
fix_type = fields.Many2One('price.fixtype',"Fixation type")
|
||||
ratio = fields.Numeric("%")
|
||||
ratio = fields.Numeric("%",digits=(16,7))
|
||||
price_index = fields.Many2One('price.price',"Curve")
|
||||
price_matrix = fields.Many2One('price.matrix', "Price Matrix")
|
||||
currency = fields.Function(fields.Many2One('currency.currency',"Curr."),'get_cur')
|
||||
auto = fields.Boolean("Auto")
|
||||
fallback = fields.Boolean("Fallback")
|
||||
@@ -194,6 +400,7 @@ class Trigger(ModelSQL,ModelView):
|
||||
'readonly': Eval('pricing_period') != None,
|
||||
})
|
||||
average = fields.Boolean("Avg")
|
||||
last = fields.Boolean("Last")
|
||||
application_period = fields.Many2One('pricing.period',"Application period")
|
||||
from_a = fields.Date("From",
|
||||
states={
|
||||
@@ -217,14 +424,11 @@ class Trigger(ModelSQL,ModelView):
|
||||
pp = PP(self.application_period)
|
||||
CO = Pool().get('pricing.component')
|
||||
co = CO(self.component)
|
||||
logger.info("DELDATEEST_:%s",co)
|
||||
if co.line:
|
||||
d = co.getEstimatedTriggerPurchase(pp.trigger)
|
||||
else:
|
||||
d = co.getEstimatedTriggerSale(pp.trigger)
|
||||
logger.info("DELDATEEST:%s",d)
|
||||
date_from,date_to,dates = pp.getDates(d)
|
||||
logger.info("DELDATEEST2:%s",dates)
|
||||
return date_from,date_to,d,pp.include,dates
|
||||
|
||||
def getApplicationListDates(self, cal):
|
||||
@@ -288,7 +492,7 @@ class Trigger(ModelSQL,ModelView):
|
||||
pi = PI(pc.price_index)
|
||||
val = {}
|
||||
val['date'] = current_date
|
||||
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency)
|
||||
val['price'] = pi.get_price(current_date,pc.line.unit if pc.line else pc.sale_line.unit,pc.line.currency if pc.line else pc.sale_line.currency,self.last)
|
||||
val['avg'] = val['price']
|
||||
val['avg_minus_1'] = val['price']
|
||||
val['isAvg'] = self.average
|
||||
@@ -330,8 +534,6 @@ class Period(ModelSQL,ModelView):
|
||||
date_from = None
|
||||
date_to = None
|
||||
dates = []
|
||||
logger.info("GETDATES:%s",t)
|
||||
logger.info("GETDATES:%s",self.every)
|
||||
if t:
|
||||
if self.every:
|
||||
if t:
|
||||
@@ -348,21 +550,18 @@ class Period(ModelSQL,ModelView):
|
||||
while current.month == t.month:
|
||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||
current += datetime.timedelta(days=7)
|
||||
logger.info("GETDATES:%s",dates)
|
||||
elif self.nb_quotation > 0:
|
||||
days_to_add = (weekday_target - t.weekday()) % 7
|
||||
current = t + datetime.timedelta(days=days_to_add)
|
||||
while len(dates) < self.nb_quotation:
|
||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||
current += datetime.timedelta(days=7)
|
||||
logger.info("GETDATES:%s",dates)
|
||||
elif self.nb_quotation < 0:
|
||||
days_to_sub = (t.weekday() - weekday_target) % 7
|
||||
current = t - datetime.timedelta(days=days_to_sub)
|
||||
while len(dates) < -self.nb_quotation:
|
||||
dates.append(datetime.datetime(current.year, current.month, current.day))
|
||||
current -= datetime.timedelta(days=7)
|
||||
logger.info("GETDATES:%s",dates)
|
||||
|
||||
else:
|
||||
if self.startday == 'before':
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.icon" id="mtm_icon">
|
||||
<field name="name">tradon-mtm</field>
|
||||
<field name="path">icons/tradon-mtm.svg</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="summary_view_tree_sequence">
|
||||
<field name="model">sale.pricing.summary</field>
|
||||
<field name="type">tree</field>
|
||||
@@ -104,5 +109,84 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="type">form</field>
|
||||
<field name="name">period_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mtm_scenario_view_form">
|
||||
<field name="model">mtm.scenario</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">mtm_scenario_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mtm_scenario_view_list">
|
||||
<field name="model">mtm.scenario</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">mtm_scenario_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mtm_strategy_view_form">
|
||||
<field name="model">mtm.strategy</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">mtm_strategy_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mtm_strategy_view_list">
|
||||
<field name="model">mtm.strategy</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">mtm_strategy_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="price_matrix_view_form">
|
||||
<field name="model">price.matrix</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">price_matrix_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="price_matrix_view_list">
|
||||
<field name="model">price.matrix</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">price_matrix_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="price_matrix_line_view_list">
|
||||
<field name="model">price.matrix.line</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">price_matrix_line_tree</field>
|
||||
</record>
|
||||
<!-- <record model="ir.ui.view" id="price_matrix_line_view_form">
|
||||
<field name="model">price.matrix.line</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">price_matrix_line_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mtm_snapshot_view_form">
|
||||
<field name="model">mtm.snapshot</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">mtm_snapshot_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mtm_snapshot_view_list">
|
||||
<field name="model">mtm.snapshot</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">mtm_snapshot_tree</field>
|
||||
</record> -->
|
||||
|
||||
<record model="ir.action.act_window" id="act_strategy_form">
|
||||
<field name="name">Strategy</field>
|
||||
<field name="res_model">mtm.strategy</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_strategy_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="mtm_strategy_view_list"/>
|
||||
<field name="act_window" ref="act_strategy_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_strategy_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="mtm_strategy_view_form"/>
|
||||
<field name="act_window" ref="act_strategy_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Mtm"
|
||||
sequence="99"
|
||||
id="menu_mtm"
|
||||
icon="tradon-mtm" />
|
||||
<menuitem
|
||||
name="Strategy"
|
||||
action="act_strategy_form"
|
||||
parent="menu_mtm"
|
||||
sequence="10"
|
||||
id="menu_strategy" />
|
||||
|
||||
</data>
|
||||
</tryton>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -126,6 +126,25 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="wiz_name">pnl.report</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="position_bi_view_graph">
|
||||
<field name="model">position.bi</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">position_bi_graph</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window" id="act_position_bi">
|
||||
<field name="name">Position BI</field>
|
||||
<field name="res_model">position.bi</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_position_bi_view">
|
||||
<field name="sequence" eval="30"/>
|
||||
<field name="view" ref="position_bi_view_graph"/>
|
||||
<field name="act_window" ref="act_position_bi"/>
|
||||
</record>
|
||||
<record model="ir.action.wizard" id="act_position_report">
|
||||
<field name="name">Position report</field>
|
||||
<field name="wiz_name">position.report</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mtm_view_form">
|
||||
<field name="model">mtm.component</field>
|
||||
<field name="type">form</field>
|
||||
@@ -137,6 +156,77 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="name">mtm_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="price_composition_view_tree">
|
||||
<field name="model">price.composition</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">price_composition_tree</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="quality_analysis_view_tree">
|
||||
<field name="model">quality.analysis</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">quality_analysis_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="quality_analysis_view_form">
|
||||
<field name="model">quality.analysis</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">quality_analysis_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="assay_view_tree">
|
||||
<field name="model">assay.assay</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">assay_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="assay_view_form">
|
||||
<field name="model">assay.assay</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">assay_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="assay_line_view_tree">
|
||||
<field name="model">assay.line</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">assay_line_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="assay_element_view_form">
|
||||
<field name="model">assay.element</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">assay_element_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="concentrate_view_tree">
|
||||
<field name="model">concentrate.term</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">concentrate_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="concentrate_view_form">
|
||||
<field name="model">concentrate.term</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">concentrate_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="payable_rule_view_form">
|
||||
<field name="model">payable.rule</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">payable_rule_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="penalty_rule_view_form">
|
||||
<field name="model">penalty.rule</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">penalty_rule_form</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="penalty_rule_view_tree">
|
||||
<field name="model">penalty.rule</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">penalty_rule_tree</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="penalty_rule_tier_view_tree">
|
||||
<field name="model">penalty.rule.tier</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">penalty_rule_tier_tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Pnl Report"
|
||||
parent="purchase_trade.menu_global_reporting"
|
||||
@@ -144,6 +234,13 @@ this repository contains the full copyright notices and license terms. -->
|
||||
sequence="110"
|
||||
id="menu_pnl_bi"/>
|
||||
|
||||
<menuitem
|
||||
name="Position Report"
|
||||
parent="purchase_trade.menu_global_reporting"
|
||||
action="act_position_bi"
|
||||
sequence="120"
|
||||
id="menu_position_bi"/>
|
||||
|
||||
<menuitem
|
||||
parent="purchase_trade.menu_global_reporting"
|
||||
sequence="100"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user