Compare commits
782 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 036a2801e5 | |||
| c2838dcfd3 | |||
| 6e26f178ce | |||
| 4458871423 | |||
| 99782b5b0e | |||
| d09d183cdc | |||
| ec45440400 | |||
| edb23b008c | |||
| fca279a038 | |||
| 512862113a | |||
| 1ed79a2363 | |||
| 05a75f000a | |||
| 77a5366622 | |||
| c203e1b300 | |||
| 22ad8547e4 | |||
| 2ea2165653 | |||
| 5e72d4f98d | |||
| b1680cc96c | |||
| cfb719a7da | |||
| 9e4a12605c | |||
| 4013f9d74c | |||
| 280cff5fdb | |||
| 897c6f6824 | |||
| 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,
|
cursor.execute(*ir_model.select(ir_model.id,
|
||||||
where=ir_model.model == model.__name__))
|
where=ir_model.model == model.__name__))
|
||||||
model_id = None
|
model_id = None
|
||||||
|
logger.info("MODEL_NAME:%s",model.__name__)
|
||||||
if cursor.rowcount == -1 or cursor.rowcount is None:
|
if cursor.rowcount == -1 or cursor.rowcount is None:
|
||||||
data = cursor.fetchone()
|
data = cursor.fetchone()
|
||||||
if data:
|
if data:
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class Binary(Field):
|
|||||||
on_change_with=None, depends=None, context=None, loading='lazy',
|
on_change_with=None, depends=None, context=None, loading='lazy',
|
||||||
filename=None, file_id=None, store_prefix=None):
|
filename=None, file_id=None, store_prefix=None):
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.file_id = file_id
|
self.file_id = None #file_id
|
||||||
self.store_prefix = store_prefix
|
self.store_prefix = None #store_prefix
|
||||||
super(Binary, self).__init__(string=string, help=help,
|
super(Binary, self).__init__(string=string, help=help,
|
||||||
required=required, readonly=readonly, domain=domain, states=states,
|
required=required, readonly=readonly, domain=domain, states=states,
|
||||||
on_change=on_change, on_change_with=on_change_with,
|
on_change=on_change, on_change_with=on_change_with,
|
||||||
|
|||||||
@@ -103,15 +103,26 @@ class Model(URLMixin, PoolBase, metaclass=ModelMeta):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_name(cls):
|
def _get_name(cls):
|
||||||
'''
|
if cls.__doc__ is None:
|
||||||
Returns the first non-empty line of the model docstring.
|
print("\n💥 MODELE SANS DOCSTRING :", cls.__name__, " (module:", cls.__module__, ")")
|
||||||
'''
|
raise Exception("MODELE SANS DOCSTRING")
|
||||||
assert cls.__doc__, '%s has no docstring' % cls
|
|
||||||
lines = cls.__doc__.splitlines()
|
lines = cls.__doc__.splitlines()
|
||||||
for line in lines:
|
if lines:
|
||||||
line = line.strip()
|
return lines[0]
|
||||||
if line:
|
return cls.__name__
|
||||||
return line
|
|
||||||
|
# @classmethod
|
||||||
|
# def _get_name(cls):
|
||||||
|
# '''
|
||||||
|
# Returns the first non-empty line of the model docstring.
|
||||||
|
# '''
|
||||||
|
# assert cls.__doc__, '%s has no docstring' % cls
|
||||||
|
# lines = cls.__doc__.splitlines()
|
||||||
|
# for line in lines:
|
||||||
|
# line = line.strip()
|
||||||
|
# if line:
|
||||||
|
# return line
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __register__(cls, module_name):
|
def __register__(cls, module_name):
|
||||||
|
|||||||
10
modules/AGENTS.md
Normal file
10
modules/AGENTS.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# AGENTS.md - Regles globales des modules
|
||||||
|
|
||||||
|
Ce guide complete le `AGENTS.md` racine et s'applique a tous les modules sous `modules/`.
|
||||||
|
|
||||||
|
## Regles transversales
|
||||||
|
|
||||||
|
- Ne pas introduire de `fields.DateTime` dans les modules metier sauf demande explicite et code cible deja base sur ce type.
|
||||||
|
- Pour afficher une date issue d'un champ technique comme `create_date`, preferer un champ fonctionnel en `fields.Date` plutot qu'un acces direct en vue.
|
||||||
|
- Si une vue doit afficher une information technique non declaree explicitement sur le modele, ajouter un champ fonctionnel dedie cote Python avant de modifier le XML.
|
||||||
|
|
||||||
@@ -117,6 +117,7 @@ class Move(DescriptionOriginMixin, ModelSQL, ModelView):
|
|||||||
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
|
date = fields.Date('Effective Date', required=True, states=_MOVE_STATES)
|
||||||
post_date = fields.Date('Post Date', readonly=True)
|
post_date = fields.Date('Post Date', readonly=True)
|
||||||
description = fields.Char('Description', states=_MOVE_STATES)
|
description = fields.Char('Description', states=_MOVE_STATES)
|
||||||
|
ext_ref = fields.Char('Ext. Ref')
|
||||||
origin = fields.Reference('Origin', selection='get_origin',
|
origin = fields.Reference('Origin', selection='get_origin',
|
||||||
states=_MOVE_STATES)
|
states=_MOVE_STATES)
|
||||||
state = fields.Selection([
|
state = fields.Selection([
|
||||||
@@ -921,6 +922,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView):
|
|||||||
fields.Reference("Move Origin", selection='get_move_origin'),
|
fields.Reference("Move Origin", selection='get_move_origin'),
|
||||||
'get_move_field', searcher='search_move_field')
|
'get_move_field', searcher='search_move_field')
|
||||||
description = fields.Char('Description', states=_states)
|
description = fields.Char('Description', states=_states)
|
||||||
|
ext_ref = fields.Char('Ext. Ref')
|
||||||
move_description_used = fields.Function(
|
move_description_used = fields.Function(
|
||||||
fields.Char("Move Description", states=_states),
|
fields.Char("Move Description", states=_states),
|
||||||
'get_move_field',
|
'get_move_field',
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="origin" colspan="3"/>
|
<field name="origin" colspan="3"/>
|
||||||
<label name="description_used"/>
|
<label name="description_used"/>
|
||||||
<field name="description_used" colspan="3"/>
|
<field name="description_used" colspan="3"/>
|
||||||
|
<label name="ext_ref"/>
|
||||||
|
<field name="ext_ref" colspan="3"/>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page name="lines">
|
<page name="lines">
|
||||||
<field name="lines" colspan="4"
|
<field name="lines" colspan="4"
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="origin"/>
|
<field name="origin"/>
|
||||||
<label name="description_used"/>
|
<label name="description_used"/>
|
||||||
<field name="description_used" colspan="3"/>
|
<field name="description_used" colspan="3"/>
|
||||||
|
<label name="ext_ref"/>
|
||||||
|
<field name="ext_ref" colspan="3"/>
|
||||||
<notebook colspan="4">
|
<notebook colspan="4">
|
||||||
<page string="Other Info" id="info">
|
<page string="Other Info" id="info">
|
||||||
<label name="date"/>
|
<label name="date"/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<tree editable="1">
|
<tree editable="1">
|
||||||
<field name="move"/>
|
<field name="move"/>
|
||||||
<field name="account" expand="1"/>
|
<field name="account" expand="1"/>
|
||||||
|
<field name="ext_ref" expand="1" optional="1"/>
|
||||||
<field name="party" expand="1"/>
|
<field name="party" expand="1"/>
|
||||||
<field name="debit" sum="1"/>
|
<field name="debit" sum="1"/>
|
||||||
<field name="credit" sum="1"/>
|
<field name="credit" sum="1"/>
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
})
|
})
|
||||||
cls.__rpc__.update({
|
cls.__rpc__.update({
|
||||||
'post': RPC(
|
'post': RPC(
|
||||||
readonly=False, instantiate=0, fresh_session=True),
|
readonly=False, instantiate=0, fresh_session=False),
|
||||||
})
|
})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1286,9 +1286,14 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
remainder = sum(l.debit - l.credit for l in move_lines)
|
remainder = sum(l.debit - l.credit for l in move_lines)
|
||||||
if self.payment_term:
|
if self.payment_term:
|
||||||
payment_date = self.payment_term_date or self.invoice_date or today
|
payment_date = self.payment_term_date or self.invoice_date or today
|
||||||
purchase_line = int(str(self.lines[0].origin).split(",")[1]) if self.lines[0].origin else None
|
model = str(self.lines[0].origin).split(",")[0] if self.lines[0].origin else None
|
||||||
term_lines = self.payment_term.compute(
|
logger.info("MODEL:%s",model)
|
||||||
self.total_amount, self.currency, payment_date, purchase_line)
|
if model:
|
||||||
|
Line = Pool().get(model)
|
||||||
|
line = Line(int(str(self.lines[0].origin).split(",")[1]))
|
||||||
|
logger.info("LINE:%s",line)
|
||||||
|
term_lines = self.payment_term.compute(
|
||||||
|
self.total_amount, self.currency, payment_date, line)
|
||||||
else:
|
else:
|
||||||
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
term_lines = [(self.payment_term_date or today, self.total_amount)]
|
||||||
past_payment_term_dates = []
|
past_payment_term_dates = []
|
||||||
@@ -1885,17 +1890,15 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
cls._check_taxes(invoices)
|
cls._check_taxes(invoices)
|
||||||
# cls._check_similar(invoices)
|
# cls._check_similar(invoices)
|
||||||
|
|
||||||
invoices_in = cls.browse([i for i in invoices if i.type == 'in'])
|
cls.set_number(invoices)
|
||||||
cls.set_number(invoices_in)
|
|
||||||
cls._store_cache(invoices)
|
cls._store_cache(invoices)
|
||||||
|
|
||||||
moves = []
|
moves = []
|
||||||
for invoice in invoices:
|
for invoice in invoices:
|
||||||
if invoice.type == 'in':
|
move = invoice.get_move()
|
||||||
move = invoice.get_move()
|
if move != invoice.move:
|
||||||
if move != invoice.move:
|
invoice.move = move
|
||||||
invoice.move = move
|
moves.append(move)
|
||||||
moves.append(move)
|
|
||||||
invoice.do_lot_invoicing()
|
invoice.do_lot_invoicing()
|
||||||
if moves:
|
if moves:
|
||||||
Move.save(moves)
|
Move.save(moves)
|
||||||
@@ -1960,14 +1963,16 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
if amount < 0:
|
if amount < 0:
|
||||||
move_line.debit = Decimal(0)
|
move_line.debit = Decimal(0)
|
||||||
move_line.credit = -amount
|
move_line.credit = -amount
|
||||||
move_line.account = gl.product.account_stock_used
|
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
|
||||||
|
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
|
||||||
move_line_.credit = Decimal(0)
|
move_line_.credit = Decimal(0)
|
||||||
move_line_.debit = -amount
|
move_line_.debit = -amount
|
||||||
move_line_.account = gl.product.account_stock_in_used
|
move_line_.account = gl.product.account_stock_in_used
|
||||||
else:
|
else:
|
||||||
move_line.debit = amount
|
move_line.debit = amount
|
||||||
move_line.credit = Decimal(0)
|
move_line.credit = Decimal(0)
|
||||||
move_line.account = gl.product.account_stock_used
|
move_line.account = gl.product.account_stock_used if not (move_line.lot.sale_invoice_line or move_line.lot.sale_invoice_line_prov) else gl.product.account_stock_out_used
|
||||||
|
move_line.account = gl.product.account_cogs_used if gl.fee else move_line.account
|
||||||
move_line_.debit = Decimal(0)
|
move_line_.debit = Decimal(0)
|
||||||
move_line_.credit = amount
|
move_line_.credit = amount
|
||||||
move_line_.account = gl.product.account_stock_in_used
|
move_line_.account = gl.product.account_stock_in_used
|
||||||
@@ -2031,7 +2036,11 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
|
|||||||
var_qt = sum([i.quantity for i in gl])
|
var_qt = sum([i.quantity for i in gl])
|
||||||
logger.info("LOT_TO_PROCESS:%s",lot)
|
logger.info("LOT_TO_PROCESS:%s",lot)
|
||||||
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
|
logger.info("FEE_TO_PROCESS:%s",gl[0].fee)
|
||||||
if lot:
|
if (gl[0].fee and not gl[0].product.landed_cost):
|
||||||
|
diff = gl[0].fee.amount - gl[0].fee.get_non_cog(lot)
|
||||||
|
account_move = gl[0].fee._get_account_move_fee(lot,'in',diff)
|
||||||
|
Move.save([account_move])
|
||||||
|
if (lot and not gl[0].fee) or (gl[0].fee and gl[0].product.landed_cost):
|
||||||
adjust_move_lines = []
|
adjust_move_lines = []
|
||||||
mov = None
|
mov = None
|
||||||
if self.type == 'in':
|
if self.type == 'in':
|
||||||
@@ -3684,13 +3693,19 @@ class InvoiceReport(Report):
|
|||||||
Invoice = pool.get('account.invoice')
|
Invoice = pool.get('account.invoice')
|
||||||
# Re-instantiate because records are TranslateModel
|
# Re-instantiate because records are TranslateModel
|
||||||
invoice, = Invoice.browse(records)
|
invoice, = Invoice.browse(records)
|
||||||
if invoice.invoice_report_cache:
|
report_path = cls._get_action_report_path(action)
|
||||||
|
use_cache = (
|
||||||
|
report_path in (None, 'account_invoice/invoice.fodt')
|
||||||
|
and invoice.invoice_report_cache
|
||||||
|
)
|
||||||
|
if use_cache:
|
||||||
return (
|
return (
|
||||||
invoice.invoice_report_format,
|
invoice.invoice_report_format,
|
||||||
invoice.invoice_report_cache)
|
invoice.invoice_report_cache)
|
||||||
else:
|
else:
|
||||||
result = super()._execute(records, header, data, action)
|
result = super()._execute(records, header, data, action)
|
||||||
if invoice.invoice_report_versioned:
|
if (invoice.invoice_report_versioned
|
||||||
|
and report_path in (None, 'account_invoice/invoice.fodt')):
|
||||||
format_, data = result
|
format_, data = result
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = bytes(data, 'utf-8')
|
data = bytes(data, 'utf-8')
|
||||||
@@ -3707,6 +3722,12 @@ class InvoiceReport(Report):
|
|||||||
with Transaction().set_context(language=False):
|
with Transaction().set_context(language=False):
|
||||||
return super().render(*args, **kwargs)
|
return super().render(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_action_report_path(action):
|
||||||
|
if isinstance(action, dict):
|
||||||
|
return action.get('report')
|
||||||
|
return getattr(action, 'report', None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, ids, data):
|
def execute(cls, ids, data):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
|
|||||||
@@ -264,11 +264,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
|
<field name="wiz_name">account.invoice.refresh_invoice_report</field>
|
||||||
<field name="model">account.invoice</field>
|
<field name="model">account.invoice</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.action.keyword" id="refresh_invoice_report_keyword">
|
|
||||||
<field name="keyword">form_print</field>
|
|
||||||
<field name="model">account.invoice,-1</field>
|
|
||||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
|
||||||
</record>
|
|
||||||
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
|
<record model="ir.action-res.group" id="refresh_invoice_report-group_account_admin">
|
||||||
<field name="action" ref="refresh_invoice_report_wizard"/>
|
<field name="action" ref="refresh_invoice_report_wizard"/>
|
||||||
<field name="group" ref="account.group_account_admin"/>
|
<field name="group" ref="account.group_account_admin"/>
|
||||||
@@ -318,6 +313,19 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="action" ref="report_prepayment"/>
|
<field name="action" ref="report_prepayment"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.action.report" id="report_invoice_ict_final">
|
||||||
|
<field name="name">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">
|
<record model="ir.sequence.type" id="sequence_type_account_invoice">
|
||||||
<field name="name">Invoice</field>
|
<field name="name">Invoice</field>
|
||||||
</record>
|
</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
1499
modules/account_invoice/packing_list.fodt
Normal file
1499
modules/account_invoice/packing_list.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.pyson import Eval
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.wizard import Button, StateView, Wizard
|
from trytond.wizard import Button, StateView, Wizard
|
||||||
|
|
||||||
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
|
from .exceptions import PaymentTermComputeError, PaymentTermValidationError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
||||||
@@ -46,7 +48,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
|||||||
'.msg_payment_term_missing_last_remainder',
|
'.msg_payment_term_missing_last_remainder',
|
||||||
payment_term=term.rec_name))
|
payment_term=term.rec_name))
|
||||||
|
|
||||||
def compute(self, amount, currency, date, purchase_line = None):
|
def compute(self, amount, currency, date, line_ = None):
|
||||||
"""Calculate payment terms and return a list of tuples
|
"""Calculate payment terms and return a list of tuples
|
||||||
with (date, amount) for each payment term line.
|
with (date, amount) for each payment term line.
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ class PaymentTerm(DeactivableMixin, ModelSQL, ModelView):
|
|||||||
remainder = amount
|
remainder = amount
|
||||||
for line in self.lines:
|
for line in self.lines:
|
||||||
value = line.get_value(remainder, amount, currency)
|
value = line.get_value(remainder, amount, currency)
|
||||||
value_date = line.get_date(date, purchase_line)
|
value_date = line.get_date(date, line_)
|
||||||
if value is None or not value_date:
|
if value is None or not value_date:
|
||||||
continue
|
continue
|
||||||
if ((remainder - value) * sign) < Decimal(0):
|
if ((remainder - value) * sign) < Decimal(0):
|
||||||
@@ -155,12 +157,11 @@ class PaymentTermLine(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
self.ratio = self.round(1 / self.divisor,
|
self.ratio = self.round(1 / self.divisor,
|
||||||
self.__class__.ratio.digits[1])
|
self.__class__.ratio.digits[1])
|
||||||
|
|
||||||
def get_date(self, date, purchase_line = None):
|
def get_date(self, date, line = None):
|
||||||
#find date based on trigger:
|
#find date based on trigger:
|
||||||
if purchase_line and self.trigger_event:
|
if line and self.trigger_event:
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
trigger_date = line.get_date(self.trigger_event)
|
||||||
purchase_line = PurchaseLine(purchase_line)
|
logger.info("DATE_FROM_LINE:%s",trigger_date)
|
||||||
trigger_date = purchase_line.get_date(self.trigger_event)
|
|
||||||
if trigger_date:
|
if trigger_date:
|
||||||
date = trigger_date
|
date = trigger_date
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from trytond.modules.account_invoice.exceptions import (
|
from trytond.modules.account_invoice.exceptions import (
|
||||||
PaymentTermValidationError)
|
PaymentTermValidationError)
|
||||||
@@ -251,5 +252,70 @@ class AccountInvoiceTestCase(
|
|||||||
(datetime.date(2012, 1, 14), Decimal('-1.0')),
|
(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
|
del ModuleTestCase
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="invoice_report_revisions" colspan="4"/>
|
<field name="invoice_report_revisions" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Rate management" id="rate">
|
<page string="Rate management" id="rate">
|
||||||
<label name="warning"/>
|
<!-- <label name="warning"/>
|
||||||
<field name="warning"/>
|
<field name="warning"/> -->
|
||||||
<newline/>
|
<newline/>
|
||||||
<label name="rate"/>
|
<label name="rate"/>
|
||||||
<field name="rate"/>
|
<field name="rate"/>
|
||||||
|
|||||||
14
modules/account_itsa/__init__.py
Normal file
14
modules/account_itsa/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||||
|
# this repository contains the full copyright notices and license terms.
|
||||||
|
|
||||||
|
from trytond.pool import Pool
|
||||||
|
|
||||||
|
from . import account
|
||||||
|
|
||||||
|
def register():
|
||||||
|
Pool.register(
|
||||||
|
account.AccountTemplate,
|
||||||
|
module='account_itsa', type_='model')
|
||||||
|
Pool.register(
|
||||||
|
account.CreateChart,
|
||||||
|
module='account_itsa', type_='wizard')
|
||||||
40
modules/account_itsa/account.py
Normal file
40
modules/account_itsa/account.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||||
|
# this repository contains the full copyright notices and license terms.
|
||||||
|
import csv
|
||||||
|
from io import BytesIO, TextIOWrapper
|
||||||
|
|
||||||
|
from sql import Table
|
||||||
|
from sql.aggregate import Sum
|
||||||
|
from sql.conditionals import Coalesce
|
||||||
|
|
||||||
|
from trytond.config import config
|
||||||
|
from trytond.model import ModelStorage, ModelView, fields
|
||||||
|
from trytond.pool import Pool, PoolMeta
|
||||||
|
from trytond.pyson import Eval
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||||
|
|
||||||
|
class AccountTemplate(metaclass=PoolMeta):
|
||||||
|
__name__ = 'account.account.template'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __register__(cls, module_name):
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
model_data = Table('ir_model_data')
|
||||||
|
super().__register__(module_name)
|
||||||
|
|
||||||
|
class CreateChart(metaclass=PoolMeta):
|
||||||
|
__name__ = 'account.create_chart'
|
||||||
|
|
||||||
|
def default_properties(self, fields):
|
||||||
|
pool = Pool()
|
||||||
|
ModelData = pool.get('ir.model.data')
|
||||||
|
defaults = super().default_properties(fields)
|
||||||
|
# template_id = ModelData.get_id('account_ch.root')
|
||||||
|
# if self.account.account_template.id == template_id:
|
||||||
|
# defaults['account_receivable'] = self.get_account(
|
||||||
|
# 'account_ch.3400')
|
||||||
|
# defaults['account_payable'] = self.get_account(
|
||||||
|
# 'account_ch.6040')
|
||||||
|
return defaults
|
||||||
|
|
||||||
3336
modules/account_itsa/account_itsa.xml
Normal file
3336
modules/account_itsa/account_itsa.xml
Normal file
File diff suppressed because it is too large
Load Diff
10
modules/account_itsa/tryton.cfg
Normal file
10
modules/account_itsa/tryton.cfg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[tryton]
|
||||||
|
version=7.2.3
|
||||||
|
depends:
|
||||||
|
account
|
||||||
|
extras_depend:
|
||||||
|
account_invoice
|
||||||
|
xml:
|
||||||
|
account_itsa.xml
|
||||||
|
#tax_ict.xml
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from decimal import Decimal
|
|||||||
from trytond.i18n import gettext
|
from trytond.i18n import gettext
|
||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
|
from trytond.exceptions import UserWarning, UserError
|
||||||
from .exceptions import COGSWarning
|
from .exceptions import COGSWarning
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ class InvoiceLine(metaclass=PoolMeta):
|
|||||||
if move_line.second_currency:
|
if move_line.second_currency:
|
||||||
move_line.amount_second_currency = amount
|
move_line.amount_second_currency = amount
|
||||||
else:
|
else:
|
||||||
move_line.debit = Decimal(0)
|
move_line.debit = -amount_converted
|
||||||
move_line.credit = -amount_converted
|
move_line.credit = Decimal(0)
|
||||||
move_line.account = self.product.account_stock_out_used
|
move_line.account = self.product.account_stock_out_used
|
||||||
if move_line.second_currency:
|
if move_line.second_currency:
|
||||||
move_line.amount_second_currency = amount
|
move_line.amount_second_currency = amount
|
||||||
@@ -171,10 +171,28 @@ class InvoiceLine(metaclass=PoolMeta):
|
|||||||
cost = self.amount
|
cost = self.amount
|
||||||
else:
|
else:
|
||||||
cost = self.lot.get_cog()
|
cost = self.lot.get_cog()
|
||||||
|
if not cost or cost == 0:
|
||||||
|
raise UserError('No COG for this invoice, please generate the reception of the goods')
|
||||||
|
if self.amount < 0 :
|
||||||
|
cost *= -1
|
||||||
logger.info("GETMOVELINES_COST:%s",cost)
|
logger.info("GETMOVELINES_COST:%s",cost)
|
||||||
|
anglo_saxon_move_lines_ = []
|
||||||
with Transaction().set_context(
|
with Transaction().set_context(
|
||||||
company=self.invoice.company.id, date=accounting_date):
|
company=self.invoice.company.id, date=accounting_date):
|
||||||
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
|
anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(
|
||||||
cost, type_)
|
cost, type_)
|
||||||
|
if type_ == 'in_supplier' and (self.lot.sale_invoice_line_prov or self.lot.sale_invoice_line) and not self.fee:
|
||||||
|
anglo_saxon_move_lines_ = self._get_anglo_saxon_move_lines(cost, 'out_customer')
|
||||||
result.extend(anglo_saxon_move_lines)
|
result.extend(anglo_saxon_move_lines)
|
||||||
|
result.extend(anglo_saxon_move_lines_)
|
||||||
|
#Fee inventoried delivery management
|
||||||
|
if self.lot and type_ != 'in_supplier':
|
||||||
|
FeeLots = Pool().get('fee.lots')
|
||||||
|
fees = FeeLots.search(['lot','=',self.lot.id])
|
||||||
|
for fl in fees:
|
||||||
|
if fl.fee.type == 'ordered' and fl.fee.product.template.landed_cost:
|
||||||
|
AccountMove = Pool().get('account.move')
|
||||||
|
account_move = fl.fee._get_account_move_fee(fl.lot,'out')
|
||||||
|
AccountMove.save([account_move])
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ account_names = [
|
|||||||
class Category(metaclass=PoolMeta):
|
class Category(metaclass=PoolMeta):
|
||||||
__name__ = 'product.category'
|
__name__ = 'product.category'
|
||||||
account_stock = fields.MultiValue(fields.Many2One(
|
account_stock = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock",
|
'account.account', "Account Stock/Cost Income",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
# ('type.stock', '=', True),
|
||||||
('type.statement', '=', 'balance'),
|
# ('type.statement', '=', 'balance'),
|
||||||
('company', '=', Eval('context', {}).get('company', -1)),
|
('company', '=', Eval('context', {}).get('company', -1)),
|
||||||
],
|
],
|
||||||
states={
|
states={
|
||||||
@@ -29,7 +29,7 @@ class Category(metaclass=PoolMeta):
|
|||||||
| ~Eval('accounting', False)),
|
| ~Eval('accounting', False)),
|
||||||
}))
|
}))
|
||||||
account_stock_in = fields.MultiValue(fields.Many2One(
|
account_stock_in = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock IN",
|
'account.account', "Account Stock IN/Cost liability",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
('type.stock', '=', True),
|
||||||
@@ -41,7 +41,7 @@ class Category(metaclass=PoolMeta):
|
|||||||
| ~Eval('accounting', False)),
|
| ~Eval('accounting', False)),
|
||||||
}))
|
}))
|
||||||
account_stock_out = fields.MultiValue(fields.Many2One(
|
account_stock_out = fields.MultiValue(fields.Many2One(
|
||||||
'account.account', "Account Stock OUT",
|
'account.account', "Account Stock OUT/Cost liability",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
('type.stock', '=', True),
|
||||||
@@ -103,8 +103,8 @@ class CategoryAccount(metaclass=PoolMeta):
|
|||||||
'account.account', "Account Stock",
|
'account.account', "Account Stock",
|
||||||
domain=[
|
domain=[
|
||||||
('closed', '!=', True),
|
('closed', '!=', True),
|
||||||
('type.stock', '=', True),
|
# ('type.stock', '=', True),
|
||||||
('type.statement', '=', 'balance'),
|
# ('type.statement', '=', 'balance'),
|
||||||
('company', '=', Eval('company', -1)),
|
('company', '=', Eval('company', -1)),
|
||||||
])
|
])
|
||||||
account_stock_in = fields.Many2One(
|
account_stock_in = fields.Many2One(
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
from . import automation,rules #, document
|
from . import automation,rules,freight_booking,cron #, document
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
automation.AutomationDocument,
|
automation.AutomationDocument,
|
||||||
rules.AutomationRuleSet,
|
rules.AutomationRuleSet,
|
||||||
|
freight_booking.FreightBookingInfo,
|
||||||
|
cron.Cron,
|
||||||
|
cron.AutomationCron,
|
||||||
module='automation', type_='model')
|
module='automation', type_='model')
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
from trytond.model import ModelSQL, ModelView, fields, Workflow
|
from trytond.model import ModelSQL, ModelView, fields, Workflow
|
||||||
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.wizard import Button
|
from trytond.wizard import Button
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
from sql import Table
|
||||||
|
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||||
import requests
|
import requests
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,6 +22,7 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
('invoice', 'Invoice'),
|
('invoice', 'Invoice'),
|
||||||
('statement_of_facts', 'Statement of Facts'),
|
('statement_of_facts', 'Statement of Facts'),
|
||||||
('weight_report', 'Weight Report'),
|
('weight_report', 'Weight Report'),
|
||||||
|
('controller', 'Controller'),
|
||||||
('bol', 'Bill of Lading'),
|
('bol', 'Bill of Lading'),
|
||||||
('controller_invoice', 'Controller Invoice'),
|
('controller_invoice', 'Controller Invoice'),
|
||||||
], 'Type')
|
], 'Type')
|
||||||
@@ -57,25 +63,53 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
def run_ocr(cls, docs):
|
def run_ocr(cls, docs):
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
try:
|
try:
|
||||||
# Décoder le fichier depuis le champ Binary
|
if doc.type == 'weight_report':
|
||||||
file_data = doc.document.data or b""
|
# Décoder le fichier depuis le champ Binary
|
||||||
logger.info(f"File size: {len(file_data)} bytes")
|
file_data = doc.document.data or b""
|
||||||
logger.info(f"First 20 bytes: {file_data[:20]}")
|
logger.info(f"File size: {len(file_data)} bytes")
|
||||||
logger.info(f"Last 20 bytes: {file_data[-20:]}")
|
logger.info(f"First 20 bytes: {file_data[:20]}")
|
||||||
|
logger.info(f"Last 20 bytes: {file_data[-20:]}")
|
||||||
|
|
||||||
file_name = doc.document.name or "document"
|
file_name = doc.document.name or "document"
|
||||||
|
|
||||||
# Envoyer le fichier au service OCR
|
# Envoyer le fichier au service OCR
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"http://automation-service:8006/ocr",
|
"http://automation-service:8006/ocr",
|
||||||
files={"file": (file_name, io.BytesIO(file_data))}
|
files={"file": (file_name, io.BytesIO(file_data))}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
logger.info("RUN_OCR_RESPONSE:%s",data)
|
logger.info("RUN_OCR_RESPONSE:%s",data)
|
||||||
doc.ocr_text = data.get("ocr_text", "")
|
doc.ocr_text = data.get("ocr_text", "")
|
||||||
doc.state = "ocr_done"
|
doc.state = "ocr_done"
|
||||||
doc.notes = (doc.notes or "") + "OCR done\n"
|
doc.notes = (doc.notes or "") + "OCR done\n"
|
||||||
|
else:
|
||||||
|
doc.ocr_text = (doc.document.data or b"").decode('utf-8', errors='replace')
|
||||||
|
match = re.search(r"\bID\s*:\s*(\d+)", doc.ocr_text)
|
||||||
|
if match:
|
||||||
|
request_id = match.group(1)
|
||||||
|
match = re.search(r"\bBL\s*number\s*:\s*([A-Za-z0-9_-]+)", doc.ocr_text, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
bl_number = match.group(1)
|
||||||
|
ShipmentIn = Pool().get('stock.shipment.in')
|
||||||
|
sh = ShipmentIn.search(['bl_number','=',bl_number])
|
||||||
|
if sh:
|
||||||
|
sh[0].returned_id = request_id
|
||||||
|
ShipmentIn.save(sh)
|
||||||
|
doc.notes = (doc.notes or "") + "Id returned: " + request_id
|
||||||
|
|
||||||
|
so_payload = {
|
||||||
|
"ServiceOrderKey": sh[0].service_order_key,
|
||||||
|
"ID_Number": request_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
"http://automation-service:8006/service-order-update",
|
||||||
|
json=so_payload,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
doc.notes = (doc.notes or "") + " SO updated"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
doc.state = "error"
|
doc.state = "error"
|
||||||
@@ -154,7 +188,8 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
|
logger.info("Sending OCR text to metadata API: %s", doc.ocr_text)
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"http://automation-service:8006/metadata",
|
#"http://automation-service:8006/metadata",
|
||||||
|
"http://automation-service:8006/parse",
|
||||||
json={"text": doc.ocr_text or ""}
|
json={"text": doc.ocr_text or ""}
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
@@ -176,6 +211,18 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
logger.error("Metadata processing error: %s", e)
|
logger.error("Metadata processing error: %s", e)
|
||||||
|
|
||||||
doc.save()
|
doc.save()
|
||||||
|
|
||||||
|
def create_weight_report(self,wr_payload):
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
"http://automation-service:8006/weight-report",
|
||||||
|
json=wr_payload, # 👈 ICI la correction
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
# FULL PIPELINE
|
# FULL PIPELINE
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
@@ -184,18 +231,47 @@ class AutomationDocument(ModelSQL, ModelView, Workflow):
|
|||||||
def run_pipeline(cls, docs):
|
def run_pipeline(cls, docs):
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
try:
|
try:
|
||||||
if cls.rule_set.ocr_required:
|
logger.info("DATA_TYPE:%s",type(doc.metadata_json))
|
||||||
cls.run_ocr([doc])
|
metadata = json.loads(str(doc.metadata_json))
|
||||||
if cls.rule_set.structure_required and doc.state != "error":
|
logger.info("JSON STRUCTURE:%s",metadata)
|
||||||
cls.run_structure([doc])
|
|
||||||
if cls.rule_set.table_required and doc.state != "error":
|
WeightReport = Pool().get('weight.report')
|
||||||
cls.run_tables([doc])
|
wr = WeightReport.create_from_json(metadata)
|
||||||
if cls.rule_set.metadata_required and doc.state != "error":
|
|
||||||
cls.run_metadata([doc])
|
ShipmentIn = Pool().get('stock.shipment.in')
|
||||||
if doc.state != "error":
|
ShipmentWR = Pool().get('shipment.wr')
|
||||||
doc.state = "validated"
|
sh = ShipmentIn.search([('bl_number','ilike',wr.bl_no)])
|
||||||
doc.notes = (doc.notes or "") + "Pipeline completed\n"
|
if sh:
|
||||||
|
swr = ShipmentWR()
|
||||||
|
swr.shipment_in = sh[0]
|
||||||
|
swr.wr = wr
|
||||||
|
ShipmentWR.save([swr])
|
||||||
|
doc.notes = (doc.notes or "") + f"Shipment found: {sh[0].number}\n"
|
||||||
|
logger.info("BL_NUMBER:%s",sh[0].bl_number)
|
||||||
|
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:
|
except Exception as e:
|
||||||
|
logger.exception("PIPELINE FAILED") # 👈 TRACE COMPLETE
|
||||||
doc.state = "error"
|
doc.state = "error"
|
||||||
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
doc.notes = (doc.notes or "") + f"Pipeline error: {e}\n"
|
||||||
doc.save()
|
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">
|
<record model="ir.model.button" id="auto_button1">
|
||||||
<field name="model">automation.document</field>
|
<field name="model">automation.document</field>
|
||||||
<field name="name">run_pipeline</field>
|
<field name="name">run_pipeline</field>
|
||||||
<field name="string">Run Full Pipeline</field>
|
<field name="string">Create Weight Report</field>
|
||||||
</record>
|
</record>
|
||||||
<record model="ir.model.button" id="auto_button2">
|
<record model="ir.model.button" id="auto_button2">
|
||||||
<field name="model">automation.document</field>
|
<field name="model">automation.document</field>
|
||||||
|
|||||||
408
modules/automation/cron.py
Normal file
408
modules/automation/cron.py
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
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 a traiter : {len(rows)}")
|
||||||
|
|
||||||
|
# Premiere transaction : creation des objets de reference
|
||||||
|
with Transaction().new_transaction() as trans1:
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
"Debut de la creation des objets de reference...")
|
||||||
|
|
||||||
|
parties_to_save = []
|
||||||
|
vessels_to_save = []
|
||||||
|
locations_to_save = []
|
||||||
|
|
||||||
|
parties_cache = {}
|
||||||
|
vessels_cache = {}
|
||||||
|
locations_cache = {}
|
||||||
|
|
||||||
|
# Collecter les donnees des objets de reference
|
||||||
|
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
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
existing = Party.search(
|
||||||
|
[('name', '=', name_upper)], limit=1)
|
||||||
|
if existing:
|
||||||
|
parties_cache[name_upper] = existing[0]
|
||||||
|
return existing[0]
|
||||||
|
|
||||||
|
new_p = Party()
|
||||||
|
new_p.name = name_upper
|
||||||
|
parties_cache[name_upper] = new_p
|
||||||
|
parties_to_save.append(new_p)
|
||||||
|
return new_p
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
_ = 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')
|
||||||
|
|
||||||
|
if parties_to_save:
|
||||||
|
logger.info(f"Creation de {len(parties_to_save)} parties...")
|
||||||
|
Party.save(parties_to_save)
|
||||||
|
|
||||||
|
if vessels_to_save:
|
||||||
|
logger.info(f"Creation de {len(vessels_to_save)} vessels...")
|
||||||
|
Vessel.save(vessels_to_save)
|
||||||
|
|
||||||
|
if locations_to_save:
|
||||||
|
logger.info(
|
||||||
|
f"Creation de {len(locations_to_save)} locations...")
|
||||||
|
Location.save(locations_to_save)
|
||||||
|
|
||||||
|
trans1.commit()
|
||||||
|
logger.info(
|
||||||
|
"Premiere transaction commitee : objets de reference crees")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
trans1.rollback()
|
||||||
|
logger.error(
|
||||||
|
f"Erreur dans la creation des objets de reference : {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Transactions individuelles pour chaque shipment
|
||||||
|
successful_shipments = 0
|
||||||
|
failed_shipments = []
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Transaction().new_transaction() as trans_shipment:
|
||||||
|
logger.info(f"Debut transaction pour SI {si_number}")
|
||||||
|
|
||||||
|
existing_shipment = ShipmentIn.search([
|
||||||
|
('reference', '=', si_number)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if existing_shipment:
|
||||||
|
shipment = existing_shipment[0]
|
||||||
|
if shipment.incoming_moves:
|
||||||
|
logger.info(
|
||||||
|
"Shipment %s existe deja avec lots, ignore",
|
||||||
|
si_number)
|
||||||
|
trans_shipment.commit()
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Shipment %s existe deja sans lots, verification freight_booking_lots",
|
||||||
|
si_number)
|
||||||
|
inv_date, inv_nb = shipment._create_lots_from_fintrade()
|
||||||
|
shipment = ShipmentIn(shipment.id)
|
||||||
|
if shipment.incoming_moves:
|
||||||
|
shipment.controller = shipment.get_controller()
|
||||||
|
shipment.controller_target = controller
|
||||||
|
if not shipment.fees:
|
||||||
|
shipment.create_fee(shipment.controller)
|
||||||
|
shipment.instructions = shipment.get_instructions_html(
|
||||||
|
inv_date, inv_nb)
|
||||||
|
ShipmentIn.save([shipment])
|
||||||
|
logger.info(
|
||||||
|
"Shipment %s mis a jour avec %s incoming move(s)",
|
||||||
|
si_number, len(shipment.incoming_moves))
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Shipment %s existe sans lots et aucun lot disponible pour l'instant",
|
||||||
|
si_number)
|
||||||
|
trans_shipment.commit()
|
||||||
|
continue
|
||||||
|
|
||||||
|
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 trouve pour {si_number}: {carrier.name}")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Carrier NON TROUVE 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]
|
||||||
|
|
||||||
|
if not carrier:
|
||||||
|
error_msg = (
|
||||||
|
f"ERREUR CRITIQUE: Carrier manquant pour SI {si_number} "
|
||||||
|
f"(valeur: '{carrier_name}')")
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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} cree avec succes")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
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}")
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info("RESUME DE L'EXECUTION")
|
||||||
|
logger.info("=" * 60)
|
||||||
|
logger.info(f"Total de shipments a traiter : {len(rows2)}")
|
||||||
|
logger.info(f"Shipments crees avec succes : {successful_shipments}")
|
||||||
|
logger.info(f"Shipments en echec : {len(failed_shipments)}")
|
||||||
|
|
||||||
|
if failed_shipments:
|
||||||
|
logger.info("\nDetail des echecs :")
|
||||||
|
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']}")
|
||||||
|
|
||||||
|
logger.info("\nAnalyse des carriers problematiques :")
|
||||||
|
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} echec(s)")
|
||||||
|
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(" -> N'EXISTE PAS DANS LA BASE")
|
||||||
|
|
||||||
|
logger.info("=" * 60)
|
||||||
37
modules/automation/cron.xml
Normal file
37
modules/automation/cron.xml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<tryton>
|
||||||
|
<data>
|
||||||
|
<record model="ir.ui.view" id="cron_view_list">
|
||||||
|
<field name="model">automation.cron</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">cron_list</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="cron_view_form">
|
||||||
|
<field name="model">automation.cron</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="name">cron_form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.action.act_window" id="act_cron_form">
|
||||||
|
<field name="name">Update shipment from freight booking</field>
|
||||||
|
<field name="res_model">automation.cron</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.act_window.view" id="act_cron_form_view1">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view" ref="cron_view_list"/>
|
||||||
|
<field name="act_window" ref="act_cron_form"/>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.act_window.view" id="act_cron_form_view2">
|
||||||
|
<field name="sequence" eval="20"/>
|
||||||
|
<field name="view" ref="cron_view_form"/>
|
||||||
|
<field name="act_window" ref="act_cron_form"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.cron" id="cron_cron">
|
||||||
|
<field name="method">automation.cron|update_shipment</field>
|
||||||
|
<field name="interval_number" eval="1"/>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
72
modules/automation/execution_automation.py
Normal file
72
modules/automation/execution_automation.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from trytond.model import ModelSQL, fields
|
||||||
|
|
||||||
|
class ExecutionFollowUp(ModelSQL):
|
||||||
|
"Execution Follow Up"
|
||||||
|
__name__ = 'execution.automation'
|
||||||
|
|
||||||
|
port_of_loading = fields.Char("Port of Loading")
|
||||||
|
fb_loading = fields.Char("FB Loading")
|
||||||
|
warehouse = fields.Char("Warehouse")
|
||||||
|
origin = fields.Char("Origin")
|
||||||
|
agent = fields.Char("Agent")
|
||||||
|
operator = fields.Char("Operator")
|
||||||
|
|
||||||
|
fintrade_lc_nb = fields.Char("Fintrade LC Nb")
|
||||||
|
lc_number = fields.Char("LC Number")
|
||||||
|
si = fields.Char("SI")
|
||||||
|
|
||||||
|
port_of_destination = fields.Char("Port of Destination")
|
||||||
|
fb_destination = fields.Char("FB Destination")
|
||||||
|
status = fields.Char("Status")
|
||||||
|
|
||||||
|
etd_date = fields.Date("ETD Date")
|
||||||
|
bl_date = fields.Date("BL Date")
|
||||||
|
etd_sob = fields.Date("ETD SOB")
|
||||||
|
|
||||||
|
sale_contract_no = fields.Char("Sale Contract No")
|
||||||
|
customer = fields.Char("Customer")
|
||||||
|
elt_quantity = fields.Float("Elt Quantity")
|
||||||
|
|
||||||
|
number_of_container = fields.Integer("Containers")
|
||||||
|
vessel = fields.Char("Vessel")
|
||||||
|
shipping_company = fields.Char("Shipping Company")
|
||||||
|
booking_ref = fields.Char("Booking Ref")
|
||||||
|
freight_forwarder = fields.Char("Freight Forwarder")
|
||||||
|
|
||||||
|
instrument_status = fields.Char("Instrument Status")
|
||||||
|
ip_date = fields.Date("IP Date")
|
||||||
|
ip_status = fields.Char("IP Status")
|
||||||
|
latest_shipment_date = fields.Date("Latest Shipment Date")
|
||||||
|
|
||||||
|
countersigned = fields.Boolean("Countersigned")
|
||||||
|
comments = fields.Text("Comments")
|
||||||
|
docs_internal_comments = fields.Text("Docs Internal Comments")
|
||||||
|
|
||||||
|
alloc_quantity = fields.Float("Allocated Quantity")
|
||||||
|
si_comments = fields.Text("SI Comments")
|
||||||
|
is_archived = fields.Boolean("Archived")
|
||||||
|
|
||||||
|
price_cont = fields.Numeric("Price / Cont")
|
||||||
|
price_cont_curr = fields.Char("Currency")
|
||||||
|
|
||||||
|
ct_period_start = fields.Date("CT Start")
|
||||||
|
ct_period_end = fields.Date("CT End")
|
||||||
|
|
||||||
|
lsd_check = fields.Char("LSD Check")
|
||||||
|
bl2lsd_delta = fields.Integer("BL → LSD Delta")
|
||||||
|
|
||||||
|
fintrade_booking = fields.Char("Fintrade Booking")
|
||||||
|
alloc_unit_price = fields.Numeric("Alloc Unit Price")
|
||||||
|
alloc_price_curr = fields.Char("Alloc Price Curr")
|
||||||
|
alloc_price_unit = fields.Char("Alloc Price Unit")
|
||||||
|
|
||||||
|
left_time = fields.Integer("Days Left")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def table_query(cls):
|
||||||
|
return (
|
||||||
|
"SELECT "
|
||||||
|
"row_number() OVER () AS id, "
|
||||||
|
"* "
|
||||||
|
"FROM singa_execution_follow_up"
|
||||||
|
)
|
||||||
9
modules/automation/execution_automation.xml
Normal file
9
modules/automation/execution_automation.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<tryton>
|
||||||
|
<data>
|
||||||
|
<record model="ir.ui.view" id="execution_followup_tree">
|
||||||
|
<field name="model">execution.automation</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">execution_automation_tree</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
56
modules/automation/freight_booking.py
Normal file
56
modules/automation/freight_booking.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from trytond.model import ModelSQL, ModelView, fields
|
||||||
|
from sql import Table
|
||||||
|
from sql.functions import CurrentTimestamp
|
||||||
|
from sql import Column, Literal
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class FreightBookingInfo(ModelSQL, ModelView):
|
||||||
|
"Freight Booking"
|
||||||
|
__name__ = 'freight.booking.info'
|
||||||
|
|
||||||
|
booking_number = fields.Char("Booking Number")
|
||||||
|
agent = fields.Char("Agent")
|
||||||
|
controller = fields.Char("Customer")
|
||||||
|
origin = fields.Char("Origin")
|
||||||
|
destination = fields.Char("Destination")
|
||||||
|
etd = fields.Date("ETD")
|
||||||
|
bl_date = fields.Date("BL date")
|
||||||
|
bl_number = fields.Char("BL Nb")
|
||||||
|
carrier = fields.Char("Carrier")
|
||||||
|
vessel = fields.Char("Vessel")
|
||||||
|
container_count = fields.Float("Containers")
|
||||||
|
quantity = fields.Float("Gross Weight")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def table_query(cls):
|
||||||
|
t = Table('freight_booking_info')
|
||||||
|
|
||||||
|
query = t.select(
|
||||||
|
Literal(None).as_('create_uid'),
|
||||||
|
CurrentTimestamp().as_('create_date'),
|
||||||
|
Literal(None).as_('write_uid'),
|
||||||
|
Literal(None).as_('write_date'),
|
||||||
|
Column(t, 'FintradeBookingKey').as_('id'),
|
||||||
|
Column(t, 'ShippingInstructionNumber').as_('booking_number'),
|
||||||
|
Column(t, 'BookingAgent').as_('agent'),
|
||||||
|
Column(t, 'ExpectedController').as_('controller'),
|
||||||
|
Column(t, 'Loading').as_('origin'),
|
||||||
|
Column(t, 'Destination').as_('destination'),
|
||||||
|
Column(t, 'ETD_Date').as_('etd'),
|
||||||
|
Column(t, 'BL_Date').as_('bl_date'),
|
||||||
|
Column(t, 'BL_Number').as_('bl_number'),
|
||||||
|
Column(t, 'Carrier').as_('carrier'),
|
||||||
|
Column(t, 'Vessel').as_('vessel'),
|
||||||
|
Column(t, 'NumberOfContainers').as_('container_count'),
|
||||||
|
Column(t, 'ShippingInstructionQuantity').as_('quantity'),
|
||||||
|
)
|
||||||
|
#logger.info("*****QUERY*****:%s",query)
|
||||||
|
return query
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __setup__(cls):
|
||||||
|
super().__setup__()
|
||||||
|
cls._order = [
|
||||||
|
('etd', 'DESC'),
|
||||||
|
]
|
||||||
25
modules/automation/freight_booking.xml
Normal file
25
modules/automation/freight_booking.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<tryton>
|
||||||
|
<data>
|
||||||
|
<record model="ir.ui.view" id="freight_booking_info_tree">
|
||||||
|
<field name="model">freight.booking.info</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">freight_booking_info_tree</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.act_window" id="act_freight_booking_info">
|
||||||
|
<field name="name">Freight Bookings</field>
|
||||||
|
<field name="res_model">freight.booking.info</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.act_window.view" id="act_freight_booking_info_view1">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view" ref="freight_booking_info_tree"/>
|
||||||
|
<field name="act_window" ref="act_freight_booking_info"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
name="Freight Booking"
|
||||||
|
action="act_freight_booking_info"
|
||||||
|
parent="menu_automation"
|
||||||
|
sequence="10"
|
||||||
|
id="menu_freight_booking" />
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
@@ -5,4 +5,6 @@ depends:
|
|||||||
res
|
res
|
||||||
document_incoming
|
document_incoming
|
||||||
xml:
|
xml:
|
||||||
automation.xml
|
automation.xml
|
||||||
|
freight_booking.xml
|
||||||
|
cron.xml
|
||||||
60
modules/automation/view/execution_automation_tree.xml
Normal file
60
modules/automation/view/execution_automation_tree.xml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<tree>
|
||||||
|
<field name="port_of_loading"/>
|
||||||
|
<field name="fb_loading"/>
|
||||||
|
<field name="warehouse"/>
|
||||||
|
<field name="origin"/>
|
||||||
|
<field name="agent"/>
|
||||||
|
<field name="operator"/>
|
||||||
|
|
||||||
|
<field name="fintrade_lc_nb"/>
|
||||||
|
<field name="lc_number"/>
|
||||||
|
<field name="si"/>
|
||||||
|
|
||||||
|
<field name="port_of_destination"/>
|
||||||
|
<field name="fb_destination"/>
|
||||||
|
<field name="status"/>
|
||||||
|
|
||||||
|
<field name="etd_date"/>
|
||||||
|
<field name="bl_date"/>
|
||||||
|
<field name="etd_sob"/>
|
||||||
|
|
||||||
|
<field name="sale_contract_no"/>
|
||||||
|
<field name="customer"/>
|
||||||
|
<field name="elt_quantity"/>
|
||||||
|
|
||||||
|
<field name="number_of_container"/>
|
||||||
|
<field name="vessel"/>
|
||||||
|
<field name="shipping_company"/>
|
||||||
|
<field name="booking_ref"/>
|
||||||
|
<field name="freight_forwarder"/>
|
||||||
|
|
||||||
|
<field name="instrument_status"/>
|
||||||
|
<field name="ip_date"/>
|
||||||
|
<field name="ip_status"/>
|
||||||
|
<field name="latest_shipment_date"/>
|
||||||
|
|
||||||
|
<field name="countersigned"/>
|
||||||
|
<field name="comments"/>
|
||||||
|
<field name="docs_internal_comments"/>
|
||||||
|
|
||||||
|
<field name="alloc_quantity"/>
|
||||||
|
<field name="si_comments"/>
|
||||||
|
<field name="is_archived"/>
|
||||||
|
|
||||||
|
<field name="price_cont"/>
|
||||||
|
<field name="price_cont_curr"/>
|
||||||
|
|
||||||
|
<field name="ct_period_start"/>
|
||||||
|
<field name="ct_period_end"/>
|
||||||
|
|
||||||
|
<field name="lsd_check"/>
|
||||||
|
<field name="bl2lsd_delta"/>
|
||||||
|
|
||||||
|
<field name="fintrade_booking"/>
|
||||||
|
<field name="alloc_unit_price"/>
|
||||||
|
<field name="alloc_price_curr"/>
|
||||||
|
<field name="alloc_price_unit"/>
|
||||||
|
|
||||||
|
<field name="left_time"/>
|
||||||
|
|
||||||
|
</tree>
|
||||||
13
modules/automation/view/freight_booking_info_tree.xml
Normal file
13
modules/automation/view/freight_booking_info_tree.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<tree>
|
||||||
|
<field name="booking_number"/>
|
||||||
|
<field name="agent"/>
|
||||||
|
<field name="controller"/>
|
||||||
|
<field name="origin"/>
|
||||||
|
<field name="destination"/>
|
||||||
|
<field name="etd"/>
|
||||||
|
<field name="bl_date"/>
|
||||||
|
<field name="bl_number"/>
|
||||||
|
<field name="carrier"/>
|
||||||
|
<field name="vessel"/>
|
||||||
|
<field name="container_count"/>
|
||||||
|
</tree>
|
||||||
@@ -27,6 +27,14 @@ class Sale(metaclass=PoolMeta):
|
|||||||
invoice.save()
|
invoice.save()
|
||||||
return invoice
|
return invoice
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_agent(self):
|
||||||
|
if self.agent:
|
||||||
|
return (self.agent.party.address_get(
|
||||||
|
type='delivery')).full_address
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
@Workflow.transition('quotation')
|
@Workflow.transition('quotation')
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class Company(ModelSQL, ModelView):
|
|||||||
help="Used to compute the today date.")
|
help="Used to compute the today date.")
|
||||||
employees = fields.One2Many('company.employee', 'company', 'Employees',
|
employees = fields.One2Many('company.employee', 'company', 'Employees',
|
||||||
help="Add employees to the company.")
|
help="Add employees to the company.")
|
||||||
|
|
||||||
|
logo = fields.Binary("Logo")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def header_used(self):
|
def header_used(self):
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="header"/>
|
<field name="header"/>
|
||||||
<separator name="footer"/>
|
<separator name="footer"/>
|
||||||
<field name="footer"/>
|
<field name="footer"/>
|
||||||
|
<separator name="logo"/>
|
||||||
|
<field name="logo" widget="image" stretch="true"/>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -137,6 +137,13 @@ class Currency(
|
|||||||
closer = date
|
closer = date
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, name):
|
||||||
|
currencies = cls.search([('symbol', '=', name)], limit=1)
|
||||||
|
if not currencies:
|
||||||
|
return None
|
||||||
|
return currencies[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_rate(currencies, tdate=None):
|
def _get_rate(currencies, tdate=None):
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from trytond.transaction import Transaction
|
|||||||
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||||
|
|
||||||
from .exceptions import DocumentIncomingSplitError
|
from .exceptions import DocumentIncomingSplitError
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if config.getboolean('document_incoming', 'filestore', default=True):
|
if config.getboolean('document_incoming', 'filestore', default=True):
|
||||||
file_id = 'file_id'
|
file_id = 'file_id'
|
||||||
@@ -179,30 +181,112 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
|||||||
def _split_mime_types(cls):
|
def _split_mime_types(cls):
|
||||||
return ['application/pdf']
|
return ['application/pdf']
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def from_inbound_email(cls, email_, rule):
|
||||||
|
# message = email_.as_dict()
|
||||||
|
# attachments = message.get('attachments')
|
||||||
|
# active = False
|
||||||
|
# data = message.get('text', message.get('html'))
|
||||||
|
# logger.info("DATA_FROM_INBOUND_MAIL:%s",data)
|
||||||
|
# if isinstance(data, str):
|
||||||
|
# data = data.encode()
|
||||||
|
# body = message.get('text') or message.get('html') or ''
|
||||||
|
# if isinstance(body, str):
|
||||||
|
# body_bytes = body.encode('utf-8')
|
||||||
|
# else:
|
||||||
|
# body_bytes = body
|
||||||
|
# document = cls(
|
||||||
|
# active=active,
|
||||||
|
# name=message.get('subject', 'No Subject'),
|
||||||
|
# company=rule.document_incoming_company,
|
||||||
|
# data=data,
|
||||||
|
# type=rule.document_incoming_type if active else None,
|
||||||
|
# source='inbound_email',
|
||||||
|
# )
|
||||||
|
# children = []
|
||||||
|
# if attachments:
|
||||||
|
# for attachment in attachments:
|
||||||
|
# child = cls(
|
||||||
|
# name=attachment['filename'] or 'data.bin',
|
||||||
|
# company=rule.document_incoming_company,
|
||||||
|
# data=attachment['data'],
|
||||||
|
# type=rule.document_incoming_type,
|
||||||
|
# source='inbound_email')
|
||||||
|
# children.append(child)
|
||||||
|
# else:
|
||||||
|
# child = cls(
|
||||||
|
# name='mail_' + message.get('subject', 'No Subject') + '.txt',
|
||||||
|
# company=rule.document_incoming_company,
|
||||||
|
# data=body_bytes,
|
||||||
|
# type=rule.document_incoming_type,
|
||||||
|
# source='inbound_email',
|
||||||
|
# )
|
||||||
|
# children.append(child)
|
||||||
|
# document.children = children
|
||||||
|
# document.save()
|
||||||
|
# return document
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_inbound_email(cls, email_, rule):
|
def from_inbound_email(cls, email_, rule):
|
||||||
message = email_.as_dict()
|
message = email_.as_dict()
|
||||||
active = not message.get('attachments')
|
|
||||||
|
def clean(value):
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
return (
|
||||||
|
value
|
||||||
|
.replace('\n', ' ')
|
||||||
|
.replace('\r', ' ')
|
||||||
|
.replace("'", '')
|
||||||
|
.replace('"', '')
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = clean(message.get('subject', 'No Subject'))
|
||||||
|
|
||||||
|
attachments = message.get('attachments')
|
||||||
|
active = False
|
||||||
data = message.get('text', message.get('html'))
|
data = message.get('text', message.get('html'))
|
||||||
|
logger.info("DATA_FROM_INBOUND_MAIL:%s", data)
|
||||||
|
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode()
|
data = data.encode()
|
||||||
|
|
||||||
|
body = message.get('text') or message.get('html') or ''
|
||||||
|
if isinstance(body, str):
|
||||||
|
body_bytes = body.encode('utf-8')
|
||||||
|
else:
|
||||||
|
body_bytes = body
|
||||||
|
|
||||||
document = cls(
|
document = cls(
|
||||||
active=active,
|
active=active,
|
||||||
name=message.get('subject', 'No Subject'),
|
name=subject,
|
||||||
company=rule.document_incoming_company,
|
company=rule.document_incoming_company,
|
||||||
data=data,
|
data=data,
|
||||||
type=rule.document_incoming_type if active else None,
|
type=rule.document_incoming_type if active else None,
|
||||||
source='inbound_email',
|
source='inbound_email',
|
||||||
)
|
)
|
||||||
|
|
||||||
children = []
|
children = []
|
||||||
for attachment in message.get('attachments', []):
|
if attachments:
|
||||||
|
for attachment in attachments:
|
||||||
|
filename = clean(attachment['filename'] or 'data.bin')
|
||||||
|
child = cls(
|
||||||
|
name=filename,
|
||||||
|
company=rule.document_incoming_company,
|
||||||
|
data=attachment['data'],
|
||||||
|
type=rule.document_incoming_type,
|
||||||
|
source='inbound_email')
|
||||||
|
children.append(child)
|
||||||
|
else:
|
||||||
child = cls(
|
child = cls(
|
||||||
name=attachment['filename'] or 'data.bin',
|
name='mail_' + subject + '.txt',
|
||||||
company=rule.document_incoming_company,
|
company=rule.document_incoming_company,
|
||||||
data=attachment['data'],
|
data=body_bytes,
|
||||||
type=rule.document_incoming_type,
|
type=rule.document_incoming_type,
|
||||||
source='inbound_email')
|
source='inbound_email',
|
||||||
|
)
|
||||||
children.append(child)
|
children.append(child)
|
||||||
|
|
||||||
document.children = children
|
document.children = children
|
||||||
document.save()
|
document.save()
|
||||||
return document
|
return document
|
||||||
@@ -265,7 +349,6 @@ class Incoming(DeactivableMixin, Workflow, ModelSQL, ModelView):
|
|||||||
default.setdefault('children')
|
default.setdefault('children')
|
||||||
return super().copy(documents, default=default)
|
return super().copy(documents, default=default)
|
||||||
|
|
||||||
|
|
||||||
def iter_pages(expression, size):
|
def iter_pages(expression, size):
|
||||||
ranges = set()
|
ranges = set()
|
||||||
for pages in expression.split(','):
|
for pages in expression.split(','):
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
from trytond.model import fields
|
from trytond.model import fields
|
||||||
from trytond.pool import Pool, PoolMeta
|
from trytond.pool import Pool, PoolMeta
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Rule(metaclass=PoolMeta):
|
class Rule(metaclass=PoolMeta):
|
||||||
__name__ = 'inbound.email.rule'
|
__name__ = 'inbound.email.rule'
|
||||||
@@ -53,4 +54,4 @@ class Rule(metaclass=PoolMeta):
|
|||||||
if (self.action == 'document.incoming|from_inbound_email'
|
if (self.action == 'document.incoming|from_inbound_email'
|
||||||
and self.document_incoming_process):
|
and self.document_incoming_process):
|
||||||
document = email_.result
|
document = email_.result
|
||||||
DocumentIncoming.process([document], with_children=True)
|
DocumentIncoming.process([document], with_children=True)
|
||||||
15
modules/document_incoming_wr/__init__.py
Normal file
15
modules/document_incoming_wr/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||||
|
# this repository contains the full copyright notices and license terms.
|
||||||
|
|
||||||
|
from trytond.pool import Pool
|
||||||
|
|
||||||
|
from . import document
|
||||||
|
|
||||||
|
__all__ = ['register']
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
Pool.register(
|
||||||
|
document.IncomingConfiguration,
|
||||||
|
document.Incoming,
|
||||||
|
module='document_incoming_wr', type_='model')
|
||||||
90
modules/document_incoming_wr/document.py
Normal file
90
modules/document_incoming_wr/document.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 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'
|
||||||
|
|
||||||
|
created_at = fields.Function(
|
||||||
|
fields.Date("Created At"),
|
||||||
|
'get_created_at')
|
||||||
|
result_notes = fields.Function(
|
||||||
|
fields.Text("Result Notes"),
|
||||||
|
'get_result_notes')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __setup__(cls):
|
||||||
|
super().__setup__()
|
||||||
|
cls.type.selection.append(
|
||||||
|
('weight_report', "Weight Report"))
|
||||||
|
cls.type.selection.append(
|
||||||
|
('controller', "Controller"))
|
||||||
|
cls._order = [('create_date', 'DESC')]
|
||||||
|
|
||||||
|
@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])
|
||||||
|
WR.run_pipeline([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
|
||||||
|
|
||||||
|
def get_created_at(self, name=None):
|
||||||
|
create_date = getattr(self, 'create_date', None)
|
||||||
|
if not create_date:
|
||||||
|
return None
|
||||||
|
return create_date.date()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def order_created_at(cls, tables):
|
||||||
|
table, _ = tables[None]
|
||||||
|
return [table.create_date]
|
||||||
|
|
||||||
|
def get_result_notes(self, name=None):
|
||||||
|
result = getattr(self, 'result', None)
|
||||||
|
if not result:
|
||||||
|
return ''
|
||||||
|
if getattr(result, '__name__', None) == 'automation.document':
|
||||||
|
return getattr(result, 'notes', '') or ''
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def supplier_invoice_company(self):
|
||||||
|
# pass
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def supplier_invoice_party(self):
|
||||||
|
# pass
|
||||||
17
modules/document_incoming_wr/document.xml
Normal file
17
modules/document_incoming_wr/document.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?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>
|
||||||
|
<record model="ir.ui.view" id="document_incoming_view_list_wr">
|
||||||
|
<field name="model">document.incoming</field>
|
||||||
|
<field name="inherit" ref="document_incoming.document_incoming_view_list"/>
|
||||||
|
<field name="name">document_incoming_list</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
10
modules/document_incoming_wr/tryton.cfg
Normal file
10
modules/document_incoming_wr/tryton.cfg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[tryton]
|
||||||
|
version=7.2.0
|
||||||
|
depends:
|
||||||
|
document_incoming
|
||||||
|
automation
|
||||||
|
ir
|
||||||
|
party
|
||||||
|
xml:
|
||||||
|
document.xml
|
||||||
|
view/document_incoming_list.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>
|
||||||
11
modules/document_incoming_wr/view/document_incoming_list.xml
Normal file
11
modules/document_incoming_wr/view/document_incoming_list.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?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="/tree/field[@name='name']" position="before">
|
||||||
|
<field name="created_at" expand="2"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="/tree/field[@name='result']" position="after">
|
||||||
|
<field name="result_notes" expand="2"/>
|
||||||
|
</xpath>
|
||||||
|
</data>
|
||||||
@@ -17,6 +17,9 @@ from trytond.pyson import Eval
|
|||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.url import http_host
|
from trytond.url import http_host
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if config.getboolean('inbound_email', 'filestore', default=True):
|
if config.getboolean('inbound_email', 'filestore', default=True):
|
||||||
file_id = 'data_id'
|
file_id = 'data_id'
|
||||||
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
|
store_prefix = config.get('inbound_email', 'store_prefix', default=None)
|
||||||
@@ -74,6 +77,7 @@ class Inbox(ModelSQL, ModelView):
|
|||||||
assert email_.inbox == self
|
assert email_.inbox == self
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
if rule.match(email_.as_dict()):
|
if rule.match(email_.as_dict()):
|
||||||
|
logger.info("RULE_MATCHED:%s",rule)
|
||||||
email_.rule = rule
|
email_.rule = rule
|
||||||
rule.run(email_)
|
rule.run(email_)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ __all__ = ['IncotermMixin', 'IncotermAvailableMixin']
|
|||||||
class IncotermMixin(Model):
|
class IncotermMixin(Model):
|
||||||
|
|
||||||
incoterm = fields.Many2One(
|
incoterm = fields.Many2One(
|
||||||
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'),
|
'incoterm.incoterm', lazy_gettext('incoterm.msg_incoterm'), required=False,
|
||||||
ondelete='RESTRICT')
|
ondelete='RESTRICT')
|
||||||
incoterm_location = fields.Many2One(
|
incoterm_location = fields.Many2One(
|
||||||
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),
|
'party.address', lazy_gettext('incoterm.msg_incoterm_location'),
|
||||||
|
|||||||
@@ -477,15 +477,16 @@ class Lot(ModelSQL, ModelView):
|
|||||||
else:
|
else:
|
||||||
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
||||||
|
|
||||||
def get_hist_quantity(self,seq):
|
def get_hist_quantity(self,state_id=0):
|
||||||
qt = Decimal(0)
|
qt = Decimal(0)
|
||||||
gross_qt = Decimal(0)
|
gross_qt = Decimal(0)
|
||||||
if self.lot_state:
|
if self.lot_state:
|
||||||
if self.lot_hist:
|
if self.lot_hist:
|
||||||
if seq != 0:
|
if state_id != 0:
|
||||||
st = seq
|
st = state_id
|
||||||
else:
|
else:
|
||||||
st = self.lot_state.id
|
st = self.lot_state.id
|
||||||
|
logger.info("GET_HIST_QT:%s",st)
|
||||||
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
|
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
|
||||||
qt = round(lot.quantity,5)
|
qt = round(lot.quantity,5)
|
||||||
gross_qt = round(lot.gross_quantity,5)
|
gross_qt = round(lot.gross_quantity,5)
|
||||||
@@ -499,24 +500,48 @@ class Lot(ModelSQL, ModelView):
|
|||||||
physic_sum = Decimal(0)
|
physic_sum = Decimal(0)
|
||||||
for l in line.lots:
|
for l in line.lots:
|
||||||
if l.lot_type == 'physic' :
|
if l.lot_type == 'physic' :
|
||||||
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit)),5)
|
factor = None
|
||||||
|
rate = None
|
||||||
|
if l.lot_unit_line.category.id != l.line.unit.category.id:
|
||||||
|
factor = 1
|
||||||
|
rate = 1
|
||||||
|
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit, True, factor, rate)),5)
|
||||||
return line.quantity_theorical - physic_sum
|
return line.quantity_theorical - physic_sum
|
||||||
|
|
||||||
def get_current_quantity(self,name=None):
|
def get_current_quantity(self,name=None):
|
||||||
# if self.lot_type == 'physic':
|
# if self.lot_type == 'physic':
|
||||||
qt, gross_qt = self.get_hist_quantity(0)
|
qt, gross_qt = self.get_hist_quantity()
|
||||||
return qt
|
return qt
|
||||||
# else:
|
# else:
|
||||||
# return self.get_virtual_diff()
|
# return self.get_virtual_diff()
|
||||||
|
|
||||||
def get_current_quantity_converted(self,name=None):
|
def get_current_quantity_converted(self,state_id=0,unit=None):
|
||||||
Uom = Pool().get('product.uom')
|
Uom = Pool().get('product.uom')
|
||||||
unit = self.line.unit if self.line else self.sale_line.unit
|
if not unit:
|
||||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
|
unit = self.line.unit if self.line else self.sale_line.unit
|
||||||
|
qt, gross_qt = self.get_hist_quantity(state_id)
|
||||||
|
factor = None
|
||||||
|
rate = None
|
||||||
|
if self.lot_unit_line.category.id != unit.category.id:
|
||||||
|
factor = 1
|
||||||
|
rate = 1
|
||||||
|
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(qt), unit, True, factor, rate)),5)
|
||||||
|
|
||||||
|
def get_current_gross_quantity_converted(self,state_id=0,unit=None):
|
||||||
|
Uom = Pool().get('product.uom')
|
||||||
|
if not unit:
|
||||||
|
unit = self.line.unit if self.line else self.sale_line.unit
|
||||||
|
qt, gross_qt = self.get_hist_quantity(state_id)
|
||||||
|
factor = None
|
||||||
|
rate = None
|
||||||
|
if self.lot_unit_line.category.id != unit.category.id:
|
||||||
|
factor = 1
|
||||||
|
rate = 1
|
||||||
|
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(gross_qt), unit, True, factor, rate)),5)
|
||||||
|
|
||||||
def get_current_gross_quantity(self,name=None):
|
def get_current_gross_quantity(self,name=None):
|
||||||
if self.lot_type == 'physic':
|
if self.lot_type == 'physic':
|
||||||
qt, gross_qt = self.get_hist_quantity(0)
|
qt, gross_qt = self.get_hist_quantity()
|
||||||
return gross_qt
|
return gross_qt
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@@ -526,6 +551,7 @@ class Lot(ModelSQL, ModelView):
|
|||||||
lqh = LotQtHist()
|
lqh = LotQtHist()
|
||||||
lqh.quantity_type = qt_type
|
lqh.quantity_type = qt_type
|
||||||
lqh.quantity = net
|
lqh.quantity = net
|
||||||
|
logger.info("ADD_QUANTITY_TO_HIST:%s",gross)
|
||||||
lqh.gross_quantity = gross
|
lqh.gross_quantity = gross
|
||||||
lqh.lot = self
|
lqh.lot = self
|
||||||
return lqh
|
return lqh
|
||||||
@@ -542,6 +568,7 @@ class Lot(ModelSQL, ModelView):
|
|||||||
if existing:
|
if existing:
|
||||||
hist = existing[0]
|
hist = existing[0]
|
||||||
hist.quantity = net
|
hist.quantity = net
|
||||||
|
logger.info("SET_CURRENT_HIST:%s",gross)
|
||||||
hist.gross_quantity = gross
|
hist.gross_quantity = gross
|
||||||
else:
|
else:
|
||||||
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
|
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
|
||||||
@@ -633,6 +660,7 @@ class SplitLine(ModelView):
|
|||||||
weight = fields.Numeric('Weight', digits=(16,5))
|
weight = fields.Numeric('Weight', digits=(16,5))
|
||||||
|
|
||||||
class SplitWizardStart(ModelView):
|
class SplitWizardStart(ModelView):
|
||||||
|
"Split Line Start"
|
||||||
__name__ = 'lot.split.wizard.start'
|
__name__ = 'lot.split.wizard.start'
|
||||||
|
|
||||||
mode = fields.Selection([
|
mode = fields.Selection([
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Price(
|
|||||||
price_composite = fields.One2Many('price.composite','price',"Composites")
|
price_composite = fields.One2Many('price.composite','price',"Composites")
|
||||||
price_product = fields.One2Many('price.product', 'price', "Product")
|
price_product = fields.One2Many('price.product', 'price', "Product")
|
||||||
price_ct_size = fields.Numeric("Ct size")
|
price_ct_size = fields.Numeric("Ct size")
|
||||||
|
|
||||||
def get_qt(self,nb_ct,unit):
|
def get_qt(self,nb_ct,unit):
|
||||||
Uom = Pool().get('product.uom')
|
Uom = Pool().get('product.uom')
|
||||||
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
|
return round(Decimal(Uom.compute_qty(self.price_unit, float(self.price_ct_size * nb_ct), unit)),4)
|
||||||
@@ -71,7 +71,6 @@ class Price(
|
|||||||
def get_price(self,dt,unit,currency,last=False):
|
def get_price(self,dt,unit,currency,last=False):
|
||||||
price = float(0)
|
price = float(0)
|
||||||
PV = Pool().get('price.price_value')
|
PV = Pool().get('price.price_value')
|
||||||
logger.info("ASKED_PRICE_FOR:%s",dt)
|
|
||||||
if self.price_values:
|
if self.price_values:
|
||||||
dt = dt.strftime("%Y-%m-%d")
|
dt = dt.strftime("%Y-%m-%d")
|
||||||
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
|
pv = PV.search([('price','=',self.id),('price_date','=',dt)])
|
||||||
@@ -115,7 +114,6 @@ class Calendar(DeactivableMixin,ModelSQL,ModelView,MultiValueMixin):
|
|||||||
dt = dt.strftime("%Y-%m-%d")
|
dt = dt.strftime("%Y-%m-%d")
|
||||||
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
|
cl = CL.search([('calendar','=',self.id),('price_date','=',dt)])
|
||||||
if cl:
|
if cl:
|
||||||
#logger.info("ISQUOTE:%s",cl)
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@@ -136,3 +134,20 @@ class Product(ModelSQL,ModelView):
|
|||||||
__name__ = 'price.product'
|
__name__ = 'price.product'
|
||||||
price = fields.Many2One('price.price',"Price index")
|
price = fields.Many2One('price.price',"Price index")
|
||||||
product = fields.Many2One('product.product',"Product")
|
product = fields.Many2One('product.product',"Product")
|
||||||
|
attributes = fields.Many2One('product.attribute',"Attribute",domain=[
|
||||||
|
('sets', '=', Eval('attribute_set')),
|
||||||
|
],
|
||||||
|
states={
|
||||||
|
'readonly': ~Eval('attribute_set'),
|
||||||
|
},
|
||||||
|
depends=['product', 'attribute_set'])
|
||||||
|
attribute_set = fields.Function(
|
||||||
|
fields.Many2One('product.attribute.set', "Attribute Set"),
|
||||||
|
'on_change_with_attribute_set'
|
||||||
|
)
|
||||||
|
|
||||||
|
@fields.depends('product')
|
||||||
|
def on_change_with_attribute_set(self, name=None):
|
||||||
|
if self.product and self.product.template and self.product.template.attribute_set:
|
||||||
|
return self.product.template.attribute_set.id
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<form>
|
<form>
|
||||||
<label name="price"/>
|
|
||||||
<field name="price"/>
|
|
||||||
<label name="product"/>
|
<label name="product"/>
|
||||||
<field name="product"/>
|
<field name="product"/>
|
||||||
|
<label name="attributes"/>
|
||||||
|
<field name="attributes"/>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<tree>
|
<tree>
|
||||||
<field name="price"/>
|
<field name="price"/>
|
||||||
<field name="product"/>
|
<field name="product"/>
|
||||||
|
<field name="attributes"/>
|
||||||
</tree>
|
</tree>
|
||||||
|
|||||||
@@ -609,6 +609,26 @@ class Product(
|
|||||||
('template.code', operator, code_value, *extra),
|
('template.code', operator, code_value, *extra),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, name, type_='goods'):
|
||||||
|
pool = Pool()
|
||||||
|
Template = pool.get('product.template')
|
||||||
|
Uom = pool.get('product.uom')
|
||||||
|
|
||||||
|
templates = Template.search([('name', '=', name)], limit=1)
|
||||||
|
if templates:
|
||||||
|
return templates[0].products[0]
|
||||||
|
|
||||||
|
unit_uom, = Uom.search([('name', '=', 'Mt')], limit=1)
|
||||||
|
|
||||||
|
template, = Template.create([{
|
||||||
|
'name': name,
|
||||||
|
'type': type_,
|
||||||
|
'default_uom': unit_uom.id,
|
||||||
|
'cost_price_method': 'fixed',
|
||||||
|
}])
|
||||||
|
return template.products[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_price_uom(products, name):
|
def get_price_uom(products, name):
|
||||||
Uom = Pool().get('product.uom')
|
Uom = Pool().get('product.uom')
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ class Uom(SymbolMixin, DigitsMixin, DeactivableMixin, ModelSQL, ModelView):
|
|||||||
def default_digits():
|
def default_digits():
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_name(cls, name):
|
||||||
|
uom = cls.search([('symbol', '=', name)], limit=1)
|
||||||
|
if not uom:
|
||||||
|
return None
|
||||||
|
return uom[0]
|
||||||
|
|
||||||
@fields.depends('factor')
|
@fields.depends('factor')
|
||||||
def on_change_factor(self):
|
def on_change_factor(self):
|
||||||
if (self.factor or 0.0) == 0.0:
|
if (self.factor or 0.0) == 0.0:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Month(ModelView, ModelSQL):
|
|||||||
is_cotation = fields.Boolean("Cotation month")
|
is_cotation = fields.Boolean("Cotation month")
|
||||||
beg_date = fields.Date("Date from")
|
beg_date = fields.Date("Date from")
|
||||||
end_date = fields.Date("Date end")
|
end_date = fields.Date("Date end")
|
||||||
|
description = fields.Char("Description")
|
||||||
|
|
||||||
class ProductMonth(ModelView, ModelSQL):
|
class ProductMonth(ModelView, ModelSQL):
|
||||||
"Product month"
|
"Product month"
|
||||||
|
|||||||
105
modules/purchase/AGENTS.md
Normal file
105
modules/purchase/AGENTS.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# AGENTS.md - Module `purchase`
|
||||||
|
|
||||||
|
Ce guide complete le `AGENTS.md` racine.
|
||||||
|
Pour ce module, les regles locales ci-dessous priment.
|
||||||
|
|
||||||
|
## 1) Perimetre metier
|
||||||
|
|
||||||
|
Le module `purchase` gere le cycle d'achat fournisseur:
|
||||||
|
|
||||||
|
- commande d'achat (`purchase.purchase`, `purchase.line`)
|
||||||
|
- facturation fournisseur (`account.invoice` liee a l'achat)
|
||||||
|
- reception/retour de stock (`stock.move`, `stock.shipment.in`, `stock.shipment.in.return`)
|
||||||
|
- reporting achats (axes temporels, fournisseur, produit)
|
||||||
|
|
||||||
|
## 2) Fichiers pivots
|
||||||
|
|
||||||
|
- Logique coeur:
|
||||||
|
- `modules/purchase/purchase.py`
|
||||||
|
- Extensions metier connexes:
|
||||||
|
- `modules/purchase/product.py`
|
||||||
|
- `modules/purchase/stock.py`
|
||||||
|
- `modules/purchase/invoice.py`
|
||||||
|
- `modules/purchase/party.py`
|
||||||
|
- `modules/purchase/configuration.py`
|
||||||
|
- `modules/purchase/purchase_reporting.py`
|
||||||
|
- Vues et actions:
|
||||||
|
- `modules/purchase/purchase.xml`
|
||||||
|
- `modules/purchase/stock.xml`
|
||||||
|
- `modules/purchase/invoice.xml`
|
||||||
|
- `modules/purchase/purchase_reporting.xml`
|
||||||
|
- Manifest et dependances:
|
||||||
|
- `modules/purchase/tryton.cfg`
|
||||||
|
- Documentation metier:
|
||||||
|
- `modules/purchase/docs/business-rules.template.md` (template)
|
||||||
|
- `modules/purchase/docs/business-rules.md` (instance a remplir)
|
||||||
|
|
||||||
|
## 3) Etats et flux critiques a preserver
|
||||||
|
|
||||||
|
Workflow de commande (dans `purchase.py`):
|
||||||
|
|
||||||
|
- `draft -> quotation -> confirmed -> processing -> done`
|
||||||
|
- transitions de retour existent aussi (`cancelled`, retour a `draft`, etc.)
|
||||||
|
|
||||||
|
Invariants importants:
|
||||||
|
|
||||||
|
- `invoice_state` et `shipment_state` doivent rester coherents apres `process()`.
|
||||||
|
- `process()` orchestre facture + stock + recalcul d'etats, ne pas contourner sans raison.
|
||||||
|
- `delete()` exige une commande annulee.
|
||||||
|
- Les methodes `create_invoice()` et `create_move()` sont sensibles (gestion `lots` et `action`).
|
||||||
|
|
||||||
|
## 4) Couplages a surveiller
|
||||||
|
|
||||||
|
- Facture:
|
||||||
|
- `purchase.py` <-> `invoice.py`
|
||||||
|
- gestion des exceptions facture (`purchase_exception_state`)
|
||||||
|
- Stock:
|
||||||
|
- `purchase.py` <-> `stock.py`
|
||||||
|
- liens `moves`, expeditions entrantes, retours
|
||||||
|
- Produit/fournisseur/prix:
|
||||||
|
- `product.py` impacte prix d'achat, UoM, fournisseurs
|
||||||
|
- Tiers:
|
||||||
|
- `party.py` impacte adresses/parametres fournisseur et contraintes d'effacement
|
||||||
|
|
||||||
|
## 5) Convention de modification pour ce module
|
||||||
|
|
||||||
|
1. Modifier d'abord le coeur minimal dans `purchase.py` ou le fichier specialise adequat.
|
||||||
|
2. Mettre a jour XML uniquement si comportement UI/action change.
|
||||||
|
3. Si regle metier impactee, mettre a jour `docs/business-rules.md`.
|
||||||
|
4. Ajouter un test proche du flux reel (scenario `.rst` prioritaire si possible).
|
||||||
|
5. Verifier les impacts transverses facture/stock avant rendu.
|
||||||
|
|
||||||
|
## 6) Strategie de test recommandee
|
||||||
|
|
||||||
|
Priorite 1 (rapide):
|
||||||
|
|
||||||
|
- `modules/purchase/tests/test_module.py`
|
||||||
|
|
||||||
|
Priorite 2 (comportement metier):
|
||||||
|
|
||||||
|
- `modules/purchase/tests/test_scenario.py`
|
||||||
|
- Scenarios cibles selon la modif:
|
||||||
|
- `scenario_purchase.rst`
|
||||||
|
- `scenario_purchase_manual_invoice.rst`
|
||||||
|
- `scenario_purchase_line_cancelled.rst`
|
||||||
|
- `scenario_purchase_line_cancelled_on_shipment.rst`
|
||||||
|
- `scenario_purchase_return_wizard.rst`
|
||||||
|
- `scenario_purchase_reporting.rst`
|
||||||
|
|
||||||
|
Si la modif touche prix/UoM/fournisseur:
|
||||||
|
|
||||||
|
- ajouter un cas dans `test_module.py` ou un scenario dedie.
|
||||||
|
|
||||||
|
## 7) Cas qui exigent validation humaine
|
||||||
|
|
||||||
|
- Changement du workflow d'etats
|
||||||
|
- Changement des regles de creation facture/mouvement
|
||||||
|
- Changement de logique sur retours fournisseur
|
||||||
|
- Changement qui altere les ecritures comptables ou le statut de paiement
|
||||||
|
|
||||||
|
## 8) Definition of done (module `purchase`)
|
||||||
|
|
||||||
|
- Le flux metier cible fonctionne de bout en bout.
|
||||||
|
- Les etats `state`, `invoice_state`, `shipment_state` restent coherents.
|
||||||
|
- Les tests du module pertinents passent.
|
||||||
|
- Le patch est limite aux fichiers necessaires.
|
||||||
122
modules/purchase/docs/business-rules.template.md
Normal file
122
modules/purchase/docs/business-rules.template.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Business Rules Template - Purchase
|
||||||
|
|
||||||
|
Statut: `draft` | `reviewed` | `approved`
|
||||||
|
Version: `v0.1`
|
||||||
|
Derniere mise a jour: `YYYY-MM-DD`
|
||||||
|
Owner metier: `Nom / Equipe`
|
||||||
|
Owner technique: `Nom / Equipe`
|
||||||
|
|
||||||
|
## 1) Scope
|
||||||
|
|
||||||
|
- Domaine: `ex: achats fournisseur`
|
||||||
|
- Hors scope: `ex: achats intercompany`
|
||||||
|
- Modules impactes:
|
||||||
|
- `purchase`
|
||||||
|
- `stock` (si applicable)
|
||||||
|
- `account_invoice` (si applicable)
|
||||||
|
|
||||||
|
## 2) Glossaire
|
||||||
|
|
||||||
|
- `Purchase`: commande d'achat fournisseur.
|
||||||
|
- `Line`: ligne de commande.
|
||||||
|
- `Invoice State`: etat facture calcule.
|
||||||
|
- `Shipment State`: etat reception calcule.
|
||||||
|
- Ajouter ici les termes metier propres a ton contexte.
|
||||||
|
|
||||||
|
## 3) Regles metier (source de verite)
|
||||||
|
|
||||||
|
### BR-001 - [Titre court]
|
||||||
|
|
||||||
|
- Intent: `Pourquoi cette regle existe`
|
||||||
|
- Description:
|
||||||
|
- `Enonce clair et testable`
|
||||||
|
- Conditions d'entree:
|
||||||
|
- `Etat`
|
||||||
|
- `Type de ligne (goods/service)`
|
||||||
|
- `Contexte (societe, devise, fournisseur, lot, etc.)`
|
||||||
|
- Resultat attendu:
|
||||||
|
- `Etat/valeur/action attendue`
|
||||||
|
- Exceptions:
|
||||||
|
- `Cas ou la regle ne s'applique pas`
|
||||||
|
- Priorite:
|
||||||
|
- `bloquante | importante | informative`
|
||||||
|
- Source:
|
||||||
|
- `Ticket / spec / decision metier`
|
||||||
|
|
||||||
|
### BR-002 - [Titre court]
|
||||||
|
|
||||||
|
- Intent:
|
||||||
|
- Description:
|
||||||
|
- Conditions d'entree:
|
||||||
|
- Resultat attendu:
|
||||||
|
- Exceptions:
|
||||||
|
- Priorite:
|
||||||
|
- Source:
|
||||||
|
|
||||||
|
## 4) Matrice d'etats (optionnel mais recommande)
|
||||||
|
|
||||||
|
| Regle | Etat initial | Evenement | Etat attendu | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| BR-001 | `draft` | `quote` | `quotation` | |
|
||||||
|
| BR-002 | `quotation` | `confirm` | `confirmed/processing` | |
|
||||||
|
|
||||||
|
## 5) Exemples concrets
|
||||||
|
|
||||||
|
### Exemple E1 - Cas nominal
|
||||||
|
|
||||||
|
- Donnees:
|
||||||
|
- `fournisseur = X`
|
||||||
|
- `produit = Y`
|
||||||
|
- `quantite = 10`
|
||||||
|
- Attendu:
|
||||||
|
- `invoice_state = pending`
|
||||||
|
- `shipment_state = waiting`
|
||||||
|
|
||||||
|
### Exemple E2 - Cas limite
|
||||||
|
|
||||||
|
- Donnees:
|
||||||
|
- Attendu:
|
||||||
|
|
||||||
|
## 6) Impact code attendu
|
||||||
|
|
||||||
|
- Fichiers Python potentiellement concernes:
|
||||||
|
- `modules/purchase/purchase.py`
|
||||||
|
- `modules/purchase/stock.py`
|
||||||
|
- `modules/purchase/invoice.py`
|
||||||
|
- `modules/purchase/product.py`
|
||||||
|
- Fichiers XML potentiellement concernes:
|
||||||
|
- `modules/purchase/purchase.xml`
|
||||||
|
- `modules/purchase/stock.xml`
|
||||||
|
- `modules/purchase/invoice.xml`
|
||||||
|
|
||||||
|
## 7) Strategie de tests
|
||||||
|
|
||||||
|
- Unitaires:
|
||||||
|
- `modules/purchase/tests/test_module.py`
|
||||||
|
- Scenarios:
|
||||||
|
- `modules/purchase/tests/scenario_purchase.rst`
|
||||||
|
- `modules/purchase/tests/scenario_purchase_manual_invoice.rst`
|
||||||
|
- `modules/purchase/tests/scenario_purchase_return_wizard.rst`
|
||||||
|
|
||||||
|
Pour chaque regle BR-xxx, lister le test associe:
|
||||||
|
|
||||||
|
| Regle | Test existant | Nouveau test a ajouter | Statut |
|
||||||
|
|---|---|---|---|
|
||||||
|
| BR-001 | `...` | `...` | `todo` |
|
||||||
|
|
||||||
|
## 8) Compatibilite et migration
|
||||||
|
|
||||||
|
- Effet retroactif sur commandes existantes: `oui/non`
|
||||||
|
- Migration necessaire: `oui/non`
|
||||||
|
- Plan de rollback:
|
||||||
|
- `comment revenir en arriere sans corruption metier`
|
||||||
|
|
||||||
|
## 9) Validation
|
||||||
|
|
||||||
|
- Valide par metier:
|
||||||
|
- `Nom` - `date`
|
||||||
|
- Valide par technique:
|
||||||
|
- `Nom` - `date`
|
||||||
|
- Decision finale:
|
||||||
|
- `approved / rejected / needs update`
|
||||||
|
|
||||||
@@ -89,14 +89,14 @@ class Purchase(
|
|||||||
number = fields.Char("Number", readonly=True)
|
number = fields.Char("Number", readonly=True)
|
||||||
reference = fields.Char("Reference")
|
reference = fields.Char("Reference")
|
||||||
description = fields.Char('Description', size=None, states=_states)
|
description = fields.Char('Description', size=None, states=_states)
|
||||||
purchase_date = fields.Date('Purchase Date',
|
purchase_date = fields.Date('Purchase Date', required=True,
|
||||||
states={
|
states={
|
||||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||||
'required': ~Eval('state').in_(
|
'required': ~Eval('state').in_(
|
||||||
['draft', 'quotation', 'cancelled']),
|
['draft', 'quotation', 'cancelled']),
|
||||||
})
|
})
|
||||||
payment_term = fields.Many2One(
|
payment_term = fields.Many2One(
|
||||||
'account.invoice.payment_term', "Payment Term", ondelete='RESTRICT',
|
'account.invoice.payment_term', "Payment Term", required=True, ondelete='RESTRICT',
|
||||||
states={
|
states={
|
||||||
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
'readonly': ~Eval('state').in_(['draft', 'quotation']),
|
||||||
})
|
})
|
||||||
@@ -389,6 +389,11 @@ class Purchase(
|
|||||||
def default_state():
|
def default_state():
|
||||||
return 'draft'
|
return 'draft'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_purchase_date(cls):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
return Date.today()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_currency(cls, **pattern):
|
def default_currency(cls, **pattern):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
@@ -462,6 +467,8 @@ class Purchase(
|
|||||||
self.tol_min = self.party.tol_min
|
self.tol_min = self.party.tol_min
|
||||||
if self.party.tol_max:
|
if self.party.tol_max:
|
||||||
self.tol_max = self.party.tol_max
|
self.tol_max = self.party.tol_max
|
||||||
|
if self.party.origin:
|
||||||
|
self.product_origin = self.party.origin
|
||||||
if self.party.wb:
|
if self.party.wb:
|
||||||
self.wb = self.party.wb
|
self.wb = self.party.wb
|
||||||
if self.party.association:
|
if self.party.association:
|
||||||
@@ -734,6 +741,7 @@ class Purchase(
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, purchases, default=None):
|
def copy(cls, purchases, default=None):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
if default is None:
|
if default is None:
|
||||||
default = {}
|
default = {}
|
||||||
else:
|
else:
|
||||||
@@ -742,7 +750,7 @@ class Purchase(
|
|||||||
default.setdefault('invoice_state', 'none')
|
default.setdefault('invoice_state', 'none')
|
||||||
default.setdefault('invoices_ignored', None)
|
default.setdefault('invoices_ignored', None)
|
||||||
default.setdefault('shipment_state', 'none')
|
default.setdefault('shipment_state', 'none')
|
||||||
default.setdefault('purchase_date', None)
|
default.setdefault('purchase_date', Date.today())
|
||||||
default.setdefault('quoted_by')
|
default.setdefault('quoted_by')
|
||||||
default.setdefault('confirmed_by')
|
default.setdefault('confirmed_by')
|
||||||
default.setdefault('untaxed_amount_cache')
|
default.setdefault('untaxed_amount_cache')
|
||||||
@@ -1021,9 +1029,15 @@ class Purchase(
|
|||||||
pool = Pool()
|
pool = Pool()
|
||||||
Invoice = pool.get('account.invoice')
|
Invoice = pool.get('account.invoice')
|
||||||
|
|
||||||
Invoice.save(invoices.values())
|
Invoice.save(invoices.values())
|
||||||
|
|
||||||
for purchase, invoice in invoices.items():
|
for purchase, invoice in invoices.items():
|
||||||
|
#check if forex
|
||||||
|
forex_rate = invoice.get_forex()
|
||||||
|
if forex_rate:
|
||||||
|
invoice.selection_rate = 'forex'
|
||||||
|
invoice.rate = invoice.on_change_with_rate()
|
||||||
|
Invoice.save([invoice])
|
||||||
purchase.copy_resources_to(invoice)
|
purchase.copy_resources_to(invoice)
|
||||||
if len(invoices)==1:
|
if len(invoices)==1:
|
||||||
if prepayment:
|
if prepayment:
|
||||||
@@ -1215,7 +1229,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
()),
|
()),
|
||||||
If(Eval('type') != 'line',
|
If(Eval('type') != 'line',
|
||||||
('id', '=', None),
|
('id', '=', None),
|
||||||
()),
|
())
|
||||||
],
|
],
|
||||||
states={
|
states={
|
||||||
'invisible': Eval('type') != 'line',
|
'invisible': Eval('type') != 'line',
|
||||||
@@ -1685,7 +1699,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
|
|
||||||
@fields.depends(
|
@fields.depends(
|
||||||
'type', 'quantity', 'unit_price',
|
'type', 'quantity', 'unit_price',
|
||||||
'purchase', '_parent_purchase.currency')
|
'purchase', '_parent_purchase.currency','premium')
|
||||||
def on_change_with_amount(self):
|
def on_change_with_amount(self):
|
||||||
if (self.type == 'line'
|
if (self.type == 'line'
|
||||||
and self.quantity is not None
|
and self.quantity is not None
|
||||||
@@ -1857,77 +1871,93 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
else:
|
else:
|
||||||
lots_to_invoice = self.lots
|
lots_to_invoice = self.lots
|
||||||
for l in lots_to_invoice:
|
for l in lots_to_invoice:
|
||||||
#if l.lot_type == 'physic':
|
if l.lot_type == 'physic':
|
||||||
invoice_line = InvoiceLine()
|
invoice_line = InvoiceLine()
|
||||||
invoice_line.type = self.type
|
invoice_line.type = self.type
|
||||||
invoice_line.currency = self.currency
|
invoice_line.currency = self.currency
|
||||||
invoice_line.company = self.company
|
invoice_line.company = self.company
|
||||||
invoice_line.description = self.description
|
invoice_line.description = self.description
|
||||||
invoice_line.note = self.note
|
invoice_line.note = self.note
|
||||||
invoice_line.origin = self
|
invoice_line.origin = self
|
||||||
qt, gross_qt = l.get_hist_quantity(0)
|
qt, gross_qt = l.get_hist_quantity(0)
|
||||||
quantity = float(qt)
|
quantity = float(qt)
|
||||||
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
quantity = Uom.compute_qty(l.lot_unit_line, quantity, self.unit)
|
||||||
if self.unit:
|
if self.unit:
|
||||||
quantity = self.unit.round(quantity)
|
quantity = self.unit.round(quantity)
|
||||||
invoice_line.unit_price = l.get_lot_price()
|
invoice_line.unit_price = l.get_lot_price()
|
||||||
invoice_line.product = l.lot_product
|
invoice_line.product = l.lot_product
|
||||||
invoice_line.quantity = quantity
|
invoice_line.quantity = quantity
|
||||||
if not invoice_line.quantity:
|
logger.info("GETINVOICELINE_QT:%s",quantity)
|
||||||
return []
|
if not invoice_line.quantity:
|
||||||
invoice_line.unit = self.unit
|
return []
|
||||||
invoice_line.taxes = self.taxes
|
invoice_line.unit = self.unit
|
||||||
if self.company.purchase_taxes_expense:
|
invoice_line.taxes = self.taxes
|
||||||
invoice_line.taxes_deductible_rate = 0
|
if self.company.purchase_taxes_expense:
|
||||||
elif self.product:
|
invoice_line.taxes_deductible_rate = 0
|
||||||
invoice_line.taxes_deductible_rate = (
|
elif self.product:
|
||||||
self.product.supplier_taxes_deductible_rate_used)
|
invoice_line.taxes_deductible_rate = (
|
||||||
invoice_line.invoice_type = 'in'
|
self.product.supplier_taxes_deductible_rate_used)
|
||||||
if self.product:
|
invoice_line.invoice_type = 'in'
|
||||||
invoice_line.account = self.product.account_stock_in_used
|
if self.product:
|
||||||
if not invoice_line.account:
|
if self.product.type == 'service' and not self.product.landed_cost:
|
||||||
raise AccountError(
|
invoice_line.account = self.product.account_stock_in_used
|
||||||
gettext('purchase'
|
else:
|
||||||
'.msg_purchase_product_missing_account_expense',
|
invoice_line.account = self.product.account_stock_in_used
|
||||||
purchase=self.purchase.rec_name,
|
if not invoice_line.account:
|
||||||
product=self.product.rec_name))
|
raise AccountError(
|
||||||
else:
|
gettext('purchase'
|
||||||
invoice_line.account = account_config.get_multivalue(
|
'.msg_purchase_product_missing_account_expense',
|
||||||
'default_category_account_expense', company=self.company.id)
|
purchase=self.purchase.rec_name,
|
||||||
if not invoice_line.account:
|
product=self.product.rec_name))
|
||||||
raise AccountError(
|
else:
|
||||||
gettext('purchase'
|
invoice_line.account = account_config.get_multivalue(
|
||||||
'.msg_purchase_missing_account_expense',
|
'default_category_account_expense', company=self.company.id)
|
||||||
purchase=self.purchase.rec_name))
|
if not invoice_line.account:
|
||||||
if action == 'prov':
|
raise AccountError(
|
||||||
invoice_line.description = 'Pro forma'
|
gettext('purchase'
|
||||||
elif action == 'final':
|
'.msg_purchase_missing_account_expense',
|
||||||
invoice_line.description = 'Final'
|
purchase=self.purchase.rec_name))
|
||||||
elif action == 'service':
|
if action == 'prov':
|
||||||
invoice_line.description = 'Service'
|
invoice_line.description = 'Pro forma'
|
||||||
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
elif action == 'final':
|
||||||
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
invoice_line.description = 'Final'
|
||||||
invoice_line.lot = l.id
|
elif action == 'service':
|
||||||
if self.product.type == 'service':
|
invoice_line.description = 'Service'
|
||||||
invoice_line.unit_price = self.unit_price
|
#invoice_line.stock_moves = self._get_invoice_line_moves()
|
||||||
invoice_line.product = self.product
|
#invoice_line.stock_moves = [l.get_current_supplier_move()]
|
||||||
invoice_line.stock_moves = []
|
invoice_line.lot = l.id
|
||||||
Fee = Pool().get('fee.fee')
|
if self.product.type == 'service':
|
||||||
fee = Fee.search(['purchase','=',self.purchase.id])
|
invoice_line.unit_price = self.unit_price
|
||||||
if fee:
|
invoice_line.product = self.product
|
||||||
invoice_line.fee = fee[0]
|
invoice_line.stock_moves = []
|
||||||
lines.append(invoice_line)
|
Fee = Pool().get('fee.fee')
|
||||||
logger.info("GETINVLINE:%s",self.product.type)
|
fee = Fee.search(['purchase','=',self.purchase.id])
|
||||||
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
|
if fee:
|
||||||
if l.invoice_line_prov and self.product.type != 'service':
|
invoice_line.fee = fee[0]
|
||||||
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
|
if fee[0].mode == 'lumpsum':
|
||||||
'invoice': None,
|
invoice_line.quantity = 1
|
||||||
'quantity': -l.invoice_line_prov.quantity,
|
elif fee[0].mode == 'ppack':
|
||||||
'unit_price': l.invoice_line_prov.unit_price,
|
invoice_line.quantity = fee[0].quantity
|
||||||
'party': l.invoice_line_prov.invoice.party,
|
else:
|
||||||
'origin': str(self),
|
state_id = 0
|
||||||
})
|
LotQtType = Pool().get('lot.qt.type')
|
||||||
lines.append(invoice_line_)
|
lqt = LotQtType.search([('name','=','BL')])
|
||||||
|
if lqt:
|
||||||
|
state_id = lqt[0].id
|
||||||
|
invoice_line.quantity = fee[0].get_fee_lots_qt(state_id)
|
||||||
|
|
||||||
|
lines.append(invoice_line)
|
||||||
|
logger.info("GETINVLINE:%s",self.product.type)
|
||||||
|
logger.info("GETINVLINE2:%s",l.invoice_line_prov)
|
||||||
|
if l.invoice_line_prov and self.product.type != 'service':
|
||||||
|
invoice_line_, = InvoiceLine.copy([l.invoice_line_prov], default={
|
||||||
|
'invoice': None,
|
||||||
|
'quantity': -l.invoice_line_prov.quantity,
|
||||||
|
'unit_price': l.invoice_line_prov.unit_price,
|
||||||
|
'party': l.invoice_line_prov.invoice.party,
|
||||||
|
'origin': str(self),
|
||||||
|
})
|
||||||
|
lines.append(invoice_line_)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _get_invoice_line_quantity(self):
|
def _get_invoice_line_quantity(self):
|
||||||
@@ -1990,8 +2020,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
'''
|
'''
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
Move = pool.get('stock.move')
|
Move = pool.get('stock.move')
|
||||||
InvoiceLine = pool.get('account.invoice.line')
|
Location = pool.get('stock.location')
|
||||||
Uom = pool.get('product.uom')
|
|
||||||
if self.type != 'line':
|
if self.type != 'line':
|
||||||
return
|
return
|
||||||
if not self.product:
|
if not self.product:
|
||||||
@@ -2047,26 +2076,28 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
to_location = self.purchase.to_location
|
to_location = self.purchase.to_location
|
||||||
|
|
||||||
move.from_location = from_location
|
move.from_location = from_location
|
||||||
|
logger.info("FROM_LOCATION:%s",self.purchase.from_location)
|
||||||
if to_location.type != 'customer':
|
logger.info("TO_LOCATION:%s",self.purchase.to_location)
|
||||||
move.to_location = 8
|
if to_location:
|
||||||
else:
|
if to_location.type != 'customer':
|
||||||
move.to_location = to_location
|
move.to_location = Location.get_transit_id()
|
||||||
|
else:
|
||||||
|
move.to_location = to_location
|
||||||
|
|
||||||
unit_price = l.get_lot_price()
|
unit_price = l.get_lot_price()
|
||||||
if l.invoice_line_prov != None :
|
# if l.invoice_line_prov != None :
|
||||||
prov_inv = InvoiceLine(l.invoice_line_prov)
|
# prov_inv = InvoiceLine(l.invoice_line_prov)
|
||||||
quantity -= prov_inv.quantity
|
# quantity -= prov_inv.quantity
|
||||||
if quantity < 0 :
|
# if quantity < 0 :
|
||||||
move.from_location = self.purchase.to_location
|
# move.from_location = self.purchase.to_location
|
||||||
move.to_location = 16
|
# move.to_location = 16
|
||||||
elif quantity > 0 :
|
# elif quantity > 0 :
|
||||||
move.from_location = 16
|
# move.from_location = 16
|
||||||
move.to_location = self.purchase.to_location
|
# move.to_location = self.purchase.to_location
|
||||||
quantity = abs(quantity)
|
# quantity = abs(quantity)
|
||||||
unit_price = prov_inv.unit_price
|
# unit_price = prov_inv.unit_price
|
||||||
if quantity == 0:
|
# if quantity == 0:
|
||||||
continue
|
# continue
|
||||||
move.quantity = quantity
|
move.quantity = quantity
|
||||||
move.unit = self.unit
|
move.unit = self.unit
|
||||||
move.product = l.lot_product
|
move.product = l.lot_product
|
||||||
@@ -2086,7 +2117,7 @@ class Line(sequence_ordered(), ModelSQL, ModelView):
|
|||||||
moves.append(move)
|
moves.append(move)
|
||||||
if move.to_location.type != 'customer':
|
if move.to_location.type != 'customer':
|
||||||
move_to, = Move.copy([move.id], default={
|
move_to, = Move.copy([move.id], default={
|
||||||
'from_location': 8,
|
'from_location': Location.get_transit_id(),
|
||||||
'to_location': to_location,
|
'to_location': to_location,
|
||||||
})
|
})
|
||||||
moves.append(move_to)
|
moves.append(move_to)
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="payment_term"/>
|
<field name="payment_term"/>
|
||||||
<label name="currency"/>
|
<label name="currency"/>
|
||||||
<field name="currency"/>
|
<field name="currency"/>
|
||||||
<newline/>
|
|
||||||
<label name="certif"/>
|
|
||||||
<field name="certif"/>
|
|
||||||
</group>
|
</group>
|
||||||
<group col="2" colspan="2" id="hd" yfill="1">
|
<group col="2" colspan="2" id="hd" yfill="1">
|
||||||
<field name="viewer" widget="html_viewer" height="300" width="600"/>
|
<field name="viewer" widget="html_viewer" height="300" width="600"/>
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<label name="product_supplier"/>
|
<label name="product_supplier"/>
|
||||||
<field name="product_supplier"/>
|
<field name="product_supplier"/>
|
||||||
<newline/>
|
<newline/>
|
||||||
|
<label name="attributes_name"/>
|
||||||
|
<field name="attributes_name"/>
|
||||||
|
<label name="concentration"/>
|
||||||
|
<field name="concentration"/>
|
||||||
|
<newline/>
|
||||||
|
<label name="del_period"/>
|
||||||
|
<field name="del_period"/>
|
||||||
|
<label name="period_at"/>
|
||||||
|
<field name="period_at"/>
|
||||||
|
<newline/>
|
||||||
<label id="delivery_date" string="Delivery Date:"/>
|
<label id="delivery_date" string="Delivery Date:"/>
|
||||||
<group id="delivery_date" col="-1">
|
<group id="delivery_date" col="-1">
|
||||||
<field name="delivery_date" xexpand="0"/>
|
<field name="delivery_date" xexpand="0"/>
|
||||||
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
|
<field name="delivery_date_edit" xexpand="0" xalign="0"/>
|
||||||
</group>
|
</group>
|
||||||
<label name="del_period"/>
|
|
||||||
<field name="del_period"/>
|
|
||||||
<newline/>
|
<newline/>
|
||||||
<label name="from_del"/>
|
<label name="from_del"/>
|
||||||
<field name="from_del"/>
|
<field name="from_del"/>
|
||||||
@@ -40,6 +48,10 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<separator name="description" colspan="4"/>
|
<separator name="description" colspan="4"/>
|
||||||
<field name="description" colspan="4"/>
|
<field name="description" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Attributes" id="att">
|
||||||
|
<label name="attributes"/>
|
||||||
|
<field name="attributes"/>
|
||||||
|
</page>
|
||||||
<page string="Taxes" id="taxes">
|
<page string="Taxes" id="taxes">
|
||||||
<field name="taxes" colspan="4"/>
|
<field name="taxes" colspan="4"/>
|
||||||
</page>
|
</page>
|
||||||
|
|||||||
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 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():
|
def register():
|
||||||
Pool.register(
|
Pool.register(
|
||||||
@@ -23,9 +55,10 @@ def register():
|
|||||||
incoming.ImportSwift,
|
incoming.ImportSwift,
|
||||||
lc.LCMT700,
|
lc.LCMT700,
|
||||||
lc.LCMessage,
|
lc.LCMessage,
|
||||||
lc.CreateLCStart,
|
lc.CreateLCStart,
|
||||||
global_reporting.GRConfiguration,
|
global_reporting.GRConfiguration,
|
||||||
module='purchase_trade', type_='model')
|
configuration.Configuration,
|
||||||
|
module='purchase_trade', type_='model')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
incoming.ImportSwift,
|
incoming.ImportSwift,
|
||||||
incoming.PrepareDocuments,
|
incoming.PrepareDocuments,
|
||||||
@@ -47,6 +80,9 @@ def register():
|
|||||||
dashboard.News,
|
dashboard.News,
|
||||||
dashboard.Demos,
|
dashboard.Demos,
|
||||||
party.Party,
|
party.Party,
|
||||||
|
party.PartyExecution,
|
||||||
|
party.PartyExecutionSla,
|
||||||
|
party.PartyExecutionPlace,
|
||||||
payment_term.PaymentTerm,
|
payment_term.PaymentTerm,
|
||||||
payment_term.PaymentTermLine,
|
payment_term.PaymentTermLine,
|
||||||
purchase.Purchase,
|
purchase.Purchase,
|
||||||
@@ -69,9 +105,15 @@ def register():
|
|||||||
fee.Fee,
|
fee.Fee,
|
||||||
fee.FeeLots,
|
fee.FeeLots,
|
||||||
purchase.FeeLots,
|
purchase.FeeLots,
|
||||||
fee.Valuation,
|
valuation.Valuation,
|
||||||
fee.ValuationDyn,
|
valuation.ValuationLine,
|
||||||
derivative.Derivative,
|
valuation.ValuationDyn,
|
||||||
|
valuation.ValuationReport,
|
||||||
|
valuation.ValuationReportContext,
|
||||||
|
valuation.ValuationProcessDimension,
|
||||||
|
valuation.ValuationProcessStart,
|
||||||
|
valuation.ValuationProcessResult,
|
||||||
|
derivative.Derivative,
|
||||||
derivative.DerivativeMatch,
|
derivative.DerivativeMatch,
|
||||||
derivative.MatchWizardStart,
|
derivative.MatchWizardStart,
|
||||||
derivative.DerivativeReport,
|
derivative.DerivativeReport,
|
||||||
@@ -82,9 +124,12 @@ def register():
|
|||||||
forex.PForex,
|
forex.PForex,
|
||||||
forex.ForexBI,
|
forex.ForexBI,
|
||||||
purchase.PnlBI,
|
purchase.PnlBI,
|
||||||
|
purchase.PositionBI,
|
||||||
stock.Move,
|
stock.Move,
|
||||||
|
stock.Location,
|
||||||
stock.InvoiceLine,
|
stock.InvoiceLine,
|
||||||
stock.ShipmentIn,
|
stock.ShipmentIn,
|
||||||
|
stock.ShipmentWR,
|
||||||
stock.ShipmentInternal,
|
stock.ShipmentInternal,
|
||||||
stock.ShipmentOut,
|
stock.ShipmentOut,
|
||||||
stock.StatementOfFacts,
|
stock.StatementOfFacts,
|
||||||
@@ -128,14 +173,41 @@ def register():
|
|||||||
purchase.ContractDocumentType,
|
purchase.ContractDocumentType,
|
||||||
purchase.DocTemplate,
|
purchase.DocTemplate,
|
||||||
purchase.DocTypeTemplate,
|
purchase.DocTypeTemplate,
|
||||||
purchase.Mtm,
|
purchase.PurchaseStrategy,
|
||||||
|
purchase.PriceComposition,
|
||||||
|
purchase.QualityAnalysis,
|
||||||
|
purchase.Assay,
|
||||||
|
purchase.AssayLine,
|
||||||
|
purchase.AssayElement,
|
||||||
|
purchase.AssayUnit,
|
||||||
|
purchase.PayableRule,
|
||||||
|
purchase.PenaltyRule,
|
||||||
|
purchase.PenaltyRuleTier,
|
||||||
|
purchase.ConcentrateTerm,
|
||||||
|
backtoback.Backtoback,
|
||||||
|
dimension.AnalyticDimension,
|
||||||
|
dimension.AnalyticDimensionValue,
|
||||||
|
dimension.AnalyticDimensionAssignment,
|
||||||
|
weight_report.WeightReport,
|
||||||
module='purchase', type_='model')
|
module='purchase', type_='model')
|
||||||
|
Pool.register(
|
||||||
|
account.PhysicalTradeIFRS,
|
||||||
|
module='purchase_trade', type_='model')
|
||||||
|
Pool.register(
|
||||||
|
invoice.Invoice,
|
||||||
|
invoice.InvoiceLine,
|
||||||
|
module='account_invoice', type_='model')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
forex.Forex,
|
forex.Forex,
|
||||||
forex.ForexCoverFees,
|
forex.ForexCoverFees,
|
||||||
forex.ForexCategory,
|
forex.ForexCategory,
|
||||||
pricing.Component,
|
pricing.Component,
|
||||||
pricing.Mtm,
|
pricing.Mtm,
|
||||||
|
pricing.MtmStrategy,
|
||||||
|
pricing.MtmScenario,
|
||||||
|
pricing.MtmSnapshot,
|
||||||
|
pricing.PriceMatrix,
|
||||||
|
pricing.PriceMatrixLine,
|
||||||
pricing.Estimated,
|
pricing.Estimated,
|
||||||
pricing.Pricing,
|
pricing.Pricing,
|
||||||
pricing.Period,
|
pricing.Period,
|
||||||
@@ -151,6 +223,9 @@ def register():
|
|||||||
sale.SaleCreatePurchaseInput,
|
sale.SaleCreatePurchaseInput,
|
||||||
sale.Derivative,
|
sale.Derivative,
|
||||||
sale.Valuation,
|
sale.Valuation,
|
||||||
|
sale.ValuationLine,
|
||||||
|
sale.ValuationDyn,
|
||||||
|
sale.ValuationReport,
|
||||||
sale.Fee,
|
sale.Fee,
|
||||||
sale.Lot,
|
sale.Lot,
|
||||||
sale.FeeLots,
|
sale.FeeLots,
|
||||||
@@ -161,8 +236,11 @@ def register():
|
|||||||
forex.SForex,
|
forex.SForex,
|
||||||
forex.ForexCoverPhysicalSale,
|
forex.ForexCoverPhysicalSale,
|
||||||
sale.ContractDocumentType,
|
sale.ContractDocumentType,
|
||||||
sale.Mtm,
|
sale.SaleStrategy,
|
||||||
sale.OpenPosition,
|
sale.OpenPosition,
|
||||||
|
sale.Backtoback,
|
||||||
|
sale.AnalyticDimensionAssignment,
|
||||||
|
sale.PriceComposition,
|
||||||
module='sale', type_='model')
|
module='sale', type_='model')
|
||||||
Pool.register(
|
Pool.register(
|
||||||
lot.LotShipping,
|
lot.LotShipping,
|
||||||
@@ -185,13 +263,24 @@ def register():
|
|||||||
purchase.InvoicePayment,
|
purchase.InvoicePayment,
|
||||||
stock.ImportSoFWizard,
|
stock.ImportSoFWizard,
|
||||||
dashboard.BotWizard,
|
dashboard.BotWizard,
|
||||||
dashboard.DashboardLoader,
|
dashboard.DashboardLoader,
|
||||||
forex.ForexReport,
|
forex.ForexReport,
|
||||||
purchase.PnlReport,
|
purchase.PnlReport,
|
||||||
derivative.DerivativeMatchWizard,
|
purchase.PositionReport,
|
||||||
module='purchase', type_='wizard')
|
valuation.ValuationProcess,
|
||||||
Pool.register(
|
derivative.DerivativeMatchWizard,
|
||||||
sale.SaleCreatePurchase,
|
module='purchase', type_='wizard')
|
||||||
sale.SaleAllocationsWizard,
|
Pool.register(
|
||||||
module='sale', type_='wizard')
|
sale.SaleCreatePurchase,
|
||||||
|
sale.SaleAllocationsWizard,
|
||||||
|
module='sale', type_='wizard')
|
||||||
|
Pool.register(
|
||||||
|
invoice.InvoiceReport,
|
||||||
|
invoice.SaleReport,
|
||||||
|
invoice.PurchaseReport,
|
||||||
|
stock.ShipmentShippingReport,
|
||||||
|
stock.ShipmentInsuranceReport,
|
||||||
|
stock.ShipmentCOOReport,
|
||||||
|
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>
|
||||||
92
modules/purchase_trade/configuration.py
Normal file
92
modules/purchase_trade/configuration.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
|
||||||
|
from trytond.pool import Pool
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(ModelSingleton, ModelSQL, ModelView):
|
||||||
|
"Purchase Trade Configuration"
|
||||||
|
__name__ = 'purchase_trade.configuration'
|
||||||
|
|
||||||
|
_REPORT_LABELS = (
|
||||||
|
('sale_report_label', 'sale', 'report_sale', 'Proforma'),
|
||||||
|
('sale_bill_report_label', 'sale', 'report_bill', 'Draft'),
|
||||||
|
('invoice_report_label', 'account_invoice', 'report_invoice',
|
||||||
|
'Invoice'),
|
||||||
|
('invoice_cndn_report_label', 'account_invoice',
|
||||||
|
'report_invoice_ict_final', 'CN/DN'),
|
||||||
|
('invoice_prepayment_report_label', 'account_invoice',
|
||||||
|
'report_prepayment', 'Prepayment'),
|
||||||
|
('invoice_packing_list_report_label', 'purchase_trade',
|
||||||
|
'report_invoice_packing_list', 'Packing List'),
|
||||||
|
('invoice_payment_order_report_label', 'purchase_trade',
|
||||||
|
'report_payment_order', 'Payment Order'),
|
||||||
|
('purchase_report_label', 'purchase', 'report_purchase', 'Purchase'),
|
||||||
|
('shipment_shipping_report_label', 'stock',
|
||||||
|
'report_shipment_in_shipping', 'Shipping instructions'),
|
||||||
|
('shipment_insurance_report_label', 'purchase_trade',
|
||||||
|
'report_shipment_in_insurance', 'Insurance'),
|
||||||
|
('shipment_coo_report_label', 'purchase_trade',
|
||||||
|
'report_shipment_in_coo', 'COO'),
|
||||||
|
('shipment_packing_list_report_label', 'purchase_trade',
|
||||||
|
'report_shipment_in_packing_list', 'Packing List'),
|
||||||
|
)
|
||||||
|
|
||||||
|
pricing_rule = fields.Text("Pricing Rule")
|
||||||
|
sale_report_template = fields.Char("Sale Template")
|
||||||
|
sale_report_label = fields.Char("Sale Menu Label")
|
||||||
|
sale_bill_report_template = fields.Char("Sale Bill Template")
|
||||||
|
sale_bill_report_label = fields.Char("Sale Bill Menu Label")
|
||||||
|
sale_final_report_template = fields.Char("Sale Final Template")
|
||||||
|
invoice_report_template = fields.Char("Invoice Template")
|
||||||
|
invoice_report_label = fields.Char("Invoice Menu Label")
|
||||||
|
invoice_cndn_report_template = fields.Char("CN/DN Template")
|
||||||
|
invoice_cndn_report_label = fields.Char("CN/DN Menu Label")
|
||||||
|
invoice_prepayment_report_template = fields.Char("Prepayment Template")
|
||||||
|
invoice_prepayment_report_label = fields.Char("Prepayment Menu Label")
|
||||||
|
invoice_packing_list_report_template = fields.Char("Packing List Template")
|
||||||
|
invoice_packing_list_report_label = fields.Char("Packing List Menu Label")
|
||||||
|
invoice_payment_order_report_template = fields.Char("Payment Order Template")
|
||||||
|
invoice_payment_order_report_label = fields.Char(
|
||||||
|
"Payment Order Menu Label")
|
||||||
|
purchase_report_template = fields.Char("Purchase Template")
|
||||||
|
purchase_report_label = fields.Char("Purchase Menu Label")
|
||||||
|
shipment_shipping_report_template = fields.Char("Shipping Template")
|
||||||
|
shipment_shipping_report_label = fields.Char("Shipping Menu Label")
|
||||||
|
shipment_insurance_report_template = fields.Char("Insurance Template")
|
||||||
|
shipment_insurance_report_label = fields.Char("Insurance Menu Label")
|
||||||
|
shipment_coo_report_template = fields.Char("COO Template")
|
||||||
|
shipment_coo_report_label = fields.Char("COO Menu Label")
|
||||||
|
shipment_packing_list_report_template = fields.Char("Packing List Template")
|
||||||
|
shipment_packing_list_report_label = fields.Char(
|
||||||
|
"Packing List Menu Label")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, vlist):
|
||||||
|
records = super().create(vlist)
|
||||||
|
cls._sync_report_labels(records)
|
||||||
|
return records
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def write(cls, *args):
|
||||||
|
super().write(*args)
|
||||||
|
cls._sync_report_labels(sum(args[::2], []))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _sync_report_labels(cls, records):
|
||||||
|
if not records:
|
||||||
|
return
|
||||||
|
pool = Pool()
|
||||||
|
ModelData = pool.get('ir.model.data')
|
||||||
|
ActionReport = pool.get('ir.action.report')
|
||||||
|
to_write = []
|
||||||
|
for record in records:
|
||||||
|
for field_name, module, xml_id, default_label in cls._REPORT_LABELS:
|
||||||
|
label = (getattr(record, field_name, '') or '').strip()
|
||||||
|
action_id = ModelData.get_id(module, xml_id)
|
||||||
|
action = ActionReport(action_id)
|
||||||
|
target_label = label or default_label
|
||||||
|
if getattr(action, 'name', '') != target_label:
|
||||||
|
to_write.extend(([action], {'name': target_label}))
|
||||||
|
if to_write:
|
||||||
|
with Transaction().set_user(0):
|
||||||
|
ActionReport.write(*to_write)
|
||||||
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:
|
if overdue > 0:
|
||||||
# scale by overdue relative to limit
|
# scale by overdue relative to limit
|
||||||
limit = self.credit_limit or 1
|
limit = self.credit_limit or 1
|
||||||
score += int(min(40, (overdue / float(limit)) * 100))
|
score += int(min(40, (float(overdue) / float(limit)) * 100))
|
||||||
|
|
||||||
# cap
|
# cap
|
||||||
if score > 100:
|
if score > 100:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||||
# this repository contains the full copyright notices and license terms.
|
# this repository contains the full copyright notices and license terms.
|
||||||
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
|
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, ModelSingleton
|
||||||
from trytond.pyson import Eval
|
from trytond.pyson import Eval
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
@@ -193,7 +193,7 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
|
self.chatbot = 'chatbot:' + json.dumps(dial, ensure_ascii=False)
|
||||||
logger.info("EXITONCHANGE",self.chatbot)
|
logger.info("EXITONCHANGE",self.chatbot)
|
||||||
|
|
||||||
def get_last_two_fx_rates(self, from_code='USD', to_code='EUR'):
|
def get_last_five_fx_rates(self, from_code='USD', to_code='EUR'):
|
||||||
"""
|
"""
|
||||||
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
|
Retourne (dernier_taux, avant_dernier_taux) pour le couple de devises.
|
||||||
"""
|
"""
|
||||||
@@ -208,29 +208,93 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
rates = CurrencyRate.search(
|
rates = CurrencyRate.search(
|
||||||
[('currency', '=', to_currency.id)],
|
[('currency', '=', to_currency.id)],
|
||||||
order=[('date', 'DESC')],
|
order=[('date', 'DESC')],
|
||||||
limit=2,
|
limit=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not rates:
|
if not rates:
|
||||||
return None, None
|
return None, None, None, None, None
|
||||||
|
|
||||||
# Calcul du taux EUR/USD
|
# Calcul du taux EUR/USD
|
||||||
# Si la devise principale de la société est EUR, et que le taux stocké est
|
# Si la devise principale de la société est EUR, et que le taux stocké est
|
||||||
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
|
# "1 USD = X EUR", on veut l'inverse pour avoir EUR/USD
|
||||||
last_rate = rates[0].rate
|
f1 = rates[0].rate
|
||||||
prev_rate = rates[1].rate if len(rates) > 1 else None
|
f2 = rates[1].rate if len(rates) > 1 else None
|
||||||
|
f3 = rates[2].rate if len(rates) > 2 else None
|
||||||
|
f4 = rates[3].rate if len(rates) > 3 else None
|
||||||
|
f5 = rates[4].rate if len(rates) > 4 else None
|
||||||
|
d1 = rates[0].date
|
||||||
|
d2 = rates[1].date if len(rates) > 1 else None
|
||||||
|
d3 = rates[2].date if len(rates) > 2 else None
|
||||||
|
d4 = rates[3].date if len(rates) > 3 else None
|
||||||
|
d5 = rates[4].date if len(rates) > 4 else None
|
||||||
|
|
||||||
# if from_currency != to_currency:
|
# if from_currency != to_currency:
|
||||||
# last_rate = 1 / last_rate if last_rate else None
|
# last_rate = 1 / last_rate if last_rate else None
|
||||||
# prev_rate = 1 / prev_rate if prev_rate else None
|
# prev_rate = 1 / prev_rate if prev_rate else None
|
||||||
|
|
||||||
if last_rate and prev_rate:
|
return round(1/f1,6), round(1/f2,6) if f2 else None, round(1/f3,6) if f3 else None, round(1/f4,6) if f4 else None, round(1/f5,6) if f5 else None, d1, d2, d3, d4, d5
|
||||||
return round(1/last_rate,6), round(1/prev_rate,6)
|
|
||||||
|
|
||||||
def get_tremor(self,name):
|
def get_tremor(self,name):
|
||||||
Pnl = Pool().get('valuation.valuation')
|
Date = Pool().get('ir.date')
|
||||||
pnls = Pnl.search(['id','>',0])
|
Configuration = Pool().get('gr.configuration')
|
||||||
pnl_amount = "{:,.0f}".format(round(sum([e.amount for e in pnls]),0))
|
config = Configuration.search(['id','>',0])[0]
|
||||||
|
Shipment = Pool().get('stock.shipment.in')
|
||||||
|
DocumentIncoming = Pool().get('document.incoming')
|
||||||
|
Fee = Pool().get('fee.fee')
|
||||||
|
WR = Pool().get('weight.report')
|
||||||
|
if config.automation:
|
||||||
|
shipment = Shipment.search([('state','!=','received')])
|
||||||
|
shipment_trend = [sh for sh in shipment if sh.create_date == Date.today()]
|
||||||
|
controller = Shipment.search([('controller','!=',None)])
|
||||||
|
controller_trend = [co for co in controller if co.create_date == Date.today()]
|
||||||
|
instruction = Shipment.search([('result','!=',None)])
|
||||||
|
instruction_trend = [si for si in instruction if si.create_date == Date.today()]
|
||||||
|
id_received = Shipment.search([('returned_id','!=',None)])
|
||||||
|
id_received_trend = [i for i in id_received if i.create_date == Date.today()]
|
||||||
|
wr = WR.search([('id','>',0)])
|
||||||
|
wr_trend = [w for w in wr if w.create_date == Date.today()]
|
||||||
|
so = Fee.search(['id','=',25])
|
||||||
|
so_trend = [s for s in so if s.create_date == Date.today()]
|
||||||
|
di = DocumentIncoming.search(['id','>',0])
|
||||||
|
di_trend = [d for d in di if d.create_date == Date.today()]
|
||||||
|
return (
|
||||||
|
config.dashboard +
|
||||||
|
"/dashboard/index.html?shipment="
|
||||||
|
+ str(len(shipment))
|
||||||
|
+ "&shipment_trend="
|
||||||
|
+ str(len(shipment_trend))
|
||||||
|
+ "&controller="
|
||||||
|
+ str(len(controller))
|
||||||
|
+ "&controller_trend="
|
||||||
|
+ str(len(controller_trend))
|
||||||
|
+ "&instruction="
|
||||||
|
+ str(len(instruction))
|
||||||
|
+ "&instruction_trend="
|
||||||
|
+ str(len(instruction_trend))
|
||||||
|
+ "&wr="
|
||||||
|
+ str(len(wr))
|
||||||
|
+ "&wr_trend="
|
||||||
|
+ str(len(wr_trend))
|
||||||
|
+ "&so="
|
||||||
|
+ str(len(so))
|
||||||
|
+ "&so_trend="
|
||||||
|
+ str(len(so_trend))
|
||||||
|
+ "&di="
|
||||||
|
+ str(len(di))
|
||||||
|
+ "&di_trend="
|
||||||
|
+ str(len(di_trend))
|
||||||
|
+ "&id_received="
|
||||||
|
+ str(len(id_received))
|
||||||
|
+ "&id_received_trend="
|
||||||
|
+ str(len(id_received_trend)))
|
||||||
|
|
||||||
|
f1,f2,f3,f4,f5,d1,d2,d3,d4,d5 = self.get_last_five_fx_rates()
|
||||||
|
Valuation = Pool().get('valuation.valuation')
|
||||||
|
last_total,last_variation = Valuation.get_totals()
|
||||||
|
pnl_amount = "{:,.0f}".format(round(last_total,0))
|
||||||
|
pnl_variation = 0
|
||||||
|
if last_total and last_variation:
|
||||||
|
pnl_variation = "{:,.2f}".format(round((last_variation/last_total)*100,0))
|
||||||
Open = Pool().get('open.position')
|
Open = Pool().get('open.position')
|
||||||
opens = Open.search(['id','>',0])
|
opens = Open.search(['id','>',0])
|
||||||
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
|
exposure = "{:,.0f}".format(round(sum([e.net_exposure for e in opens]),0))
|
||||||
@@ -258,38 +322,69 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
val_s = len(val)
|
val_s = len(val)
|
||||||
conf = Sale.search(['state','=','confirmed'])
|
conf = Sale.search(['state','=','confirmed'])
|
||||||
conf_s = len(conf)
|
conf_s = len(conf)
|
||||||
Shipment = Pool().get('stock.shipment.in')
|
|
||||||
draft = Shipment.search(['state','=','draft'])
|
draft = Shipment.search(['state','=','draft'])
|
||||||
shipment_d = len(draft)
|
shipment_d = len(draft)
|
||||||
val = Purchase.search(['state','=','started'])
|
val = Shipment.search(['state','=','started'])
|
||||||
shipment_s = len(val)
|
shipment_s = len(val)
|
||||||
conf = Purchase.search(['state','=','received'])
|
conf = Shipment.search(['state','=','received'])
|
||||||
shipment_r = len(conf)
|
shipment_r = len(conf)
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
lots = Lot.search(['sale_line','!=',None])
|
lots = Lot.search([('sale_line','!=',None),('line','!=',None),('lot_type','=','physic')])
|
||||||
lot_m = len(lots)
|
lot_m = len(lots)
|
||||||
val = Lot.search(['sale_line','=',None])
|
val = Lot.search([('sale_line','=',None),('line','!=',None),('lot_type','=','physic')])
|
||||||
lot_a = len(val)
|
lot_a = len(val)
|
||||||
conf = Lot.search(['lot_type','=','physic'])
|
|
||||||
lot_al = len(conf)
|
|
||||||
Invoice = Pool().get('account.invoice')
|
Invoice = Pool().get('account.invoice')
|
||||||
invs = Invoice.search(['type','=','in'])
|
invs = Invoice.search(['type','=','in'])
|
||||||
inv_p = len(invs)
|
inv_p = len(invs)
|
||||||
|
invs = Invoice.search([('type','=','in'),('state','=','paid')])
|
||||||
|
inv_p_p = len(invs)
|
||||||
|
invs = Invoice.search([('type','=','in'),('state','!=','paid')])
|
||||||
|
inv_p_np = len(invs)
|
||||||
invs = Invoice.search(['type','=','out'])
|
invs = Invoice.search(['type','=','out'])
|
||||||
inv_s = len(invs)
|
inv_s = len(invs)
|
||||||
|
invs = Invoice.search([('type','=','out'),('state','=','paid')])
|
||||||
|
inv_s_p = len(invs)
|
||||||
|
invs = Invoice.search([('type','=','out'),('state','!=','paid')])
|
||||||
|
inv_s_np = len(invs)
|
||||||
AccountMove = Pool().get('account.move')
|
AccountMove = Pool().get('account.move')
|
||||||
accs = AccountMove.search(['id','>',0])
|
accs = AccountMove.search([('journal','=',3),('state','!=','posted')])
|
||||||
move_cash = len(accs)
|
pay_val = len(accs)
|
||||||
|
accs = AccountMove.search([('journal','=',3),('state','=','posted')])
|
||||||
|
pay_posted = len(accs)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"https://srv413259.hstgr.cloud/dashboard/index.html?pnl_amount="
|
config.dashboard +
|
||||||
|
"/dashboard/index.html?pnl_amount="
|
||||||
+ str(pnl_amount)
|
+ str(pnl_amount)
|
||||||
|
+ "&pnl_variation="
|
||||||
|
+ str(pnl_variation)
|
||||||
+ "&exposure="
|
+ "&exposure="
|
||||||
+ str(exposure)
|
+ str(exposure)
|
||||||
+ "&topay="
|
+ "&topay="
|
||||||
+ str(topay)
|
+ str(topay)
|
||||||
+ "&toreceive="
|
+ "&toreceive="
|
||||||
+ str(toreceive)
|
+ str(toreceive)
|
||||||
|
+ "&eurusd="
|
||||||
|
+ str(f1)
|
||||||
|
+ "&eurusd="
|
||||||
|
+ str(f2)
|
||||||
|
+ "&eurusd="
|
||||||
|
+ str(f3)
|
||||||
|
+ "&eurusd="
|
||||||
|
+ str(f4)
|
||||||
|
+ "&eurusd="
|
||||||
|
+ str(f5)
|
||||||
|
+ "&eurusd_date="
|
||||||
|
+ str(d1)
|
||||||
|
+ "&eurusd_date="
|
||||||
|
+ str(d2)
|
||||||
|
+ "&eurusd_date="
|
||||||
|
+ str(d3)
|
||||||
|
+ "&eurusd_date="
|
||||||
|
+ str(d4)
|
||||||
|
+ "&eurusd_date="
|
||||||
|
+ str(d5)
|
||||||
+ "&draft_p="
|
+ "&draft_p="
|
||||||
+ str(draft_p)
|
+ str(draft_p)
|
||||||
+ "&val_p="
|
+ "&val_p="
|
||||||
@@ -312,14 +407,22 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
+ str(lot_m)
|
+ str(lot_m)
|
||||||
+ "&lot_a="
|
+ "&lot_a="
|
||||||
+ str(lot_a)
|
+ str(lot_a)
|
||||||
+ "&lot_al="
|
|
||||||
+ str(lot_al)
|
|
||||||
+ "&inv_p="
|
+ "&inv_p="
|
||||||
+ str(inv_p)
|
+ str(inv_p)
|
||||||
|
+ "&inv_p_p="
|
||||||
|
+ str(inv_p_p)
|
||||||
|
+ "&inv_p_np="
|
||||||
|
+ str(inv_p_np)
|
||||||
+ "&inv_s="
|
+ "&inv_s="
|
||||||
+ str(inv_s)
|
+ str(inv_s)
|
||||||
+ "&move_cash="
|
+ "&inv_s_p="
|
||||||
+ str(move_cash)
|
+ str(inv_s_p)
|
||||||
|
+ "&inv_s_np="
|
||||||
|
+ str(inv_s_np)
|
||||||
|
+ "&pay_val="
|
||||||
|
+ str(pay_val)
|
||||||
|
+ "&pay_posted="
|
||||||
|
+ str(pay_posted)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -327,7 +430,7 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
News = Pool().get('news.news')
|
News = Pool().get('news.news')
|
||||||
Date = Pool().get('ir.date')
|
Date = Pool().get('ir.date')
|
||||||
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
|
news_list = News.search([('active', '=', True)], limit=5, order=[('publish_date', 'DESC')])
|
||||||
last_rate,prev_rate = self.get_last_two_fx_rates()
|
last_rate,prev_rate, = self.get_last_five_fx_rates()
|
||||||
if last_rate and prev_rate:
|
if last_rate and prev_rate:
|
||||||
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
|
variation = ((last_rate - prev_rate) / prev_rate) * 100 if prev_rate else 0
|
||||||
direction = "📈" if variation > 0 else "📉"
|
direction = "📈" if variation > 0 else "📉"
|
||||||
@@ -412,7 +515,7 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
|
' <div class="demos-title" style="font-size:1.2em; font-weight:bold; margin-bottom:10px;">🎬 Available Demo</div>'
|
||||||
]
|
]
|
||||||
|
|
||||||
demos = Demos.search([('active', '=', True)])
|
demos = Demos.search([('active', '=', True)],order=[('id', 'DESC')])
|
||||||
for n in demos:
|
for n in demos:
|
||||||
icon = n.icon or "📰"
|
icon = n.icon or "📰"
|
||||||
category = n.category or "General"
|
category = n.category or "General"
|
||||||
@@ -499,12 +602,14 @@ class Dashboard(ModelSQL, ModelView):
|
|||||||
return pu
|
return pu
|
||||||
|
|
||||||
def gen_url(self,name=None):
|
def gen_url(self,name=None):
|
||||||
|
Configuration = Pool().get('gr.configuration')
|
||||||
|
config = Configuration.search(['id','>',0])[0]
|
||||||
payload = {
|
payload = {
|
||||||
"resource": {"dashboard": self.bi_id},
|
"resource": {"dashboard": self.bi_id},
|
||||||
"params": {},
|
"params": {},
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||||||
}
|
}
|
||||||
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
|
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||||||
logger.info("TOKEN:%s",token)
|
logger.info("TOKEN:%s",token)
|
||||||
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
|
return f"metabase:http://vps107.geneva.hosting:3000/embed/dashboard/{token}#bordered=true&titled=true"
|
||||||
|
|
||||||
@@ -663,6 +768,7 @@ class BotWizard(Wizard):
|
|||||||
l.lot_quantity = l.lot_qt
|
l.lot_quantity = l.lot_qt
|
||||||
l.lot_gross_quantity = l.lot_qt
|
l.lot_gross_quantity = l.lot_qt
|
||||||
l.lot_premium = Decimal(0)
|
l.lot_premium = Decimal(0)
|
||||||
|
l.lot_chunk_key = None
|
||||||
lot_id = LotQt.add_physical_lots(lqt,[l])
|
lot_id = LotQt.add_physical_lots(lqt,[l])
|
||||||
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
|
d.action_return = 'lot.lot,' + str(lot_id) + ',' + str(lot_id)
|
||||||
Dashboard.save([d])
|
Dashboard.save([d])
|
||||||
|
|||||||
79
modules/purchase_trade/dimension.py
Normal file
79
modules/purchase_trade/dimension.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from trytond.model import fields
|
||||||
|
from trytond.pool import Pool, PoolMeta
|
||||||
|
from trytond.pyson import Bool, Eval, Id
|
||||||
|
from trytond.model import (ModelSQL, ModelView)
|
||||||
|
from trytond.tools import is_full_text, lstrip_wildcard
|
||||||
|
from trytond.transaction import Transaction, inactive_records
|
||||||
|
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||||
|
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||||
|
from sql.conditionals import Case
|
||||||
|
from sql import Column, Literal
|
||||||
|
from sql.functions import CurrentTimestamp, DateTrunc
|
||||||
|
from trytond.wizard import Button, StateTransition, StateView, Wizard
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class AnalyticDimension(ModelSQL, ModelView):
|
||||||
|
'Analytic Dimension'
|
||||||
|
__name__ = 'analytic.dimension'
|
||||||
|
|
||||||
|
name = fields.Char('Name', required=True)
|
||||||
|
code = fields.Char('Code', required=True)
|
||||||
|
active = fields.Boolean('Active')
|
||||||
|
|
||||||
|
class AnalyticDimensionValue(ModelSQL, ModelView):
|
||||||
|
'Analytic Dimension Value'
|
||||||
|
__name__ = 'analytic.dimension.value'
|
||||||
|
|
||||||
|
dimension = fields.Many2One(
|
||||||
|
'analytic.dimension',
|
||||||
|
'Dimension',
|
||||||
|
required=True,
|
||||||
|
ondelete='CASCADE'
|
||||||
|
)
|
||||||
|
|
||||||
|
name = fields.Char('Name', required=True)
|
||||||
|
code = fields.Char('Code')
|
||||||
|
|
||||||
|
parent = fields.Many2One(
|
||||||
|
'analytic.dimension.value',
|
||||||
|
'Parent',
|
||||||
|
domain=[
|
||||||
|
('dimension', '=', Eval('dimension')),
|
||||||
|
],
|
||||||
|
depends=['dimension']
|
||||||
|
)
|
||||||
|
|
||||||
|
children = fields.One2Many(
|
||||||
|
'analytic.dimension.value',
|
||||||
|
'parent',
|
||||||
|
'Children'
|
||||||
|
)
|
||||||
|
|
||||||
|
active = fields.Boolean('Active')
|
||||||
|
|
||||||
|
class AnalyticDimensionAssignment(ModelSQL, ModelView):
|
||||||
|
'Analytic Dimension Assignment'
|
||||||
|
__name__ = 'analytic.dimension.assignment'
|
||||||
|
|
||||||
|
dimension = fields.Many2One(
|
||||||
|
'analytic.dimension',
|
||||||
|
'Dimension',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
value = fields.Many2One(
|
||||||
|
'analytic.dimension.value',
|
||||||
|
'Value',
|
||||||
|
required=True,
|
||||||
|
domain=[
|
||||||
|
('dimension', '=', Eval('dimension')),
|
||||||
|
],
|
||||||
|
depends=['dimension']
|
||||||
|
)
|
||||||
|
|
||||||
|
purchase = fields.Many2One(
|
||||||
|
'purchase.purchase',
|
||||||
|
'Purchase',
|
||||||
|
ondelete='CASCADE'
|
||||||
|
)
|
||||||
34
modules/purchase_trade/dimension.xml
Normal file
34
modules/purchase_trade/dimension.xml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<tryton>
|
||||||
|
<data>
|
||||||
|
<record model="ir.ui.view" id="dimension_view_form">
|
||||||
|
<field name="model">analytic.dimension</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="name">dimension_form</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="dimension_view_list">
|
||||||
|
<field name="model">analytic.dimension</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">dimension_tree</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="dimension_value_view_form">
|
||||||
|
<field name="model">analytic.dimension.value</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="name">dimension_value_form</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="dimension_value_view_list">
|
||||||
|
<field name="model">analytic.dimension.value</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">dimension_value_tree</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="dimension_ass_view_form">
|
||||||
|
<field name="model">analytic.dimension.assignment</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="name">dimension_ass_form</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="dimension_ass_view_list">
|
||||||
|
<field name="model">analytic.dimension.assignment</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">dimension_ass_tree</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
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.model import (ModelSQL, ModelView)
|
||||||
from trytond.tools import is_full_text, lstrip_wildcard
|
from trytond.tools import is_full_text, lstrip_wildcard
|
||||||
from trytond.transaction import Transaction, inactive_records
|
from trytond.transaction import Transaction, inactive_records
|
||||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
|
||||||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||||
from sql.conditionals import Case
|
from sql.conditionals import Case
|
||||||
from sql import Column, Literal
|
from sql import Column, Literal
|
||||||
@@ -18,120 +18,11 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from trytond.exceptions import UserWarning, UserError
|
from trytond.exceptions import UserWarning, UserError
|
||||||
|
from trytond.modules.account.exceptions import PeriodNotFoundError
|
||||||
|
from trytond.modules.purchase_trade.finance_tools import InterestCalculator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALTYPE = [
|
|
||||||
('priced', 'Price'),
|
|
||||||
('pur. priced', 'Pur. price'),
|
|
||||||
('pur. efp', 'Pur. efp'),
|
|
||||||
('sale priced', 'Sale price'),
|
|
||||||
('sale efp', 'Sale efp'),
|
|
||||||
('line fee', 'Line fee'),
|
|
||||||
('pur. fee', 'Pur. fee'),
|
|
||||||
('sale fee', 'Sale fee'),
|
|
||||||
('shipment fee', 'Shipment fee'),
|
|
||||||
('market', 'Market'),
|
|
||||||
('derivative', 'Derivative'),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Valuation(ModelSQL,ModelView):
|
|
||||||
"Valuation"
|
|
||||||
__name__ = 'valuation.valuation'
|
|
||||||
|
|
||||||
purchase = fields.Many2One('purchase.purchase',"Purchase")
|
|
||||||
line = fields.Many2One('purchase.line',"Purch. Line")
|
|
||||||
date = fields.Date("Date")
|
|
||||||
type = fields.Selection(VALTYPE, "Type")
|
|
||||||
reference = fields.Char("Reference")
|
|
||||||
counterparty = fields.Many2One('party.party',"Counterparty")
|
|
||||||
product = fields.Many2One('product.product',"Product")
|
|
||||||
state = fields.Char("State")
|
|
||||||
price = fields.Numeric("Price",digits='unit')
|
|
||||||
currency = fields.Many2One('currency.currency',"Cur")
|
|
||||||
quantity = fields.Numeric("Quantity",digits='unit')
|
|
||||||
unit = fields.Many2One('product.uom',"Unit")
|
|
||||||
amount = fields.Numeric("Amount",digits='unit')
|
|
||||||
mtm = fields.Numeric("Mtm",digits='unit')
|
|
||||||
lot = fields.Many2One('lot.lot',"Lot")
|
|
||||||
|
|
||||||
class ValuationDyn(ModelSQL,ModelView):
|
|
||||||
"Valuation"
|
|
||||||
__name__ = 'valuation.valuation.dyn'
|
|
||||||
|
|
||||||
r_purchase = fields.Many2One('purchase.purchase',"Purchase")
|
|
||||||
r_line = fields.Many2One('purchase.line',"Line")
|
|
||||||
r_date = fields.Date("Date")
|
|
||||||
r_type = fields.Selection(VALTYPE, "Type")
|
|
||||||
r_reference = fields.Char("Reference")
|
|
||||||
r_counterparty = fields.Many2One('party.party',"Counterparty")
|
|
||||||
r_product = fields.Many2One('product.product',"Product")
|
|
||||||
r_state = fields.Char("State")
|
|
||||||
r_price = fields.Numeric("Price",digits='r_unit')
|
|
||||||
r_currency = fields.Many2One('currency.currency',"Cur")
|
|
||||||
r_quantity = fields.Numeric("Quantity",digits='r_unit')
|
|
||||||
r_unit = fields.Many2One('product.uom',"Unit")
|
|
||||||
r_amount = fields.Numeric("Amount",digits='r_unit')
|
|
||||||
r_mtm = fields.Numeric("Mtm",digits='r_unit')
|
|
||||||
r_lot = fields.Many2One('lot.lot',"Lot")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def table_query(cls):
|
|
||||||
Valuation = Pool().get('valuation.valuation')
|
|
||||||
val = Valuation.__table__()
|
|
||||||
context = Transaction().context
|
|
||||||
group_pnl = context.get('group_pnl')
|
|
||||||
wh = (val.id > 0)
|
|
||||||
# query = val.select(
|
|
||||||
# Literal(0).as_('create_uid'),
|
|
||||||
# CurrentTimestamp().as_('create_date'),
|
|
||||||
# Literal(None).as_('write_uid'),
|
|
||||||
# Literal(None).as_('write_date'),
|
|
||||||
# val.id.as_('id'),
|
|
||||||
# val.purchase.as_('r_purchase'),
|
|
||||||
# val.line.as_('r_line'),
|
|
||||||
# val.date.as_('r_date'),
|
|
||||||
# val.type.as_('r_type'),
|
|
||||||
# val.reference.as_('r_reference'),
|
|
||||||
# val.counterparty.as_('r_counterparty'),
|
|
||||||
# val.product.as_('r_product'),
|
|
||||||
# val.state.as_('r_state'),
|
|
||||||
# val.price.as_('r_price'),
|
|
||||||
# val.currency.as_('r_currency'),
|
|
||||||
# val.quantity.as_('r_quantity'),
|
|
||||||
# val.unit.as_('r_unit'),
|
|
||||||
# val.amount.as_('r_amount'),
|
|
||||||
# val.mtm.as_('r_mtm'),
|
|
||||||
# val.lot.as_('r_lot'),
|
|
||||||
# where=wh)
|
|
||||||
|
|
||||||
#if group_pnl==True:
|
|
||||||
query = val.select(
|
|
||||||
Literal(0).as_('create_uid'),
|
|
||||||
CurrentTimestamp().as_('create_date'),
|
|
||||||
Literal(None).as_('write_uid'),
|
|
||||||
Literal(None).as_('write_date'),
|
|
||||||
Max(val.id).as_('id'),
|
|
||||||
Max(val.purchase).as_('r_purchase'),
|
|
||||||
Max(val.line).as_('r_line'),
|
|
||||||
Max(val.date).as_('r_date'),
|
|
||||||
val.type.as_('r_type'),
|
|
||||||
Max(val.reference).as_('r_reference'),
|
|
||||||
val.counterparty.as_('r_counterparty'),
|
|
||||||
Max(val.product).as_('r_product'),
|
|
||||||
val.state.as_('r_state'),
|
|
||||||
Avg(val.price).as_('r_price'),
|
|
||||||
Max(val.currency).as_('r_currency'),
|
|
||||||
Sum(val.quantity).as_('r_quantity'),
|
|
||||||
Max(val.unit).as_('r_unit'),
|
|
||||||
Sum(val.amount).as_('r_amount'),
|
|
||||||
Sum(val.mtm).as_('r_mtm'),
|
|
||||||
Max(val.lot).as_('r_lot'),
|
|
||||||
where=wh,
|
|
||||||
group_by=[val.type,val.counterparty,val.state])
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
def filter_state(state):
|
def filter_state(state):
|
||||||
def filter(func):
|
def filter(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
@@ -166,16 +57,28 @@ class Fee(ModelSQL,ModelView):
|
|||||||
('lumpsum', 'Lump sum'),
|
('lumpsum', 'Lump sum'),
|
||||||
('perqt', 'Per qt'),
|
('perqt', 'Per qt'),
|
||||||
('pprice', '% price'),
|
('pprice', '% price'),
|
||||||
|
('rate', '% rate'),
|
||||||
('pcost', '% cost price'),
|
('pcost', '% cost price'),
|
||||||
|
('ppack', 'Per packing'),
|
||||||
], 'Mode', required=True)
|
], 'Mode', required=True)
|
||||||
inherit_qt = fields.Boolean("Inh Qt")
|
auto_calculation = fields.Boolean("Auto",states={'readonly': (Eval('mode') != 'ppack')})
|
||||||
quantity = fields.Function(fields.Numeric("Qt",digits='unit'),'get_quantity')
|
inherit_qt = fields.Boolean("Inh Qt",states={'readonly': Eval('mode') != 'ppack'})
|
||||||
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
|
quantity = fields.Numeric("Qt",digits='unit',states={'readonly': (Eval('mode') != 'ppack') | Bool(Eval('auto_calculation'))})
|
||||||
|
unit = fields.Many2One('product.uom',"Unit",domain=[
|
||||||
|
If(Eval('mode') == 'ppack',
|
||||||
|
('category', '=', Eval('packing_category')),
|
||||||
|
()),
|
||||||
|
],
|
||||||
|
states={
|
||||||
|
'readonly': (Bool(Eval('mode') != 'ppack') & Bool(Eval('mode') != 'perqt')),
|
||||||
|
},
|
||||||
|
depends=['mode', 'packing_category'])
|
||||||
|
packing_category = fields.Function(fields.Many2One('product.uom.category',"Packing Category"),'on_change_with_packing_category')
|
||||||
inherit_shipment = fields.Boolean("Inh Sh",states={
|
inherit_shipment = fields.Boolean("Inh Sh",states={
|
||||||
'invisible': (Eval('shipment_in')),
|
'invisible': (Eval('shipment_in')),
|
||||||
})
|
})
|
||||||
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
|
||||||
|
qt_state = fields.Many2One('lot.qt.type',"Qt State")
|
||||||
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
|
amount = fields.Function(fields.Numeric("Amount", digits='currency'),'get_amount')
|
||||||
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
|
fee_lots = fields.Function(fields.Many2Many('lot.lot', None, None, "Lots"),'get_lots')#, searcher='search_lots')
|
||||||
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
|
lots = fields.Many2Many('fee.lots', 'fee', 'lot',"Lots",domain=[('id', 'in', Eval('fee_lots',-1))] )
|
||||||
@@ -191,9 +94,94 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
weight_type = fields.Selection([
|
weight_type = fields.Selection([
|
||||||
('net', 'Net'),
|
('net', 'Net'),
|
||||||
('brut', 'Brut'),
|
('brut', 'Gross'),
|
||||||
], string='W. type')
|
], string='W. type')
|
||||||
|
|
||||||
|
fee_date = fields.Date("Date")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_fee_date(cls):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
return Date.today()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_qt_state(cls):
|
||||||
|
LotQtType = Pool().get('lot.qt.type')
|
||||||
|
lqt = LotQtType.search([('name','=','BL')])
|
||||||
|
if lqt:
|
||||||
|
return lqt[0].id
|
||||||
|
|
||||||
|
@fields.depends('mode','unit')
|
||||||
|
def on_change_with_packing_category(self, name=None):
|
||||||
|
UnitCategory = Pool().get('product.uom.category')
|
||||||
|
packing = UnitCategory.search(['name','=','Packing'])
|
||||||
|
if packing:
|
||||||
|
return packing[0]
|
||||||
|
|
||||||
|
@fields.depends('line','sale_line','shipment_in','lots','price','unit','auto_calculation','mode','_parent_line.unit','_parent_line.lots','_parent_sale_line.unit','_parent_sale_line.lots','_parent_shipment_in.id')
|
||||||
|
def on_change_with_quantity(self, name=None):
|
||||||
|
qt = None
|
||||||
|
unit = None
|
||||||
|
line = self.line
|
||||||
|
logger.info("ON_CHANGE_WITH_LINE:%s",line)
|
||||||
|
if not line:
|
||||||
|
line = self.sale_line
|
||||||
|
if line:
|
||||||
|
if line.lots:
|
||||||
|
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in line.lots])
|
||||||
|
qt_ = sum([e.get_current_quantity_converted(0) for e in line.lots])
|
||||||
|
unit = line.lots[0].lot_unit
|
||||||
|
logger.info("ON_CHANGE_WITH_QT0:%s",qt)
|
||||||
|
logger.info("ON_CHANGE_WITH_SI:%s",self.shipment_in)
|
||||||
|
if self.shipment_in:
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
|
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
||||||
|
logger.info("ON_CHANGE_WITH_LOTS:%s",lots)
|
||||||
|
if lots:
|
||||||
|
qt = sum([e.get_current_quantity_converted(0,self.unit) for e in lots])
|
||||||
|
qt_ = sum([e.get_current_quantity_converted(0) for e in lots])
|
||||||
|
unit = lots[0].lot_unit
|
||||||
|
if not qt:
|
||||||
|
logger.info("ON_CHANGE_WITH_QT1:%s",qt)
|
||||||
|
LotQt = Pool().get('lot.qt')
|
||||||
|
if self.shipment_in:
|
||||||
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
||||||
|
if lqts:
|
||||||
|
qt = Decimal(lqts[0].lot_quantity)
|
||||||
|
qt_ = qt
|
||||||
|
unit = lqts[0].lot_unit
|
||||||
|
logger.info("ON_CHANGE_WITH_QT2:%s",qt)
|
||||||
|
if self.mode != 'ppack':
|
||||||
|
return qt
|
||||||
|
else:
|
||||||
|
if self.auto_calculation:
|
||||||
|
logger.info("AUTOCALCULATION:%s",qt)
|
||||||
|
logger.info("AUTOCALCULATION2:%s",qt_)
|
||||||
|
logger.info("AUTOCALCULATION3:%s",Decimal(unit.factor))
|
||||||
|
logger.info("AUTOCALCULATION4:%s",Decimal(self.unit.factor))
|
||||||
|
return (qt_ * Decimal(unit.factor) / Decimal(self.unit.factor)).to_integral_value(rounding=ROUND_UP)
|
||||||
|
|
||||||
|
@fields.depends('price','mode','_parent_line.lots','_parent_sale_line.lots','shipment_in')
|
||||||
|
def on_change_with_unit(self, name=None):
|
||||||
|
if self.mode != 'ppack' and self.mode != 'perqt':
|
||||||
|
line = self.line
|
||||||
|
if not line:
|
||||||
|
line = self.sale_line
|
||||||
|
if line:
|
||||||
|
if line.lots:
|
||||||
|
if len(line.lots) == 1:
|
||||||
|
return line.lots[0].lot_unit_line
|
||||||
|
else:
|
||||||
|
return line.lots[1].lot_unit_line
|
||||||
|
if self.shipment_in:
|
||||||
|
Lot = Pool().get('lot.lot')
|
||||||
|
lots = Lot.search([('lot_shipment_in','=',self.shipment_in.id)])
|
||||||
|
logger.info("ON_CHANGE_WITH_UNIT:%s",lots)
|
||||||
|
if lots:
|
||||||
|
return lots[0].lot_unit_line
|
||||||
|
else:
|
||||||
|
return self.unit
|
||||||
|
|
||||||
def get_lots(self, name):
|
def get_lots(self, name):
|
||||||
logger.info("GET_LOTS_LINE:%s",self.line)
|
logger.info("GET_LOTS_LINE:%s",self.line)
|
||||||
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
logger.info("GET_LOTS_SHIPMENT_IN:%s",self.shipment_in)
|
||||||
@@ -230,6 +218,23 @@ class Fee(ModelSQL,ModelView):
|
|||||||
if ml:
|
if ml:
|
||||||
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery fee'])),2)
|
||||||
|
|
||||||
|
def get_non_cog(self,lot):
|
||||||
|
MoveLine = Pool().get('account.move.line')
|
||||||
|
Currency = Pool().get('currency.currency')
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
AccountConfiguration = Pool().get('account.configuration')
|
||||||
|
account_configuration = AccountConfiguration(1)
|
||||||
|
Uom = Pool().get('product.uom')
|
||||||
|
ml = MoveLine.search([
|
||||||
|
('lot', '=', lot.id),
|
||||||
|
('fee', '=', self.id),
|
||||||
|
('account', '=', self.product.account_stock_in_used.id),
|
||||||
|
])
|
||||||
|
|
||||||
|
logger.info("GET_NON_COG_FEE:%s",ml)
|
||||||
|
if ml:
|
||||||
|
return round(Decimal(sum([e.credit-e.debit for e in ml])),2)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __setup__(cls):
|
def __setup__(cls):
|
||||||
super().__setup__()
|
super().__setup__()
|
||||||
@@ -248,13 +253,14 @@ class Fee(ModelSQL,ModelView):
|
|||||||
def default_p_r(cls):
|
def default_p_r(cls):
|
||||||
return 'pay'
|
return 'pay'
|
||||||
|
|
||||||
def get_unit(self, name):
|
def get_unit(self, name=None):
|
||||||
Lot = Pool().get('lot.lot')
|
FeeLots = Pool().get('fee.lots')
|
||||||
if self.lots:
|
fl = FeeLots.search(['fee','=',self.id])
|
||||||
if self.lots[0].line:
|
if fl:
|
||||||
return self.lots[0].line.unit
|
if fl[0].lot.line:
|
||||||
if self.lots[0].sale_line:
|
return fl[0].lot.line.unit
|
||||||
return self.lots[0].sale_line.unit
|
if fl[0].lot.sale_line:
|
||||||
|
return fl[0].lot.sale_line.unit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ModelView.button
|
@ModelView.button
|
||||||
@@ -285,7 +291,11 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return round(self.price / self.quantity,4)
|
return round(self.price / self.quantity,4)
|
||||||
elif self.mode == 'perqt':
|
elif self.mode == 'perqt':
|
||||||
return self.price
|
return self.price
|
||||||
elif self.mode == 'pprice':
|
elif self.mode == 'ppack':
|
||||||
|
unit = self.get_unit()
|
||||||
|
if unit and self.unit:
|
||||||
|
return round(self.price / Decimal(self.unit.factor) * Decimal(unit.factor),4)
|
||||||
|
elif self.mode == 'pprice' or self.mode == 'pcost':
|
||||||
if self.line and self.price:
|
if self.line and self.price:
|
||||||
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
return round(self.price * Decimal(self.line.unit_price) / 100,4)
|
||||||
if self.sale_line and self.price:
|
if self.sale_line and self.price:
|
||||||
@@ -305,8 +315,8 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
def get_landed_status(self,name):
|
def get_landed_status(self,name):
|
||||||
if self.product:
|
if self.product:
|
||||||
return self.product.landed_cost
|
return self.product.template.landed_cost
|
||||||
|
|
||||||
def get_quantity(self,name=None):
|
def get_quantity(self,name=None):
|
||||||
qt = self.get_fee_lots_qt()
|
qt = self.get_fee_lots_qt()
|
||||||
if qt:
|
if qt:
|
||||||
@@ -317,6 +327,7 @@ class Fee(ModelSQL,ModelView):
|
|||||||
return Decimal(lqts[0].lot_quantity)
|
return Decimal(lqts[0].lot_quantity)
|
||||||
|
|
||||||
def get_amount(self,name=None):
|
def get_amount(self,name=None):
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
sign = Decimal(1)
|
sign = Decimal(1)
|
||||||
if self.price:
|
if self.price:
|
||||||
# if self.p_r:
|
# if self.p_r:
|
||||||
@@ -324,13 +335,54 @@ class Fee(ModelSQL,ModelView):
|
|||||||
# sign = -1
|
# sign = -1
|
||||||
if self.mode == 'lumpsum':
|
if self.mode == 'lumpsum':
|
||||||
return self.price * sign
|
return self.price * sign
|
||||||
|
elif self.mode == 'ppack':
|
||||||
|
return round(self.price * self.quantity,2)
|
||||||
|
elif self.mode == 'rate':
|
||||||
|
#take period with estimated trigger date
|
||||||
|
if self.line:
|
||||||
|
if self.line.estimated_date:
|
||||||
|
beg_date = self.fee_date if self.fee_date else Date.today()
|
||||||
|
est_lines = [dd for dd in self.line.estimated_date if dd.trigger == 'bldate']
|
||||||
|
est_line = est_lines[0] if est_lines else None
|
||||||
|
if est_line and est_line.estimated_date:
|
||||||
|
est_date = est_line.estimated_date + datetime.timedelta(
|
||||||
|
days=est_line.fin_int_delta or 0
|
||||||
|
)
|
||||||
|
if est_date and beg_date:
|
||||||
|
factor = InterestCalculator.calculate(
|
||||||
|
start_date=beg_date,
|
||||||
|
end_date=est_date,
|
||||||
|
rate=self.price/100,
|
||||||
|
rate_type='annual',
|
||||||
|
convention='ACT/360',
|
||||||
|
compounding='simple'
|
||||||
|
)
|
||||||
|
|
||||||
|
return round(factor * self.line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
|
||||||
|
if self.sale_line:
|
||||||
|
if self.sale_line.sale.payment_term:
|
||||||
|
beg_date = self.fee_date if self.fee_date else Date.today()
|
||||||
|
est_date = self.sale_line.sale.payment_term.lines[0].get_date(beg_date,self.sale_line)
|
||||||
|
logger.info("EST_DATE:%s",est_date)
|
||||||
|
if est_date and beg_date:
|
||||||
|
factor = InterestCalculator.calculate(
|
||||||
|
start_date=beg_date,
|
||||||
|
end_date=est_date,
|
||||||
|
rate=self.price/100,
|
||||||
|
rate_type='annual',
|
||||||
|
convention='ACT/360',
|
||||||
|
compounding='simple'
|
||||||
|
)
|
||||||
|
logger.info("FACTOR:%s",factor)
|
||||||
|
return round(factor * self.sale_line.unit_price * (self.quantity if self.quantity else 0) * sign,2)
|
||||||
|
|
||||||
elif self.mode == 'perqt':
|
elif self.mode == 'perqt':
|
||||||
if self.shipment_in:
|
if self.shipment_in:
|
||||||
StockMove = Pool().get('stock.move')
|
StockMove = Pool().get('stock.move')
|
||||||
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
sm = StockMove.search(['shipment','=','stock.shipment.in,'+str(self.shipment_in.id)])
|
||||||
if sm:
|
if sm:
|
||||||
unique_lots = {e.lot for e in sm if e.lot}
|
unique_lots = {e.lot for e in sm if e.lot}
|
||||||
return round(self.price * Decimal(sum([e.get_current_quantity_converted() for e in unique_lots])) * sign,2)
|
return round(self.price * Decimal(sum([e.get_current_quantity_converted(0,self.unit) for e in unique_lots])) * sign,2)
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
lqts = LotQt.search(['lot_shipment_in','=',self.shipment_in.id])
|
||||||
if lqts:
|
if lqts:
|
||||||
@@ -373,12 +425,12 @@ class Fee(ModelSQL,ModelView):
|
|||||||
|
|
||||||
return super().copy(fees, default=default)
|
return super().copy(fees, default=default)
|
||||||
|
|
||||||
def get_fee_lots_qt(self):
|
def get_fee_lots_qt(self,state_id=0):
|
||||||
qt = Decimal(0)
|
qt = Decimal(0)
|
||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
fee_lots = FeeLots.search([('fee', '=', self.id)])
|
||||||
if fee_lots:
|
if fee_lots:
|
||||||
qt = sum([e.lot.get_current_quantity_converted() for e in fee_lots])
|
qt = sum([e.lot.get_current_quantity_converted(state_id,self.unit) for e in fee_lots])
|
||||||
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
logger.info("GET_FEE_LOTS_QT:%s",qt)
|
||||||
return qt
|
return qt
|
||||||
|
|
||||||
@@ -388,10 +440,18 @@ class Fee(ModelSQL,ModelView):
|
|||||||
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
logger.info("ADJUST_PURCHASE_VALUES:%s",self)
|
||||||
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
|
if self.type == 'ordered' and self.state == 'not invoiced' and self.purchase:
|
||||||
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
logger.info("ADJUST_PURCHASE_VALUES_QT:%s",self.purchase.lines[0].quantity)
|
||||||
if self.price != self.purchase.lines[0].unit_price:
|
if self.mode == 'lumpsum':
|
||||||
self.purchase.lines[0].unit_price = self.price
|
if self.amount != self.purchase.lines[0].unit_price:
|
||||||
if self.quantity != self.purchase.lines[0].quantity:
|
self.purchase.lines[0].unit_price = self.amount
|
||||||
self.purchase.lines[0].quantity = self.quantity
|
elif self.mode == 'ppack':
|
||||||
|
if self.amount != self.purchase.lines[0].amount:
|
||||||
|
self.purchase.lines[0].unit_price = self.price
|
||||||
|
self.purchase.lines[0].quantity = self.quantity
|
||||||
|
else:
|
||||||
|
if self.get_price_per_qt() != self.purchase.lines[0].unit_price:
|
||||||
|
self.purchase.lines[0].unit_price = self.get_price_per_qt()
|
||||||
|
if self.quantity != self.purchase.lines[0].quantity:
|
||||||
|
self.purchase.lines[0].quantity = self.quantity
|
||||||
if self.product != self.purchase.lines[0].product:
|
if self.product != self.purchase.lines[0].product:
|
||||||
self.purchase.lines[0].product = self.product
|
self.purchase.lines[0].product = self.product
|
||||||
PurchaseLine.save([self.purchase.lines[0]])
|
PurchaseLine.save([self.purchase.lines[0]])
|
||||||
@@ -408,46 +468,47 @@ class Fee(ModelSQL,ModelView):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, vlist):
|
def create(cls, vlist):
|
||||||
vlist = [x.copy() for x in vlist]
|
vlist = [x.copy() for x in vlist]
|
||||||
records = super(Fee, cls).create(vlist)
|
fees = super(Fee, cls).create(vlist)
|
||||||
qt_sh = Decimal(0)
|
qt_sh = Decimal(0)
|
||||||
qt_line = Decimal(0)
|
qt_line = Decimal(0)
|
||||||
unit = None
|
unit = None
|
||||||
for record in records:
|
for fee in fees:
|
||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
Lots = Pool().get('lot.lot')
|
Lots = Pool().get('lot.lot')
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
if record.line:
|
if fee.line:
|
||||||
for l in record.line.lots:
|
for l in fee.line.lots:
|
||||||
#if l.lot_type == 'physic':
|
if (l.lot_type == 'virtual' and len(fee.line.lots)==1) or (l.lot_type == 'physic' and len(fee.line.lots)>1):
|
||||||
fl = FeeLots()
|
fl = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
fl.line = l.line.id
|
fl.line = l.line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
qt_line += l.get_current_quantity_converted()
|
qt_line += l.get_current_quantity_converted()
|
||||||
unit = l.line.unit
|
unit = l.line.unit
|
||||||
if record.sale_line:
|
if fee.sale_line:
|
||||||
for l in record.sale_line.lots:
|
for l in fee.sale_line.lots:
|
||||||
#if l.lot_type == 'physic':
|
if (l.lot_type == 'virtual' and len(fee.sale_line.lots)==1) or (l.lot_type == 'physic' and len(fee.sale_line.lots)>1):
|
||||||
fl = FeeLots()
|
fl = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
fl.sale_line = l.sale_line.id
|
fl.sale_line = l.sale_line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
if record.shipment_in:
|
qt_line += l.get_current_quantity_converted()
|
||||||
if record.shipment_in.state == 'draft'or record.shipment_in.state == 'started':
|
unit = l.sale_line.unit
|
||||||
lots = Lots.search(['lot_shipment_in','=',record.shipment_in.id])
|
if fee.shipment_in:
|
||||||
|
if fee.shipment_in.state == 'draft'or fee.shipment_in.state == 'started':
|
||||||
|
lots = Lots.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||||
if lots:
|
if lots:
|
||||||
for l in lots:
|
for l in lots:
|
||||||
#if l.lot_type == 'physic':
|
|
||||||
fl = FeeLots()
|
fl = FeeLots()
|
||||||
fl.fee = record.id
|
fl.fee = fee.id
|
||||||
fl.lot = l.id
|
fl.lot = l.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
qt_sh += l.get_current_quantity_converted()
|
qt_sh += l.get_current_quantity_converted()
|
||||||
unit = l.line.unit
|
unit = l.line.unit
|
||||||
else:
|
else:
|
||||||
lqts = LotQt.search(['lot_shipment_in','=',record.shipment_in.id])
|
lqts = LotQt.search(['lot_shipment_in','=',fee.shipment_in.id])
|
||||||
if lqts:
|
if lqts:
|
||||||
for l in lqts:
|
for l in lqts:
|
||||||
qt_sh += l.lot_p.get_current_quantity_converted()
|
qt_sh += l.lot_p.get_current_quantity_converted()
|
||||||
@@ -455,32 +516,154 @@ class Fee(ModelSQL,ModelView):
|
|||||||
else:
|
else:
|
||||||
raise UserError("You cannot add fee on received shipment!")
|
raise UserError("You cannot add fee on received shipment!")
|
||||||
|
|
||||||
type = record.type
|
type = fee.type
|
||||||
if type == 'ordered':
|
if type == 'ordered':
|
||||||
Purchase = Pool().get('purchase.purchase')
|
Purchase = Pool().get('purchase.purchase')
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
PurchaseLine = Pool().get('purchase.line')
|
||||||
pl = PurchaseLine()
|
pl = PurchaseLine()
|
||||||
pl.product = record.product
|
pl.product = fee.product
|
||||||
if record.line:
|
if fee.line or fee.sale_line:
|
||||||
pl.quantity = round(qt_line,5)
|
pl.quantity = round(qt_line,5)
|
||||||
if record.shipment_in:
|
if fee.shipment_in:
|
||||||
pl.quantity = round(qt_sh,5)
|
pl.quantity = round(qt_sh,5)
|
||||||
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
|
logger.info("CREATE_PURHCASE_FOR_FEE_QT:%s",pl.quantity)
|
||||||
pl.unit = unit
|
pl.unit = unit
|
||||||
pl.fee_ = record.id
|
pl.fee_ = fee.id
|
||||||
if record.price:
|
if fee.price:
|
||||||
pl.unit_price = round(Decimal(record.price),4)
|
fee_price = fee.get_price_per_qt()
|
||||||
|
logger.info("GET_FEE_PRICE_PER_QT:%s",fee_price)
|
||||||
|
pl.unit_price = round(Decimal(fee_price),4)
|
||||||
|
if fee.mode == 'lumpsum':
|
||||||
|
pl.quantity = 1
|
||||||
|
pl.unit_price = round(Decimal(fee.amount),4)
|
||||||
|
elif fee.mode == 'ppack':
|
||||||
|
pl.unit_price = fee.price
|
||||||
p = Purchase()
|
p = Purchase()
|
||||||
p.lines = [pl]
|
p.lines = [pl]
|
||||||
p.party = record.supplier
|
p.party = fee.supplier
|
||||||
if p.party.addresses:
|
if p.party.addresses:
|
||||||
p.invoice_address = p.party.addresses[0]
|
p.invoice_address = p.party.addresses[0]
|
||||||
p.currency = record.currency
|
p.currency = fee.currency
|
||||||
p.line_type = 'service'
|
p.line_type = 'service'
|
||||||
|
p.from_location = fee.shipment_in.from_location if fee.shipment_in else (fee.line.purchase.from_location if fee.line else fee.sale_line.sale.from_location)
|
||||||
|
p.to_location = fee.shipment_in.to_location if fee.shipment_in else (fee.line.purchase.to_location if fee.line else fee.sale_line.sale.to_location)
|
||||||
|
if fee.shipment_in and fee.shipment_in.lotqt:
|
||||||
|
p.payment_term = fee.shipment_in.lotqt[0].lot_p.line.purchase.payment_term
|
||||||
|
elif fee.line:
|
||||||
|
p.payment_term = fee.line.purchase.payment_term
|
||||||
|
elif fee.sale_line:
|
||||||
|
p.payment_term = fee.sale_line.sale.payment_term
|
||||||
Purchase.save([p])
|
Purchase.save([p])
|
||||||
|
#if reception of moves done we need to generate accrual for fee
|
||||||
return records
|
if not fee.sale_line:
|
||||||
|
feelots = FeeLots.search(['fee','=',fee.id])
|
||||||
|
for fl in feelots:
|
||||||
|
if fee.product.template.landed_cost:
|
||||||
|
move = fl.lot.get_received_move()
|
||||||
|
if move:
|
||||||
|
Warning = Pool().get('res.user.warning')
|
||||||
|
warning_name = Warning.format("Lot ever received", [])
|
||||||
|
if Warning.check(warning_name):
|
||||||
|
raise UserWarning(warning_name,
|
||||||
|
"By clicking yes, an accrual for this fee will be created")
|
||||||
|
AccountMove = Pool().get('account.move')
|
||||||
|
account_move = move._get_account_stock_move_fee(fee)
|
||||||
|
AccountMove.save([account_move])
|
||||||
|
else:
|
||||||
|
AccountMove = Pool().get('account.move')
|
||||||
|
account_move = fee._get_account_move_fee(fl.lot)
|
||||||
|
AccountMove.save([account_move])
|
||||||
|
|
||||||
|
return fees
|
||||||
|
|
||||||
|
def _get_account_move_fee(self,lot,in_out='in',amt = None):
|
||||||
|
pool = Pool()
|
||||||
|
AccountMove = pool.get('account.move')
|
||||||
|
Date = pool.get('ir.date')
|
||||||
|
Period = pool.get('account.period')
|
||||||
|
AccountConfiguration = pool.get('account.configuration')
|
||||||
|
|
||||||
|
if self.product.type != 'service':
|
||||||
|
return
|
||||||
|
|
||||||
|
today = Date.today()
|
||||||
|
company = lot.line.purchase.company if lot.line else lot.sale_line.sale.company
|
||||||
|
for date in [today]:
|
||||||
|
try:
|
||||||
|
period = Period.find(company, date=date, test_state=False)
|
||||||
|
except PeriodNotFoundError:
|
||||||
|
if date < today:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if period.state != 'open':
|
||||||
|
date = today
|
||||||
|
period = Period.find(company, date=date)
|
||||||
|
|
||||||
|
AccountMoveLine = pool.get('account.move.line')
|
||||||
|
Currency = pool.get('currency.currency')
|
||||||
|
move_line = AccountMoveLine()
|
||||||
|
move_line.lot = lot
|
||||||
|
move_line.fee = self
|
||||||
|
move_line.origin = None
|
||||||
|
move_line_ = AccountMoveLine()
|
||||||
|
move_line_.lot = lot
|
||||||
|
move_line_.fee = self
|
||||||
|
move_line_.origin = None
|
||||||
|
amount = amt if amt else self.amount
|
||||||
|
|
||||||
|
if self.currency != company.currency:
|
||||||
|
with Transaction().set_context(date=today):
|
||||||
|
amount_converted = amount
|
||||||
|
amount = Currency.compute(self.currency,
|
||||||
|
amount, company.currency)
|
||||||
|
move_line.second_currency = self.currency
|
||||||
|
|
||||||
|
if self.p_r == 'pay':
|
||||||
|
move_line.debit = amount
|
||||||
|
move_line.credit = Decimal(0)
|
||||||
|
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
||||||
|
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
||||||
|
move_line.amount_second_currency = amount_converted
|
||||||
|
move_line_.debit = Decimal(0)
|
||||||
|
move_line_.credit = amount
|
||||||
|
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
||||||
|
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
||||||
|
move_line_.amount_second_currency = -amount_converted
|
||||||
|
else:
|
||||||
|
move_line.debit = Decimal(0)
|
||||||
|
move_line.credit = amount
|
||||||
|
move_line.account = self.product.account_stock_used if in_out == 'in' else self.product.account_cogs_used
|
||||||
|
if hasattr(move_line, 'second_currency') and move_line.second_currency:
|
||||||
|
move_line.amount_second_currency = -amount_converted
|
||||||
|
move_line_.debit = amount
|
||||||
|
move_line_.credit = Decimal(0)
|
||||||
|
move_line_.account = self.product.account_stock_in_used if in_out == 'in' else self.product.account_stock_out_used
|
||||||
|
if hasattr(move_line_, 'second_currency') and move_line_.second_currency:
|
||||||
|
move_line_.amount_second_currency = amount_converted
|
||||||
|
|
||||||
|
logger.info("FEE_MOVELINES_1:%s",move_line)
|
||||||
|
logger.info("FEE_MOVELINES_2:%s",move_line_)
|
||||||
|
|
||||||
|
AccountJournal = Pool().get('account.journal')
|
||||||
|
journal = AccountJournal.search(['type','=','expense'])
|
||||||
|
if journal:
|
||||||
|
journal = journal[0]
|
||||||
|
|
||||||
|
description = None
|
||||||
|
description = 'Fee'
|
||||||
|
return AccountMove(
|
||||||
|
journal=journal,
|
||||||
|
period=period,
|
||||||
|
date=date,
|
||||||
|
origin=None,
|
||||||
|
description=description,
|
||||||
|
lines=[move_line,move_line_],
|
||||||
|
)
|
||||||
|
|
||||||
class FeeLots(ModelSQL,ModelView):
|
class FeeLots(ModelSQL,ModelView):
|
||||||
|
|
||||||
"Fee lots"
|
"Fee lots"
|
||||||
|
|||||||
@@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. -->
|
|||||||
<field name="name">fee_tree_sequence</field>
|
<field name="name">fee_tree_sequence</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.ui.view" id="valuation_view_tree_sequence3">
|
|
||||||
<field name="model">valuation.valuation</field>
|
|
||||||
<field name="type">tree</field>
|
|
||||||
<field name="name">valuation_tree_sequence3</field>
|
|
||||||
</record>
|
|
||||||
<record model="ir.ui.view" id="valuation_view_graph">
|
|
||||||
<field name="model">valuation.valuation</field>
|
|
||||||
<field name="type">graph</field>
|
|
||||||
<field name="name">valuation_graph</field>
|
|
||||||
</record>
|
|
||||||
<record model="ir.ui.view" id="valuation_view_graph2">
|
|
||||||
<field name="model">valuation.valuation</field>
|
|
||||||
<field name="type">graph</field>
|
|
||||||
<field name="name">valuation_graph2</field>
|
|
||||||
</record>
|
|
||||||
<record model="ir.ui.view" id="valuation_view_tree_sequence4">
|
|
||||||
<field name="model">valuation.valuation.dyn</field>
|
|
||||||
<field name="type">tree</field>
|
|
||||||
<field name="name">valuation_tree_sequence4</field>
|
|
||||||
</record>
|
|
||||||
<record model="ir.ui.view" id="fee_view_tree_sequence2">
|
<record model="ir.ui.view" id="fee_view_tree_sequence2">
|
||||||
<field name="model">fee.fee</field>
|
<field name="model">fee.fee</field>
|
||||||
<field name="type">tree</field>
|
<field name="type">tree</field>
|
||||||
|
|||||||
141
modules/purchase_trade/finance_tools.py
Normal file
141
modules/purchase_trade/finance_tools.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
from decimal import Decimal, getcontext
|
||||||
|
from datetime import date
|
||||||
|
from calendar import isleap
|
||||||
|
|
||||||
|
getcontext().prec = 28
|
||||||
|
|
||||||
|
|
||||||
|
class DayCount:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def year_fraction(start_date, end_date, convention):
|
||||||
|
if end_date <= start_date:
|
||||||
|
return Decimal('0')
|
||||||
|
|
||||||
|
if convention == 'ACT/360':
|
||||||
|
return Decimal((end_date - start_date).days) / Decimal(360)
|
||||||
|
|
||||||
|
elif convention in ('ACT/365', 'ACT/365F'):
|
||||||
|
return Decimal((end_date - start_date).days) / Decimal(365)
|
||||||
|
|
||||||
|
elif convention == 'ACT/ACT_ISDA':
|
||||||
|
return DayCount._act_act_isda(start_date, end_date)
|
||||||
|
|
||||||
|
elif convention == '30/360_US':
|
||||||
|
return DayCount._30_360_us(start_date, end_date)
|
||||||
|
|
||||||
|
elif convention == '30E/360':
|
||||||
|
return DayCount._30e_360(start_date, end_date)
|
||||||
|
|
||||||
|
elif convention == '30E/360_ISDA':
|
||||||
|
return DayCount._30e_360_isda(start_date, end_date)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported convention {convention}")
|
||||||
|
|
||||||
|
# ---------- IMPLEMENTATIONS ----------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _act_act_isda(start_date, end_date):
|
||||||
|
total = Decimal('0')
|
||||||
|
current = start_date
|
||||||
|
|
||||||
|
while current < end_date:
|
||||||
|
year_end = date(current.year, 12, 31)
|
||||||
|
period_end = min(year_end.replace(day=31) +
|
||||||
|
(date(current.year + 1, 1, 1) - year_end),
|
||||||
|
end_date)
|
||||||
|
|
||||||
|
days_in_period = (period_end - current).days
|
||||||
|
days_in_year = 366 if isleap(current.year) else 365
|
||||||
|
|
||||||
|
total += Decimal(days_in_period) / Decimal(days_in_year)
|
||||||
|
current = period_end
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _30_360_us(d1, d2):
|
||||||
|
d1_day = 30 if d1.day == 31 else d1.day
|
||||||
|
if d1.day in (30, 31) and d2.day == 31:
|
||||||
|
d2_day = 30
|
||||||
|
else:
|
||||||
|
d2_day = d2.day
|
||||||
|
|
||||||
|
days = ((d2.year - d1.year) * 360 +
|
||||||
|
(d2.month - d1.month) * 30 +
|
||||||
|
(d2_day - d1_day))
|
||||||
|
|
||||||
|
return Decimal(days) / Decimal(360)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _30e_360(d1, d2):
|
||||||
|
d1_day = min(d1.day, 30)
|
||||||
|
d2_day = min(d2.day, 30)
|
||||||
|
|
||||||
|
days = ((d2.year - d1.year) * 360 +
|
||||||
|
(d2.month - d1.month) * 30 +
|
||||||
|
(d2_day - d1_day))
|
||||||
|
|
||||||
|
return Decimal(days) / Decimal(360)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _30e_360_isda(d1, d2):
|
||||||
|
d1_day = 30 if d1.day == 31 else d1.day
|
||||||
|
d2_day = 30 if d2.day == 31 else d2.day
|
||||||
|
|
||||||
|
days = ((d2.year - d1.year) * 360 +
|
||||||
|
(d2.month - d1.month) * 30 +
|
||||||
|
(d2_day - d1_day))
|
||||||
|
|
||||||
|
return Decimal(days) / Decimal(360)
|
||||||
|
|
||||||
|
|
||||||
|
class InterestCalculator:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate(
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
rate,
|
||||||
|
rate_type='annual', # 'annual' or 'monthly'
|
||||||
|
convention='ACT/360',
|
||||||
|
compounding='simple', # simple, annual, monthly, continuous
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Retourne le facteur d'intérêt (pas le montant).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not start_date or not end_date:
|
||||||
|
return Decimal('0')
|
||||||
|
|
||||||
|
if end_date <= start_date:
|
||||||
|
return Decimal('0')
|
||||||
|
|
||||||
|
rate = Decimal(str(rate))
|
||||||
|
|
||||||
|
# Conversion en taux annuel si besoin
|
||||||
|
if rate_type == 'monthly':
|
||||||
|
annual_rate = rate * Decimal(12)
|
||||||
|
else:
|
||||||
|
annual_rate = rate
|
||||||
|
|
||||||
|
yf = DayCount.year_fraction(start_date, end_date, convention)
|
||||||
|
|
||||||
|
if compounding == 'simple':
|
||||||
|
return annual_rate * yf
|
||||||
|
|
||||||
|
elif compounding == 'annual':
|
||||||
|
return (Decimal(1) + annual_rate) ** yf - Decimal(1)
|
||||||
|
|
||||||
|
elif compounding == 'monthly':
|
||||||
|
monthly_rate = annual_rate / Decimal(12)
|
||||||
|
months = yf * Decimal(12)
|
||||||
|
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
|
||||||
|
|
||||||
|
elif compounding == 'continuous':
|
||||||
|
from math import exp
|
||||||
|
return Decimal(exp(float(annual_rate * yf))) - Decimal(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported compounding mode")
|
||||||
259
modules/purchase_trade/financing_tools.py
Normal file
259
modules/purchase_trade/financing_tools.py
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
from decimal import Decimal, getcontext
|
||||||
|
from datetime import datetime, date
|
||||||
|
from calendar import isleap
|
||||||
|
from typing import Callable, Dict
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
getcontext().prec = 28
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "computation_type": "INTEREST_ACCRUAL",
|
||||||
|
# "input": {
|
||||||
|
# "start_date": "2026-01-01",
|
||||||
|
# "end_date": "2026-06-30",
|
||||||
|
# "notional": "1000000",
|
||||||
|
# "rate": {
|
||||||
|
# "value": "0.08",
|
||||||
|
# "type": "ANNUAL"
|
||||||
|
# },
|
||||||
|
# "day_count_convention": "ACT/360",
|
||||||
|
# "compounding_method": "SIMPLE"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# result = FinancialComputationService.execute(payload)
|
||||||
|
# interest = Decimal(result["result"]["interest_amount"])
|
||||||
|
# interest = currency.round(interest)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# VERSIONING
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
ENGINE_VERSION = "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# REGISTRY (PLUGIN SYSTEM)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
DAY_COUNT_REGISTRY: Dict[str, Callable] = {}
|
||||||
|
COMPOUNDING_REGISTRY: Dict[str, Callable] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_day_count(name: str):
|
||||||
|
def decorator(func):
|
||||||
|
DAY_COUNT_REGISTRY[name] = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def register_compounding(name: str):
|
||||||
|
def decorator(func):
|
||||||
|
COMPOUNDING_REGISTRY[name] = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOMAIN – DAY COUNT CONVENTIONS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@register_day_count("ACT/360")
|
||||||
|
def act_360(start: date, end: date) -> Decimal:
|
||||||
|
return Decimal((end - start).days) / Decimal(360)
|
||||||
|
|
||||||
|
|
||||||
|
@register_day_count("ACT/365F")
|
||||||
|
def act_365f(start: date, end: date) -> Decimal:
|
||||||
|
return Decimal((end - start).days) / Decimal(365)
|
||||||
|
|
||||||
|
|
||||||
|
@register_day_count("ACT/ACT_ISDA")
|
||||||
|
def act_act_isda(start: date, end: date) -> Decimal:
|
||||||
|
total = Decimal("0")
|
||||||
|
current = start
|
||||||
|
|
||||||
|
while current < end:
|
||||||
|
year_end = date(current.year, 12, 31)
|
||||||
|
next_year = date(current.year + 1, 1, 1)
|
||||||
|
period_end = min(next_year, end)
|
||||||
|
|
||||||
|
days = (period_end - current).days
|
||||||
|
year_days = 366 if isleap(current.year) else 365
|
||||||
|
|
||||||
|
total += Decimal(days) / Decimal(year_days)
|
||||||
|
current = period_end
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
@register_day_count("30E/360")
|
||||||
|
def thirty_e_360(start: date, end: date) -> Decimal:
|
||||||
|
d1 = min(start.day, 30)
|
||||||
|
d2 = min(end.day, 30)
|
||||||
|
|
||||||
|
days = (
|
||||||
|
(end.year - start.year) * 360 +
|
||||||
|
(end.month - start.month) * 30 +
|
||||||
|
(d2 - d1)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Decimal(days) / Decimal(360)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOMAIN – COMPOUNDING STRATEGIES
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@register_compounding("SIMPLE")
|
||||||
|
def simple(rate: Decimal, yf: Decimal) -> Decimal:
|
||||||
|
return rate * yf
|
||||||
|
|
||||||
|
|
||||||
|
@register_compounding("ANNUAL")
|
||||||
|
def annual(rate: Decimal, yf: Decimal) -> Decimal:
|
||||||
|
return (Decimal(1) + rate) ** yf - Decimal(1)
|
||||||
|
|
||||||
|
|
||||||
|
@register_compounding("MONTHLY")
|
||||||
|
def monthly(rate: Decimal, yf: Decimal) -> Decimal:
|
||||||
|
monthly_rate = rate / Decimal(12)
|
||||||
|
months = yf * Decimal(12)
|
||||||
|
return (Decimal(1) + monthly_rate) ** months - Decimal(1)
|
||||||
|
|
||||||
|
|
||||||
|
@register_compounding("CONTINUOUS")
|
||||||
|
def continuous(rate: Decimal, yf: Decimal) -> Decimal:
|
||||||
|
from math import exp
|
||||||
|
return Decimal(exp(float(rate * yf))) - Decimal(1)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# DOMAIN – INTEREST COMPUTATION OBJECT
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class InterestComputation:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
notional: Decimal,
|
||||||
|
rate_value: Decimal,
|
||||||
|
rate_type: str,
|
||||||
|
day_count: str,
|
||||||
|
compounding: str,
|
||||||
|
):
|
||||||
|
self.start_date = start_date
|
||||||
|
self.end_date = end_date
|
||||||
|
self.notional = notional
|
||||||
|
self.rate_value = rate_value
|
||||||
|
self.rate_type = rate_type
|
||||||
|
self.day_count = day_count
|
||||||
|
self.compounding = compounding
|
||||||
|
|
||||||
|
def compute(self):
|
||||||
|
|
||||||
|
if self.end_date <= self.start_date:
|
||||||
|
raise ValueError("end_date must be after start_date")
|
||||||
|
|
||||||
|
if self.day_count not in DAY_COUNT_REGISTRY:
|
||||||
|
raise ValueError("Unsupported day count convention")
|
||||||
|
|
||||||
|
if self.compounding not in COMPOUNDING_REGISTRY:
|
||||||
|
raise ValueError("Unsupported compounding method")
|
||||||
|
|
||||||
|
yf = DAY_COUNT_REGISTRY[self.day_count](
|
||||||
|
self.start_date,
|
||||||
|
self.end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize rate to annual
|
||||||
|
if self.rate_type == "MONTHLY":
|
||||||
|
annual_rate = self.rate_value * Decimal(12)
|
||||||
|
else:
|
||||||
|
annual_rate = self.rate_value
|
||||||
|
|
||||||
|
factor = COMPOUNDING_REGISTRY[self.compounding](
|
||||||
|
annual_rate,
|
||||||
|
yf
|
||||||
|
)
|
||||||
|
|
||||||
|
interest_amount = self.notional * factor
|
||||||
|
|
||||||
|
return {
|
||||||
|
"year_fraction": yf,
|
||||||
|
"interest_factor": factor,
|
||||||
|
"interest_amount": interest_amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# APPLICATION LAYER – JSON SERVICE (Camunda Ready)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
class FinancialComputationService:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def execute(payload: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Stateless JSON entrypoint.
|
||||||
|
Compatible Camunda / REST / Tryton bridge.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
request_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
input_data = payload["input"]
|
||||||
|
|
||||||
|
start_date = datetime.strptime(
|
||||||
|
input_data["start_date"], "%Y-%m-%d"
|
||||||
|
).date()
|
||||||
|
|
||||||
|
end_date = datetime.strptime(
|
||||||
|
input_data["end_date"], "%Y-%m-%d"
|
||||||
|
).date()
|
||||||
|
|
||||||
|
notional = Decimal(input_data["notional"])
|
||||||
|
rate_value = Decimal(input_data["rate"]["value"])
|
||||||
|
rate_type = input_data["rate"]["type"].upper()
|
||||||
|
day_count = input_data["day_count_convention"]
|
||||||
|
compounding = input_data["compounding_method"]
|
||||||
|
|
||||||
|
computation = InterestComputation(
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
notional=notional,
|
||||||
|
rate_value=rate_value,
|
||||||
|
rate_type=rate_type,
|
||||||
|
day_count=day_count,
|
||||||
|
compounding=compounding,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = computation.compute()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"metadata": {
|
||||||
|
"engine_version": ENGINE_VERSION,
|
||||||
|
"request_id": request_id,
|
||||||
|
"timestamp": datetime.utcnow().isoformat() + "Z"
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"year_fraction": str(result["year_fraction"]),
|
||||||
|
"interest_factor": str(result["interest_factor"]),
|
||||||
|
"interest_amount": str(result["interest_amount"]),
|
||||||
|
},
|
||||||
|
"explainability": {
|
||||||
|
"formula": "Interest = Notional × Factor",
|
||||||
|
"factor_definition": f"{compounding} compounding applied to annualized rate",
|
||||||
|
"day_count_used": day_count
|
||||||
|
},
|
||||||
|
"status": "SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "ERROR",
|
||||||
|
"message": str(e),
|
||||||
|
"engine_version": ENGINE_VERSION
|
||||||
|
}
|
||||||
@@ -385,11 +385,11 @@ class ForexBI(ModelSingleton,ModelSQL, ModelView):
|
|||||||
config = Configuration.search(['id','>',0])[0]
|
config = Configuration.search(['id','>',0])[0]
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"resource": {"dashboard": 3},
|
"resource": {"dashboard": config.forex_id},
|
||||||
"params": {},
|
"params": {},
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
|
||||||
}
|
}
|
||||||
token = jwt.encode(payload, "798f256d3119a3292bf121196c2a38dddf2cad155c0b6b0b444efc34c6db197c", algorithm="HS256")
|
token = jwt.encode(payload, config.payload, algorithm="HS256")
|
||||||
logger.info("TOKEN:%s",token)
|
logger.info("TOKEN:%s",token)
|
||||||
if config.dark:
|
if config.dark:
|
||||||
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
|
||||||
|
|||||||
@@ -12,4 +12,10 @@ class GRConfiguration(ModelSingleton, ModelSQL, ModelView):
|
|||||||
__name__ = 'gr.configuration'
|
__name__ = 'gr.configuration'
|
||||||
|
|
||||||
bi = fields.Char("BI connexion")
|
bi = fields.Char("BI connexion")
|
||||||
dark = fields.Boolean("Dark mode")
|
dashboard = fields.Char("Dashboard connexion")
|
||||||
|
dark = fields.Boolean("Dark mode")
|
||||||
|
pnl_id = fields.Integer("Pnl ID")
|
||||||
|
position_id = fields.Integer("Position ID")
|
||||||
|
forex_id = fields.Integer("Forex ID")
|
||||||
|
payload = fields.Char("Metabase payload")
|
||||||
|
automation = fields.Boolean("Automation")
|
||||||
14
modules/purchase_trade/icons/tradon-btb.svg
Normal file
14
modules/purchase_trade/icons/tradon-btb.svg
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Barre centrale (contrat / position) -->
|
||||||
|
<rect x="11" y="4" width="2" height="16" fill="#267F82"/>
|
||||||
|
|
||||||
|
<!-- Flèche gauche (achat) -->
|
||||||
|
<path d="M9 7 L5 12 L9 17 L9 14 L11 14 L11 10 L9 10 Z"
|
||||||
|
fill="#267F82"/>
|
||||||
|
|
||||||
|
<!-- Flèche droite (vente) -->
|
||||||
|
<path d="M15 7 L19 12 L15 17 L15 14 L13 14 L13 10 L15 10 Z"
|
||||||
|
fill="#267F82"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal file
16
modules/purchase_trade/icons/tradon-mtm.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="
|
||||||
|
M3 18 V7
|
||||||
|
L8 12
|
||||||
|
L12 8
|
||||||
|
L16 12
|
||||||
|
L21 7
|
||||||
|
V18
|
||||||
|
H18 V11
|
||||||
|
L12 15
|
||||||
|
L6 11
|
||||||
|
V18
|
||||||
|
Z
|
||||||
|
"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal file
13
modules/purchase_trade/icons/tradon-mtm_.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="
|
||||||
|
M4 18 V6
|
||||||
|
H7 L12 13 L17 6
|
||||||
|
H20 V18
|
||||||
|
H17 V10.5
|
||||||
|
L12 16
|
||||||
|
L7 10.5
|
||||||
|
V18
|
||||||
|
Z
|
||||||
|
" fill="#267F82"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 259 B |
1300
modules/purchase_trade/invoice.py
Normal file
1300
modules/purchase_trade/invoice.py
Normal file
File diff suppressed because it is too large
Load Diff
29
modules/purchase_trade/invoice.xml
Normal file
29
modules/purchase_trade/invoice.xml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<tryton>
|
||||||
|
<data>
|
||||||
|
<record model="ir.action.report" id="report_invoice_packing_list">
|
||||||
|
<field name="name">Packing List</field>
|
||||||
|
<field name="model">account.invoice</field>
|
||||||
|
<field name="report_name">account.invoice</field>
|
||||||
|
<field name="report">account_invoice/packing_list.fodt</field>
|
||||||
|
<field name="single" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record model="ir.action.keyword" id="report_invoice_packing_list_keyword">
|
||||||
|
<field name="keyword">form_print</field>
|
||||||
|
<field name="model">account.invoice,-1</field>
|
||||||
|
<field name="action" ref="report_invoice_packing_list"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<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 json
|
||||||
import logging
|
import logging
|
||||||
from trytond.exceptions import UserWarning, UserError
|
from trytond.exceptions import UserWarning, UserError
|
||||||
|
from trytond.modules.purchase_trade.service import ContractFactory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ class LotMove(ModelSQL,ModelView):
|
|||||||
class Lot(metaclass=PoolMeta):
|
class Lot(metaclass=PoolMeta):
|
||||||
__name__ = 'lot.lot'
|
__name__ = 'lot.lot'
|
||||||
|
|
||||||
line = fields.Many2One('purchase.line',"Purchase")
|
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
|
||||||
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
||||||
lot_move = fields.One2Many('lot.move','lot',"Move")
|
lot_move = fields.One2Many('lot.move','lot',"Move")
|
||||||
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
||||||
@@ -58,6 +59,7 @@ class Lot(metaclass=PoolMeta):
|
|||||||
delta_pr = fields.Numeric("Delta Pr")
|
delta_pr = fields.Numeric("Delta Pr")
|
||||||
delta_amt = fields.Numeric("Delta Amt")
|
delta_amt = fields.Numeric("Delta Amt")
|
||||||
warrant_nb = fields.Char("Warrant Nb")
|
warrant_nb = fields.Char("Warrant Nb")
|
||||||
|
lot_chunk_key = fields.Integer("Chunk key")
|
||||||
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
||||||
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
||||||
pivot = fields.Function(
|
pivot = fields.Function(
|
||||||
@@ -201,7 +203,7 @@ class Lot(metaclass=PoolMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pivot_data['options'] = {
|
pivot_data['options'] = {
|
||||||
"rows": ["lot","ct type","event_date","event","move","curr","rate"],
|
"rows": ["lot","ct type","event_date","event","move","Curr","rate"],
|
||||||
"cols": ["account"],
|
"cols": ["account"],
|
||||||
"aggregatorName": "Sum",
|
"aggregatorName": "Sum",
|
||||||
"vals": ["amount"]
|
"vals": ["amount"]
|
||||||
@@ -578,6 +580,14 @@ class Lot(metaclass=PoolMeta):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_received_move(self):
|
||||||
|
if self.lot_move:
|
||||||
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
||||||
|
for m in lm:
|
||||||
|
if m.move.from_location.type == 'supplier' and m.move.state == 'done':
|
||||||
|
return m.move
|
||||||
|
return None
|
||||||
|
|
||||||
def GetShipment(self,type):
|
def GetShipment(self,type):
|
||||||
if type == 'in':
|
if type == 'in':
|
||||||
m = self.get_current_supplier_move()
|
m = self.get_current_supplier_move()
|
||||||
@@ -1087,6 +1097,7 @@ class LotQt(
|
|||||||
newlot.lot_shipment_internal = self.lot_shipment_internal
|
newlot.lot_shipment_internal = self.lot_shipment_internal
|
||||||
newlot.lot_shipment_out = self.lot_shipment_out
|
newlot.lot_shipment_out = self.lot_shipment_out
|
||||||
newlot.lot_product = self.lot_p.line.product
|
newlot.lot_product = self.lot_p.line.product
|
||||||
|
newlot.lot_chunk_key = l.lot_chunk_key
|
||||||
if self.lot_s:
|
if self.lot_s:
|
||||||
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
||||||
newlot.lot_type = 'physic'
|
newlot.lot_type = 'physic'
|
||||||
@@ -1168,6 +1179,7 @@ class LotQt(
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, lotqts):
|
def validate(cls, lotqts):
|
||||||
super(LotQt, cls).validate(lotqts)
|
super(LotQt, cls).validate(lotqts)
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
#Update Move
|
#Update Move
|
||||||
for lqt in lotqts:
|
for lqt in lotqts:
|
||||||
cls.updateMove(lqt.lot_move)
|
cls.updateMove(lqt.lot_move)
|
||||||
@@ -1177,23 +1189,23 @@ class LotQt(
|
|||||||
if lqt.lot_p and lqt.lot_quantity > 0:
|
if lqt.lot_p and lqt.lot_quantity > 0:
|
||||||
pl = lqt.lot_p.line
|
pl = lqt.lot_p.line
|
||||||
logger.info("VALIDATE_LQT_PL:%s",pl)
|
logger.info("VALIDATE_LQT_PL:%s",pl)
|
||||||
Pnl = Pool().get('valuation.valuation')
|
# Pnl = Pool().get('valuation.valuation')
|
||||||
pnl = Pnl.search([('line','=',pl.id)])
|
# pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
|
||||||
if pnl:
|
# if pnl:
|
||||||
Pnl.delete(pnl)
|
# Pnl.delete(pnl)
|
||||||
pnl_lines = []
|
# pnl_lines = []
|
||||||
pnl_lines.extend(pl.get_pnl_fee_lines())
|
# pnl_lines.extend(pl.get_pnl_fee_lines())
|
||||||
pnl_lines.extend(pl.get_pnl_price_lines())
|
# pnl_lines.extend(pl.get_pnl_price_lines())
|
||||||
pnl_lines.extend(pl.get_pnl_der_lines())
|
# pnl_lines.extend(pl.get_pnl_der_lines())
|
||||||
Pnl.save(pnl_lines)
|
# Pnl.save(pnl_lines)
|
||||||
|
|
||||||
#Open position update
|
#Open position update
|
||||||
if pl.quantity_theorical:
|
# if pl.quantity_theorical:
|
||||||
OpenPosition = Pool().get('open.position')
|
# OpenPosition = Pool().get('open.position')
|
||||||
OpenPosition.create_from_purchase_line(pl)
|
# OpenPosition.create_from_purchase_line(pl)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None):
|
def getQuery(cls,purchase=None,sale=None,shipment=None,type=None,state=None,qttype=None,supplier=None,client=None,ps=None,lot_status=None,group=None,product=None,location=None,origin=None,finished=False):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
LotQt = pool.get('lot.qt')
|
LotQt = pool.get('lot.qt')
|
||||||
lqt = LotQt.__table__()
|
lqt = LotQt.__table__()
|
||||||
@@ -1241,8 +1253,12 @@ class LotQt(
|
|||||||
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
||||||
if ps == 'P':
|
if ps == 'P':
|
||||||
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
|
wh &= ((lqt.lot_p != None) & (lqt.lot_s == None))
|
||||||
|
if not finished:
|
||||||
|
wh &= (pl.finished == False)
|
||||||
elif ps == 'S':
|
elif ps == 'S':
|
||||||
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
|
wh &= (((lqt.lot_s != None) & (lqt.lot_p == None)) | ((lqt.lot_s != None) & (lqt.lot_p != None) & (lp.lot_type == 'virtual')))
|
||||||
|
if not finished:
|
||||||
|
wh &= (sl.finished == False)
|
||||||
if purchase:
|
if purchase:
|
||||||
wh &= (pu.id == purchase)
|
wh &= (pu.id == purchase)
|
||||||
if sale:
|
if sale:
|
||||||
@@ -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_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_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_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.lot_container),else_=ls.lot_container).as_('r_lot_container'),
|
||||||
Case((lp.id>0, lp.line),else_=None).as_('r_line'),
|
Case((lp.id>0, lp.line),else_=None).as_('r_line'),
|
||||||
Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
|
Case(
|
||||||
Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
|
(((lqt.lot_s != None) & (lqt.lot_p == None) & (sl.id > 0)),
|
||||||
Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
|
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'),
|
(MaQt + AvQt).as_('r_tot'),
|
||||||
pu.party.as_('r_supplier'),
|
pu.party.as_('r_supplier'),
|
||||||
sa.party.as_('r_client'),
|
sa.party.as_('r_client'),
|
||||||
@@ -1423,13 +1444,14 @@ class LotQt(
|
|||||||
lp.lot_av.as_("r_lot_av"),
|
lp.lot_av.as_("r_lot_av"),
|
||||||
lp.lot_premium.as_("r_lot_premium"),
|
lp.lot_premium.as_("r_lot_premium"),
|
||||||
lp.lot_premium_sale.as_("r_lot_premium_sale"),
|
lp.lot_premium_sale.as_("r_lot_premium_sale"),
|
||||||
lp.lot_parent.as_("r_lot_parent"),
|
lp.lot_parent.as_("r_lot_parent"),
|
||||||
lp.lot_himself.as_("r_lot_himself"),
|
lp.lot_himself.as_("r_lot_himself"),
|
||||||
lp.lot_container.as_("r_lot_container"),
|
lp.lot_container.as_("r_lot_container"),
|
||||||
lp.line.as_("r_line"),
|
lp.line.as_("r_line"),
|
||||||
Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
|
pl.del_period.as_("r_del_period"),
|
||||||
Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
|
Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
|
||||||
lp.sale_line.as_("r_sale_line"),
|
Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
|
||||||
|
lp.sale_line.as_("r_sale_line"),
|
||||||
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||||
pu.party.as_("r_supplier"),
|
pu.party.as_("r_supplier"),
|
||||||
sa.party.as_("r_client"),
|
sa.party.as_("r_client"),
|
||||||
@@ -1488,13 +1510,14 @@ class LotQt(
|
|||||||
Max(lp.lot_av).as_("r_lot_av"),
|
Max(lp.lot_av).as_("r_lot_av"),
|
||||||
Avg(lp.lot_premium).as_("r_lot_premium"),
|
Avg(lp.lot_premium).as_("r_lot_premium"),
|
||||||
Literal(None).as_("r_lot_premium_sale"),
|
Literal(None).as_("r_lot_premium_sale"),
|
||||||
Literal(None).as_("r_lot_parent"),
|
Literal(None).as_("r_lot_parent"),
|
||||||
Literal(None).as_("r_lot_himself"),
|
Literal(None).as_("r_lot_himself"),
|
||||||
Max(lp.lot_container).as_("r_lot_container"),
|
Max(lp.lot_container).as_("r_lot_container"),
|
||||||
lp.line.as_("r_line"),
|
lp.line.as_("r_line"),
|
||||||
Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
|
Max(pl.del_period).as_("r_del_period"),
|
||||||
Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
|
Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
|
||||||
lp.sale_line.as_("r_sale_line"),
|
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"),
|
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
||||||
Max(pu.party).as_("r_supplier"),
|
Max(pu.party).as_("r_supplier"),
|
||||||
Max(sa.party).as_("r_client"),
|
Max(sa.party).as_("r_client"),
|
||||||
@@ -1541,13 +1564,14 @@ class LotQt(
|
|||||||
union.r_lot_av.as_("r_lot_av"),
|
union.r_lot_av.as_("r_lot_av"),
|
||||||
union.r_lot_premium.as_("r_lot_premium"),
|
union.r_lot_premium.as_("r_lot_premium"),
|
||||||
union.r_lot_premium_sale.as_("r_lot_premium_sale"),
|
union.r_lot_premium_sale.as_("r_lot_premium_sale"),
|
||||||
union.r_lot_parent.as_("r_lot_parent"),
|
union.r_lot_parent.as_("r_lot_parent"),
|
||||||
union.r_lot_himself.as_("r_lot_himself"),
|
union.r_lot_himself.as_("r_lot_himself"),
|
||||||
union.r_lot_container.as_("r_lot_container"),
|
union.r_lot_container.as_("r_lot_container"),
|
||||||
union.r_line.as_("r_line"),
|
union.r_line.as_("r_line"),
|
||||||
union.r_purchase.as_("r_purchase"),
|
union.r_del_period.as_("r_del_period"),
|
||||||
union.r_sale.as_("r_sale"),
|
union.r_purchase.as_("r_purchase"),
|
||||||
union.r_sale_line.as_("r_sale_line"),
|
union.r_sale.as_("r_sale"),
|
||||||
|
union.r_sale_line.as_("r_sale_line"),
|
||||||
union.r_tot.as_("r_tot"),
|
union.r_tot.as_("r_tot"),
|
||||||
union.r_supplier.as_("r_supplier"),
|
union.r_supplier.as_("r_supplier"),
|
||||||
union.r_client.as_("r_client"),
|
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_out = fields.Many2One('stock.shipment.out', "Shipment Out")
|
||||||
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
|
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
|
||||||
r_lot_move = fields.Many2One('stock.move', "Move")
|
r_lot_move = fields.Many2One('stock.move', "Move")
|
||||||
r_lot_parent = fields.Many2One('lot.lot',"Parent")
|
r_lot_parent = fields.Many2One('lot.lot',"Parent")
|
||||||
r_lot_himself = fields.Many2One('lot.lot',"Lot")
|
r_lot_himself = fields.Many2One('lot.lot',"Lot")
|
||||||
r_lot_container = fields.Char("Container")
|
r_lot_container = fields.Char("Container")
|
||||||
r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
|
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 = 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_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_del_period = fields.Many2One('product.month', "Delivery Period")
|
||||||
r_sale = fields.Many2One('sale.sale',"Sale")
|
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_tot = fields.Numeric("Qt tot", digits='r_lot_unit_line')
|
||||||
r_supplier = fields.Many2One('party.party',"Supplier")
|
r_supplier = fields.Many2One('party.party',"Supplier")
|
||||||
r_client = fields.Many2One('party.party',"Client")
|
r_client = fields.Many2One('party.party',"Client")
|
||||||
@@ -1832,7 +1857,8 @@ class LotReport(
|
|||||||
supplier = context.get('supplier')
|
supplier = context.get('supplier')
|
||||||
#asof = context.get('asof')
|
#asof = context.get('asof')
|
||||||
#todate = context.get('todate')
|
#todate = context.get('todate')
|
||||||
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin)
|
finished = context.get('finished')
|
||||||
|
query = LotQt.getQuery(purchase,sale,shipment,type,state,None,supplier,None,None,wh,group,product,location,origin,finished)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1922,6 +1948,12 @@ class LotContext(ModelView):
|
|||||||
('pnl', 'Pnl'),
|
('pnl', 'Pnl'),
|
||||||
],'Mode')
|
],'Mode')
|
||||||
|
|
||||||
|
finished = fields.Boolean("Display finished")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_finished(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_asof(cls):
|
def default_asof(cls):
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
@@ -2002,17 +2034,24 @@ class LotShipping(Wizard):
|
|||||||
if r.r_lot_shipment_in:
|
if r.r_lot_shipment_in:
|
||||||
raise UserError("Please unlink before linking to a new shipment !")
|
raise UserError("Please unlink before linking to a new shipment !")
|
||||||
else:
|
else:
|
||||||
shipped_quantity = Decimal(r.r_lot_quantity)
|
shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
|
||||||
|
logger.info("LotShipping:%s",shipped_quantity)
|
||||||
shipment_origin = None
|
shipment_origin = None
|
||||||
if self.ship.quantity:
|
if self.ship.quantity:
|
||||||
shipped_quantity = self.ship.quantity
|
shipped_quantity = self.ship.quantity
|
||||||
if shipped_quantity == 0:
|
if shipped_quantity == 0:
|
||||||
shipped_quantity = Decimal(r.r_lot_matched)
|
shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
|
||||||
if self.ship.shipment == 'in':
|
if self.ship.shipment == 'in':
|
||||||
|
if not self.ship.shipment_in:
|
||||||
|
UserError("Shipment not known!")
|
||||||
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
|
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
|
||||||
elif self.ship.shipment == 'out':
|
elif self.ship.shipment == 'out':
|
||||||
|
if not self.ship.shipment_out:
|
||||||
|
UserError("Shipment not known!")
|
||||||
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
|
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
|
||||||
elif self.ship.shipment == 'int':
|
elif self.ship.shipment == 'int':
|
||||||
|
if not self.ship.shipment_internal:
|
||||||
|
UserError("Shipment not known!")
|
||||||
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
|
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
|
||||||
if r.id < 10000000 :
|
if r.id < 10000000 :
|
||||||
l = Lot(r.id)
|
l = Lot(r.id)
|
||||||
@@ -2030,15 +2069,22 @@ class LotShipping(Wizard):
|
|||||||
move = Move(l.move)
|
move = Move(l.move)
|
||||||
move.shipment = shipment_origin
|
move.shipment = shipment_origin
|
||||||
Move.save([move])
|
Move.save([move])
|
||||||
|
linked_transit_move = move.get_linked_transit_move()
|
||||||
|
if linked_transit_move:
|
||||||
|
linked_transit_move.shipment = shipment_origin
|
||||||
|
Move.save([linked_transit_move])
|
||||||
#Decrease forecasted virtual part shipped
|
#Decrease forecasted virtual part shipped
|
||||||
vlot_p = l.getVlot_p()
|
vlot_p = l.getVlot_p()
|
||||||
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
||||||
l.lot_av = 'reserved'
|
l.lot_av = 'reserved'
|
||||||
Lot.save([l])
|
Lot.save([l])
|
||||||
|
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
||||||
|
Lot.save([l])
|
||||||
else:
|
else:
|
||||||
lqt = LotQt(r.id - 10000000)
|
lqt = LotQt(r.id - 10000000)
|
||||||
#Increase forecasted virtual part shipped
|
#Increase forecasted virtual part shipped
|
||||||
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
||||||
|
logger.info("LotShipping2:%s",shipped_quantity)
|
||||||
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
||||||
#Decrease forecasted virtual part non shipped
|
#Decrease forecasted virtual part non shipped
|
||||||
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
||||||
@@ -2442,6 +2488,7 @@ class LotAddLine(ModelView):
|
|||||||
lot_gross_quantity = fields.Numeric("Gross weight")
|
lot_gross_quantity = fields.Numeric("Gross weight")
|
||||||
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
||||||
lot_premium = fields.Numeric("Premium")
|
lot_premium = fields.Numeric("Premium")
|
||||||
|
lot_chunk_key = fields.Integer("Chunk key")
|
||||||
|
|
||||||
# @fields.depends('lot_qt')
|
# @fields.depends('lot_qt')
|
||||||
# def on_change_with_lot_quantity(self):
|
# def on_change_with_lot_quantity(self):
|
||||||
@@ -2604,6 +2651,17 @@ class LotInvoice(Wizard):
|
|||||||
|
|
||||||
invoicing = StateTransition()
|
invoicing = StateTransition()
|
||||||
|
|
||||||
|
message = StateView(
|
||||||
|
'purchase.create_prepayment.message',
|
||||||
|
'purchase_trade.create_prepayment_message_form',
|
||||||
|
[
|
||||||
|
Button('OK', 'end', 'tryton-ok'),
|
||||||
|
Button('See Invoice', 'see_invoice', 'tryton-go-next'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
see_invoice = StateAction('account_invoice.act_invoice_form')
|
||||||
|
|
||||||
def transition_start(self):
|
def transition_start(self):
|
||||||
return 'inv'
|
return 'inv'
|
||||||
|
|
||||||
@@ -2650,7 +2708,7 @@ class LotInvoice(Wizard):
|
|||||||
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
|
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
|
||||||
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
|
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
|
||||||
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
|
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
|
||||||
val['lot_unit'] = lot.lot_unit_line.id
|
val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
|
||||||
unit = val['lot_unit']
|
unit = val['lot_unit']
|
||||||
val['lot_currency'] = lot.lot_price_ct_symbol
|
val['lot_currency'] = lot.lot_price_ct_symbol
|
||||||
lot_p.append(val)
|
lot_p.append(val)
|
||||||
@@ -2666,6 +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_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
|
||||||
val_s['lot_diff_amount'] = val_s['lot_amount'] - Decimal(lot.sale_invoice_line_prov.amount)
|
val_s['lot_diff_amount'] = val_s['lot_amount'] - Decimal(lot.sale_invoice_line_prov.amount)
|
||||||
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
|
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
|
||||||
|
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
|
||||||
lot_s.append(val_s)
|
lot_s.append(val_s)
|
||||||
if line:
|
if line:
|
||||||
if line.fees:
|
if line.fees:
|
||||||
@@ -2724,35 +2783,64 @@ class LotInvoice(Wizard):
|
|||||||
'action': act
|
'action': act
|
||||||
}
|
}
|
||||||
|
|
||||||
def transition_invoicing(self):
|
def transition_invoicing(self):
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
Purchase = Pool().get('purchase.purchase')
|
Purchase = Pool().get('purchase.purchase')
|
||||||
Sale = Pool().get('sale.sale')
|
Sale = Pool().get('sale.sale')
|
||||||
lots = []
|
lots = []
|
||||||
action = self.inv.action
|
purchases = []
|
||||||
for r in self.records:
|
sales = []
|
||||||
purchase = r.r_line.purchase
|
action = self.inv.action
|
||||||
sale = None
|
for r in self.records:
|
||||||
if r.r_sale_line:
|
purchase = r.r_line.purchase if r.r_line else None
|
||||||
sale = r.r_sale_line.sale
|
sale = r.r_sale_line.sale if r.r_sale_line else None
|
||||||
lot = Lot(r.r_lot_p)
|
if purchase and purchase not in purchases:
|
||||||
# if lot.move == None:
|
purchases.append(purchase)
|
||||||
# Warning = Pool().get('res.user.warning')
|
if sale and sale not in sales:
|
||||||
# warning_name = Warning.format("Lot not confirmed", [])
|
sales.append(sale)
|
||||||
# if Warning.check(warning_name):
|
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,
|
# raise QtWarning(warning_name,
|
||||||
# "Lot not confirmed, click yes to confirm and invoice")
|
# "Lot not confirmed, click yes to confirm and invoice")
|
||||||
# continue
|
# continue
|
||||||
if lot.invoice_line:
|
if lot.invoice_line:
|
||||||
continue
|
continue
|
||||||
lots.append(lot)
|
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':
|
def default_message(self, fields):
|
||||||
Purchase._process_invoice([purchase],lots,action,self.inv.pp_pur)
|
return {
|
||||||
else:
|
'message': 'The invoice has been successfully created.',
|
||||||
if sale:
|
}
|
||||||
Sale._process_invoice([sale],lots,action,self.inv.pp_sale)
|
|
||||||
return 'end'
|
def do_see_invoice(self, action):
|
||||||
|
action['views'].reverse() # pour ouvrir en form directement
|
||||||
|
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
|
||||||
|
return action, {'res_id':self.message.invoice.id}
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
@@ -2962,25 +3050,26 @@ class LotWeighing(Wizard):
|
|||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
context = Transaction().context
|
context = Transaction().context
|
||||||
ids = context.get('active_ids')
|
ids = context.get('active_ids')
|
||||||
for i in ids:
|
for i in ids:
|
||||||
if i > 10000000:
|
if i > 10000000:
|
||||||
raise UserError("Trying to do weighing on open quantity!")
|
raise UserError("Trying to do weighing on open quantity!")
|
||||||
val = {}
|
val = {}
|
||||||
lot = Lot(i)
|
lot = Lot(i)
|
||||||
val['lot'] = lot.id
|
val['lot'] = lot.id
|
||||||
val['lot_name'] = lot.lot_name
|
val['lot_name'] = lot.lot_name
|
||||||
if lot.lot_shipment_in:
|
if lot.lot_shipment_in:
|
||||||
val['lot_shipment_in'] = lot.lot_shipment_in.id
|
val['lot_shipment_in'] = lot.lot_shipment_in.id
|
||||||
if lot.lot_shipment_internal:
|
if lot.lot_shipment_internal:
|
||||||
val['lot_shipment_internal'] = lot.lot_shipment_internal.id
|
val['lot_shipment_internal'] = lot.lot_shipment_internal.id
|
||||||
if lot.lot_shipment_out:
|
if lot.lot_shipment_out:
|
||||||
val['lot_shipment_out'] = lot.lot_shipment_out.id
|
val['lot_shipment_out'] = lot.lot_shipment_out.id
|
||||||
val['lot_product'] = lot.lot_product.id
|
val['lot_product'] = lot.lot_product.id
|
||||||
val['lot_quantity'] = lot.lot_quantity
|
val['lot_qt'] = lot.lot_qt
|
||||||
val['lot_gross_quantity'] = lot.lot_gross_quantity
|
val['lot_quantity'] = lot.lot_quantity
|
||||||
val['lot_unit'] = lot.lot_unit.id
|
val['lot_gross_quantity'] = lot.lot_gross_quantity
|
||||||
val['lot_unit_line'] = lot.lot_unit_line.id
|
val['lot_unit'] = lot.lot_unit.id
|
||||||
lot_p.append(val)
|
val['lot_unit_line'] = lot.lot_unit_line.id
|
||||||
|
lot_p.append(val)
|
||||||
return {
|
return {
|
||||||
'lot_p': lot_p,
|
'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)])
|
lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
|
||||||
if lhs:
|
if lhs:
|
||||||
lh = lhs[0]
|
lh = lhs[0]
|
||||||
else:
|
else:
|
||||||
lh = LotHist()
|
lh = LotHist()
|
||||||
lh.lot = l.lot
|
lh.lot = l.lot
|
||||||
lh.quantity_type = self.w.lot_state
|
lh.quantity_type = self.w.lot_state
|
||||||
lh.quantity = round(l.lot_quantity_new,5)
|
lh.quantity = round(l.lot_quantity_new,5)
|
||||||
lh.gross_quantity = round(l.lot_gross_quantity_new,5)
|
lh.gross_quantity = round(l.lot_gross_quantity_new,5)
|
||||||
LotHist.save([lh])
|
LotHist.save([lh])
|
||||||
|
l.lot.lot_qt = l.lot_qt
|
||||||
if self.w.lot_update_state :
|
|
||||||
l.lot.lot_state = self.w.lot_state
|
if self.w.lot_update_state :
|
||||||
Lot.save([l.lot])
|
l.lot.lot_state = self.w.lot_state
|
||||||
|
Lot.save([l.lot])
|
||||||
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
|
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
|
||||||
if diff != 0 :
|
if diff != 0 :
|
||||||
#need to update virtual part
|
#need to update virtual part
|
||||||
@@ -3040,12 +3130,13 @@ class LotWeighingLot(ModelView):
|
|||||||
lot_name = fields.Char("Name",readonly=True)
|
lot_name = fields.Char("Name",readonly=True)
|
||||||
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
||||||
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
||||||
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
||||||
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
||||||
lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
|
lot_qt = fields.Integer("Qt")
|
||||||
lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
|
lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
|
||||||
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
|
||||||
lot_unit_line = fields.Many2One('product.uom',"Unit",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_quantity_new = fields.Numeric("New net weight",digits=(1,5))
|
||||||
lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
|
lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
|
||||||
lot_shipment_origin = fields.Function(
|
lot_shipment_origin = fields.Function(
|
||||||
@@ -3089,37 +3180,55 @@ class CreateContracts(Wizard):
|
|||||||
def transition_start(self):
|
def transition_start(self):
|
||||||
return 'ct'
|
return 'ct'
|
||||||
|
|
||||||
def default_ct(self, fields):
|
def default_ct(self, fields):
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
Lot = Pool().get('lot.lot')
|
Lot = Pool().get('lot.lot')
|
||||||
context = Transaction().context
|
context = Transaction().context
|
||||||
ids = context.get('active_ids')
|
ids = context.get('active_ids')
|
||||||
unit = None
|
unit = None
|
||||||
product = None
|
product = None
|
||||||
sh_in = None
|
sh_in = None
|
||||||
sh_int = None
|
sh_int = None
|
||||||
sh_out = None
|
sh_out = None
|
||||||
lot = None
|
lot = None
|
||||||
qt = None
|
qt = Decimal(0)
|
||||||
type = None
|
type = None
|
||||||
for i in ids:
|
shipment_in_values = set()
|
||||||
val = {}
|
shipment_internal_values = set()
|
||||||
if i < 10000000:
|
shipment_out_values = set()
|
||||||
raise UserError("You must create contract from an open quantity !")
|
for i in ids:
|
||||||
l = LotQt(i - 10000000)
|
if i < 10000000:
|
||||||
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
raise UserError("You must create contract from an open quantity !")
|
||||||
type = "Sale" if l.lot_p else "Purchase"
|
l = LotQt(i - 10000000)
|
||||||
unit = l.lot_unit.id
|
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
||||||
qt = l.lot_quantity
|
current_type = "Sale" if l.lot_p else "Purchase"
|
||||||
product = ll.lot_product.id
|
if type and current_type != type:
|
||||||
sh_in = l.lot_shipment_in.id if l.lot_shipment_in else None
|
raise UserError("You must select open quantities from the same side.")
|
||||||
sh_int = l.lot_shipment_internal.id if l.lot_shipment_internal else None
|
type = current_type
|
||||||
sh_out = l.lot_shipment_out.id if l.lot_shipment_out else None
|
if product and ll.lot_product.id != product:
|
||||||
lot = ll.id
|
raise UserError("You must select open quantities with the same product.")
|
||||||
|
if unit and l.lot_unit.id != unit:
|
||||||
return {
|
raise UserError("You must select open quantities with the same unit.")
|
||||||
'quantity': qt,
|
unit = l.lot_unit.id
|
||||||
'unit': unit,
|
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,
|
'product': product,
|
||||||
'shipment_in': sh_in,
|
'shipment_in': sh_in,
|
||||||
'shipment_internal': sh_int,
|
'shipment_internal': sh_int,
|
||||||
@@ -3129,136 +3238,13 @@ class CreateContracts(Wizard):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def transition_creating(self):
|
def transition_creating(self):
|
||||||
SaleLine = Pool().get('sale.line')
|
ContractFactory.create_contracts(
|
||||||
Sale = Pool().get('sale.sale')
|
self.ct.contracts,
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
type_=self.ct.type,
|
||||||
Purchase = Pool().get('purchase.purchase')
|
ct=self.ct,
|
||||||
LotQt = Pool().get('lot.qt')
|
)
|
||||||
LotQtHist = Pool().get('lot.qt.hist')
|
|
||||||
LotQtType = Pool().get('lot.qt.type')
|
|
||||||
Lot = Pool().get('lot.lot')
|
|
||||||
Date = Pool().get('ir.date')
|
|
||||||
self.sale_lines = []
|
|
||||||
type = self.ct.type
|
|
||||||
base_contract = self.ct.lot.sale_line.sale if type == 'Purchase' else self.ct.lot.line.purchase
|
|
||||||
for c in self.ct.contracts:
|
|
||||||
contract = Purchase() if type == 'Purchase' else Sale()
|
|
||||||
contract_line = PurchaseLine() if type == 'Purchase' else SaleLine()
|
|
||||||
parts = c.currency_unit.split("_")
|
|
||||||
if int(parts[0]) != 0:
|
|
||||||
contract.currency = int(parts[0])
|
|
||||||
else:
|
|
||||||
contract.currency = 1
|
|
||||||
contract.party = c.party
|
|
||||||
contract.crop = c.crop
|
|
||||||
contract.tol_min = c.tol_min
|
|
||||||
contract.tol_max = c.tol_max
|
|
||||||
if type == 'Purchase':
|
|
||||||
contract.purchase_date = Date.today()
|
|
||||||
else:
|
|
||||||
contract.sale_date = Date.today()
|
|
||||||
contract.reference = c.reference
|
|
||||||
if base_contract.from_location and base_contract.to_location:
|
|
||||||
if type == 'Purchase':
|
|
||||||
contract.to_location = base_contract.from_location
|
|
||||||
else:
|
|
||||||
contract.from_location = base_contract.to_location
|
|
||||||
if base_contract.from_location.type == 'supplier' and base_contract.to_location.type == 'customer':
|
|
||||||
contract.from_location = base_contract.from_location
|
|
||||||
contract.to_location = base_contract.to_location
|
|
||||||
if c.party.wb:
|
|
||||||
contract.wb = c.party.wb
|
|
||||||
if c.party.association:
|
|
||||||
contract.association = c.party.association
|
|
||||||
if type == 'Purchase':
|
|
||||||
if c.party.supplier_payment_term:
|
|
||||||
contract.payment_term = c.party.supplier_payment_term
|
|
||||||
else:
|
|
||||||
if c.party.customer_payment_term:
|
|
||||||
contract.payment_term = c.party.customer_payment_term
|
|
||||||
contract.incoterm = c.incoterm
|
|
||||||
if c.party.addresses:
|
|
||||||
contract.invoice_address = c.party.addresses[0]
|
|
||||||
if type == 'Sale':
|
|
||||||
contract.shipment_address = c.party.addresses[0]
|
|
||||||
contract.__class__.save([contract])
|
|
||||||
contract_line.quantity = c.quantity
|
|
||||||
contract_line.quantity_theorical = c.quantity
|
|
||||||
contract_line.product = self.ct.product
|
|
||||||
contract_line.price_type = c.price_type
|
|
||||||
contract_line.unit = self.ct.unit
|
|
||||||
if type == 'Purchase':
|
|
||||||
contract_line.purchase = contract.id
|
|
||||||
else:
|
|
||||||
contract_line.sale = contract.id
|
|
||||||
contract_line.created_by_code = self.ct.matched
|
|
||||||
contract_line.premium = Decimal(0)
|
|
||||||
if int(parts[0]) == 0:
|
|
||||||
contract_line.enable_linked_currency = True
|
|
||||||
contract_line.linked_currency = 1
|
|
||||||
contract_line.linked_unit = int(parts[1])
|
|
||||||
contract_line.linked_price = c.price
|
|
||||||
contract_line.unit_price = contract_line.get_price_linked_currency()
|
|
||||||
else:
|
|
||||||
contract_line.unit_price = c.price if c.price else Decimal(0)
|
|
||||||
contract_line.del_period = c.del_period
|
|
||||||
contract_line.from_del = c.from_del
|
|
||||||
contract_line.to_del = c.to_del
|
|
||||||
contract_line.__class__.save([contract_line])
|
|
||||||
logger.info("CREATE_ID:%s",contract.id)
|
|
||||||
logger.info("CREATE_LINE_ID:%s",contract_line.id)
|
|
||||||
if self.ct.matched:
|
|
||||||
lot = Lot()
|
|
||||||
if type == 'Purchase':
|
|
||||||
lot.line = contract_line.id
|
|
||||||
else:
|
|
||||||
lot.sale_line = contract_line.id
|
|
||||||
lot.lot_qt = None
|
|
||||||
lot.lot_unit = None
|
|
||||||
lot.lot_unit_line = contract_line.unit
|
|
||||||
lot.lot_quantity = round(contract_line.quantity,5)
|
|
||||||
lot.lot_gross_quantity = None
|
|
||||||
lot.lot_status = 'forecast'
|
|
||||||
lot.lot_type = 'virtual'
|
|
||||||
lot.lot_product = contract_line.product
|
|
||||||
lqtt = LotQtType.search([('sequence','=',1)])
|
|
||||||
if lqtt:
|
|
||||||
lqh = LotQtHist()
|
|
||||||
lqh.quantity_type = lqtt[0]
|
|
||||||
lqh.quantity = round(lot.lot_quantity,5)
|
|
||||||
lqh.gross_quantity = round(lot.lot_quantity,5)
|
|
||||||
lot.lot_hist = [lqh]
|
|
||||||
Lot.save([lot])
|
|
||||||
vlot = self.ct.lot
|
|
||||||
shipment_origin = None
|
|
||||||
if self.ct.shipment_in:
|
|
||||||
shipment_origin = 'stock.shipment.in,' + str(self.ct.shipment_in.id)
|
|
||||||
elif self.ct.shipment_internal:
|
|
||||||
shipment_origin = 'stock.shipment.internal,' + str(self.ct.shipment_internal.id)
|
|
||||||
elif self.ct.shipment_out:
|
|
||||||
shipment_origin = 'stock.shipment.out,' + str(self.ct.shipment_out.id)
|
|
||||||
|
|
||||||
qt = c.quantity
|
|
||||||
if type == 'Purchase':
|
|
||||||
if not lot.updateVirtualPart(qt,shipment_origin,vlot):
|
|
||||||
lot.createVirtualPart(qt,shipment_origin,vlot)
|
|
||||||
#Decrease forecasted virtual part non matched
|
|
||||||
lot.updateVirtualPart(-qt,shipment_origin,vlot,'only sale')
|
|
||||||
else:
|
|
||||||
if not vlot.updateVirtualPart(qt,shipment_origin,lot):
|
|
||||||
vlot.createVirtualPart(qt,shipment_origin,lot)
|
|
||||||
#Decrease forecasted virtual part non matched
|
|
||||||
vlot.updateVirtualPart(-qt,shipment_origin,None)
|
|
||||||
|
|
||||||
|
|
||||||
return 'end'
|
return 'end'
|
||||||
|
|
||||||
# def do_matching(self, action):
|
|
||||||
# return action, {
|
|
||||||
# 'ids': self.sale_lines,
|
|
||||||
# 'model': str(self.ct.lot.id),
|
|
||||||
# }
|
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
return 'reload'
|
return 'reload'
|
||||||
|
|
||||||
@@ -3288,7 +3274,6 @@ class ContractsStart(ModelView):
|
|||||||
def default_matched(cls):
|
def default_matched(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ContractDetail(ModelView):
|
class ContractDetail(ModelView):
|
||||||
|
|
||||||
"Contract Detail"
|
"Contract Detail"
|
||||||
@@ -3296,26 +3281,29 @@ class ContractDetail(ModelView):
|
|||||||
|
|
||||||
category = fields.Integer("Category")
|
category = fields.Integer("Category")
|
||||||
cd = fields.Many2One('contracts.start',"Contracts")
|
cd = fields.Many2One('contracts.start',"Contracts")
|
||||||
party = fields.Many2One('party.party',"Party",domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
||||||
currency = fields.Many2One('currency.currency',"Currency")
|
currency = fields.Many2One('currency.currency',"Currency", required=True)
|
||||||
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm")
|
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
|
||||||
quantity = fields.Numeric("Quantity",digits=(1,5))
|
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
|
||||||
unit = fields.Many2One('product.uom',"Unit")
|
unit = fields.Many2One('product.uom',"Unit", required=True)
|
||||||
qt_unit = fields.Many2One('product.uom',"Unit")
|
qt_unit = fields.Many2One('product.uom',"Unit")
|
||||||
tol_min = fields.Numeric("Tol - in %")
|
tol_min = fields.Numeric("Tol - in %", required=True)
|
||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %", required=True)
|
||||||
crop = fields.Many2One('purchase.crop',"Crop")
|
crop = fields.Many2One('purchase.crop',"Crop")
|
||||||
del_period = fields.Many2One('product.month',"Delivery Period")
|
del_period = fields.Many2One('product.month',"Delivery Period")
|
||||||
from_del = fields.Date("From")
|
from_del = fields.Date("From")
|
||||||
to_del = fields.Date("To")
|
to_del = fields.Date("To")
|
||||||
price = fields.Numeric("Price",digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
|
price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
|
||||||
price_type = price_type = fields.Selection([
|
price_type = price_type = fields.Selection([
|
||||||
('cash', 'Cash Price'),
|
('cash', 'Cash Price'),
|
||||||
('priced', 'Priced'),
|
('priced', 'Priced'),
|
||||||
('basis', 'Basis'),
|
('basis', 'Basis'),
|
||||||
], 'Price type')
|
], 'Price type', required=True)
|
||||||
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
||||||
reference = fields.Char("Reference")
|
reference = fields.Char("Reference")
|
||||||
|
from_location = fields.Many2One('stock.location',"From location")
|
||||||
|
to_location = fields.Many2One('stock.location',"To location")
|
||||||
|
payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_category(cls):
|
def default_category(cls):
|
||||||
@@ -3372,7 +3360,7 @@ class ContractDetail(ModelView):
|
|||||||
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
|
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
|
||||||
return lqt.lot_p.line.purchase.crop.id
|
return lqt.lot_p.line.purchase.crop.id
|
||||||
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
|
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
|
||||||
return lqt.lot_s.line.sale.crop.id
|
return lqt.lot_s.sale_line.sale.crop.id
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_currency(cls):
|
def default_currency(cls):
|
||||||
@@ -3415,4 +3403,4 @@ class ContractDetail(ModelView):
|
|||||||
if self.del_period:
|
if self.del_period:
|
||||||
self.from_del = self.del_period.beg_date
|
self.from_del = self.del_period.beg_date
|
||||||
self.to_del = self.del_period.end_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,198 @@
|
|||||||
from trytond.model import ModelSQL, ModelView, fields
|
from trytond.model import ModelSQL, ModelView, fields
|
||||||
from trytond.pool import PoolMeta
|
from trytond.pool import PoolMeta, Pool
|
||||||
from trytond.exceptions import UserError
|
from trytond.exceptions import UserError
|
||||||
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
|
from trytond.modules.purchase_trade.purchase import (TRIGGERS)
|
||||||
|
from trytond.transaction import Transaction
|
||||||
|
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||||
|
from sql import Table
|
||||||
|
from trytond.pyson import Bool, Eval, Id, If
|
||||||
|
|
||||||
__all__ = ['Party']
|
class PartyExecution(ModelSQL,ModelView):
|
||||||
__metaclass__ = PoolMeta
|
"Party Execution"
|
||||||
|
__name__ = 'party.execution'
|
||||||
|
|
||||||
class Party(metaclass=PoolMeta):
|
party = fields.Many2One('party.party',"Party")
|
||||||
__name__ = '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_min = fields.Numeric("Tol - in %")
|
||||||
tol_max = fields.Numeric("Tol + in %")
|
tol_max = fields.Numeric("Tol + in %")
|
||||||
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
|
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
|
||||||
association = fields.Many2One('purchase.association',"Association")
|
association = fields.Many2One('purchase.association',"Association")
|
||||||
|
origin =fields.Char("Origin")
|
||||||
|
execution = fields.One2Many('party.execution','party',"")
|
||||||
|
sla = fields.One2Many('party.execution.sla','party', "Sla")
|
||||||
|
initial = fields.Char("Initials")
|
||||||
|
|
||||||
|
def IsAvailableForControl(self,sh):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_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
|
||||||
|
return None, None, None, None
|
||||||
|
|
||||||
|
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 _ensure_category(cls, party, category_name):
|
||||||
|
if not (party and category_name):
|
||||||
|
return
|
||||||
|
Category = Pool().get('party.category')
|
||||||
|
PartyCategory = Pool().get('party.party-party.category')
|
||||||
|
cat = Category.search(['name', '=', category_name], limit=1)
|
||||||
|
if not cat:
|
||||||
|
return
|
||||||
|
cat = cat[0]
|
||||||
|
existing = PartyCategory.search([
|
||||||
|
('party', '=', party.id),
|
||||||
|
('category', '=', cat.id),
|
||||||
|
], limit=1)
|
||||||
|
if existing:
|
||||||
|
return
|
||||||
|
pc = PartyCategory()
|
||||||
|
pc.party = party.id
|
||||||
|
pc.category = cat.id
|
||||||
|
PartyCategory.save([pc])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getPartyByName(cls, party, category=None):
|
||||||
|
party = party.upper()
|
||||||
|
p = cls.search([('name', '=', party)], limit=1)
|
||||||
|
if p:
|
||||||
|
cls._ensure_category(p[0], category)
|
||||||
|
return p[0]
|
||||||
|
else:
|
||||||
|
p = cls()
|
||||||
|
p.name = party
|
||||||
|
cls.save([p])
|
||||||
|
cls._ensure_category(p, category)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
<record model="ir.ui.view" id="party_view_form">
|
<tryton>
|
||||||
<field name="model">party.party</field>
|
<data>
|
||||||
<field name="inherit" ref="party.party_view_form"/>
|
<record model="ir.ui.view" id="party_view_form">
|
||||||
<field name="name">party_form</field>
|
<field name="model">party.party</field>
|
||||||
</record>
|
<field name="inherit" ref="party.party_view_form"/>
|
||||||
|
<field name="name">party_form</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="party_exec_view_list">
|
||||||
|
<field name="model">party.execution</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">party_exec_tree</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="party_exec_sla_view_form">
|
||||||
|
<field name="model">party.execution.sla</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="name">party_exec_sla_form</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="party_exec_sla_view_list">
|
||||||
|
<field name="model">party.execution.sla</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">party_exec_sla_tree</field>
|
||||||
|
</record>
|
||||||
|
<record model="ir.ui.view" id="party_exec_place_view_form">
|
||||||
|
<field name="model">party.execution.place</field>
|
||||||
|
<field name="type">tree</field>
|
||||||
|
<field name="name">party_exec_place_tree</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</tryton>
|
||||||
@@ -21,6 +21,7 @@ class PaymentTermLine(metaclass=PoolMeta):
|
|||||||
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
|
trigger_event = fields.Selection(TRIGGERS, 'Trigger Event')
|
||||||
|
|
||||||
term_type = fields.Selection([
|
term_type = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('advance', 'Advance'),
|
('advance', 'Advance'),
|
||||||
('cad', 'CAD'),
|
('cad', 'CAD'),
|
||||||
('open', 'Open'),
|
('open', 'Open'),
|
||||||
@@ -30,18 +31,21 @@ class PaymentTermLine(metaclass=PoolMeta):
|
|||||||
|
|
||||||
trigger_offset = fields.Integer('Trigger Offset')
|
trigger_offset = fields.Integer('Trigger Offset')
|
||||||
offset_unit = fields.Selection([
|
offset_unit = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('calendar', 'Calendar Days'),
|
('calendar', 'Calendar Days'),
|
||||||
('business', 'Business Days'),
|
('business', 'Business Days'),
|
||||||
], 'Offset Unit')
|
], 'Offset Unit')
|
||||||
|
|
||||||
eom_flag = fields.Boolean('EOM Flag')
|
eom_flag = fields.Boolean('EOM Flag')
|
||||||
eom_mode = fields.Selection([
|
eom_mode = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('standard', 'Standard'),
|
('standard', 'Standard'),
|
||||||
('before', 'Before EOM'),
|
('before', 'Before EOM'),
|
||||||
('after', 'After EOM'),
|
('after', 'After EOM'),
|
||||||
], 'EOM Mode')
|
], 'EOM Mode')
|
||||||
|
|
||||||
risk_classification = fields.Selection([
|
risk_classification = fields.Selection([
|
||||||
|
(None, ''),
|
||||||
('fully_secured', 'Fully Secured'),
|
('fully_secured', 'Fully Secured'),
|
||||||
('partially_secured', 'Partially Secured'),
|
('partially_secured', 'Partially Secured'),
|
||||||
('unsecured', 'Unsecured'),
|
('unsecured', 'Unsecured'),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user