Compare commits
759 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8906f00d36 | |||
| 29a719c117 | |||
| d71257720e | |||
| aee9c3277e | |||
| b68f475e22 | |||
| 90eab73430 | |||
| 472806ef06 | |||
| 0def187750 | |||
| 229b6037fb | |||
| 8a90216357 | |||
| 4bbd7a5e76 | |||
| 9c8d7f11ae | |||
| b39607d987 | |||
| 65482b4a8b | |||
| 8b9787d4c0 | |||
| a1ab7dec82 | |||
| 5ae8af84fb | |||
| c90b14fcc1 | |||
| e9ff9c76ab | |||
| 63d8266a9c | |||
| add4cdc137 | |||
| 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
|
||||
141
AGENTS.md
Normal file
141
AGENTS.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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()`.
|
||||
- Rappels session templates (2026-04-08):
|
||||
- `insurance.fodt`: le texte "insured for account of" doit afficher la compagnie courante (shipment.company.party), pas le client.
|
||||
- `insurance.fodt`: exposer des proprietes Python `report_*` sur `stock.shipment.in` pour les montants (incoming moves) et les zones client-specifiques.
|
||||
- `insurance.fodt`: "Amount insured" suit la regle metier 110% du montant incoming (base calculee via lot -> purchase.line.unit_price * quantite courante convertie).
|
||||
- `insurance.fodt`: zone "Contact the following surveyor" alimentee par une propriete dediee, avec champ `surveyor` (party.party) cote shipment.
|
||||
- `packing_list.fodt`: date en haut a droite = date du jour; unites Net/Gross = unite de `purchase.line`.
|
||||
- `bill.fodt` (sale): la 2eme date doit etre une vraie maturity date (depuis `invoice.lines_to_pay.maturity_date`), pas `payment_term.rec_name`.
|
||||
- `bill.fodt` (sale): le montant en lettres doit provenir du montant du bill (facture/total), pas du `unit_price` de ligne.
|
||||
- Quand un template affiche les placeholders en brut, verifier que les champs sont bien des placeholders Relatorio dans le XML (pas du texte litteral).
|
||||
- Eviter les apostrophes echappees style `\'` dans placeholders; preferer `"` et `'`.
|
||||
|
||||
## 4.bis) Memo templates de session
|
||||
|
||||
- Voir aussi `notes/template_business_rules.md` pour le recap detaille (business rules + decisions templates de la session).
|
||||
|
||||
## 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).
|
||||
- Rappels session 2026-04-09:
|
||||
- `invoice_ict.fodt` / `invoice_ict_final.fodt`: poids et unites depuis `lot.qt.hist` / `lot_unit_line`, priorite lots `physic`, sinon lot `virtual` unique.
|
||||
- `invoice_ict.fodt` / `invoice_ict_final.fodt`: infos shipment depuis les lots reels des lignes facture; ne rien afficher si plusieurs shipments differents.
|
||||
- `invoice_ict.fodt` / `invoice_ict_final.fodt`: `S/I` = `shipment.reference`; `NB BALES: 0` => `Unchanged` sur le final.
|
||||
- `invoice_ict.fodt` / `invoice_ict_final.fodt`: quantites uniformisees a `2` decimales; conversion `LBS` via UoM, jamais via un facteur fixe aveugle.
|
||||
- `invoice_ict.fodt` / `invoice_ict_final.fodt`: si plusieurs lignes reutilisent le meme lot, les lignes detaillees suivent la quantite facturee convertie, mais le `GROSS` global doit rester le vrai delta historique du lot.
|
||||
- `sale_ict.fodt`: meme priorite lots; les mots suivent l'unite reelle; le total convertit vers une unite commune, qui est celle du lot virtuel seulement s'il y a un seul lot virtuel sur tout le report.
|
||||
- `lot.report.r_del_period`: utiliser `sale.line.del_period` pour `lot_s` sans `lot_p`, sinon `purchase.line.del_period`.
|
||||
- `lot.do_weighing`: `lot_qt` editable et ecrasement direct de `lot.lot_qt`.
|
||||
- `account.invoice`: `Validate` cree aussi le `account.move` pour les factures client, attribue aussi le `number` a ce stade pour les factures client comme fournisseur; `Post` ne doit plus forcer une fresh session sur ce flux.
|
||||
- `pricing.pricing`: saisie manuelle autorisee meme sans composant; en manuel, l'utilisateur saisit seulement `Qt` et `Settl. price`; `fixed_qt`, `fixed_qt_price`, `unfixed_qt`, `unfixed_qt_price` et `eod_price` sont derives automatiquement.
|
||||
- `pricing.pricing`: en manuel, `fixed_qt` = cumul des `quantity`, `fixed_qt_price` = moyenne ponderee cumulee des `settl_price`, `unfixed_qt` = reste a fixer, `unfixed_qt_price` = `settl_price` de la ligne.
|
||||
- `pricing.pricing`: `eod_price` reste non editable et calcule en prix moyen pondere; `last=True` gere par groupe `line + component`, choisi sur la `pricing_date` la plus grande.
|
||||
- `purchase_trade`: `trader` filtre sur `TRADER`, `operator` sur `OPERATOR`; fallback sur `quantity` si `quantity_theorical` est vide dans les quotas/pricings.
|
||||
- `sale.line` / `purchase.line`: en mode `basis`, sans `price_component`, le `Price` et le `Fix. progress` de la ligne doivent remonter depuis la ligne `Summary` sans component.
|
||||
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"/>
|
||||
|
||||
@@ -485,7 +485,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
})
|
||||
cls.__rpc__.update({
|
||||
'post': RPC(
|
||||
readonly=False, instantiate=0, fresh_session=True),
|
||||
readonly=False, instantiate=0, fresh_session=False),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
@@ -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 = []
|
||||
@@ -1885,17 +1890,15 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
||||
cls._check_taxes(invoices)
|
||||
# cls._check_similar(invoices)
|
||||
|
||||
invoices_in = cls.browse([i for i in invoices if i.type == 'in'])
|
||||
cls.set_number(invoices_in)
|
||||
cls.set_number(invoices)
|
||||
cls._store_cache(invoices)
|
||||
|
||||
moves = []
|
||||
for invoice in invoices:
|
||||
if invoice.type == 'in':
|
||||
move = invoice.get_move()
|
||||
if move != invoice.move:
|
||||
invoice.move = move
|
||||
moves.append(move)
|
||||
move = invoice.get_move()
|
||||
if move != invoice.move:
|
||||
invoice.move = move
|
||||
moves.append(move)
|
||||
invoice.do_lot_invoicing()
|
||||
if moves:
|
||||
Move.save(moves)
|
||||
@@ -1960,14 +1963,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 +2036,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 +3693,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 +3722,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>
|
||||
|
||||
4157
modules/account_invoice/invoice_ict.fodt
Normal file
4157
modules/account_invoice/invoice_ict.fodt
Normal file
File diff suppressed because it is too large
Load Diff
4180
modules/account_invoice/invoice_ict_final.fodt
Normal file
4180
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
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from trytond.modules.account_invoice.exceptions import (
|
||||
PaymentTermValidationError)
|
||||
@@ -251,5 +252,70 @@ class AccountInvoiceTestCase(
|
||||
(datetime.date(2012, 1, 14), Decimal('-1.0')),
|
||||
])
|
||||
|
||||
def test_post_rpc_does_not_require_fresh_session(self):
|
||||
'posting invoices does not force a fresh session'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
self.assertFalse(Invoice.__rpc__['post'].fresh_session)
|
||||
|
||||
@with_transaction()
|
||||
def test_validate_invoice_creates_move_for_customer_invoice(self):
|
||||
'validating customer invoices now creates the account move'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
move = Mock()
|
||||
invoice = Invoice()
|
||||
invoice.type = 'out'
|
||||
invoice.move = None
|
||||
invoice.get_move = Mock(return_value=move)
|
||||
invoice.do_lot_invoicing = Mock()
|
||||
|
||||
move_model = Mock()
|
||||
|
||||
with patch.object(Invoice, '_check_taxes'), patch.object(
|
||||
Invoice, '_store_cache'), patch.object(
|
||||
Invoice, 'browse', return_value=[]), patch.object(
|
||||
Invoice, 'cleanMoves') as clean_moves, patch.object(
|
||||
Invoice, 'save') as save_invoices, patch(
|
||||
'trytond.modules.account_invoice.invoice.Pool'
|
||||
) as PoolMock:
|
||||
PoolMock.return_value.get.return_value = move_model
|
||||
|
||||
Invoice.validate_invoice([invoice])
|
||||
|
||||
self.assertIs(invoice.move, move)
|
||||
invoice.get_move.assert_called_once_with()
|
||||
invoice.do_lot_invoicing.assert_called_once_with()
|
||||
move_model.save.assert_called_once_with([move])
|
||||
clean_moves.assert_called_once_with([move])
|
||||
save_invoices.assert_called()
|
||||
|
||||
@with_transaction()
|
||||
def test_validate_invoice_sets_number_for_customer_invoice(self):
|
||||
'validating customer invoices now assigns the invoice number'
|
||||
Invoice = Pool().get('account.invoice')
|
||||
|
||||
move = Mock()
|
||||
invoice = Invoice()
|
||||
invoice.type = 'out'
|
||||
invoice.move = None
|
||||
invoice.get_move = Mock(return_value=move)
|
||||
invoice.do_lot_invoicing = Mock()
|
||||
|
||||
move_model = Mock()
|
||||
|
||||
with patch.object(Invoice, '_check_taxes'), patch.object(
|
||||
Invoice, '_store_cache'), patch.object(
|
||||
Invoice, 'set_number') as set_number, patch.object(
|
||||
Invoice, 'cleanMoves'), patch.object(
|
||||
Invoice, 'save'), patch(
|
||||
'trytond.modules.account_invoice.invoice.Pool'
|
||||
) as PoolMock:
|
||||
PoolMock.return_value.get.return_value = move_model
|
||||
|
||||
Invoice.validate_invoice([invoice])
|
||||
|
||||
set_number.assert_called_once_with([invoice])
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
@@ -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>
|
||||
|
||||
183
modules/purchase_trade/AGENTS.md
Normal file
183
modules/purchase_trade/AGENTS.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# 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`
|
||||
- dans `pricing.pricing` en saisie manuelle, l'utilisateur renseigne
|
||||
seulement `quantity` et `settl_price`
|
||||
- `fixed_qt`, `fixed_qt_price`, `unfixed_qt`, `unfixed_qt_price` et
|
||||
`eod_price` sont des valeurs derivees et ne doivent pas etre saisies a la
|
||||
main
|
||||
- en manuel, `fixed_qt` = cumul des `quantity` du groupe trie par
|
||||
`pricing_date`
|
||||
- en manuel, `fixed_qt_price` = moyenne ponderee cumulee des `settl_price`
|
||||
- en manuel, `unfixed_qt_price` = `settl_price` de la ligne
|
||||
- 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.
|
||||
- Sur `account.invoice`, le workflow `Validate` doit maintenant aligner
|
||||
fournisseur et client pour:
|
||||
- creation du `account.move`
|
||||
- attribution du `number`
|
||||
- `Post` ne doit pas reintroduire une difference de session/fresh login cote
|
||||
client
|
||||
|
||||
## 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>
|
||||
495
modules/purchase_trade/docs/business-rules.md
Normal file
495
modules/purchase_trade/docs/business-rules.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Business Rules - Purchase Trade
|
||||
|
||||
Statut: `draft`
|
||||
Version: `v0.5`
|
||||
Derniere mise a jour: `2026-04-10`
|
||||
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`
|
||||
|
||||
### BR-PT-016 - En pricing manuel, seules la quantite fixee du jour et le prix de marche sont saisis
|
||||
|
||||
- Intent: simplifier la saisie utilisateur et garantir une coherence unique
|
||||
entre les colonnes de `pricing.pricing`.
|
||||
- Description:
|
||||
- Pour une ligne de `pricing.pricing` en mode manuel, l'utilisateur ne doit
|
||||
saisir que:
|
||||
- `quantity`
|
||||
- `settl_price`
|
||||
- Les autres colonnes de suivi sont derivees automatiquement sur tout le
|
||||
groupe metier (`line + component` ou `sale_line + component`) trie par
|
||||
`pricing_date`.
|
||||
- Resultat attendu:
|
||||
- `fixed_qt` = cumul des `quantity`
|
||||
- `fixed_qt_price` = moyenne ponderee cumulee des `settl_price`
|
||||
- `unfixed_qt` = quantite de base de la ligne - `fixed_qt`
|
||||
- `unfixed_qt_price` = `settl_price` de la ligne
|
||||
- `eod_price` = moyenne ponderee entre jambe fixee et non fixee
|
||||
- `last=True` reste unique par groupe et suit la plus grande `pricing_date`
|
||||
- Hors scope:
|
||||
- la generation automatique des lignes quand `pricing.component.auto = True`
|
||||
ne doit pas changer de comportement
|
||||
- Priorite:
|
||||
- `structurante`
|
||||
|
||||
### BR-PT-017 - Le workflow Validate des factures client doit aussi attribuer le numero
|
||||
|
||||
- Intent: aligner le comportement des factures client et fournisseur au moment
|
||||
de `Validate`.
|
||||
- Description:
|
||||
- Lors du workflow `Validate` sur `account.invoice`, une facture client
|
||||
(`type = out`) doit maintenant:
|
||||
- creer son `account.move`
|
||||
- recevoir son `number`
|
||||
- La numerotation ne doit plus etre repoussee au `Post` cote client.
|
||||
- Resultat attendu:
|
||||
- a l'issue de `Validate`, une facture fournisseur ou client possede deja:
|
||||
- son `account.move`
|
||||
- son `number`
|
||||
- `Post` conserve son role de posting comptable sans reintroduire de
|
||||
difference de session/fresh login cote client
|
||||
- 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 |
1275
modules/purchase_trade/invoice.py
Normal file
1275
modules/purchase_trade/invoice.py
Normal file
File diff suppressed because it is too large
Load Diff
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:
|
||||
@@ -1318,11 +1334,16 @@ class LotQt(
|
||||
Case((lp.id>0, lp.lot_premium_sale),else_=ls.lot_premium_sale).as_('r_lot_premium_sale'),
|
||||
Case((lp.id>0, lp.lot_parent),else_=ls.lot_parent).as_('r_lot_parent'),
|
||||
Case((lp.id>0, lp.lot_himself),else_=ls.lot_himself).as_('r_lot_himself'),
|
||||
Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
|
||||
Case((lp.id>0, lp.line),else_=None).as_('r_line'),
|
||||
Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
|
||||
Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
|
||||
Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
|
||||
Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
|
||||
Case((lp.id>0, lp.line),else_=None).as_('r_line'),
|
||||
Case(
|
||||
(((lqt.lot_s != None) & (lqt.lot_p == None) & (sl.id > 0)),
|
||||
sl.del_period),
|
||||
else_=Case((pl.id>0, pl.del_period),else_=None)
|
||||
).as_('r_del_period'),
|
||||
Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
|
||||
Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
|
||||
Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
|
||||
(MaQt + AvQt).as_('r_tot'),
|
||||
pu.party.as_('r_supplier'),
|
||||
sa.party.as_('r_client'),
|
||||
@@ -1423,13 +1444,14 @@ class LotQt(
|
||||
lp.lot_av.as_("r_lot_av"),
|
||||
lp.lot_premium.as_("r_lot_premium"),
|
||||
lp.lot_premium_sale.as_("r_lot_premium_sale"),
|
||||
lp.lot_parent.as_("r_lot_parent"),
|
||||
lp.lot_himself.as_("r_lot_himself"),
|
||||
lp.lot_container.as_("r_lot_container"),
|
||||
lp.line.as_("r_line"),
|
||||
Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
|
||||
Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
|
||||
lp.sale_line.as_("r_sale_line"),
|
||||
lp.lot_parent.as_("r_lot_parent"),
|
||||
lp.lot_himself.as_("r_lot_himself"),
|
||||
lp.lot_container.as_("r_lot_container"),
|
||||
lp.line.as_("r_line"),
|
||||
pl.del_period.as_("r_del_period"),
|
||||
Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
|
||||
Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
|
||||
lp.sale_line.as_("r_sale_line"),
|
||||
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||
pu.party.as_("r_supplier"),
|
||||
sa.party.as_("r_client"),
|
||||
@@ -1488,13 +1510,14 @@ class LotQt(
|
||||
Max(lp.lot_av).as_("r_lot_av"),
|
||||
Avg(lp.lot_premium).as_("r_lot_premium"),
|
||||
Literal(None).as_("r_lot_premium_sale"),
|
||||
Literal(None).as_("r_lot_parent"),
|
||||
Literal(None).as_("r_lot_himself"),
|
||||
Max(lp.lot_container).as_("r_lot_container"),
|
||||
lp.line.as_("r_line"),
|
||||
Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
|
||||
Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
|
||||
lp.sale_line.as_("r_sale_line"),
|
||||
Literal(None).as_("r_lot_parent"),
|
||||
Literal(None).as_("r_lot_himself"),
|
||||
Max(lp.lot_container).as_("r_lot_container"),
|
||||
lp.line.as_("r_line"),
|
||||
Max(pl.del_period).as_("r_del_period"),
|
||||
Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
|
||||
Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
|
||||
lp.sale_line.as_("r_sale_line"),
|
||||
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||
Max(pu.party).as_("r_supplier"),
|
||||
Max(sa.party).as_("r_client"),
|
||||
@@ -1541,13 +1564,14 @@ class LotQt(
|
||||
union.r_lot_av.as_("r_lot_av"),
|
||||
union.r_lot_premium.as_("r_lot_premium"),
|
||||
union.r_lot_premium_sale.as_("r_lot_premium_sale"),
|
||||
union.r_lot_parent.as_("r_lot_parent"),
|
||||
union.r_lot_himself.as_("r_lot_himself"),
|
||||
union.r_lot_container.as_("r_lot_container"),
|
||||
union.r_line.as_("r_line"),
|
||||
union.r_purchase.as_("r_purchase"),
|
||||
union.r_sale.as_("r_sale"),
|
||||
union.r_sale_line.as_("r_sale_line"),
|
||||
union.r_lot_parent.as_("r_lot_parent"),
|
||||
union.r_lot_himself.as_("r_lot_himself"),
|
||||
union.r_lot_container.as_("r_lot_container"),
|
||||
union.r_line.as_("r_line"),
|
||||
union.r_del_period.as_("r_del_period"),
|
||||
union.r_purchase.as_("r_purchase"),
|
||||
union.r_sale.as_("r_sale"),
|
||||
union.r_sale_line.as_("r_sale_line"),
|
||||
union.r_tot.as_("r_tot"),
|
||||
union.r_supplier.as_("r_supplier"),
|
||||
union.r_client.as_("r_client"),
|
||||
@@ -1614,14 +1638,15 @@ class LotReport(
|
||||
r_lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
|
||||
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
|
||||
r_lot_move = fields.Many2One('stock.move', "Move")
|
||||
r_lot_parent = fields.Many2One('lot.lot',"Parent")
|
||||
r_lot_himself = fields.Many2One('lot.lot',"Lot")
|
||||
r_lot_container = fields.Char("Container")
|
||||
r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
|
||||
r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
|
||||
r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
|
||||
r_sale_line = fields.Many2One('sale.line',"S. line")
|
||||
r_sale = fields.Many2One('sale.sale',"Sale")
|
||||
r_lot_parent = fields.Many2One('lot.lot',"Parent")
|
||||
r_lot_himself = fields.Many2One('lot.lot',"Lot")
|
||||
r_lot_container = fields.Char("Container")
|
||||
r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
|
||||
r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
|
||||
r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
|
||||
r_del_period = fields.Many2One('product.month', "Delivery Period")
|
||||
r_sale_line = fields.Many2One('sale.line',"S. line")
|
||||
r_sale = fields.Many2One('sale.sale',"Sale")
|
||||
r_tot = fields.Numeric("Qt tot", digits='r_lot_unit_line')
|
||||
r_supplier = fields.Many2One('party.party',"Supplier")
|
||||
r_client = fields.Many2One('party.party',"Client")
|
||||
@@ -1832,7 +1857,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 +1948,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 +2034,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 +2069,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 +2488,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 +2651,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 +2708,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 +2724,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 +2783,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'
|
||||
@@ -2962,25 +3050,26 @@ class LotWeighing(Wizard):
|
||||
Lot = Pool().get('lot.lot')
|
||||
context = Transaction().context
|
||||
ids = context.get('active_ids')
|
||||
for i in ids:
|
||||
if i > 10000000:
|
||||
raise UserError("Trying to do weighing on open quantity!")
|
||||
val = {}
|
||||
lot = Lot(i)
|
||||
val['lot'] = lot.id
|
||||
val['lot_name'] = lot.lot_name
|
||||
for i in ids:
|
||||
if i > 10000000:
|
||||
raise UserError("Trying to do weighing on open quantity!")
|
||||
val = {}
|
||||
lot = Lot(i)
|
||||
val['lot'] = lot.id
|
||||
val['lot_name'] = lot.lot_name
|
||||
if lot.lot_shipment_in:
|
||||
val['lot_shipment_in'] = lot.lot_shipment_in.id
|
||||
if lot.lot_shipment_internal:
|
||||
val['lot_shipment_internal'] = lot.lot_shipment_internal.id
|
||||
if lot.lot_shipment_out:
|
||||
val['lot_shipment_out'] = lot.lot_shipment_out.id
|
||||
val['lot_product'] = lot.lot_product.id
|
||||
val['lot_quantity'] = lot.lot_quantity
|
||||
val['lot_gross_quantity'] = lot.lot_gross_quantity
|
||||
val['lot_unit'] = lot.lot_unit.id
|
||||
val['lot_unit_line'] = lot.lot_unit_line.id
|
||||
lot_p.append(val)
|
||||
if lot.lot_shipment_out:
|
||||
val['lot_shipment_out'] = lot.lot_shipment_out.id
|
||||
val['lot_product'] = lot.lot_product.id
|
||||
val['lot_qt'] = lot.lot_qt
|
||||
val['lot_quantity'] = lot.lot_quantity
|
||||
val['lot_gross_quantity'] = lot.lot_gross_quantity
|
||||
val['lot_unit'] = lot.lot_unit.id
|
||||
val['lot_unit_line'] = lot.lot_unit_line.id
|
||||
lot_p.append(val)
|
||||
return {
|
||||
'lot_p': lot_p,
|
||||
}
|
||||
@@ -2995,17 +3084,18 @@ class LotWeighing(Wizard):
|
||||
lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
|
||||
if lhs:
|
||||
lh = lhs[0]
|
||||
else:
|
||||
lh = LotHist()
|
||||
lh.lot = l.lot
|
||||
lh.quantity_type = self.w.lot_state
|
||||
lh.quantity = round(l.lot_quantity_new,5)
|
||||
lh.gross_quantity = round(l.lot_gross_quantity_new,5)
|
||||
LotHist.save([lh])
|
||||
|
||||
if self.w.lot_update_state :
|
||||
l.lot.lot_state = self.w.lot_state
|
||||
Lot.save([l.lot])
|
||||
else:
|
||||
lh = LotHist()
|
||||
lh.lot = l.lot
|
||||
lh.quantity_type = self.w.lot_state
|
||||
lh.quantity = round(l.lot_quantity_new,5)
|
||||
lh.gross_quantity = round(l.lot_gross_quantity_new,5)
|
||||
LotHist.save([lh])
|
||||
l.lot.lot_qt = l.lot_qt
|
||||
|
||||
if self.w.lot_update_state :
|
||||
l.lot.lot_state = self.w.lot_state
|
||||
Lot.save([l.lot])
|
||||
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
|
||||
if diff != 0 :
|
||||
#need to update virtual part
|
||||
@@ -3040,12 +3130,13 @@ class LotWeighingLot(ModelView):
|
||||
lot_name = fields.Char("Name",readonly=True)
|
||||
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
||||
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
||||
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
||||
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
||||
lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
|
||||
lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
|
||||
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
||||
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
||||
lot_qt = fields.Integer("Qt")
|
||||
lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
|
||||
lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
|
||||
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
|
||||
lot_quantity_new = fields.Numeric("New net weight",digits=(1,5))
|
||||
lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
|
||||
lot_shipment_origin = fields.Function(
|
||||
@@ -3089,37 +3180,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 +3238,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 +3274,6 @@ class ContractsStart(ModelView):
|
||||
def default_matched(cls):
|
||||
return True
|
||||
|
||||
|
||||
class ContractDetail(ModelView):
|
||||
|
||||
"Contract Detail"
|
||||
@@ -3296,26 +3281,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 +3360,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 +3403,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")
|
||||
@@ -119,20 +325,20 @@ class Component(ModelSQL, ModelView):
|
||||
|
||||
super(Component, cls).delete(components)
|
||||
|
||||
class Pricing(ModelSQL,ModelView):
|
||||
"Pricing"
|
||||
__name__ = 'pricing.pricing'
|
||||
|
||||
pricing_date = fields.Date("Date")
|
||||
price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE')
|
||||
quantity = fields.Numeric("Qt",digits='unit')
|
||||
settl_price = fields.Numeric("Settl. price",digits='unit')
|
||||
fixed_qt = fields.Numeric("Fixed qt",digits='unit',readonly=True)
|
||||
fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit',readonly=True)
|
||||
unfixed_qt = fields.Numeric("Unfixed qt",digits='unit',readonly=True)
|
||||
unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit',readonly=True)
|
||||
eod_price = fields.Numeric("EOD price",digits='unit',readonly=True)
|
||||
last = fields.Boolean("Last")
|
||||
class Pricing(ModelSQL,ModelView):
|
||||
"Pricing"
|
||||
__name__ = 'pricing.pricing'
|
||||
|
||||
pricing_date = fields.Date("Date")
|
||||
price_component = fields.Many2One('pricing.component', "Component")#, domain=[('id', 'in', Eval('line.price_components'))], ondelete='CASCADE')
|
||||
quantity = fields.Numeric("Qt",digits='unit')
|
||||
settl_price = fields.Numeric("Settl. price",digits='unit')
|
||||
fixed_qt = fields.Numeric("Fixed qt",digits='unit', readonly=True)
|
||||
fixed_qt_price = fields.Numeric("Fixed qt price",digits='unit', readonly=True)
|
||||
unfixed_qt = fields.Numeric("Unfixed qt",digits='unit', readonly=True)
|
||||
unfixed_qt_price = fields.Numeric("Unfixed qt price",digits='unit', readonly=True)
|
||||
eod_price = fields.Numeric("EOD price",digits='unit',readonly=True)
|
||||
last = fields.Boolean("Last")
|
||||
|
||||
@classmethod
|
||||
def default_fixed_qt(cls):
|
||||
@@ -158,24 +364,244 @@ class Pricing(ModelSQL,ModelView):
|
||||
def default_settl_price(cls):
|
||||
return Decimal(0)
|
||||
|
||||
@classmethod
|
||||
def default_eod_price(cls):
|
||||
return Decimal(0)
|
||||
|
||||
def get_fixed_price(self):
|
||||
price = Decimal(0)
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
pricings = Pricing.search(['price_component','=',self.price_component.id],order=[('pricing_date', 'ASC')])
|
||||
if pricings:
|
||||
cumul_qt = Decimal(0)
|
||||
cumul_qt_price = Decimal(0)
|
||||
for pr in pricings:
|
||||
cumul_qt += pr.quantity
|
||||
cumul_qt_price += pr.quantity * pr.settl_price
|
||||
if pr.id == self.id:
|
||||
break
|
||||
if cumul_qt > 0:
|
||||
price = cumul_qt_price / cumul_qt
|
||||
@classmethod
|
||||
def default_eod_price(cls):
|
||||
return Decimal(0)
|
||||
|
||||
@staticmethod
|
||||
def _weighted_average_price(fixed_qt, fixed_price, unfixed_qt, unfixed_price):
|
||||
fixed_qt = Decimal(str(fixed_qt or 0))
|
||||
fixed_price = Decimal(str(fixed_price or 0))
|
||||
unfixed_qt = Decimal(str(unfixed_qt or 0))
|
||||
unfixed_price = Decimal(str(unfixed_price or 0))
|
||||
total_qty = fixed_qt + unfixed_qt
|
||||
if total_qty == 0:
|
||||
return Decimal(0)
|
||||
return round(
|
||||
((fixed_qt * fixed_price) + (unfixed_qt * unfixed_price)) / total_qty,
|
||||
4,
|
||||
)
|
||||
|
||||
def compute_eod_price(self):
|
||||
if getattr(self, 'sale_line', None) and hasattr(self, 'get_eod_price_sale'):
|
||||
return self.get_eod_price_sale()
|
||||
if getattr(self, 'line', None) and hasattr(self, 'get_eod_price_purchase'):
|
||||
return self.get_eod_price_purchase()
|
||||
return self._weighted_average_price(
|
||||
self.fixed_qt,
|
||||
self.fixed_qt_price,
|
||||
self.unfixed_qt,
|
||||
self.unfixed_qt_price,
|
||||
)
|
||||
|
||||
@fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price')
|
||||
def on_change_fixed_qt(self):
|
||||
self.eod_price = self.compute_eod_price()
|
||||
|
||||
@fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price')
|
||||
def on_change_fixed_qt_price(self):
|
||||
self.eod_price = self.compute_eod_price()
|
||||
|
||||
@fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price')
|
||||
def on_change_unfixed_qt(self):
|
||||
self.eod_price = self.compute_eod_price()
|
||||
|
||||
@fields.depends('fixed_qt', 'fixed_qt_price', 'unfixed_qt', 'unfixed_qt_price')
|
||||
def on_change_unfixed_qt_price(self):
|
||||
self.eod_price = self.compute_eod_price()
|
||||
|
||||
@classmethod
|
||||
def create(cls, vlist):
|
||||
records = super(Pricing, cls).create(vlist)
|
||||
cls._sync_manual_values(records)
|
||||
cls._sync_manual_last(records)
|
||||
cls._sync_eod_price(records)
|
||||
return records
|
||||
|
||||
@classmethod
|
||||
def write(cls, *args):
|
||||
super(Pricing, cls).write(*args)
|
||||
if (Transaction().context.get('skip_pricing_eod_sync')
|
||||
or Transaction().context.get('skip_pricing_last_sync')):
|
||||
return
|
||||
records = []
|
||||
actions = iter(args)
|
||||
for record_set, values in zip(actions, actions):
|
||||
if values:
|
||||
records.extend(record_set)
|
||||
cls._sync_manual_values(records)
|
||||
cls._sync_manual_last(records)
|
||||
cls._sync_eod_price(records)
|
||||
|
||||
@classmethod
|
||||
def _sync_eod_price(cls, records):
|
||||
if not records:
|
||||
return
|
||||
with Transaction().set_context(skip_pricing_eod_sync=True):
|
||||
for record in records:
|
||||
eod_price = record.compute_eod_price()
|
||||
if Decimal(str(record.eod_price or 0)) == Decimal(str(eod_price or 0)):
|
||||
continue
|
||||
super(Pricing, cls).write([record], {
|
||||
'eod_price': eod_price,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _is_manual_pricing_record(cls, record):
|
||||
component = getattr(record, 'price_component', None)
|
||||
if component is None:
|
||||
return True
|
||||
return not bool(getattr(component, 'auto', False))
|
||||
|
||||
@classmethod
|
||||
def _get_pricing_group_domain(cls, record):
|
||||
component = getattr(record, 'price_component', None)
|
||||
if getattr(record, 'sale_line', None):
|
||||
return [
|
||||
('sale_line', '=', record.sale_line.id),
|
||||
('price_component', '=',
|
||||
component.id if getattr(component, 'id', None) else None),
|
||||
]
|
||||
if getattr(record, 'line', None):
|
||||
return [
|
||||
('line', '=', record.line.id),
|
||||
('price_component', '=',
|
||||
component.id if getattr(component, 'id', None) else None),
|
||||
]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_base_quantity(cls, record):
|
||||
owner = getattr(record, 'sale_line', None) or getattr(record, 'line', None)
|
||||
if not owner:
|
||||
return Decimal(0)
|
||||
if hasattr(owner, '_get_pricing_base_quantity'):
|
||||
return Decimal(str(owner._get_pricing_base_quantity() or 0))
|
||||
quantity = getattr(owner, 'quantity_theorical', None)
|
||||
if quantity is None:
|
||||
quantity = getattr(owner, 'quantity', None)
|
||||
return Decimal(str(quantity or 0))
|
||||
|
||||
@classmethod
|
||||
def _sync_manual_values(cls, records):
|
||||
if (not records
|
||||
or Transaction().context.get('skip_pricing_manual_sync')):
|
||||
return
|
||||
domains = []
|
||||
seen = set()
|
||||
for record in records:
|
||||
if not cls._is_manual_pricing_record(record):
|
||||
continue
|
||||
domain = cls._get_pricing_group_domain(record)
|
||||
if not domain:
|
||||
continue
|
||||
key = tuple(domain)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
domains.append(domain)
|
||||
if not domains:
|
||||
return
|
||||
with Transaction().set_context(
|
||||
skip_pricing_manual_sync=True,
|
||||
skip_pricing_last_sync=True,
|
||||
skip_pricing_eod_sync=True):
|
||||
for domain in domains:
|
||||
pricings = cls.search(
|
||||
domain,
|
||||
order=[('pricing_date', 'ASC'), ('id', 'ASC')])
|
||||
if not pricings:
|
||||
continue
|
||||
base_quantity = cls._get_base_quantity(pricings[0])
|
||||
cumul_qt = Decimal(0)
|
||||
cumul_qt_price = Decimal(0)
|
||||
total = len(pricings)
|
||||
for index, pricing in enumerate(pricings):
|
||||
quantity = Decimal(str(pricing.quantity or 0))
|
||||
settl_price = Decimal(str(pricing.settl_price or 0))
|
||||
cumul_qt += quantity
|
||||
cumul_qt_price += quantity * settl_price
|
||||
fixed_qt = cumul_qt
|
||||
if fixed_qt > 0:
|
||||
fixed_qt_price = round(cumul_qt_price / fixed_qt, 4)
|
||||
else:
|
||||
fixed_qt_price = Decimal(0)
|
||||
unfixed_qt = base_quantity - fixed_qt
|
||||
if unfixed_qt < Decimal('0.001'):
|
||||
unfixed_qt = Decimal(0)
|
||||
fixed_qt = base_quantity
|
||||
values = {
|
||||
'fixed_qt': fixed_qt,
|
||||
'fixed_qt_price': fixed_qt_price,
|
||||
'unfixed_qt': unfixed_qt,
|
||||
'unfixed_qt_price': settl_price,
|
||||
'last': index == (total - 1),
|
||||
}
|
||||
eod_price = cls._weighted_average_price(
|
||||
values['fixed_qt'],
|
||||
values['fixed_qt_price'],
|
||||
values['unfixed_qt'],
|
||||
values['unfixed_qt_price'],
|
||||
)
|
||||
values['eod_price'] = eod_price
|
||||
super(Pricing, cls).write([pricing], values)
|
||||
|
||||
@classmethod
|
||||
def _get_manual_last_group_domain(cls, record):
|
||||
return cls._get_pricing_group_domain(record)
|
||||
|
||||
@classmethod
|
||||
def _sync_manual_last(cls, records):
|
||||
if not records:
|
||||
return
|
||||
domains = []
|
||||
seen = set()
|
||||
for record in records:
|
||||
domain = cls._get_manual_last_group_domain(record)
|
||||
if not domain:
|
||||
continue
|
||||
key = tuple(domain)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
domains.append(domain)
|
||||
if not domains:
|
||||
return
|
||||
with Transaction().set_context(
|
||||
skip_pricing_last_sync=True,
|
||||
skip_pricing_eod_sync=True):
|
||||
for domain in domains:
|
||||
pricings = cls.search(
|
||||
domain,
|
||||
order=[('pricing_date', 'ASC'), ('id', 'ASC')])
|
||||
if not pricings:
|
||||
continue
|
||||
last_pricing = pricings[-1]
|
||||
for pricing in pricings[:-1]:
|
||||
if pricing.last:
|
||||
super(Pricing, cls).write([pricing], {'last': False})
|
||||
if not last_pricing.last:
|
||||
super(Pricing, cls).write([last_pricing], {'last': True})
|
||||
|
||||
def get_fixed_price(self):
|
||||
price = Decimal(0)
|
||||
Pricing = Pool().get('pricing.pricing')
|
||||
domain = self._get_pricing_group_domain(self)
|
||||
if not domain:
|
||||
return price
|
||||
pricings = Pricing.search(domain, order=[('pricing_date', 'ASC'), ('id', 'ASC')])
|
||||
if pricings:
|
||||
cumul_qt = Decimal(0)
|
||||
cumul_qt_price = Decimal(0)
|
||||
for pr in pricings:
|
||||
quantity = Decimal(str(pr.quantity or 0))
|
||||
settl_price = Decimal(str(pr.settl_price or 0))
|
||||
cumul_qt += quantity
|
||||
cumul_qt_price += quantity * settl_price
|
||||
if pr.id == self.id:
|
||||
break
|
||||
if cumul_qt > 0:
|
||||
price = cumul_qt_price / cumul_qt
|
||||
return round(price,4)
|
||||
|
||||
|
||||
@@ -194,6 +620,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 +644,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 +712,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 +754,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 +770,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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user