diff --git a/.hgtags b/.hgtags index 70d57413cd..a33273a04c 100644 --- a/.hgtags +++ b/.hgtags @@ -22,3 +22,294 @@ eeab3256f17f6b9bc111d74d6d7f984e75d70f17 trytond-gis-5.8.0 4d33daf649c10711b782ef82b71e49d1a725ccee trytond-gis-6.0.0 0000000000000000000000000000000000000000 6.0.1 ed86455b7bf99bb7a18309e809adc54e6a4864d8 trytond-gis-6.0.1 +951bb3311b916cb893adc5294cfb3216d2940169 1.0.0 +9f5ce178a7c403a0865fd4b6c1a82a911df0e998 1.2.0 +e2d31d59de7745de70199b7c340d78b7c4775723 1.4.0 +64691214959ac19983f3b3d1dbb89ff693d1545c 1.6.0 +af3a6063b46bf8739a513a06400fbb9858b5869b 1.8.0 +3475040a8fe081b1b8609350186b779cc640fba4 2.0.0 +1df3f0702c621e65279af52d1f87725747ae8fa4 2.2.0 +640af4420c8eace7ee59a432bec54692f4808b20 2.4.0 +36530840a1d0dce354bbaa58ec849a409d306d83 2.6.0 +2893d6c3efd6df3759ab09a28ea16773a160118e 2.8.0 +759c6ff44b90b8c685e9d9a4124bcd85af204649 3.0.0 +b3d84aa2c3a6ed3b76bfb58a40fc687f68824c29 3.2.0 +f0a9fa755cde703be54efd747f376b096ea8316c 3.4.0 +1e4ba5a3f16e6afd59131b49841eb004e706619d 3.6.0 +0a29a9dba7f56fbdef067772eea3f037934999c9 3.8.0 +49a0caa894dae201ed4ec707f277d9859b5521fb 4.0.0 +eb8949ec5bcedba563b5799f1315a0cecfee61f6 4.2.0 +f253ce8ed02f4c46a69b8b2f29311ad8959698be 4.4.0 +e7a6d0e8002237f624ecc8a45a6a472f7baadd08 4.6.0 +321b0104a732419c11aa4926785502e0615b89c9 4.8.0 +e8cadc044d59edff8a2ca73932c723d507517ec8 5.0.0 +ef34e4e92d45ab2b2b2956bbb6aa8b3ca259eb13 5.2.0 +32170ff46339dbc8609225791ab737a0f3eb0291 5.4.0 +5738a22adbb45390167194e4089adc46c6b638a3 5.6.0 +e21456b81972188261b8a6fc876afc18f641c85b 5.8.0 +2164597ffa5f4f0443e4804944b30f5491070990 6.0.0 +12a321529713d3e0b04fa94883baf66bddf76b75 6.0.1 +10e05cdf4fb3fcd545d8b47d03df8cfb78b38a76 6.0.2 +7b3f3676cf9d72147edcdb0cd477b77b1af7af9e 6.0.3 +68ea8ab81fc3d67e3efe925c3694644a46b7c111 6.0.4 +133f0b26de213e0b5668ae891e176de8f79025fc 6.0.5 +1719ce1a669285333424c02c9e3e45fd636b995c 6.0.6 +67fe2d478dbf413873c0919cfd5d6f3f6f4c2275 6.0.7 +ed230154446f04aed350b7fd0d416a80145645ee 6.0.8 +df3e4a189c249ff54e118a4943c03ecaab5fee60 6.0.9 +32f14e8a77caa727f2dfcec93aa15e05f45ad0b6 6.0.10 +6cb11cbdabc3b93d9698fc17653e8cb24b8154d4 6.0.11 +f34c0f0f0010e95ba38d3378f2e86d7e7a966516 6.0.12 +c1e2890b414d6d9c0bae303577fb784c8e8c715b 6.0.13 +78607bfea2986fe335c98a6ef87905cded1f5b2d 6.0.14 +26fba5237ff3ec9aed2d8aebdc1115e595e76c0b 6.0.15 +c4cf78bb401aec7d52a007eb7bda958658edaf95 6.0.16 +246ad7a2addde11a7b3e828ace36cf5fb1bc6696 6.0.17 +ca63f7f2eef1998de82eae76636c00211f0e3a80 6.0.18 +2973349bc8ca02dc5aba4fbf6a2e4ba270af9780 6.0.19 +9d5f5e04e2b0105bf4ca644bf98811d267054514 6.0.20 +221212011e95b8004c93ec76d21f5ffa12e866ee 6.0.21 +3ff41e43408ddccd4ce91170b6536a2f5cd0112b 6.0.22 +7d81a0faef794489d63fb164d0fb2ba2196c878b 6.0.23 +9b36d68c67a48a2203ef3598f5379d708781dc9e 6.0.24 +0000000000000000000000000000000000000000 1.0.0 +951bb3311b916cb893adc5294cfb3216d2940169 trytond-1.0.0 +0000000000000000000000000000000000000000 1.2.0 +9f5ce178a7c403a0865fd4b6c1a82a911df0e998 trytond-1.2.0 +0000000000000000000000000000000000000000 1.4.0 +e2d31d59de7745de70199b7c340d78b7c4775723 trytond-1.4.0 +0000000000000000000000000000000000000000 1.6.0 +64691214959ac19983f3b3d1dbb89ff693d1545c trytond-1.6.0 +0000000000000000000000000000000000000000 1.8.0 +af3a6063b46bf8739a513a06400fbb9858b5869b trytond-1.8.0 +0000000000000000000000000000000000000000 2.0.0 +3475040a8fe081b1b8609350186b779cc640fba4 trytond-2.0.0 +0000000000000000000000000000000000000000 2.2.0 +1df3f0702c621e65279af52d1f87725747ae8fa4 trytond-2.2.0 +0000000000000000000000000000000000000000 2.4.0 +640af4420c8eace7ee59a432bec54692f4808b20 trytond-2.4.0 +0000000000000000000000000000000000000000 2.6.0 +36530840a1d0dce354bbaa58ec849a409d306d83 trytond-2.6.0 +0000000000000000000000000000000000000000 2.8.0 +2893d6c3efd6df3759ab09a28ea16773a160118e trytond-2.8.0 +0000000000000000000000000000000000000000 3.0.0 +759c6ff44b90b8c685e9d9a4124bcd85af204649 trytond-3.0.0 +0000000000000000000000000000000000000000 3.2.0 +b3d84aa2c3a6ed3b76bfb58a40fc687f68824c29 trytond-3.2.0 +0000000000000000000000000000000000000000 3.4.0 +f0a9fa755cde703be54efd747f376b096ea8316c trytond-3.4.0 +0000000000000000000000000000000000000000 3.6.0 +1e4ba5a3f16e6afd59131b49841eb004e706619d trytond-3.6.0 +0000000000000000000000000000000000000000 3.8.0 +0a29a9dba7f56fbdef067772eea3f037934999c9 trytond-3.8.0 +0000000000000000000000000000000000000000 4.0.0 +49a0caa894dae201ed4ec707f277d9859b5521fb trytond-4.0.0 +0000000000000000000000000000000000000000 4.2.0 +eb8949ec5bcedba563b5799f1315a0cecfee61f6 trytond-4.2.0 +0000000000000000000000000000000000000000 4.4.0 +f253ce8ed02f4c46a69b8b2f29311ad8959698be trytond-4.4.0 +0000000000000000000000000000000000000000 4.6.0 +e7a6d0e8002237f624ecc8a45a6a472f7baadd08 trytond-4.6.0 +0000000000000000000000000000000000000000 4.8.0 +321b0104a732419c11aa4926785502e0615b89c9 trytond-4.8.0 +0000000000000000000000000000000000000000 5.0.0 +e8cadc044d59edff8a2ca73932c723d507517ec8 trytond-5.0.0 +0000000000000000000000000000000000000000 5.2.0 +ef34e4e92d45ab2b2b2956bbb6aa8b3ca259eb13 trytond-5.2.0 +0000000000000000000000000000000000000000 5.4.0 +32170ff46339dbc8609225791ab737a0f3eb0291 trytond-5.4.0 +0000000000000000000000000000000000000000 5.6.0 +5738a22adbb45390167194e4089adc46c6b638a3 trytond-5.6.0 +0000000000000000000000000000000000000000 5.8.0 +e21456b81972188261b8a6fc876afc18f641c85b trytond-5.8.0 +0000000000000000000000000000000000000000 6.0.0 +2164597ffa5f4f0443e4804944b30f5491070990 trytond-6.0.0 +0000000000000000000000000000000000000000 6.0.1 +12a321529713d3e0b04fa94883baf66bddf76b75 trytond-6.0.1 +0000000000000000000000000000000000000000 6.0.10 +32f14e8a77caa727f2dfcec93aa15e05f45ad0b6 trytond-6.0.10 +0000000000000000000000000000000000000000 6.0.11 +6cb11cbdabc3b93d9698fc17653e8cb24b8154d4 trytond-6.0.11 +0000000000000000000000000000000000000000 6.0.12 +f34c0f0f0010e95ba38d3378f2e86d7e7a966516 trytond-6.0.12 +0000000000000000000000000000000000000000 6.0.13 +c1e2890b414d6d9c0bae303577fb784c8e8c715b trytond-6.0.13 +0000000000000000000000000000000000000000 6.0.14 +78607bfea2986fe335c98a6ef87905cded1f5b2d trytond-6.0.14 +0000000000000000000000000000000000000000 6.0.15 +26fba5237ff3ec9aed2d8aebdc1115e595e76c0b trytond-6.0.15 +0000000000000000000000000000000000000000 6.0.16 +c4cf78bb401aec7d52a007eb7bda958658edaf95 trytond-6.0.16 +0000000000000000000000000000000000000000 6.0.17 +246ad7a2addde11a7b3e828ace36cf5fb1bc6696 trytond-6.0.17 +0000000000000000000000000000000000000000 6.0.18 +ca63f7f2eef1998de82eae76636c00211f0e3a80 trytond-6.0.18 +0000000000000000000000000000000000000000 6.0.19 +2973349bc8ca02dc5aba4fbf6a2e4ba270af9780 trytond-6.0.19 +0000000000000000000000000000000000000000 6.0.2 +10e05cdf4fb3fcd545d8b47d03df8cfb78b38a76 trytond-6.0.2 +0000000000000000000000000000000000000000 6.0.20 +9d5f5e04e2b0105bf4ca644bf98811d267054514 trytond-6.0.20 +0000000000000000000000000000000000000000 6.0.21 +221212011e95b8004c93ec76d21f5ffa12e866ee trytond-6.0.21 +0000000000000000000000000000000000000000 6.0.22 +3ff41e43408ddccd4ce91170b6536a2f5cd0112b trytond-6.0.22 +0000000000000000000000000000000000000000 6.0.23 +7d81a0faef794489d63fb164d0fb2ba2196c878b trytond-6.0.23 +0000000000000000000000000000000000000000 6.0.24 +9b36d68c67a48a2203ef3598f5379d708781dc9e trytond-6.0.24 +0000000000000000000000000000000000000000 6.0.3 +7b3f3676cf9d72147edcdb0cd477b77b1af7af9e trytond-6.0.3 +0000000000000000000000000000000000000000 6.0.4 +68ea8ab81fc3d67e3efe925c3694644a46b7c111 trytond-6.0.4 +0000000000000000000000000000000000000000 6.0.5 +133f0b26de213e0b5668ae891e176de8f79025fc trytond-6.0.5 +0000000000000000000000000000000000000000 6.0.6 +1719ce1a669285333424c02c9e3e45fd636b995c trytond-6.0.6 +0000000000000000000000000000000000000000 6.0.7 +67fe2d478dbf413873c0919cfd5d6f3f6f4c2275 trytond-6.0.7 +0000000000000000000000000000000000000000 6.0.8 +ed230154446f04aed350b7fd0d416a80145645ee trytond-6.0.8 +0000000000000000000000000000000000000000 6.0.9 +df3e4a189c249ff54e118a4943c03ecaab5fee60 trytond-6.0.9 +923b3073b686c6dc5a921d0561c934adf3eeb879 1.0.0 +9a6434d63616f46520b6fa1b2be31906ff6f4f6a 1.2.0 +de84c1443d4bad908eabb99c381578b988c2117f 1.4.0 +3976912b23dfc5db2525d4c2e55d17f099d9492d 1.6.0 +8aa050da53cbdfabe1353a37da91c011b7fff549 1.8.0 +1876747b3f963ba6011b4bcf6968e681624c6e24 2.0.0 +5364f8ca6cd2cccfb5f7f39c69bb20bae2b5f495 2.2.0 +fe15da23f7333e0e90d2693aadddf6bed53ed602 2.4.0 +d069f9f1cdc3fea39e71415268582abb2b333ea1 2.6.0 +21fbf8bd36096d926c472acfecd3963369204af8 2.8.0 +9197d90b0257ec7c4576a38092fd592b3e87f7a4 3.0.0 +9721289ee2fdaab8b990de9327c3eab87363c88c 3.2.0 +8e5cba59137f9d0a063fa6d7855e09a51bc5889c 3.4.0 +1ac658605d42d82988cfb232afebfe359aba7776 3.6.0 +f4b9c8a84cf61935afec788c3c639982731452ef 3.8.0 +44d8ed92ff90cf98c4c01d37f12d344d9d66e7f1 4.0.0 +94037cff031ef897c9533a579356977c8a3cf695 4.2.0 +763dc5d8e1d7e483edbf9977dd96afc114555555 4.4.0 +d7ed5002e47249d019fbc546f6ac562db44c54f2 4.6.0 +2a2b48ea60fbb5201ea12443000d3a106a2fa27c 4.8.0 +70ade9d1fe04c734fe2dd7d35dd75b13f1af902a 5.0.0 +d5652dee45a43fccc1f33d39050af6e00e073273 5.2.0 +9f3fbb33df184ca19350cc71054dd9afd55f1cb7 5.4.0 +45d3f736ffb4396ee0dac0e2110fcc17237337ba 5.6.0 +df9de0ceeb27b38b165da7526accad0ad6682e86 5.8.0 +d18d03f0c540dfd2aae81dfaac048378a62c0ef6 6.0.0 +6cdc11292120ffb6df929721adfd738c4916a594 6.0.1 +23df79b60988e64c45be329933d803e0add7ae20 6.0.2 +5ab29a68b330847559add6204df341aeec38c223 6.0.3 +4493ce6c2dfcebde5701360689c635dbd6253d10 6.0.4 +0a8b71b80cc781e88f6a5a7467be9cdb3de50562 6.0.5 +be96d41f66a7bae727ecaf072a955ada01cd73e8 6.0.6 +0d0d910f4edfa1e493d8526dea6bc5f2ad78c30e 6.0.7 +1f7935eb899fb1ccdd363a085ba3a7a24349a3be 6.0.8 +375c664763440caa3f3ac4cdff2f694eedab7f7c 6.0.9 +a8f9ff596e3476c4fa73002524a78011b62b7fb8 6.0.10 +986c3f1e1d5ee57983adf34bf84c348abdba5b42 6.0.11 +e2cd492204aa2dd300eaaad5caca022b0114bea2 6.0.12 +1cbf7901b52d92f993ceba19357085386475c08d 6.0.13 +1474928ec1aa12d34e5fd79675bb284890bafd76 6.0.14 +0ee8d81a43a12bdd3064bcc100ef4a35ab86bfdb 6.0.15 +d1f3f6a0d5722332cddd2cb0837a266e5f5a1881 6.0.16 +dd458525c9d4c92eca991b832f06ec46a639c3a4 6.0.17 +40c3f7f6d487197698ac644ef28fa09dbce7e46a 6.0.18 +bd7b3f9a3c2f9cda09c2d5f89ed1c1f92dd78ff3 6.0.19 +4b448222df4c6c37b0223112c0c2c14f09675d34 6.0.20 +90566b9897fd57fb36e427dd7d8f2bbfa0a9ed5f 6.0.21 +0000000000000000000000000000000000000000 1.0.0 +923b3073b686c6dc5a921d0561c934adf3eeb879 tryton-1.0.0 +0000000000000000000000000000000000000000 1.2.0 +9a6434d63616f46520b6fa1b2be31906ff6f4f6a tryton-1.2.0 +0000000000000000000000000000000000000000 1.4.0 +de84c1443d4bad908eabb99c381578b988c2117f tryton-1.4.0 +0000000000000000000000000000000000000000 1.6.0 +3976912b23dfc5db2525d4c2e55d17f099d9492d tryton-1.6.0 +0000000000000000000000000000000000000000 1.8.0 +8aa050da53cbdfabe1353a37da91c011b7fff549 tryton-1.8.0 +0000000000000000000000000000000000000000 2.0.0 +1876747b3f963ba6011b4bcf6968e681624c6e24 tryton-2.0.0 +0000000000000000000000000000000000000000 2.2.0 +5364f8ca6cd2cccfb5f7f39c69bb20bae2b5f495 tryton-2.2.0 +0000000000000000000000000000000000000000 2.4.0 +fe15da23f7333e0e90d2693aadddf6bed53ed602 tryton-2.4.0 +0000000000000000000000000000000000000000 2.6.0 +d069f9f1cdc3fea39e71415268582abb2b333ea1 tryton-2.6.0 +0000000000000000000000000000000000000000 2.8.0 +21fbf8bd36096d926c472acfecd3963369204af8 tryton-2.8.0 +0000000000000000000000000000000000000000 3.0.0 +9197d90b0257ec7c4576a38092fd592b3e87f7a4 tryton-3.0.0 +0000000000000000000000000000000000000000 3.2.0 +9721289ee2fdaab8b990de9327c3eab87363c88c tryton-3.2.0 +0000000000000000000000000000000000000000 3.4.0 +8e5cba59137f9d0a063fa6d7855e09a51bc5889c tryton-3.4.0 +0000000000000000000000000000000000000000 3.6.0 +1ac658605d42d82988cfb232afebfe359aba7776 tryton-3.6.0 +0000000000000000000000000000000000000000 3.8.0 +f4b9c8a84cf61935afec788c3c639982731452ef tryton-3.8.0 +0000000000000000000000000000000000000000 4.0.0 +44d8ed92ff90cf98c4c01d37f12d344d9d66e7f1 tryton-4.0.0 +0000000000000000000000000000000000000000 4.2.0 +94037cff031ef897c9533a579356977c8a3cf695 tryton-4.2.0 +0000000000000000000000000000000000000000 4.4.0 +763dc5d8e1d7e483edbf9977dd96afc114555555 tryton-4.4.0 +0000000000000000000000000000000000000000 4.6.0 +d7ed5002e47249d019fbc546f6ac562db44c54f2 tryton-4.6.0 +0000000000000000000000000000000000000000 4.8.0 +2a2b48ea60fbb5201ea12443000d3a106a2fa27c tryton-4.8.0 +0000000000000000000000000000000000000000 5.0.0 +70ade9d1fe04c734fe2dd7d35dd75b13f1af902a tryton-5.0.0 +0000000000000000000000000000000000000000 5.2.0 +d5652dee45a43fccc1f33d39050af6e00e073273 tryton-5.2.0 +0000000000000000000000000000000000000000 5.4.0 +9f3fbb33df184ca19350cc71054dd9afd55f1cb7 tryton-5.4.0 +0000000000000000000000000000000000000000 5.6.0 +45d3f736ffb4396ee0dac0e2110fcc17237337ba tryton-5.6.0 +0000000000000000000000000000000000000000 5.8.0 +df9de0ceeb27b38b165da7526accad0ad6682e86 tryton-5.8.0 +0000000000000000000000000000000000000000 6.0.0 +d18d03f0c540dfd2aae81dfaac048378a62c0ef6 tryton-6.0.0 +0000000000000000000000000000000000000000 6.0.1 +6cdc11292120ffb6df929721adfd738c4916a594 tryton-6.0.1 +0000000000000000000000000000000000000000 6.0.10 +a8f9ff596e3476c4fa73002524a78011b62b7fb8 tryton-6.0.10 +0000000000000000000000000000000000000000 6.0.11 +986c3f1e1d5ee57983adf34bf84c348abdba5b42 tryton-6.0.11 +0000000000000000000000000000000000000000 6.0.12 +e2cd492204aa2dd300eaaad5caca022b0114bea2 tryton-6.0.12 +0000000000000000000000000000000000000000 6.0.13 +1cbf7901b52d92f993ceba19357085386475c08d tryton-6.0.13 +0000000000000000000000000000000000000000 6.0.14 +1474928ec1aa12d34e5fd79675bb284890bafd76 tryton-6.0.14 +0000000000000000000000000000000000000000 6.0.15 +0ee8d81a43a12bdd3064bcc100ef4a35ab86bfdb tryton-6.0.15 +0000000000000000000000000000000000000000 6.0.16 +d1f3f6a0d5722332cddd2cb0837a266e5f5a1881 tryton-6.0.16 +0000000000000000000000000000000000000000 6.0.17 +dd458525c9d4c92eca991b832f06ec46a639c3a4 tryton-6.0.17 +0000000000000000000000000000000000000000 6.0.18 +40c3f7f6d487197698ac644ef28fa09dbce7e46a tryton-6.0.18 +0000000000000000000000000000000000000000 6.0.19 +bd7b3f9a3c2f9cda09c2d5f89ed1c1f92dd78ff3 tryton-6.0.19 +0000000000000000000000000000000000000000 6.0.2 +23df79b60988e64c45be329933d803e0add7ae20 tryton-6.0.2 +0000000000000000000000000000000000000000 6.0.20 +4b448222df4c6c37b0223112c0c2c14f09675d34 tryton-6.0.20 +0000000000000000000000000000000000000000 6.0.21 +90566b9897fd57fb36e427dd7d8f2bbfa0a9ed5f tryton-6.0.21 +0000000000000000000000000000000000000000 6.0.3 +5ab29a68b330847559add6204df341aeec38c223 tryton-6.0.3 +0000000000000000000000000000000000000000 6.0.4 +4493ce6c2dfcebde5701360689c635dbd6253d10 tryton-6.0.4 +0000000000000000000000000000000000000000 6.0.5 +0a8b71b80cc781e88f6a5a7467be9cdb3de50562 tryton-6.0.5 +0000000000000000000000000000000000000000 6.0.6 +be96d41f66a7bae727ecaf072a955ada01cd73e8 tryton-6.0.6 +0000000000000000000000000000000000000000 6.0.7 +0d0d910f4edfa1e493d8526dea6bc5f2ad78c30e tryton-6.0.7 +0000000000000000000000000000000000000000 6.0.8 +1f7935eb899fb1ccdd363a085ba3a7a24349a3be tryton-6.0.8 +0000000000000000000000000000000000000000 6.0.9 +375c664763440caa3f3ac4cdff2f694eedab7f7c tryton-6.0.9 diff --git a/tryton/.drone.yml b/tryton/.drone.yml new file mode 100644 index 0000000000..1e33a514f1 --- /dev/null +++ b/tryton/.drone.yml @@ -0,0 +1,36 @@ +clone: + hg: + image: plugins/hg + environment: + - HG_SHARE_POOL=/root/.cache/hg + volumes: + - cache:/root/.cache + +pipeline: + tox: + image: ${IMAGE} + environment: + - CFLAGS=-O0 + - TOX_TESTENV_PASSENV=CFLAGS CI_BUILD_NUMBER CI_JOB_NUMBER CI_JOB_ID + commands: + - pip install tox + - tox -e "${TOXENV}" + volumes: + - cache:/root/.cache + check_dist: + image: ${IMAGE} + commands: + - pip install twine + - python setup.py sdist + - twine check dist/* + +matrix: + include: + - IMAGE: tryton/tryton-test:3.6 + TOXENV: py36 + - IMAGE: tryton/tryton-test:3.7 + TOXENV: py37 + - IMAGE: tryton/tryton-test:3.8 + TOXENV: py38 + - IMAGE: tryton/tryton-test:3.9 + TOXENV: py39 diff --git a/tryton/.flake8 b/tryton/.flake8 new file mode 100644 index 0000000000..fa6b4f66d1 --- /dev/null +++ b/tryton/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore=E123,E124,E126,E128,E741,W503 diff --git a/tryton/CHANGELOG b/tryton/CHANGELOG new file mode 100644 index 0000000000..66b0766db6 --- /dev/null +++ b/tryton/CHANGELOG @@ -0,0 +1,507 @@ +Version 6.0.21 - 2022-11-17 +--------------------------- +* Bug fixes (see mercurial logs for details) + +Version 6.0.20 - 2022-11-05 +--------------------------- +* Bug fixes (see mercurial logs for details) + +Version 6.0.19 - 2022-10-17 +* Bug fixes (see mercurial logs for details) + +Version 6.0.18 - 2022-09-06 +* Bug fixes (see mercurial logs for details) + +Version 6.0.17 - 2022-08-17 +* Bug fixes (see mercurial logs for details) + +Version 6.0.16 - 2022-05-06 +* Bug fixes (see mercurial logs for details) + +Version 6.0.15 - 2022-04-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.14 - 2022-02-16 +* Bug fixes (see mercurial logs for details) + +Version 6.0.13 - 2022-02-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.12 - 2022-01-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.11 - 2022-01-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.10 - 2021-12-03 +* Bug fixes (see mercurial logs for details) + +Version 6.0.9 - 2021-11-04 +* Bug fixes (see mercurial logs for details) + +Version 6.0.8 - 2021-10-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.7 - 2021-10-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.6 - 2021-09-16 +* Bug fixes (see mercurial logs for details) + +Version 6.0.5 - 2021-09-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.4 - 2021-07-21 +* Bug fixes (see mercurial logs for details) + +Version 6.0.3 - 2021-07-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.2 - 2021-06-17 +* Bug fixes (see mercurial logs for details) + +Version 6.0.1 - 2021-05-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.0 - 2021-05-03 +* Bug fixes (see mercurial logs for details) +* Support printing zip archive +* Add direct print to UNIX +* Add attachment preview +* Support document widget +* Support icon of type URL +* Allow sending email with existing attachments +* Add paths and model context to action data +* Add indentation in CSV export of tree +* Manage which records to use for actions +* Handle device cookie +* Add breadcrumb as title of window form +* Display revision on dialog +* Execute report asynchronously +* Add support for Python 3.9 +* Support empty value for timedelta converter +* Add interactive search on tree view +* Unify PYSON string format + +Version 5.8.0 - 2020-11-02 +* Bug fixes (see mercurial logs for details) +* Remove support for Python 3.5 +* Format timedelta in CSV export +* Manage symbol widget +* Fallback to model name as title +* Manage deletable and writable state +* Support e-mail template +* Send e-mail via the server +* Support PYSON comparison of date and datetime +* Position copied records based on order +* Allow configuration of default colors for graph and calendar +* Use existing context for get_preferences +* Add context to export URL + +Version 5.6.0 - 2020-05-04 +* Bug fixes (see mercurial logs for details) +* Support link button on form +* Add URL for the current export +* Add option to export listed records +* Retire App Menu +* Add support for Python 3.8 +* Manage readonly dates in calendar view +* Manage editable on calendar view +* Manage model access on calendar view +* Set client title at the end of header bar +* Manage named separator as label +* Add MultiSelection entry to Dict field +* Position new record based on order +* Allow relation field to be selected in CSV export +* Support validate attribute of wizard's Button + +Version 5.4.0 - 2019-11-04 +* Bug fixes (see mercurial logs for details) +* Support visual context on tree view +* Support multiselection field +* Support dot notation on PYSON Eval +* Order Dict keys by sequence +* Support relation fields in domain parser +* Add Dict widget for tree view +* Add handle to reordable line of tree views + +Version 5.2.0 - 2019-05-06 +* Bug fixes (see mercurial logs for details) +* Add domain inversion for reference fields +* Add list-form view +* Remove support for Python 3.4 +* Replace ColorSelectionDialog by ColorChooserDialog +* Support HTML widget +* Use locale format for data import and export +* Add day view on calendar +* Add CSV exports into print toolbar +* Display unread and total of notes +* Support badge on icon +* Use resources method +* Replace Arrow by Image +* Remove Fast Tabbing option +* Remove usage of focus_chain +* Add shortcuts for switch and remove on One2Many +* Rename roundup.url configuration to bug.url +* Remove send error to bug tracker +* Replace Table by Grid +* Use separate XML parser in Views +* Use between numbers in filter box +* Rename email and calendar file +* Use markup for styling +* Replace ImageMenuItem by MenuItem +* Replace ComboBoxEntry by ComboBox or ComboBoxText +* Replace GObject.idle_add by GLibe.idle_add +* Remove positional arguments for PyGObject +* Remove pygtkcompat +* Implement Reference cell with Selection and Many2One cells +* Replace CellRendererBinary with clickable pixbuf cells +* Allow cell to have multiple renderers +* Fill 'to' with 'from' value on filter box +* Add tab domain in URL +* Use Popover for search filter +* Display version on login dialog +* Use tab key for tab navigation +* Use ShortcutsWindow +* Improve default size of widgets and dialog +* Use record and field properties of widget instead of method arguments +* Reduce offset when no search result +* Ensure URL scheme of bus (issue7792) +* Add drag & drop support on binary widget + +Version 5.0.0 - 2018-10-01 +* Bug fixes (see mercurial logs for details) +* Use dropdown for attachment +* Apply factor on domain parser +* Use tab name in CSV Export/Import +* Add bus management +* Use CSS to style label +* New icons +* Support timestamp field +* Add support for Python 3.7 +* Use GtkApplication +* Validate dictionary items +* Add (i)like support to domain inversion +* Use Python 3 +* Improve loading strategy by using views +* Use non conflicting keyboard shortcuts in sync with sao +* Use the context to get the suffix of window name +* Call on_change methods after setting default dtstart on calendar view + +Version 4.8.0 - 2018-04-23 +* Bug fixes (see mercurial logs for details) +* Manage active field on search widget +* Reset record to its original state when discarding the popup window +* Add support of expand attribute on group tag +* Add the context model name in the screen context +* Use limit to expand tree +* Add keyword attribute to button tag +* Support field name on image tag +* Add option to check new version +* Show related record names for all windows +* Remove support of GTK+ 2.0 +* Add icons on Many2One in editable tree +* Use translated values when exporting Reference and Selection fields + +Version 4.6.0 - 2017-10-30 +* Bug fixes (see mercurial logs for details) +* Allow to export model and record name of Reference fields +* Re-position buttons on Binary/Image widget +* Improve treeview headers +* Update states of both toolbar and menu +* Improve toolbar order +* Make readonly, required and invalid widget themable +* Load user CSS theme +* Use profile name or login details in title + +Version 4.4.0 - 2017-05-01 +* Bug fixes (see mercurial logs for details) +* Verify certificate with default CA +* Replace URL entry by a toolbar button + +Version 4.2.0 - 2016-11-28 +* Bug fixes (see mercurial logs for details) +* Add support for GTK+ 3.0 +* Add PYSON Widget +* Show records names in wizard title +* Add support for datetime_field to Reference +* Show non editable widget as insensitive +* Move info-bar at the bottom +* Manage readonly on fields translation dialog +* Restore tab default order when no manual change +* Call autocompletion when setting record value +* Remove database management +* Limit readonly state for xxx2Many +* Show records names in relate window title +* Add support for count on Action Window Domains +* Manage custom login process +* Add clear icon on Many2One +* Re-position icons on Many2One widget + +Version 4.0.0 - 2016-05-02 +* Bug fixes (see mercurial logs for details) +* Improvement of charts design +* Timedelta uses 30 days for month +* Add all available encoding for import/export +* Add CSV parameters to export window +* Add DnD on import window +* Manage context model of ir.action.act_window +* Add Note +* Manage view_ids on tree view +* Use Apple key for copy/past of list + +Version 3.8.0 - 2015-11-02 +* Bug fixes (see mercurial logs for details) +* Use italic labels for editable fields +* Escape '_' in button string and page string +* Export selected records only +* Use HTML for RichText widget +* Change Progressbar to work with float between 0 and 1 +* Add Fast Tabbing option +* Remove colors on widgets +* Bold label of required fields +* Explicit error message for invalid record +* Add reversed operators to PYSON expressions + +Version 3.6.0 - 2015-04-20 +* Bug fixes (see mercurial logs for details) +* Hide columns containing always the same value +* Remove Tabs Position option +* Manage product attribute +* Remove float_time widget +* Add TimeDelta field +* Improve date/time widgets +* Remove datetime widget on list/tree view +* Allow to put many times the same field on tree view +* Add search completion on dictionary widget +* Merge host and port field in profile editor +* New color scheme for graph +* Replace img_{width,height} by width and height attributes + +Version 3.4.0 - 2014-10-20 +* Bug fixes (see mercurial logs for details) +* Add DnD on export window +* Allow to overide predefined export +* Prefill export window with current view fields +* Manage field context on Group value +* Add export of selection string +* Load plugins from local user directory +* Manage tree_state attribute +* Add simple tree_state support on form view +* Change range operator of search widget to be included +* Explicitly set value of parent field + +Version 3.2.0 - 2014-04-21 +* Bug fixes (see mercurial logs for details) +* Add option to show revisions +* Add a multi selection widget for many2many +* Remove auto-refresh +* Add support of domain for non-relation field +* Allow drag & drop on the attachment button +* Replace sha widget by password +* Add Len to PYSON +* Use a pool of connection +* Manage client actions from button and wizard +* Add tree_invisible attribute to button in tree view +* Add buttons of the view in actions menu +* Don't evaluate anymore relate action with the record +* Paste on editable list create new records if needed +* Drop support of Python 2.6 +* Allow to search on rec_name of Reference fields +* Use local timezone +* Sanitize report file extension + +Version 3.0.0 - 2013-10-21 +* Bug fixes (see mercurial logs for details) +* Add factor on number widgets +* Add calendar view +* Add URL entry +* Remove request + +Version 2.8.0 - 2013-04-22 +* Bug fixes (see mercurial logs for details) +* Manage dynamic label +* Manage prefix, suffix on tree view +* Manage selection_change_with on Selection and Reference fields +* Add Dict fields on form +* Remove Goto +* Add global search +* Add toggle button for menu +* Replace shortcuts by menu favorites +* Move Plugins into toolbar Actions +* Add url to list view +* Add dynamic icon for url +* Add completion on Many2One, Many2Many and One2Many +* Add bookmark on search widget +* Manage domains on Action Window +* Use range for Date/Time fields in filter box +* Allow multi-selection for Selection field in filter box + +Version 2.6.0 - 2012-10-22 +* Bug fixes (see mercurial logs for details) +* Allow to paste in editable list view +* Manage readonly state on group +* Remove "Please wait" box +* Refactorize date widgets +* Manage tuple on Reference +* Add constant interpolation on line graph +* Make Import/Export Data no-modal +* Deactivate switch view button when there is only 1 view +* Manage create/delete field access +* Add dynamic size limit on the One2Many, Many2Many and Char +* Search only on fields in XML view +* Cache action keyword +* Always use cached views +* Manage model access +* Manage time format +* Deactivate attachment button when record is not created + +Version 2.4.0 - 2012-04-23 +* Bug fixes (see mercurial logs for details) +* Remove workflow +* Improve contextual menu: + - Actions for all relation fields on list view + - Use icons + - Add attachments entry +* Use RPCProgress almost everywhere +* Add a simple search box +* New domain parser using shlex +* Better translation dialog box +* Add the richtext widget for WYSIWYG Editor on text fields +* Add a record pool to prefetch more records +* Add Time widget +* Add support for fuzzy_translation +* Activate save button on editing +* Refactor set/get and set/get_client of fields +* Display binary size alongside the buttons in treeview +* Set correctly the focus on tab switching +* Add shortcut to focus search entry +* Add binary field to tree views +* Improve board view: + - use the same search widget as form + - link double click to open popup + +Version 2.2.0 - 2011-10-24 +* Bug fixes (see mercurial logs for details) +* New search widget +* Improve memory management of Binary fields +* Support buffer for Binary fields +* Remove delete on Escape in editable tree +* Use JSON-RPC +* Limit size of field when possible +* Add xalign and yalign as fields attributes +* Convert many popup to be no-modal +* Add window manager for: + - replace current window + - prevent simmilar window +* Merge and review toolbars in form and board +* Drop support of Python 2.5 +* Use the same design for Many2Many than One2Many +* Allow resize columns smaller than the header + +Version 2.0.0 - 2011-04-26 +* Bug fixes (see mercurial logs for details) +* Popup form dialog has 3 buttons (close, ok, new) +* New UI layout +* Add DnD on tree view +* Merge tree and list views +* Remove generic default value on right-click +* Made numpad locale aware +* Selection widget used for many2one dynamically change their content following + the domain specification +* Add open button on binary and image widgets +* Hide buttons on image widget if readonly +* Added a connection manager à la gajim +* Fix warning in wizards +* Added possibility to use server-side icons +* Added additional gtk.Entry for filename on BinaryField +* Display deleted lines in One2Many and Many2Many +* Handle URL +* Add communication between boards +* Added domain inversion feature +* Handle loading attribute on fields +* Use default format for value in wizard form +* Add One2One field + +Version 1.8.0 - 2010-11-01 +* Bug fixes (see mercurial logs for details) +* More fully integrate GTK menubar for Mac OS +* Allow to configure search limit +* Add Previous/Next on list view +* Set non-applicable form controls in one2many view to be insensitive + +Version 1.6.0 - 2010-05-08 +* Bug fixes (see mercurial logs for details) +* Don't stop wizard execution when exception occurs +* Use ir.attachment view instead of a custom one +* Add fingerprint and CA check for SSL connection +* Use lazy load in Import/Export windows +* Validate record before switching view +* Refactoring: + * Better naming in model + * Group extends list + * Add an index to Group + * New common windows for dialog of many2one, one2many and many2many + * one2many and many2many dialog use the same screen than the widget + * Reduce the number of option in Screen + * Remove RPCProxy to handle logout exception on every server call + * Better naming of event signals + * Fix on_change detection for many2many for issue1400 +* Add PySON to replace python string evaluation +* Don't show "Spell Checking" option if gtkspell is not present +* Use the same internal model for many2many and one2many fields +* Remove egenix-mx-base and replace it by python-dateutil +* Add cache to safe_eval +* Use versioned configuration directory +* Next and Previous scroll per page on list and don't loop +* Add AccelGroup on search windows (CTRL+Return) +* Use same keyboard shortcut for xxx2many than for other fields + +Version 1.4.0 - 2009-10-19 +* Bug fixes (see mercurial logs for details) +* Handle datetime_field in xxx2Many +* Add new safe_eval +* Ask previous password for set_preferences on password change +* Add "Statusbar" option +* Add default filename for database backup +* Add checkbox on restore to update database +* Add 'login.host' options to hide server connection +* Handle required attribute with local domains +* Allow to run wizard in tabs +* Remove statusbar on form for more space +* Add "Change Accelerators" option +* Use gzip in pysocket +* Use the report name to create the temporary file to open it +* Allow to store wizard size +* Add reset default on fields +* Store in config default width and height of main window +* Added arrow navigation if supported by gtk +* Add 'starts with' and 'ends with' on char search +* Handle domain with '=' or '!=' as operator on selection +* Extend fields domain with local domains +* Improve float time widget to handle year, month, week and day + and handle float_time attribute for contextual time convertion + +Version 1.2.0 - 2009-04-20 +* Bug fixes (see mercurial logs for details) +* Make graph works also with datetime +* Add edition on Many2Many +* Fix open in csv export to use file actions +* Update client labels at language change +* Handle datetime_field in Many2One +* Set readonly on records with _datetime in the context +* Display values of reference fields even if there is no model +* Allow to directly print or create email with reports +* Handle invisible states on list view +* Add user warnings +* Add Model in logs +* Allow to duplicate many records at once +* Improve netrpc communication speed +* Improve date widget to display mask only when having focus +* Fix for host with IPv6 enable but without default IPv6 route +* Add desktop entry +* Add win32 single executable +* Allow egg installation + +Version 1.0.0 - 2008-11-17 +* Initial release diff --git a/tryton/COPYRIGHT b/tryton/COPYRIGHT new file mode 100644 index 0000000000..61d50af8ec --- /dev/null +++ b/tryton/COPYRIGHT @@ -0,0 +1,26 @@ +Copyright (C) 2004-2008 Tiny SPRL. +Copyright (C) 2007-2009 Lorenzo Gil Sanchez. +Copyright (C) 2007-2013 Bertrand Chenal. +Copyright (C) 2007-2022 Cédric Krier. +Copyright (C) 2008-2011 Udo Spallek. +Copyright (C) 2008-2011 virtual things - Preisler & Spallek GbR. +Copyright (C) 2008-2022 B2CK SPRL. +Copyright (C) 2010-2022 Nicolas Évrard. +Copyright (C) 2011-2012 Rodrigo Hübner. +Copyright (C) 2012-2013 Antoine Smolders. +Copyright (C) 2020-2021 Maxime Richez +Copyright (C) 2020-2021 SALUC SA + + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/tryton/LICENSE b/tryton/LICENSE new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/tryton/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/tryton/MANIFEST.in b/tryton/MANIFEST.in new file mode 100644 index 0000000000..b6a39d604f --- /dev/null +++ b/tryton/MANIFEST.in @@ -0,0 +1,17 @@ +include LICENSE +include COPYRIGHT +include README.rst +include CHANGELOG +include setup.nsi +include setup-single.nsi +include setup-bundle.sh +include tryton.desktop +include *.nsh +include setup-freeze.py +include make-*-installer.sh +include */gtk-3.0/* +include doc/* +recursive-include doc *.rst +recursive-include doc *.po +recursive-include doc *.pot +include tryton/data/pixmaps/tryton/LICENSE diff --git a/tryton/README.rst b/tryton/README.rst new file mode 100644 index 0000000000..7af2c227af --- /dev/null +++ b/tryton/README.rst @@ -0,0 +1,7 @@ +tryton +====== + +The desktop client of Tryton. +Tryton is business software, ideal for companies of any size, easy to use, +complete and 100% Open Source. +It provides modularity, scalability and security. diff --git a/tryton/bin/tryton b/tryton/bin/tryton new file mode 100755 index 0000000000..374596124a --- /dev/null +++ b/tryton/bin/tryton @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import sys +import os +try: + DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'tryton'))) + if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) +except NameError: + pass + +if hasattr(sys, 'frozen'): + if not ('-v' in sys.argv or '--verbose' in sys.argv or + '-l' in sys.argv or '--log-level' in sys.argv): + sys.stdout = open(os.devnull, 'w') + sys.stderr = open(os.devnull, 'w') + prefix = os.path.dirname(sys.executable) + os.environ['GTK_EXE_PREFIX'] = prefix + os.environ['GTK_DATA_PREFIX'] = prefix + os.environ['EV_BACKENDS_DIR'] = prefix + os.environ['XDG_DATA_DIRS'] = os.path.join(prefix, 'share') + etc = os.path.join(prefix, 'etc') + os.environ['GDK_PIXBUF_MODULE_FILE'] = os.path.join( + etc, 'gtk-3.0', 'gdk-pixbuf.loaders') + os.environ['GTK_IM_MODULE_FILE'] = os.path.join( + etc, 'gtk-3.0', 'gtk.immodules') + os.environ['GI_TYPELIB_PATH'] = os.path.join( + prefix, 'lib', 'girepository-1.0') + os.environ.setdefault('SSL_CERT_FILE', + os.path.join(etc, 'ssl', 'cert.pem')) + os.environ.setdefault('SSL_CERT_DIR', + os.path.join(etc, 'ssl', 'certs')) + + if sys.platform == 'win32': + # cx_freeze >= 5 put python modules under lib directory + # and dependencies of gdk-pixbuf loaders + sys.path.append(os.path.join(prefix, 'lib')) + sys.path.append(os.path.join( + prefix, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders')) + + # On first launch the MacOSX app launcher may append an extra unique + # argument starting with -psn_. This must be filtered to not crash the + # option parser. + if sys.platform == 'darwin': + sys.argv = [a for a in sys.argv if not a.startswith('-psn_')] + + +# Disable dbusmenu to show second menu in tabs +os.environ['UBUNTU_MENUPROXY'] = '0' +# overlay-scrollbar breaks treeview height +os.environ['LIBOVERLAY_SCROLLBAR'] = '0' + +from tryton.client import main +main() diff --git a/tryton/catalan.nsh b/tryton/catalan.nsh new file mode 100644 index 0000000000..a5b848f0fc --- /dev/null +++ b/tryton/catalan.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_CATALAN} + +LangString LicenseText ${CURLANG} "Tryton està alliberat sota la llicència «GNU General Public License» publicada per la Free Software Foundation, o bé la versió 3 de la llicència, o (sota la vostra elecció) qualsevol versió posterior. Llegiu detingudament la llicència. Premeu «Següent» per continuar." +LangString LicenseNext ${CURLANG} "&Següent" +LangString PreviousInstall ${CURLANG} "Tryton ja està instal·lat.$\n$\nPremeu `OK` per eliminar la versió anterior o `Cancel·la` per cancel·lar l'actualització." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Instal·la tryton.exe i altres fitxers necessaris" +LangString SecStartMenuName ${CURLANG} "Accessos directes al menú d'inici i a l'escriptori" +LangString SecStartMenuDesc ${CURLANG} "Crea accessos directes al menú d'inici i a l'escriptori" diff --git a/tryton/darwin/gtk-3.0/gdk-pixbuf.loaders b/tryton/darwin/gtk-3.0/gdk-pixbuf.loaders new file mode 100644 index 0000000000..fe2e0bee31 --- /dev/null +++ b/tryton/darwin/gtk-3.0/gdk-pixbuf.loaders @@ -0,0 +1,107 @@ +# GdkPixbuf Image Loader Modules file +# Automatically generated file, do not edit +# Created by gdk-pixbuf-query-loaders from gdk-pixbuf-2.34.0 +# +# LoaderDir = @executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders +# +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-ani.so" +"ani" 4 "gdk-pixbuf" "Windows animated cursor" "LGPL" +"application/x-navi-animation" "" +"ani" "" +"RIFF ACON" " xxxx " 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-bmp.so" +"bmp" 5 "gdk-pixbuf" "BMP" "LGPL" +"image/bmp" "image/x-bmp" "image/x-MS-bmp" "" +"bmp" "" +"BM" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-gif.so" +"gif" 4 "gdk-pixbuf" "GIF" "LGPL" +"image/gif" "" +"gif" "" +"GIF8" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-icns.so" +"icns" 4 "gdk-pixbuf" "MacOS X icon" "GPL" +"image/x-icns" "" +"icns" "" +"icns" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-ico.so" +"ico" 5 "gdk-pixbuf" "Windows icon" "LGPL" +"image/x-icon" "image/x-ico" "image/x-win-bitmap" "image/vnd.microsoft.icon" "application/ico" "image/ico" "image/icon" "text/ico" "" +"ico" "cur" "" +" \001 " "zz znz" 100 +" \002 " "zz znz" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-jpeg.so" +"jpeg" 5 "gdk-pixbuf" "JPEG" "LGPL" +"image/jpeg" "" +"jpeg" "jpe" "jpg" "" +"\377\330" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-png.so" +"png" 5 "gdk-pixbuf" "PNG" "LGPL" +"image/png" "" +"png" "" +"\211PNG\r\n\032\n" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-pnm.so" +"pnm" 4 "gdk-pixbuf" "PNM/PBM/PGM/PPM" "LGPL" +"image/x-portable-anymap" "image/x-portable-bitmap" "image/x-portable-graymap" "image/x-portable-pixmap" "" +"pnm" "pbm" "pgm" "ppm" "" +"P1" "" 100 +"P2" "" 100 +"P3" "" 100 +"P4" "" 100 +"P5" "" 100 +"P6" "" 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-qtif.so" +"qtif" 4 "gdk-pixbuf" "QuickTime" "LGPL" +"image/x-quicktime" "image/qtif" "" +"qtif" "qif" "" +"abcdidsc" "xxxx " 100 +"abcdidat" "xxxx " 100 + +"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so" +"svg" 6 "gdk-pixbuf" "Scalable Vector Graphics" "LGPL" +"image/svg+xml" "image/svg" "image/svg-xml" "image/vnd.adobe.svg+xml" "text/xml-svg" "image/svg+xml-compressed" "" +"svg" "svgz" "svg.gz" "" +" [:]//model/[/][;parameters]` + + `tryton://[:]//wizard/[;parameters]` + + `tryton://[:]//report/[;parameters]` + + +where `parameters` are the corresponding fields of actions encoded in +`JSON`_. + +.. _JSON: http://en.wikipedia.org/wiki/Json +.. Note:: `model` is for `act_window` +.. Note:: `report` must have at least a data parameter with `ids`, `id` and + `model name` + + +Overview +******** + +On startup the login dialog is displayed. It allows to select a existing +profile (or to manage them) or to enter the host and database information. + +The following schematic illustration of the Tryton client shows the names of +all important visual parts. + +Figure: Tryton client application:: + + Client Window ________________________________________________________________ + |T| Search | Favorites Tryton _ o x| + |----------------------------------------------------------------| + | + | ______ | + Tabs | |-+ | [Tab1] |[Tab2]| [Tab3]... | + | | |- | +-------+ +--------------------------------+| + | + | | Menu Tab2 || + | |-+ | |-----------------------------------------------|| + Tool bar | | |- | | New Save Switch Reload | Prev Next | Attach v || + | | |- | |-----------------------------------------------|| + | + | | _______________________ || + Search widget | |-+ | | Filter | *| Bookmark <- -> || + | | |- | |-----------------------------------------------|| + | | |- | | || + | + | | || + View | |-+ | | || + | |- | | || + | |- | | || + | | | || + | | | || + | | | || + | | | || + |_____________| |_______________________________________________|| + |________________________________________________________________| + + +Tabbed Main Frame +^^^^^^^^^^^^^^^^^ +This part of the client contains all the related contents and +functions provided by the :term:`Tryton server` :term:`modules`. +All aspects inside the *main frame* depend at least on the individual set +of activated modules. + +The main frame provides a `tabbed document interface`__ to arrange different +views side by side. New :term:`tabs` are opened by special :term:`actions`, +like choosing a menu item or clicking some action buttons. All tabs include +titles which show the name of the provided view. + +:term:`Tabs` can be arranged by Drag and Drop. + +__ TDI_ + +.. _TDI: http://en.wikipedia.org/wiki/Tabbed_document_interface + +.. Note:: Inside :term:`views` there can be tabs, too. + + +Menu +++++ +The *menu* does not contain fixed menu items. +All of them are dynamically provided by the actual set of the activated +:term:`modules` depending on the access rules of the current user. If a menu +item is clicked, the appropriate action will open in a new tab. + +A search field allows to quickly filter the menu items by name and to search in +models for which the global search is enabled. + + +Application Menu +**************** +The following section describes the action of the application menu. +A rule of thumb: All items of the menu bar that are suffixed by three dots +(...) will open an intermediate :term:`dialog` for setting up the provided +menu action. Most dialog provide a *Cancel* button, used to stop the +complete dialog process. + +.. _Menu-Preferences: + +Preferences: + A preference dialog opens, where the actual user can show and edit his + personal settings. All user preferences are stored server side. + I.e. logging in with the same credentials from different computers + always restores the same preferences. + +Options +^^^^^^^ +The Options menu sets up several visual and context depending preferences. + + +.. _Menu-Options-Toolbar: + +Toolbar: + + * Default: + Shows labels and icons as defaulted in the GTK configuration. + + * Text and Icons: + Shows labels and icons in the tool bar. + + * Icons: + Shows icons only in the tool bar. + + * Text: + Shows labels only in the tool bar. + +.. _Menu-Options-Form: + +Form: + + * Save Column Width: + Check box to enable saving of manually adjusted widths of columns in lists + and trees. + + * Save Tree Expanded State: + Check box to enable saving of expanded and selected nodes in trees/lists. + + * Spell Checking: + Check box to enable spell checking in fields. + +.. _Menu-Options-PDA-Mode: + +PDA Mode: + When activated, the client display in a condensed mode. + +.. _Menu-Options-Search-Limit: + +Search Limit: + Open a dialog to set up the maximum number of records displayed on a list. + +.. _Menu-Options-Check_Version: + +Check Version: + Check box to enable the check of new bug-fix version. + +Help +^^^^ + +.. _Menu-Help-Keyboard_Shortcuts: + +Keyboard Shortcuts...: + Shows the information dialog of the predefined keyboard shortcut map. + + * Edition Widgets: Shows shortcuts working on text entries, relation entries + and date/time entries. + +.. _Menu-Help-About: + +About...: + License, Contributors, Authors of Tryton + +Tool Bar +******** +The tool bar contains the functionalities linked to the current tab. +Some operations are working with one record or with a selection of +:term:`records`. In :term:`form view` the actual record is selected for +operations. In :term:`tree view` all selected records are used for operations. + +.. _Toolbar-New: + +New: + Creates a new record. + +.. _Toolbar-Save: + +Save: + Saves the actual record. + +.. _Toolbar-Switch_View: + +Switch View: + Switches the actual view aspect to: + + * :term:`Form view` + * :term:`Tree view` + * :term:`Graph view` + + Not all views provide all aspects. + +.. _Toolbar-Reload_Undo: + +Reload/Undo: + Reloads the content of the actual tab. Undoes changes, if save request for + the current record is denied. + +.. _Toolbar-Duplicate: + +Duplicate: + Duplicates the content of the actual record in a newly created record. + +.. _Toolbar-Delete: + +Delete: + Deletes the selected or actual record. + +.. _Toolbar-Previous: + +Previous: + Goes to the last record in a list (sequence). + +.. _Toolbar-Next: + +Next: + Goes to the next record in a list (sequence). + +.. _Toolbar-Search: + +Search: + Goes to the search widget. + +.. _Toolbar-View_Logs: + +View Logs...: + Shows generic information of the current record. + +.. _Toolbar-Show revisions: + +Show revisions...: + Reload the current view/record at a specific revision. + +.. _Toolbar-Close: + +Close Tab: + Closes the current tab. A Request :term:`Dialog` opens in case of unsaved + changes. + +.. _Toolbar-Attachment: + +Attachment: + The attachment item handles the document management system of + Tryton which is able to attach files to any arbitrary :term:`model`. + On click it opens the attachments :term:`dialog`. The default dialog + shows a list view of the attached files and links. + +.. _Toolbar-Actions: + +Actions...: + Shows all actions for the actual view, model and record. + +.. _Toolbar-Relate: + +Relate...: + Shows all relate view for the actual view, model and record. + +.. _Toolbar-Report: + +Report...: + Shows all reports for the actual view, model and record. + +.. _Toolbar-Email: + +E-Mail...: + Open an editor to send an email related to the actual record. + +.. _Toolbar-Print: + +Print...: + Shows all print actions for the actual view, model and record. + +.. _Toolbar-Copy-URL: + +Copy URL: + Copy the URL of the form into the clipboard. + +.. _Toolbar-Export_Data: + +Export Data...: + Export of current/selected records into :term:`CSV`-file or open it in Excel. + + * Predefined exports + + - Choose preferences of already saved exports. + + * All Fields: Fields available from the model. + * Fields to export: Defines the specific fields to export. + * Options: + + - Save: Save export as a CSV file. + - Open: Open export in spread sheet application. + + * Add field names: Add a header row with field names to the export data. + * Actions: + + - Add: Adds selected fields to *Fields to export*. + - Remove: Removes selected fields from *Fields to export*. + - Clear: Removes all fields from *Fields to export*. + - Save Export: Saves field mapping to a *Predefined export* with a name. + - Delete Export: Deletes a selected *Predefined export*. + - OK: Exports the data (action depending on *Options*). + - Cancel + +.. _Toolbar-Import_Data: + +Import Data...: + Import records from :term:`CSV`-file. + + * All Fields: Fields available in the model (required fields are marked up). + * Fields to Import: Exact sequence of all columns in the CSV file. + * File to Import: File :term:`dialog` for choosing a CSV file to import. + * CSV Parameters: Setup specific parameters for chosen CSV file. + + - Field Separator: Character which separates CSV fields. + - Text Delimiter: Character which encloses text in CSV. + - Encoding: :term:`Character encoding` of CSV file. + - Lines to Skip: Count of lines to skip a headline or another offset. + + * Actions: + + - Add: Adds fields to *Fields to Import*. + - Remove: Deletes fields from *Fields to Import*. + - Clear: Removes all fields from *Fields to Import*. + - Auto-Detect: Tries to auto detect fields in the CSV *File to Import*. + - OK: Proceeds the data import. + - Cancel + +Widgets +******* + +There are a several widgets used on Tryton in client side. The follow sections +will explains some of them. + + +Date/DateTime/Time Widgets +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These widgets have several key shortcuts to quickly modify the value. Each key +increases if lower case or decreases if upper case: + + - `y`: by one year + - `m`: by one month + - `w`: by one week + - `d`: by one day + - `h`: by one hour + - `i`: by one minute + - `s`: by one second + +The `=` key sets the widget value to the current date and time. + +TimeDelta Widgets +^^^^^^^^^^^^^^^^^ + +This widget represent a duration using different symbol of time separated by +space: + + - `Y`: for years (default: 365 days) + - `M`: for months (default: 30 days) + - `w`: for weeks (default: 7 days) + - `d`: for days (default: 24 hours) + - `h`: for hours (default: 60 minutes) + - `m`: for minutes (default: 60 seconds) + - `s`: for seconds (default: 1 seconds) + +The hours, minutes and seconds are also represented as `H:M:s`. + +For example: ``2w 3d 4:30`` which represents: two weeks, three days and four +and an half hours. + +The value of each symbol may be changed by the context of the widget. For +example, a day could be configured as 8 hours. + +Search Widget +^^^^^^^^^^^^^ + +The search widget adds the ability to easily search for records on the current +tab. This widget is visible only on :term:`tree view`. + +The Syntax +++++++++++ + +A query is composed of search clauses. +A clause is composed of a field name (with `:` at the end), an operator and a value. +The field name is optional and defaults to the record name. +The operator is also optional and defaults to `like` or `equal` depending on +the type of the field. The default operator is `=` except for fields of type +`char`, `text` and `many2one` which is `ilike`. + +Field Names ++++++++++++ + +All field names shown in the :term:`tree view` can be searched. Field names +must be followed by a `:` + + For example: ``Name:`` + +If the field name contains spaces, it is possible to +escape it using double quotes. + + For example: ``"Receivable Today":`` + +Operators ++++++++++ + +The following operators can be used: + + * `=`: equal to + * `<`: less then + * `<=`: less then or equal to + * `>`: greater then + * `>=`: greater then or equal to + * `!=`: not equal + * `!`: not equal or not like (depending of the type of field) + + For example: ``Name: != Dwight`` + +.. note:: The `ilike` operator is never explicit and `%` is appended to the + value to make it behaves like `starts with` + +Values +++++++ + +The format of the value depends on the type of the field. +A list of values can be set using `;` as separator. + + For example: ``Name: Michael; Pam`` + + It will find all records having the `Name` starting with `Michael` or + `Pam`. + +A range of number values can be set using `..`. + + For example: ``Amount: 100..500`` + + It will find all records with `Amount` between `100` and `500` included. + +There are two wildcards: + + * `%`: matches any string of zero or more characters. + * `_`: matches any single character. + +It is possible to escape special characters in values by using double quotes. + + For example: ``Name: "Michael:Scott"`` + + Here it will search with the value `Michael:Scott`. + +Clause composition +++++++++++++++++++ + +The clauses can be composed using the two boolean operators `and` and `or`. +By default, there is an implicit `and` between each clause if no operator is +specified. + + For example: ``Name: Michael Amount: 100`` + + is the same as ``Name: Michael and Amount: 100`` + +The `and` operator has a highest precedence than `or` but you can change it by +using parenthesis. + + For example: ``(Name: Michael or Name: Pam) and Amount: 100`` + + is different than ``Name: Michael or Name: Pam and Amount: 100`` + + which is evaluated as ``Name: Michael or (Name: Pam and Amount: 100)`` + +RichText Editor +^^^^^^^^^^^^^^^ + +This feature create a rich text editor with various features that allow for +text formatting. The features are: + + * Bold: On/off style of bold text + * Italic: On/off style of italic text + * Underline: On/off style of underline text + * Choose font family: Choice from a combo box the desired font family + * Choose font size: Choice from a combo box the desired size font + * Text justify: Choice between four options for alignment of the line (left, + right, center, fill) + * Background color: Choose the background color of text from a color palette + * Foreground color: Choose the foreground color of text from a color palette + +Besides these features, it can change and edit text markup. The text markup +feature has a similar HTML tags and is used to describe the format specified by +the user and is a way of storing this format for future opening of a correct +formatted text. The tags are explain follows: + + * Bold: Tag `b` is used, i.e. text + * Italic: Tag `i` is used, i.e. text + * Underline: Tag `u` is used, i.e. text + * Font family: It is a attribute `font-family` for `span` tag, i.e. + text + * Font size: It is a attribute `size` for `span` tag, i.e. + text + * Text Justify: For justification text is used paragraph tag `p`. The + paragraph tag is used to create new lines and the alignment is applied + across the board. Example:

some text

+ * Background color: It is a attribute `background` for `span` tag, i.e. + text + * Foreground color: It is a attribute `foreground` for `span` tag, i.e. + text + +CSS +*** + +The client can be styled using the file `theme.css`. + +Here are the list of custom selectors: + + * `.readonly`: readonly widget or label + + * `.required`: widget or label of required field + + * `.invalid`: widget for which the field value is not valid + + * `headerbar.profile-`: the name of the connection profile is set on the + main window + +For more information about style option see `GTK+ CSS`_ + +.. _GTK+ CSS: https://developer.gnome.org/gtk3/stable/chap-css-overview.html + +Appendix +******** + + +Configuration File +^^^^^^^^^^^^^^^^^^ + +:: + + ~/.config/tryton/x.y/tryton.conf # General configuration + ~/.config/tryton/x.y/accel.map # Accelerators configuration + ~/.config/tryton/x.y/known_hosts # Fingerprints + ~/.config/tryton/x.y/ca_certs # Certification Authority (http://docs.python.org/library/ssl.html#ssl-certificates) + ~/.config/tryton/x.y/profiles.cfg # Profile configuration + ~/.config/tryton/x.y/plugins # Local user plugins directory + ~/.config.tryton/x.y/theme.css # Custom CSS theme + +.. note:: + `~` means the home directory of the user. + But on Windows system it is the `APPDATA` directory. diff --git a/tryton/english.nsh b/tryton/english.nsh new file mode 100644 index 0000000000..7b2ee98071 --- /dev/null +++ b/tryton/english.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_ENGLISH} + +LangString LicenseText ${CURLANG} "Tryton is released under the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please carefully read the license. Click Next to continue." +LangString LicenseNext ${CURLANG} "&Next" +LangString PreviousInstall ${CURLANG} "Tryton is already installed.$\n$\nClick `OK` to remove the previous version or `Cancel` to cancel this upgrade." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Install tryton.exe and other required files" +LangString SecStartMenuName ${CURLANG} "Start Menu and Desktop Shortcuts" +LangString SecStartMenuDesc ${CURLANG} "Create shortcuts in the start menu and on desktop" diff --git a/tryton/farsi.nsh b/tryton/farsi.nsh new file mode 100644 index 0000000000..590c343b49 Binary files /dev/null and b/tryton/farsi.nsh differ diff --git a/tryton/french.nsh b/tryton/french.nsh new file mode 100644 index 0000000000..1c5dfd1300 --- /dev/null +++ b/tryton/french.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_FRENCH} + +LangString LicenseText ${CURLANG} "Tryton est publié sous la GNU General Public License comme publiée par la Free Software Foundation, soit la version 3 de la License, ou (à votre choix) toute version ultérieure. S'il vous plaît lisez attentivement la license. Cliquez sur Suivant pour continuer." +LangString LicenseNext ${CURLANG} "&Suivant" +LangString PreviousInstall ${CURLANG} "Tryton est déjà installé.$\n$\nCliquez `OK` pour supprimer la précédente version ou `Annuler` pour annuler cette mis à jour." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Installe tryton.exe et d'autres fichiers requis" +LangString SecStartMenuName ${CURLANG} "Raccourcis dans le menu Démarrer et sur le bureau" +LangString SecStartMenuDesc ${CURLANG} "Crée les raccourcis dans le menu Démarrer et sur le bureau" diff --git a/tryton/german.nsh b/tryton/german.nsh new file mode 100644 index 0000000000..e191c43c8b --- /dev/null +++ b/tryton/german.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_GERMAN} + +LangString LicenseText ${CURLANG} "Tryton wird unter der GNU General Public License wie von der Free Software Foundation veröffentlicht freigegeben, entweder Version 3 der Lizenz oder (nach Ihrer Wahl) jeder späteren Version. Bitte lesen Sie die Lizenz aufmerksam. Klicken Sie auf Weiter, um fortzufahren." +LangString LicenseNext ${CURLANG} "&Weiter" +LangString PreviousInstall ${CURLANG} "Tryton ist bereits installiert.$\n$\nWählen Sie `OK`, um die bisherige Version zu entfernen oder `Abbrechen` um die Aktualisierung abzubrechen." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "tryton.exe und andere benötigte Dateien installieren" +LangString SecStartMenuName ${CURLANG} "Startmenü und Desktop-Verknüpfungen" +LangString SecStartMenuDesc ${CURLANG} "Verknüpfungen im Startmenü und auf dem Desktop erstellen" diff --git a/tryton/make-darwin-installer.sh b/tryton/make-darwin-installer.sh new file mode 100644 index 0000000000..2b1d7386d0 --- /dev/null +++ b/tryton/make-darwin-installer.sh @@ -0,0 +1,15 @@ +#!/bin/sh +set -e +version=`./setup.py --version` +rm -rf build dist +./setup.py compile_catalog +./setup-freeze.py bdist_mac +mkdir dist +mv build/Tryton.app dist/ +for f in CHANGELOG COPYRIGHT LICENSE; do + cp ${f} dist/${f}.txt +done +cp -r doc dist/ +rm -f "tryton-${version}.dmg" +hdiutil create "tryton-${version}.dmg" -volname "Tryton Client ${version}" \ + -fs HFS+ -srcfolder dist diff --git a/tryton/make-win32-installer.sh b/tryton/make-win32-installer.sh new file mode 100644 index 0000000000..36387935b8 --- /dev/null +++ b/tryton/make-win32-installer.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e +version=`./setup.py --version` +series=${version%.*} +bits=`python -c "import platform; print(platform.architecture()[0])"` +rm -rf build dist +./setup.py compile_catalog +./setup-freeze.py install_exe -d dist +cp `which gdbus.exe` dist/ +makensis -DVERSION=${version} -DSERIES=${series} -DBITS=${bits} setup.nsi diff --git a/tryton/portuguese.nsh b/tryton/portuguese.nsh new file mode 100644 index 0000000000..c68dc0ba56 --- /dev/null +++ b/tryton/portuguese.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_ENGLISH} + +LangString LicenseText ${CURLANG} "Tryton é licenciado sob a General Public License conforme publicada pela Free Software Foundation, conforme a versão 3 da licença, ou (segundo sua escolha) qualquer versão posterior. Favor ler a licença cuidadosamente. Clique em Seguinte para continuar." +LangString LicenseNext ${CURLANG} "&Seguinte" +LangString PreviousInstall ${CURLANG} "O Tryton já está instalado.$\n$\nClique em 'OK' para remover a versão anterior ou 'Cancelar' para cancelar esta atualização." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Instala tryton.exe e outros arquivos necessários" +LangString SecStartMenuName ${CURLANG} "Menu Iniciar e Atalhos da Área de Trabalho" +LangString SecStartMenuDesc ${CURLANG} "Criar atalhos no menu iniciar e na área de trabalho" diff --git a/tryton/setup-freeze.py b/tryton/setup-freeze.py new file mode 100755 index 0000000000..4d8f8d5e4d --- /dev/null +++ b/tryton/setup-freeze.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# 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 glob +import os +import re +import ssl +import sys +import tempfile +from subprocess import Popen, PIPE, check_call + +from cx_Freeze import setup, Executable +from setuptools import find_packages + +home = os.path.expanduser('~/') +pythonrc = os.path.join(home, '.pythonrc.py') +try: + with open(pythonrc) as fp: + exec(fp.read()) +except IOError: + pass + + +include_files = [ + (os.path.join('tryton', 'data'), 'data'), + (os.path.join('tryton', 'plugins'), 'plugins'), + (os.path.join(sys.prefix, 'share', 'glib-2.0', 'schemas'), + os.path.join('share', 'glib-2.0', 'schemas')), + (os.path.join(sys.prefix, 'lib', 'gtk-3.0'), + os.path.join('lib', 'gtk-3.0')), + (os.path.join(sys.prefix, 'lib', 'gdk-pixbuf-2.0'), + os.path.join('lib', 'gdk-pixbuf-2.0')), + (os.path.join(sys.prefix, 'lib', 'evince'), + os.path.join('lib', 'evince')), + (os.path.join(sys.prefix, 'share', 'locale'), + os.path.join('share', 'locale')), + (os.path.join(sys.prefix, 'share', 'icons', 'Adwaita'), + os.path.join('share', 'icons', 'Adwaita')), + (os.path.join(sys.platform, 'gtk-3.0', 'gtk.immodules'), + os.path.join('etc', 'gtk-3.0', 'gtk.immodules')), + (os.path.join(sys.platform, 'gtk-3.0', 'gdk-pixbuf.loaders'), + os.path.join('etc', 'gtk-3.0', 'gdk-pixbuf.loaders')), + ] + +required_gi_namespaces = [ + 'Atk-1.0', + 'EvinceDocument-3.0', + 'EvinceView-3.0', + 'GLib-2.0', + 'GModule-2.0', + 'GObject-2.0', + 'Gdk-3.0', + 'GdkPixbuf-2.0', + 'Gio-2.0', + 'GooCanvas-[2-3].0', + 'Gtk-3.0', + 'HarfBuzz-0.0', + 'Pango-1.0', + 'PangoCairo-1.0', + 'PangoFT2-1.0', + 'Rsvg-2.0', + 'cairo-1.0', + 'fontconfig-2.0', + 'freetype2-2.0', + ] + + +def replace_path(match): + libs = [os.path.basename(p) for p in match.group(1).split(',')] + required_libs.update(libs) + if sys.platform == 'darwin': + libs = [os.path.join('@executable_path', l) for l in libs] + return 'shared-library="%s"' % ','.join(libs) + + +lib_re = re.compile(r'shared-library="([^\"]*)"') +required_libs = set() +temp = tempfile.mkdtemp() +for ns in required_gi_namespaces: + gir_name = '%s.gir' % ns + gir_file = glob.glob( + os.path.join(sys.prefix, 'share', 'gir-1.0', gir_name))[0] + gir_name = os.path.basename(gir_file) + ns = os.path.splitext(gir_name)[0] + gir_tmp = os.path.join(temp, gir_name) + with open(gir_file, 'r', encoding='utf-8') as src: + with open(gir_tmp, 'w', encoding='utf-8') as dst: + for line in src: + dst.write(lib_re.sub(replace_path, line)) + typefile_name = '%s.typelib' % ns + typefile_file = os.path.join('lib', 'girepository-1.0', typefile_name) + typefile_tmp = os.path.join(temp, typefile_name) + check_call(['g-ir-compiler', '--output=' + typefile_tmp, gir_tmp]) + + include_files.append((typefile_tmp, typefile_file)) + +if sys.platform == 'win32': + required_libs.update([ + 'libepoxy-0.dll', + ]) + lib_path = os.getenv('PATH', os.defpath).split(os.pathsep) +else: + lib_path = [os.path.join(sys.prefix, 'lib')] +for lib in required_libs: + for path in lib_path: + path = os.path.join(path, lib) + if os.path.isfile(path): + break + else: + raise Exception('%s not found' % lib) + include_files.append((path, lib)) + +ssl_paths = ssl.get_default_verify_paths() +include_files.append( + (ssl_paths.openssl_cafile, os.path.join('etc', 'ssl', 'cert.pem'))) +if os.path.exists(ssl_paths.openssl_capath): + include_files.append( + (ssl_paths.openssl_capath, os.path.join('etc', 'ssl', 'certs'))) + +version = Popen( + 'python setup.py --version', stdout=PIPE, shell=True, encoding='utf-8' + ).stdout.read() +version = version.strip() + +setup(name='tryton', + version=version, + packages=find_packages(), + options={ + 'build_exe': { + 'no_compress': True, + 'include_files': include_files, + 'excludes': ['tkinter'], + 'silent': True, + 'packages': ['gi'], + 'include_msvcr': True, + }, + 'bdist_mac': { + 'iconfile': os.path.join( + 'tryton', 'data', 'pixmaps', 'tryton', 'tryton.icns'), + 'bundle_name': 'Tryton', + } + }, + executables=[Executable( + 'bin/tryton', + base='Win32GUI' if sys.platform == 'win32' else None, + icon=os.path.join( + 'tryton', 'data', 'pixmaps', 'tryton', 'tryton.ico'), + )]) diff --git a/tryton/setup.cfg b/tryton/setup.cfg new file mode 100644 index 0000000000..fe4ff1d7fe --- /dev/null +++ b/tryton/setup.cfg @@ -0,0 +1,19 @@ +[extract_messages] +copyright_holder = Tryton +msgid_bugs_address = issue_tracker@tryton.org +output_file = tryton/data/locale/tryton.pot +keywords = _, gettext + +[init_catalog] +domain = tryton +input_file = tryton/data/locale/tryton.pot +output_dir = tryton/data/locale + +[compile_catalog] +domain = tryton +directory = tryton/data/locale + +[update_catalog] +domain = tryton +input_file = tryton/data/locale/tryton.pot +output_dir = tryton/data/locale diff --git a/tryton/setup.nsi b/tryton/setup.nsi new file mode 100644 index 0000000000..e831d92f54 --- /dev/null +++ b/tryton/setup.nsi @@ -0,0 +1,184 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +;Check version +!ifndef VERSION + !error "Missing VERSION! Specify it with '/DVERSION='" +!endif +;Check series +!ifndef SERIES + !error "Missing SERIES! Specify if with '/DSERIES='" +!endif +;Check bits +!ifndef BITS + !error "Missing BITS! Specify it with '/DBITS='" +!endif + +;Include Modern UI +!include "MUI.nsh" + +;General +Name "Tryton ${BITS} ${VERSION}" +OutFile "tryton-${BITS}-${VERSION}.exe" +SetCompressor lzma +SetCompress auto +Unicode true + +;Default installation folder +InstallDir "$PROGRAMFILES\Tryton-${BITS}-${SERIES}" + +;Get installation folder from registry if available +InstallDirRegKey HKCU "Software\Tryton-${BITS}-${SERIES}" "" + +BrandingText "Tryton ${BITS} ${SERIES}" + +;Vista redirects $SMPROGRAMS to all users without this +RequestExecutionLevel admin + +;Variables +Var MUI_TEMP +Var STARTMENU_FOLDER + +;Interface Settings +!define MUI_ABORTWARNING + +;Language Selection Dialog Settings +;Remember the installer language +!define MUI_LANGDLL_REGISTRY_ROOT "HKCU" +!define MUI_LANGDLL_REGISTRY_KEY "Software\Modern UI Test" +!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" + +;Pages + +!define MUI_ICON "tryton\data\pixmaps\tryton\tryton.ico" +!define MUI_LICENSEPAGE_TEXT_BOTTOM "$(LicenseText)" +!define MUI_LICENSEPAGE_BUTTON "$(LicenseNext)" + +!insertmacro MUI_PAGE_LICENSE "LICENSE" +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER +!insertmacro MUI_PAGE_INSTFILES + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +;Languages + +!insertmacro MUI_LANGUAGE "English" ; First is the default +!include "english.nsh" +!insertmacro MUI_LANGUAGE "Catalan" +!include "catalan.nsh" +!insertmacro MUI_LANGUAGE "French" +!include "french.nsh" +!insertmacro MUI_LANGUAGE "German" +!include "german.nsh" +!insertmacro MUI_LANGUAGE "Farsi" +!include "farsi.nsh" +!insertmacro MUI_LANGUAGE "Slovenian" +!include "slovenian.nsh" +!insertmacro MUI_LANGUAGE "Spanish" +!include "spanish.nsh" + +;Reserve Files + +;If you are using solid compression, files that are required before +;the actual installation should be stored first in the data block, +;because this will make your installer start faster. +!insertmacro MUI_RESERVEFILE_LANGDLL + +;Installer Sections +Function .onInit + ClearErrors + ReadRegStr $0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "UninstallString" + StrCmp $0 "" DoInstall + + MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(PreviousInstall)" /SD IDOK IDOK Uninstall + Quit + + Uninstall: + ReadRegStr $1 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "InstallLocation" + ClearErrors + StrCpy $2 "/S" + IfSilent +2 + StrCpy $2 "" + ExecWait '$0 $2 _?=$1' + IfErrors 0 DoInstall + Quit + DoInstall: +FunctionEnd + +Section $(SecTrytonName) SecTryton +SectionIn 1 2 RO + ;Set output path to the installation directory + SetOutPath "$INSTDIR" + + ;Put file + File /r "dist\*" + File "COPYRIGHT" + File "LICENSE" + File "CHANGELOG" + + SetOutPath "$INSTDIR\doc" + File /r "doc\*" + + ;Register URL protocol + WriteRegStr HKCR "tryton" "" "URL:Tryton Protocol" + WriteRegStr HKCR "tryton" "URL Protocol" "" + WriteRegStr HKCR "tryton\DefaultIcon" "" "$INSTDIR\tryton.exe,1" + WriteRegStr HKCR "tryton\shell\open\command" "" '$INSTDIR\tryton.exe "%1"' + + ;Write the installation path into the registry + WriteRegStr HKLM "Software\Tryton-${BITS}-${SERIES}" "" $INSTDIR + + ;Write the uninstall keys for Windows + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "DisplayName" "Tryton ${BITS} ${VERSION} (remove only)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "DisplayIcon" "$INSTDIR\tryton.exe,1" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" "InstallLocation" "$INSTDIR" + + ;Create the uninstaller + WriteUninstaller uninstall.exe +SectionEnd + +Section $(SecStartMenuName) SecStartMenu +SectionIn 1 2 + + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + SetShellVarContext all + + CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER" + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Tryton-${BITS}-${SERIES}.lnk" "$INSTDIR\tryton.exe" "" "$INSTDIR\tryton.exe" 0 + CreateShortCut "$DESKTOP\Tryton-${BITS}-${SERIES}.lnk" "$INSTDIR\tryton.exe" "" "$INSTDIR\tryton.exe" 0 + + !insertmacro MUI_STARTMENU_WRITE_END + +SectionEnd + +;Descriptions +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SecTryton} $(SecTrytonDesc) + !insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} $(SecStartMenuDesc) +!insertmacro MUI_FUNCTION_DESCRIPTION_END + +Section "Uninstall" + ;Add your stuff here + RMDIR /r "$INSTDIR" + + ;remove registry keys + DeleteRegKey HKLM "Software\Tryton-${BITS}-${SERIES}" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\tryton-${BITS}-${SERIES}" + + SetShellVarContext all + Delete "$DESKTOP\Tryton-${BITS}-${SERIES}.lnk" + + !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP + + StrCmp $MUI_TEMP "" noshortcuts + Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk" + Delete "$SMPROGRAMS\$MUI_TEMP\Tryton-${BITS}-${SERIES}.lnk" + RMDir "$SMPROGRAMS\$MUI_TEMP" + noshortcuts: + + +SectionEnd diff --git a/tryton/setup.py b/tryton/setup.py new file mode 100755 index 0000000000..54f8a4970d --- /dev/null +++ b/tryton/setup.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# 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 setuptools import setup, find_packages +import io +import os +import re + + +def read(fname): + return io.open( + os.path.join(os.path.dirname(__file__), fname), + 'r', encoding='utf-8').read() + + +args = {} +try: + from babel.messages import frontend as babel + + class extract_messages(babel.extract_messages): + def initialize_options(self): + super().initialize_options() + self.omit_header = True + self.no_location = True + + class update_catalog(babel.update_catalog): + def initialize_options(self): + super().initialize_options() + self.omit_header = True + self.ignore_obsolete = True + + args['cmdclass'] = { + 'compile_catalog': babel.compile_catalog, + 'extract_messages': extract_messages, + 'init_catalog': babel.init_catalog, + 'update_catalog': update_catalog, + } + + args['message_extractors'] = { + 'tryton': [ + ('**.py', 'python', None), + ], + } + +except ImportError: + pass + +package_data = { + 'tryton': ['data/pixmaps/tryton/*.png', + 'data/pixmaps/tryton/*.svg', + 'data/locale/*/LC_MESSAGES/*.mo', + 'data/locale/*/LC_MESSAGES/*.po', + ] + } +data_files = [] + + +def get_version(): + init = read(os.path.join('tryton', '__init__.py')) + return re.search('__version__ = "([0-9.]*)"', init).group(1) + + +version = get_version() +major_version, minor_version, _ = version.split('.', 2) +major_version = int(major_version) +minor_version = int(minor_version) +name = 'tryton' + +download_url = 'http://downloads.tryton.org/%s.%s/' % ( + major_version, minor_version) +if minor_version % 2: + version = '%s.%s.dev0' % (major_version, minor_version) + download_url = 'hg+http://hg.tryton.org/%s#egg=%s-%s' % ( + name, name, version) +local_version = [] +if os.environ.get('CI_JOB_ID'): + local_version.append(os.environ['CI_JOB_ID']) +else: + for build in ['CI_BUILD_NUMBER', 'CI_JOB_NUMBER']: + if os.environ.get(build): + local_version.append(os.environ[build]) + else: + local_version = [] + break +if local_version: + version += '+' + '.'.join(local_version) + +dependency_links = [] +if minor_version % 2: + dependency_links.append( + 'https://trydevpi.tryton.org/?local_version=' + + '.'.join(local_version)) + +dist = setup(name=name, + version=version, + description='Tryton desktop client', + long_description=read('README.rst'), + author='Tryton', + author_email='bugs@tryton.org', + url='http://www.tryton.org/', + download_url=download_url, + project_urls={ + "Bug Tracker": 'https://bugs.tryton.org/', + "Documentation": 'https://docs.tryton.org/', + "Forum": 'https://www.tryton.org/forum', + "Source Code": 'https://hg.tryton.org/tryton', + }, + keywords='business application ERP', + packages=find_packages(), + package_data=package_data, + data_files=data_files, + scripts=['bin/tryton'], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: X11 Applications :: GTK', + 'Framework :: Tryton', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: Bulgarian', + 'Natural Language :: Catalan', + 'Natural Language :: Chinese (Simplified)', + 'Natural Language :: Czech', + 'Natural Language :: Dutch', + 'Natural Language :: English', + 'Natural Language :: Finnish', + 'Natural Language :: French', + 'Natural Language :: German', + 'Natural Language :: Hungarian', + 'Natural Language :: Indonesian', + 'Natural Language :: Italian', + 'Natural Language :: Persian', + 'Natural Language :: Polish', + 'Natural Language :: Portuguese (Brazilian)', + 'Natural Language :: Romanian', + 'Natural Language :: Russian', + 'Natural Language :: Slovenian', + 'Natural Language :: Spanish', + 'Natural Language :: Turkish', + 'Natural Language :: Japanese', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Topic :: Office/Business', + ], + platforms='any', + license='GPL-3', + python_requires='>=3.6', + install_requires=[ + 'pycairo', + "python-dateutil", + 'PyGObject>=3.19', + ], + extras_require={ + 'calendar': ['GooCalendar>=0.7'], + }, + dependency_links=dependency_links, + zip_safe=False, + test_suite='tryton.tests', + **args + ) diff --git a/tryton/slovenian.nsh b/tryton/slovenian.nsh new file mode 100644 index 0000000000..1bb6974cb0 --- /dev/null +++ b/tryton/slovenian.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_SLOVENIAN} + +LangString LicenseText ${CURLANG} "Tryton je izdan pod licenco GNU General Public License, kot jo je objavila Free Software Foundation, bodisi pod razlièico 3 ali (po va¹i izbiri) katerokoli poznej¹o razlièico. Licenco pozorno preberite. Kliknite Naprej za nadaljevanje." +LangString LicenseNext ${CURLANG} "&Naprej" +LangString PreviousInstall ${CURLANG} "Tryton je že nameščen.$\n$\nKliknite »V redu«, da odstranite prejšnjo različico ali »Prekliči«, da prekličete to nadgradnjo." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Namestitev tryton.exe in ostalih potrebnih datotek" +LangString SecStartMenuName ${CURLANG} "Bli¾njici v zaèetnem meniju in na namizju" +LangString SecStartMenuDesc ${CURLANG} "Ustvari bli¾njici v zaèetnem meniju in na namizju" diff --git a/tryton/spanish.nsh b/tryton/spanish.nsh new file mode 100644 index 0000000000..58602cfea8 --- /dev/null +++ b/tryton/spanish.nsh @@ -0,0 +1,17 @@ +;This file is part of Tryton. The COPYRIGHT file at the top level of +;this repository contains the full copyright notices and license terms. + +!verbose 3 + +!ifdef CURLANG + !undef CURLANG +!endif +!define CURLANG ${LANG_SPANISH} + +LangString LicenseText ${CURLANG} "Tryton está liberado bajo la licencia «GNU General Public License» publicada por la Free Software Foundation, o bien la versión 3 de la licencia, o (a su elección) cualquier versión posterior. Lea cuidadosamente la licencia. Pulse «Siguiente» para continuar." +LangString LicenseNext ${CURLANG} "&Siguiente" +LangString PreviousInstall ${CURLANG} "Tryton ya está instalado.$\n$\nPulse `OK` para eliminar la versión anterior o `Cancelar` para cancelar la actualización." +LangString SecTrytonName ${CURLANG} "Tryton" +LangString SecTrytonDesc ${CURLANG} "Instalar tryton.exe y otros archivos necesarios" +LangString SecStartMenuName ${CURLANG} "Accesos directos en el menú de inicio y en el escritorio" +LangString SecStartMenuDesc ${CURLANG} "Crear accesos directos en el menú de inicio y en el escritorio" diff --git a/tryton/tox.ini b/tryton/tox.ini new file mode 100644 index 0000000000..08a6566d3a --- /dev/null +++ b/tryton/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py36,py37,py38,py39 + +[testenv] +commands = {envpython} setup.py test +install_command = pip install --pre --find-links https://trydevpi.tryton.org/?local_version={env:CI_JOB_ID:{env:CI_BUILD_NUMBER}.{env:CI_JOB_NUMBER}} {opts} {packages} diff --git a/tryton/tryton.desktop b/tryton/tryton.desktop new file mode 100644 index 0000000000..e9018bdd62 --- /dev/null +++ b/tryton/tryton.desktop @@ -0,0 +1,30 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Tryton +GenericName=Client for the Tryton Application Platform +GenericName[ca_ES]=Client per l'aplicació Tryton +GenericName[de]=Client für die Tryton Applikationsplattform +GenericName[es_AR]=Cliente para la aplicación Tryton +GenericName[es_EC]=Cliente para la aplicación Tryton +GenericName[es_ES]=Cliente para la aplicación Tryton +GenericName[es_CO]=Cliente para la Aplicación Tryton +GenericName[fr]=Client de la plate-forme applicative Tryton +GenericName[ru]=Клиент для платформы приложений Tryton +GenericName[sl]=Odjemalec za Tryton programsko platformo +Comment=Access Tryton server +Comment[de]=Verbindet zu einem Tryton Server +Comment[es_AR]=Acceso al servidor Tryton +Comment[es_EC]=Acceso al servidor Tryton +Comment[ca_ES]=Accés al servidor Tryton +Comment[es_ES]=Acceso al servidor Tryton +Comment[es_CO]=Acceso al servidor Tryton +Comment[fr]=Accéder au serveur Tryton +Comment[ru]=Подключение к серверу Tryton +Comment[sl]=Dostop do Tryton strežnika +Keywords=Business;Management;Enterprise;ERP;Framework;Client; +Exec=tryton %u +Icon=tryton-icon +Terminal=false +MimeType=application/tryton; +Categories=Office;Finance; diff --git a/tryton/tryton/__init__.py b/tryton/tryton/__init__.py new file mode 100644 index 0000000000..051767df4d --- /dev/null +++ b/tryton/tryton/__init__.py @@ -0,0 +1,45 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +__version__ = "6.0.22" +import gi +import locale + +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +gi.require_foreign('cairo') +try: + gi.require_version('GtkSpell', '3.0') +except ValueError: + pass +try: + gi.require_version('EvinceDocument', '3.0') + gi.require_version('EvinceView', '3.0') +except ValueError: + pass + +try: + # Import earlier otherwise there is a segmentation fault on MSYS2 + import goocalendar # noqa: F401 +except ImportError: + pass + +if not hasattr(locale, 'localize'): + def localize(formatted, grouping=False, monetary=False): + if '.' in formatted: + seps = 0 + parts = formatted.split('.') + if grouping: + parts[0], seps = locale._group(parts[0], monetary=monetary) + decimal_point = locale.localeconv()[ + monetary and 'mon_decimal_point' or 'decimal_point'] + formatted = decimal_point.join(parts) + if seps: + formatted = locale._strip_padding(formatted, seps) + else: + seps = 0 + if grouping: + formatted, seps = locale._group(formatted, monetary=monetary) + if seps: + formatted = locale._strip_padding(formatted, seps) + return formatted + setattr(locale, 'localize', localize) diff --git a/tryton/tryton/action/__init__.py b/tryton/tryton/action/__init__.py new file mode 100644 index 0000000000..3dc05cfd93 --- /dev/null +++ b/tryton/tryton/action/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from .main import Action + +__all__ = [Action] diff --git a/tryton/tryton/action/main.py b/tryton/tryton/action/main.py new file mode 100644 index 0000000000..e7a6ce21b1 --- /dev/null +++ b/tryton/tryton/action/main.py @@ -0,0 +1,188 @@ +# 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 gettext +import webbrowser + +import tryton.rpc as rpc +from tryton.common import RPCExecute, RPCException +from tryton.common import message, selection, file_write, file_open +from tryton.config import CONFIG +from tryton.pyson import PYSONDecoder + +_ = gettext.gettext + + +class Action(object): + + @staticmethod + def exec_report(name, data, direct_print=False, context=None): + if context is None: + context = {} + else: + context = context.copy() + context['direct_print'] = direct_print + ids = data.get('ids', []) + + def callback(result): + try: + result = result() + except RPCException: + return + type, data, print_p, name = result + if not print_p and direct_print: + print_p = True + fp_name = file_write((name, type), data) + file_open(fp_name, type, print_p=print_p) + RPCExecute( + 'report', name, 'execute', ids, data, context=context, + callback=callback) + + @staticmethod + def execute(action, data, context=None, keyword=False): + if isinstance(action, int): + # Must be executed synchronously to avoid double execution + # on double click. + action = RPCExecute( + 'model', 'ir.action', 'get_action_value', action, + context=context) + if keyword: + keywords = { + 'ir.action.report': 'form_report', + 'ir.action.wizard': 'form_action', + 'ir.action.act_window': 'form_relate', + } + action.setdefault('keyword', keywords.get(action['type'], '')) + Action._exec_action(action, data, context=context) + + @staticmethod + def _exec_action(action, data=None, context=None): + from tryton.gui.window import Window + if context is None: + context = {} + else: + context = context.copy() + if data is None: + data = {} + else: + data = data.copy() + if 'type' not in (action or {}): + return + + context.pop('active_id', None) + context.pop('active_ids', None) + context.pop('active_model', None) + + def add_name_suffix(name, context=None): + if not data.get('ids') or not data.get('model'): + return name + max_records = 5 + ids = list(filter(lambda id: id >= 0, data['ids']))[:max_records] + if not ids: + return name + rec_names = RPCExecute('model', data['model'], + 'read', ids, ['rec_name'], + context=context) + name_suffix = _(', ').join([x['rec_name'] for x in rec_names]) + if len(data['ids']) > len(ids): + name_suffix += _(',...') + return _('%s (%s)') % (name, name_suffix) + + data['action_id'] = action['id'] + if action['type'] == 'ir.action.act_window': + view_ids = [] + view_mode = None + if action.get('views', []): + view_ids = [x[0] for x in action['views']] + view_mode = [x[1] for x in action['views']] + elif action.get('view_id', False): + view_ids = [action['view_id'][0]] + + action.setdefault('pyson_domain', '[]') + ctx = { + 'active_model': data.get('model'), + 'active_id': data.get('id'), + 'active_ids': data.get('ids') or [], + } + ctx.update(rpc.CONTEXT) + ctx['_user'] = rpc._USER + decoder = PYSONDecoder(ctx) + action_ctx = context.copy() + action_ctx.update( + decoder.decode(action.get('pyson_context') or '{}')) + ctx.update(action_ctx) + + ctx['context'] = ctx + decoder = PYSONDecoder(ctx) + domain = decoder.decode(action['pyson_domain']) + order = decoder.decode(action['pyson_order']) + search_value = decoder.decode(action['pyson_search_value'] or '[]') + tab_domain = [(n, decoder.decode(d), c) + for n, d, c in action['domains']] + + name = action.get('name', '') + if action.get('keyword', ''): + name = add_name_suffix(name, action_ctx) + + res_model = action.get('res_model', data.get('res_model')) + res_id = action.get('res_id', data.get('res_id')) + limit = action.get('limit') + if limit is None: + limit = CONFIG['client.limit'] + + Window.create(res_model, + view_ids=view_ids, + res_id=res_id, + domain=domain, + context=action_ctx, + order=order, + mode=view_mode, + name=name, + limit=limit, + search_value=search_value, + icon=(action.get('icon.rec_name') or ''), + tab_domain=tab_domain, + context_model=action['context_model'], + context_domain=action['context_domain']) + elif action['type'] == 'ir.action.wizard': + name = action.get('name', '') + if action.get('keyword', 'form_action') == 'form_action': + name = add_name_suffix(name, context) + Window.create_wizard(action['wiz_name'], data, + direct_print=action.get('direct_print', False), + name=name, + context=context, icon=(action.get('icon.rec_name') or ''), + window=action.get('window', False)) + + elif action['type'] == 'ir.action.report': + Action.exec_report( + action['report_name'], data, + direct_print=action.get('direct_print', False), + context=context) + + elif action['type'] == 'ir.action.url': + if action['url']: + webbrowser.open(action['url'], new=2) + + @staticmethod + def exec_keyword(keyword, data=None, context=None, warning=True, + alwaysask=False): + actions = [] + model_id = data.get('id', False) + try: + actions = RPCExecute('model', 'ir.action.keyword', + 'get_keyword', keyword, (data['model'], model_id)) + except RPCException: + return False + + keyact = {} + for action in actions: + keyact[action['name'].split(' / ')[-1]] = action + + res = selection(_('Select your action'), keyact, alwaysask=alwaysask) + if res: + (name, action) = res + Action.execute(action, data, context=context) + return (name, action) + elif not len(keyact) and warning: + message(_('No action defined.')) + return False diff --git a/tryton/tryton/bus.py b/tryton/tryton/bus.py new file mode 100644 index 0000000000..8920e825e6 --- /dev/null +++ b/tryton/tryton/bus.py @@ -0,0 +1,98 @@ +# 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 base64 +import json +import logging +import socket +import threading +import time +import uuid +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +from gi.repository import GLib + +from tryton.jsonrpc import object_hook +from tryton.config import CONFIG + + +logger = logging.getLogger(__name__) + + +ID = str(uuid.uuid4()) +CHANNELS = [ + 'client:%s' % ID, + ] + + +def listen(connection): + listener = threading.Thread( + target=_listen, args=(connection,), daemon=True) + listener.start() + + +def _listen(connection): + bus_timeout = CONFIG['client.bus_timeout'] + session = connection.session + authorization = base64.b64encode(session.encode('utf-8')) + headers = { + 'Content-Type': 'application/json', + 'Authorization': b'Session ' + authorization, + } + + wait = 1 + last_message = None + url = None + while connection.session == session: + if url is None: + if connection.url is None: + time.sleep(1) + continue + url = connection.url + '/bus' + request = Request(url, + data=json.dumps({ + 'last_message': last_message, + 'channels': CHANNELS, + }).encode('utf-8'), + headers=headers) + logger.info('poll channels %s with last message %s', + CHANNELS, last_message) + try: + response = urlopen(request, timeout=bus_timeout) + wait = 1 + except socket.timeout: + wait = 1 + continue + except Exception as error: + if isinstance(error, HTTPError): + if error.code in (301, 302, 303, 307, 308): + url = error.headers.get('Location') + continue + elif error.code == 501: + logger.info("Bus not supported") + break + logger.error( + "An exception occured while connecting to the bus." + "Sleeping for %s seconds", + wait, exc_info=error) + time.sleep(min(wait, bus_timeout)) + wait *= 2 + continue + + if connection.session != session: + break + + data = json.loads(response.read(), object_hook=object_hook) + if data['message']: + last_message = data['message']['message_id'] + GLib.idle_add(handle, data['message']) + + +def handle(message): + from tryton.gui.main import Main + + app = Main() + if message['type'] == 'notification': + app.show_notification( + message.get('title', ''), message.get('body', ''), + message.get('priority', 1)) diff --git a/tryton/tryton/client.py b/tryton/tryton/client.py new file mode 100644 index 0000000000..e898b113af --- /dev/null +++ b/tryton/tryton/client.py @@ -0,0 +1,91 @@ +# 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 hashlib +import os +import sys +import traceback +from urllib.parse import urlparse + +from gi.repository import Gdk, Gtk, Gio + +from tryton import common +from tryton import gui +from tryton import translate +from tryton.config import CONFIG, get_config_dir +from tryton.gui.window.dblogin import DBLogin + + +def main(): + CSS = b""" + .readonly entry, .readonly text { + background-color: @insensitive_bg_color; + } + .required entry, entry.required, .required text, text.required { + border-color: darker(@unfocused_borders); + } + .invalid entry, entry.invalid, .invalid text, text.invalid { + border-color: @error_color; + } + label.required { + font-weight: bold; + } + label.editable { + font-style: italic; + } + .window-title, .wizard-title { + font-size: large; + font-weight: bold; + } + .window-title .status { + font-size: medium; + font-weight: normal; + } + """ + + screen = Gdk.Screen.get_default() + style_context = Gtk.StyleContext() + provider = Gtk.CssProvider() + provider.load_from_data(CSS) + style_context.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + theme_path = os.path.join(get_config_dir(), 'theme.css') + if os.path.exists(theme_path): + provider = Gtk.CssProvider() + provider.load_from_path(theme_path) + style_context.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + + def excepthook(type_, value, traceback_): + common.error(value, ''.join(traceback.format_tb(traceback_))) + sys.excepthook = excepthook + + CONFIG.parse() + if CONFIG.arguments: + url = CONFIG.arguments[0] + urlp = urlparse(url) + if urlp.scheme == 'tryton': + urlp = urlparse('http' + url[6:]) + database, _ = (urlp.path[1:].split('/', 1) + [None])[:2] + CONFIG['login.host'] = urlp.netloc + CONFIG['login.db'] = database + CONFIG['login.expanded'] = True + else: + CONFIG.arguments = [] + translate.set_language_direction(CONFIG['client.language_direction']) + translate.setlang(CONFIG['client.lang']) + if CONFIG.arguments or DBLogin().run(): + server = '%(hostname)s:%(port)s/%(database)s' % { + 'hostname': common.get_hostname(CONFIG['login.host']), + 'port': common.get_port(CONFIG['login.host']), + 'database': CONFIG['login.db'], + } + server = hashlib.md5(server.encode('utf-8')).hexdigest() + application_id = 'org.tryton._' + server + app = gui.Main( + application_id=application_id, + flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE) + app.run([sys.argv[0]] + CONFIG.arguments) + + +if __name__ == "__main__": + main() diff --git a/tryton/tryton/common/__init__.py b/tryton/tryton/common/__init__.py new file mode 100644 index 0000000000..650b4c8fc4 --- /dev/null +++ b/tryton/tryton/common/__init__.py @@ -0,0 +1,33 @@ +# 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 .common import ( + IconFactory, MODELACCESS, MODELHISTORY, MODELNAME, VIEW_SEARCH, + get_toplevel_window, get_sensible_widget, selection, file_selection, + slugify, file_write, file_open, mailto, message, warning, userwarning, sur, + sur_3b, ask, concurrency, error, to_xml, process_exception, Login, Logout, + node_attributes, hex2rgb, highlight_rgb, generateColorscheme, RPCException, + RPCProgress, RPCExecute, RPCContextReload, Tooltips, COLOR_SCHEMES, + filter_domain, timezoned_date, untimezoned_date, humanize, get_hostname, + get_port, resize_pixbuf, data2pixbuf, apply_label_attributes, ellipsize, + get_align, date_format, idle_add, check_version, TRYTON_ICON, setup_window) +from .domain_inversion import ( + domain_inversion, eval_domain, localize_domain, merge, inverse_leaf, + filter_leaf, prepare_reference_domain, extract_reference_models, concat, + simplify, unique_value) +from .environment import EvalEnvironment +from . import timedelta + +__all__ = [ + IconFactory, MODELACCESS, MODELHISTORY, MODELNAME, VIEW_SEARCH, + get_toplevel_window, get_sensible_widget, selection, file_selection, + slugify, file_write, file_open, mailto, message, warning, userwarning, sur, + sur_3b, ask, concurrency, error, to_xml, process_exception, Login, Logout, + node_attributes, hex2rgb, highlight_rgb, generateColorscheme, RPCException, + RPCProgress, RPCExecute, RPCContextReload, Tooltips, COLOR_SCHEMES, + filter_domain, timezoned_date, untimezoned_date, humanize, get_hostname, + get_port, resize_pixbuf, data2pixbuf, apply_label_attributes, ellipsize, + get_align, date_format, idle_add, domain_inversion, eval_domain, + localize_domain, merge, inverse_leaf, filter_leaf, + prepare_reference_domain, extract_reference_models, concat, simplify, + unique_value, EvalEnvironment, timedelta, check_version, TRYTON_ICON, + setup_window] diff --git a/tryton/tryton/common/button.py b/tryton/tryton/common/button.py new file mode 100644 index 0000000000..17ded2afaa --- /dev/null +++ b/tryton/tryton/common/button.py @@ -0,0 +1,62 @@ +# 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 gettext + +from gi.repository import Gtk + +from tryton.common import IconFactory + +_ = gettext.gettext + + +class Button(Gtk.Button): + + def __init__(self, attrs=None): + self.attrs = attrs or {} + self.label = '_' + attrs.get('string', '').replace('_', '__') + super(Button, self).__init__(label=self.label, stock=None, + use_underline=True) + self._set_icon(attrs.get('icon')) + + def _set_icon(self, stock): + self.set_always_show_image(bool(stock)) + image = self.get_image() + if not image and not stock: + return + if not stock: + self.set_image(None) + return + self.set_image(IconFactory.get_image(stock, Gtk.IconSize.BUTTON)) + + def state_set(self, record): + if record: + states = record.expr_eval(self.attrs.get('states', {})) + else: + states = {} + if states.get('invisible', False): + self.hide() + else: + self.show() + self.set_sensitive(not states.get('readonly', False)) + self._set_icon(states.get('icon', self.attrs.get('icon'))) + + if self.attrs.get('rule'): + label = self.label + tip = self.attrs.get('help', '') + if record: + clicks = record.get_button_clicks(self.attrs['name']) + if clicks: + label += ' (%s)' % len(clicks) + if tip: + tip += '\n' + tip += _('By: ') + _(', ').join(iter(clicks.values())) + self.set_label(label) + self.set_tooltip_text(tip) + + if self.attrs.get('type', 'class') == 'class': + parent = record.parent if record else None + while parent: + if parent.modified: + self.set_sensitive(False) + break + parent = parent.parent diff --git a/tryton/tryton/common/cellrendererbinary.py b/tryton/tryton/common/cellrendererbinary.py new file mode 100644 index 0000000000..256e5062c6 --- /dev/null +++ b/tryton/tryton/common/cellrendererbinary.py @@ -0,0 +1,183 @@ +# 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 gi.repository import Gtk, Gdk, GObject, Pango + +from .common import IconFactory + +BUTTON_BORDER = 2 +BUTTON_SPACING = 1 + + +class CellRendererBinary(Gtk.CellRenderer): + __gproperties__ = { + 'visible': (GObject.TYPE_BOOLEAN, 'Visible', 'Visible', True, + GObject.ParamFlags.READWRITE), + 'editable': (GObject.TYPE_BOOLEAN, 'Editable', 'Editable', False, + GObject.ParamFlags.READWRITE), + 'size': (GObject.TYPE_STRING, 'Size', 'Size', '', + GObject.ParamFlags.READWRITE), + } + __gsignals__ = { + 'select': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + 'open': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + 'save': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + 'clear': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + } + + def __init__(self, use_filename): + Gtk.CellRenderer.__init__(self) + self.visible = True + self.editable = False + self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE) + self.use_filename = use_filename + self.images = {} + for key, icon in ( + ('select', 'tryton-search'), + ('open', 'tryton-open'), + ('save', 'tryton-save'), + ('clear', 'tryton-clear')): + img_sensitive = IconFactory.get_pixbuf( + icon, Gtk.IconSize.SMALL_TOOLBAR) + img_insensitive = img_sensitive.copy() + img_sensitive.saturate_and_pixelate(img_insensitive, 0, False) + width = img_sensitive.get_width() + height = img_sensitive.get_height() + self.images[key] = (img_sensitive, img_insensitive, width, height) + + @property + def buttons(self): + buttons = [] + if self.size: + if self.use_filename: + buttons.append('open') + buttons.append('save') + buttons.append('clear') + else: + buttons.append('select') + return buttons + + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) + + def do_get_property(self, pspec): + return getattr(self, pspec.name) + + def button_width(self): + return (sum(width for n, (_, _, width, _) in self.images.items() + if n in self.buttons) + + (2 * (BUTTON_BORDER + BUTTON_SPACING) * len(self.buttons)) + - 2 * BUTTON_SPACING) + + def do_get_size(self, widget, cell_area=None): + if cell_area is None: + return (0, 0, 30, 18) + else: + return (cell_area.x, cell_area.y, + cell_area.width, cell_area.height) + + def do_activate( + self, event, widget, path, background_area, cell_area, flags): + if event is None: + return + button_width = self.button_width() + + for index, button_name in enumerate(self.buttons): + _, _, pxbf_width, _ = self.images[button_name] + if index == 0 and button_name == 'open': + x_offset = 0 + else: + x_offset = (cell_area.width - button_width + + (pxbf_width + (2 * BUTTON_BORDER) + BUTTON_SPACING) + * index) + x_button = cell_area.x + x_offset + if x_button < event.x < (x_button + pxbf_width + + (2 * BUTTON_BORDER)): + break + else: + button_name = None + if not self.visible or not button_name: + return + elif not self.editable and button_name in ('select', 'clear'): + return + elif not self.size and button_name in {'open', 'save'}: + return + self.emit(button_name, path) + return True + + def do_render(self, cr, widget, background_area, cell_area, flags): + if not self.visible: + return + + button_width = self.button_width() + + state = self.get_state(widget, flags) + + context = widget.get_style_context() + context.save() + context.add_class('button') + + xpad, ypad = self.get_padding() + x = cell_area.x + xpad + y = cell_area.y + ypad + w = cell_area.width - 2 * xpad + h = cell_area.height - 2 * ypad + + padding = context.get_padding(state) + layout = widget.create_pango_layout(self.size) + lwidth = w - button_width - padding.left - padding.right + if lwidth < 0: + lwidth = 0 + layout.set_width(lwidth * Pango.SCALE) + layout.set_ellipsize(Pango.EllipsizeMode.END) + layout.set_wrap(Pango.WrapMode.CHAR) + layout.set_alignment(Pango.Alignment.RIGHT) + + if lwidth > 0: + lw, lh = layout.get_size() # Can not use get_pixel_extents + lw /= Pango.SCALE + lh /= Pango.SCALE + + lx = x + padding.left + if self.buttons and self.buttons[0] == 'open': + pxbf_width = self.images['open'][2] + lx += pxbf_width + 2 * BUTTON_BORDER + BUTTON_SPACING + ly = y + padding.top + 0.5 * ( + h - padding.top - padding.bottom - lh) + + Gtk.render_layout(context, cr, lx, ly, layout) + + for index, button_name in enumerate(self.buttons): + pxbf_sens, pxbf_insens, pxbf_width, pxbf_height = \ + self.images[button_name] + if (not self.editable and button_name in {'select', 'clear'} + or not self.size and button_name in {'open', 'save'}): + pixbuf = pxbf_insens + else: + pixbuf = pxbf_sens + + if index == 0 and button_name == 'open': + x_offset = 0 + else: + x_offset = (w - button_width + + (pxbf_width + (2 * BUTTON_BORDER) + BUTTON_SPACING) + * index) + if x_offset < 0: + continue + bx = cell_area.x + x_offset + by = cell_area.y + bw = pxbf_width + (2 * BUTTON_BORDER) + + Gtk.render_background(context, cr, bx, by, bw, h) + Gtk.render_frame(context, cr, bx, by, bw, h) + + Gdk.cairo_set_source_pixbuf( + cr, pixbuf, bx + BUTTON_BORDER, by + (h - pxbf_height) / 2) + cr.paint() + context.restore() + + +GObject.type_register(CellRendererBinary) diff --git a/tryton/tryton/common/cellrendererbutton.py b/tryton/tryton/common/cellrendererbutton.py new file mode 100644 index 0000000000..a93bd6a529 --- /dev/null +++ b/tryton/tryton/common/cellrendererbutton.py @@ -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 gi.repository import Gtk, GObject, Pango + + +class CellRendererButton(Gtk.CellRenderer): + __gproperties__ = { + "text": (GObject.TYPE_STRING, None, "Text", + "Displayed text", GObject.ParamFlags.READWRITE), + 'visible': (GObject.TYPE_INT, 'Visible', + 'Visible', 0, 10, 0, GObject.ParamFlags.READWRITE), + 'sensitive': (GObject.TYPE_INT, 'Sensitive', + 'Sensitive', 0, 10, 0, GObject.ParamFlags.READWRITE), + } + + __gsignals__ = { + 'clicked': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + } + + def __init__(self, text=""): + Gtk.CellRenderer.__init__(self) + self.text = text + self.set_property('mode', Gtk.CellRendererMode.EDITABLE) + self.visible = True + self.sensitive = True + + def do_set_property(self, pspec, value): + setattr(self, pspec.name, value) + + def do_get_property(self, pspec): + return getattr(self, pspec.name) + + def do_render(self, cr, widget, background_area, cell_area, flags): + if not self.visible: + return + + if not self.sensitive: + state = Gtk.StateFlags.INSENSITIVE + else: + state = Gtk.StateFlags.NORMAL + + context = widget.get_style_context() + context.save() + context.add_class('text-button') + context.set_state(state) + + xpad, ypad = self.get_padding() + x = cell_area.x + xpad + y = cell_area.y + ypad + w = cell_area.width - 2 * xpad + h = cell_area.height - 2 * ypad + + Gtk.render_background(context, cr, x, y, w, h) + Gtk.render_frame(context, cr, x, y, w, h) + + padding = context.get_padding(state) + layout = widget.create_pango_layout(self.text) + layout.set_width((w - padding.left - padding.right) * Pango.SCALE) + layout.set_ellipsize(Pango.EllipsizeMode.END) + layout.set_wrap(Pango.WrapMode.CHAR) + + lw, lh = layout.get_size() # Can not use get_pixel_extents + lw /= Pango.SCALE + lh /= Pango.SCALE + if w < lw: + x = x + padding.left + else: + x = x + padding.left + 0.5 * ( + w - padding.left - padding.right - lw) + y = y + padding.top + 0.5 * (h - padding.top - padding.bottom - lh) + + Gtk.render_layout(context, cr, x, y, layout) + context.restore() + + def do_get_size(self, widget, cell_area=None): + if cell_area is None: + return (0, 0, 30, 18) + else: + return (cell_area.x, cell_area.y, + cell_area.width, cell_area.height) + + def do_start_editing( + self, event, widget, path, background_area, cell_area, flags): + if not self.visible or not self.sensitive: + return + self.emit("clicked", path) + + +GObject.type_register(CellRendererButton) diff --git a/tryton/tryton/common/cellrendererclickablepixbuf.py b/tryton/tryton/common/cellrendererclickablepixbuf.py new file mode 100644 index 0000000000..d26b36eb53 --- /dev/null +++ b/tryton/tryton/common/cellrendererclickablepixbuf.py @@ -0,0 +1,21 @@ +# 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 gi.repository import Gtk, GObject + + +class CellRendererClickablePixbuf(Gtk.CellRendererPixbuf): + __gsignals__ = { + 'clicked': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING, )), + } + + def __init__(self): + Gtk.CellRendererPixbuf.__init__(self) + self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE) + + def do_activate( + self, event, widget, path, background_area, cell_area, flags): + self.emit('clicked', path) + + +GObject.type_register(CellRendererClickablePixbuf) diff --git a/tryton/tryton/common/cellrenderercombo.py b/tryton/tryton/common/cellrenderercombo.py new file mode 100644 index 0000000000..28eaede53d --- /dev/null +++ b/tryton/tryton/common/cellrenderercombo.py @@ -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 gi.repository import GObject, Gtk + +from tryton.common.selection import selection_shortcuts + + +class CellRendererCombo(Gtk.CellRendererCombo): + + def on_editing_started(self, editable, path): + super().on_editing_started(editable, path) + selection_shortcuts(editable) + + +GObject.type_register(CellRendererCombo) diff --git a/tryton/tryton/common/cellrendererfloat.py b/tryton/tryton/common/cellrendererfloat.py new file mode 100644 index 0000000000..b819a8d87a --- /dev/null +++ b/tryton/tryton/common/cellrendererfloat.py @@ -0,0 +1,61 @@ +# 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 locale + +from gi.repository import Gdk, GObject + +from .cellrendererinteger import CellRendererInteger + + +class CellRendererFloat(CellRendererInteger): + + def __init__(self): + super(CellRendererFloat, self).__init__() + self.digits = None + + def on_editing_started(self, editable, path): + super().on_editing_started(editable, path) + editable.connect('key-press-event', self.key_press_event) + + @property + def __decimal_point(self): + return locale.localeconv()['decimal_point'] + + @property + def __thousands_sep(self): + return locale.localeconv()['thousands_sep'] + + def key_press_event(self, widget, event): + for name in ('KP_Decimal', 'KP_Separator'): + if event.keyval == Gdk.keyval_from_name(name): + text = self.__decimal_point + try: + start_pos, end_pos = widget.get_selection_bounds() + except ValueError: + start_pos = widget.get_position() + end_pos = None + if self._can_insert_text(widget, text, start_pos, end_pos): + buffer_ = widget.get_buffer() + if end_pos: + buffer_.delete_text(start_pos, end_pos - start_pos) + buffer_.insert_text(start_pos, text, len(text)) + widget.set_position(widget.get_position() + len(text)) + return True + + def _can_insert_text(self, entry, new_text, start_pos, end_pos=None): + value = entry.get_text() + if end_pos is None: + end_pos = start_pos + new_value = value[:start_pos] + new_text + value[end_pos:] + if new_value not in {'-', self.__decimal_point, self.__thousands_sep}: + try: + value = locale.atof(new_value) + except ValueError: + return False + if (self.digits + and not (round(value, self.digits[1]) == float(value))): + return False + return True + + +GObject.type_register(CellRendererFloat) diff --git a/tryton/tryton/common/cellrendererinteger.py b/tryton/tryton/common/cellrendererinteger.py new file mode 100644 index 0000000000..c60d0316c3 --- /dev/null +++ b/tryton/tryton/common/cellrendererinteger.py @@ -0,0 +1,33 @@ +# 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 locale + +from gi.repository import GObject + +from .cellrenderertext import CellRendererText + + +class CellRendererInteger(CellRendererText): + + def on_editing_started(self, editable, path): + super().on_editing_started(editable, path) + editable.set_alignment(1.0) + editable.connect('insert_text', self.sig_insert_text) + + def _can_insert_text(self, entry, new_text, position): + value = entry.get_text() + new_value = value[:position] + new_text + value[position:] + if new_value != '-': + try: + locale.atoi(new_value) + except ValueError: + return False + return True + + def sig_insert_text(self, entry, new_text, new_text_length, position): + position = entry.get_position() + if not self._can_insert_text(entry, new_text, position): + entry.stop_emission_by_name('insert-text') + + +GObject.type_register(CellRendererInteger) diff --git a/tryton/tryton/common/cellrenderertext.py b/tryton/tryton/common/cellrenderertext.py new file mode 100644 index 0000000000..428cc438d9 --- /dev/null +++ b/tryton/tryton/common/cellrenderertext.py @@ -0,0 +1,28 @@ +# 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 gi.repository import Gtk, GObject + + +class CellRendererText(Gtk.CellRendererText): + + def __init__(self): + super(CellRendererText, self).__init__() + self.connect('editing-started', self.__class__.on_editing_started) + + def on_editing_started(self, editable, path): + pass + + +class CellRendererTextCompletion(CellRendererText): + + def __init__(self, set_completion): + super(CellRendererTextCompletion, self).__init__() + self.set_completion = set_completion + + def on_editing_started(self, editable, path): + super().on_editing_started(editable, path) + self.set_completion(editable, path) + + +GObject.type_register(CellRendererText) +GObject.type_register(CellRendererTextCompletion) diff --git a/tryton/tryton/common/cellrenderertoggle.py b/tryton/tryton/common/cellrenderertoggle.py new file mode 100644 index 0000000000..ff222afc59 --- /dev/null +++ b/tryton/tryton/common/cellrenderertoggle.py @@ -0,0 +1,10 @@ +# 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 gi.repository import GObject, Gtk + + +class CellRendererToggle(Gtk.CellRendererToggle): + pass + + +GObject.type_register(CellRendererToggle) diff --git a/tryton/tryton/common/common.py b/tryton/tryton/common/common.py new file mode 100644 index 0000000000..2485131f13 --- /dev/null +++ b/tryton/tryton/common/common.py @@ -0,0 +1,1336 @@ +# 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 gettext +import os +import platform +import subprocess +import tempfile +import re +import logging +import unicodedata +import colorsys +import xml.etree.ElementTree as ET +from collections import defaultdict +from decimal import Decimal +try: + from http import HTTPStatus +except ImportError: + from http import client as HTTPStatus +from functools import wraps, lru_cache +from tryton.config import CONFIG +from tryton.config import TRYTON_ICON, PIXMAPS_DIR +import sys +import webbrowser +import traceback +import tryton.rpc as rpc +import socket +import _thread +import urllib.request +import urllib.parse +import urllib.error +from string import Template +import shlex +try: + import ssl +except ImportError: + ssl = None +import zipfile +from threading import Lock + +from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk + +from tryton import __version__ +from tryton.exceptions import TrytonServerError, TrytonError +from tryton.pyson import PYSONEncoder +from .underline import set_underline +from .widget_style import widget_class + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +class IconFactory: + + batchnum = 10 + _tryton_icons = [] + _name2id = {} + _icons = {} + _local_icons = {} + _pixbufs = defaultdict(dict) + + @classmethod + def load_local_icons(cls): + for fname in os.listdir(PIXMAPS_DIR): + name = os.path.splitext(fname)[0] + path = os.path.join(PIXMAPS_DIR, fname) + cls._local_icons[name] = path + + @classmethod + def load_icons(cls, refresh=False): + if not refresh: + cls._name2id.clear() + cls._icons.clear() + del cls._tryton_icons[:] + + try: + icons = rpc.execute('model', 'ir.ui.icon', 'list_icons', + rpc.CONTEXT) + except TrytonServerError: + icons = [] + for icon_id, icon_name in icons: + if refresh and icon_name in cls._icons: + continue + cls._tryton_icons.append((icon_id, icon_name)) + cls._name2id[icon_name] = icon_id + + @classmethod + def register_icon(cls, iconname): + # iconname might be '' when page do not define icon + if (not iconname + or iconname in cls._icons + or iconname in cls._local_icons): + return + if iconname not in cls._name2id: + cls.load_icons(refresh=True) + try: + icon_ref = (cls._name2id[iconname], iconname) + except KeyError: + return + idx = cls._tryton_icons.index(icon_ref) + to_load = slice(max(0, idx - cls.batchnum // 2), + idx + cls.batchnum // 2) + ids = [e[0] for e in cls._tryton_icons[to_load]] + try: + icons = rpc.execute('model', 'ir.ui.icon', 'read', ids, + ['name', 'icon'], rpc.CONTEXT) + except TrytonServerError: + icons = [] + for icon in icons: + name = icon['name'] + data = icon['icon'].encode('utf-8') + cls._icons[name] = data + cls._tryton_icons.remove((icon['id'], icon['name'])) + del cls._name2id[icon['name']] + + @classmethod + def get_pixbuf(cls, iconname, size=16, color=None, badge=None): + if not iconname: + return + colors = CONFIG['icon.colors'].split(',') + cls.register_icon(iconname) + if iconname not in cls._pixbufs[(size, badge)]: + if iconname in cls._icons: + data = cls._icons[iconname] + elif iconname in cls._local_icons: + path = cls._local_icons[iconname] + with open(path, 'rb') as fp: + data = fp.read() + else: + logger.error("Unknown icon %s" % iconname) + return + if not color: + color = colors[0] + try: + ET.register_namespace('', 'http://www.w3.org/2000/svg') + root = ET.fromstring(data) + root.attrib['fill'] = color + if badge: + if not isinstance(badge, str): + try: + badge = colors[badge] + except IndexError: + badge = color + ET.SubElement(root, 'circle', { + 'cx': '20', + 'cy': '4', + 'r': '4', + 'fill': badge, + }) + data = ET.tostring(root) + except ET.ParseError: + pass + width = height = { + Gtk.IconSize.MENU: 16, + Gtk.IconSize.SMALL_TOOLBAR: 16, + Gtk.IconSize.LARGE_TOOLBAR: 24, + Gtk.IconSize.BUTTON: 16, + Gtk.IconSize.DND: 12, + Gtk.IconSize.DIALOG: 48, + }.get(size, size) + pixbuf = data2pixbuf(data, width, height) + cls._pixbufs[(size, badge)][iconname] = pixbuf + return cls._pixbufs[(size, badge)][iconname] + + @classmethod + def get_image(cls, iconname, size=16, color=None, badge=None): + image = Gtk.Image() + if iconname: + pixbuf = cls.get_pixbuf(iconname, size, color, badge) + image.set_from_pixbuf(pixbuf) + return image + + @classmethod + def _convert_url(cls, value, size=16, size_param=None): + if not value: + return + parts = urllib.parse.urlsplit(value) + parts = list(parts) + if not parts[0]: + parts[0] = 'https' if rpc.CONNECTION.ssl else 'http' + if not parts[1]: + hostname = get_hostname(CONFIG['login.host']) + port = get_port(CONFIG['login.host']) + parts[1] = '%s:%s' % (hostname, port) + if size_param: + query = urllib.parse.parse_qsl(parts[4]) + query.append((size_param, size)) + parts[4] = urllib.parse.urlencode(query) + return urllib.parse.urlunsplit(parts) + + @classmethod + @lru_cache(maxsize=CONFIG['image.cache_size']) + def get_pixbuf_url(cls, url, size=16, size_param=None): + if not url: + return + url = cls._convert_url(url, size, size_param=size_param) + try: + with urllib.request.urlopen(url) as response: + return data2pixbuf(response.read(), size, size) + except urllib.error.URLError: + logger.info("Can not fetch %s", url, exc_info=True) + + +IconFactory.load_local_icons() + + +class ModelAccess(object): + + batchnum = 100 + _access = {} + _models = [] + + def load_models(self, refresh=False): + if not refresh: + self._access.clear() + del self._models[:] + + try: + self._models = rpc.execute('model', 'ir.model', 'list_models', + rpc.CONTEXT) + except TrytonServerError: + pass + + def __getitem__(self, model): + if model in self._access: + return self._access[model] + if model not in self._models: + self.load_models(refresh=True) + idx = self._models.index(model) + to_load = slice(max(0, idx - self.batchnum // 2), + idx + self.batchnum // 2) + try: + access = rpc.execute('model', 'ir.model.access', 'get_access', + self._models[to_load], rpc.CONTEXT) + except TrytonServerError: + access = {} + self._access.update(access) + return self._access[model] + + +MODELACCESS = ModelAccess() + + +class ModelHistory(object): + _models = set() + + def load_history(self): + self._models.clear() + try: + self._models.update(rpc.execute('model', 'ir.model', + 'list_history', rpc.CONTEXT)) + except TrytonServerError: + pass + + def __contains__(self, model): + return model in self._models + + +MODELHISTORY = ModelHistory() + + +class ModelName: + _names = {} + + def load_names(self): + try: + self._names = rpc.execute( + 'model', 'ir.model', 'get_names', rpc.CONTEXT) + except TrytonServerError: + pass + + def get(self, model): + if not self._names: + self.load_names() + return self._names.get(model, '') + + def clear(self): + return self._names.clear() + + +MODELNAME = ModelName() + + +class ViewSearch(object): + searches = {} + + def __init__(self): + class Encoder(PYSONEncoder): + def default(self, obj): + if isinstance(obj, Decimal): + return float(obj) + return super(Encoder, self).default(obj) + self.encoder = Encoder() + + def load_searches(self): + try: + self.searches = rpc.execute('model', 'ir.ui.view_search', + 'get_search', rpc.CONTEXT) + except TrytonServerError: + self.searches = {} + + def __getitem__(self, model): + return self.searches.get(model, []) + + def add(self, model, name, domain): + try: + id_, = RPCExecute('model', 'ir.ui.view_search', + 'create', [{ + 'model': model, + 'name': name, + 'domain': self.encoder.encode(domain), + }]) + except RPCException: + return + self.searches.setdefault(model, []).append((id_, name, domain, True)) + + def remove(self, model, id_): + try: + RPCExecute('model', 'ir.ui.view_search', 'delete', [id_]) + except RPCException: + return + for i, domain in enumerate(self.searches[model]): + if domain[0] == id_: + del self.searches[model][i] + break + + +VIEW_SEARCH = ViewSearch() + + +def get_toplevel_window(): + from tryton.gui.main import Main + return Main().get_active_window() + + +def get_sensible_widget(window): + from tryton.gui.main import Main + main = Main() + if main and window == main.window: + focus_widget = window.get_focus() + page = main.get_page() + if page and focus_widget and focus_widget.is_ancestor(page.widget): + return page.widget + return window + + +def selection(title, values, alwaysask=False): + if not values or len(values) == 0: + return None + elif len(values) == 1 and (not alwaysask): + key = list(values.keys())[0] + return (key, values[key]) + + parent = get_toplevel_window() + dialog = Gtk.Dialog( + title=_('Selection'), transient_for=parent, + modal=True, destroy_with_parent=True) + dialog.add_button(set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + dialog.add_button(set_underline(_("OK")), Gtk.ResponseType.OK) + dialog.set_icon(TRYTON_ICON) + dialog.set_default_response(Gtk.ResponseType.OK) + dialog.set_default_size(400, 400) + + label = Gtk.Label(title or _('Your selection:')) + dialog.vbox.pack_start(label, expand=False, fill=False, padding=0) + dialog.vbox.pack_start( + Gtk.HSeparator(), expand=False, fill=True, padding=0) + + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + treeview = Gtk.TreeView() + treeview.set_headers_visible(False) + scrolledwindow.add(treeview) + dialog.vbox.pack_start(scrolledwindow, expand=True, fill=True, padding=0) + + treeview.get_selection().set_mode(Gtk.SelectionMode.SINGLE) + cell = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Widget", cell, text=0) + treeview.append_column(column) + treeview.set_search_column(0) + + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) + keys = list(values.keys()) + keys.sort() + i = 0 + for val in keys: + model.append([str(val), i]) + i += 1 + + treeview.set_model(model) + treeview.connect('row-activated', + lambda x, y, z: dialog.response(Gtk.ResponseType.OK) or True) + + dialog.show_all() + response = dialog.run() + res = None + if response == Gtk.ResponseType.OK: + sel = treeview.get_selection().get_selected() + if sel: + (model, i) = sel + if i: + index = model.get_value(i, 1) + value = keys[index] + res = (value, values[value]) + parent.present() + dialog.destroy() + return res + + +def file_selection(title, filename='', + action=Gtk.FileChooserAction.OPEN, preview=True, multi=False, + filters=None): + parent = get_toplevel_window() + if action == Gtk.FileChooserAction.OPEN: + buttons = (set_underline(_("Cancel")), Gtk.ResponseType.CANCEL, + set_underline(_("Select")), Gtk.ResponseType.OK) + else: + buttons = (set_underline(_("Cancel")), Gtk.ResponseType.CANCEL, + set_underline(_("Save")), Gtk.ResponseType.OK) + win = Gtk.FileChooserDialog( + title=title, transient_for=parent, action=action) + win.add_buttons(*buttons) + win.set_icon(TRYTON_ICON) + if filename: + if action in (Gtk.FileChooserAction.SAVE, + Gtk.FileChooserAction.CREATE_FOLDER): + win.set_current_name(filename) + else: + win.set_filename(filename) + if hasattr(win, 'set_do_overwrite_confirmation'): + win.set_do_overwrite_confirmation(True) + win.set_select_multiple(multi) + win.set_default_response(Gtk.ResponseType.OK) + if filters is not None: + for filt in filters: + win.add_filter(filt) + + def update_preview_cb(win, img): + have_preview = False + filename = win.get_preview_filename() + if filename: + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( + filename, 128, 128) + img.set_from_pixbuf(pixbuf) + have_preview = True + except (IOError, GLib.GError): + pass + win.set_preview_widget_active(have_preview) + return + + if preview: + img_preview = Gtk.Image() + win.set_preview_widget(img_preview) + win.connect('update-preview', update_preview_cb, img_preview) + + button = win.run() + if button != Gtk.ResponseType.OK: + result = None + elif not multi: + result = win.get_filename() + else: + result = win.get_filenames() + parent.present() + win.destroy() + return result + + +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') + + +def slugify(value): + if not isinstance(value, str): + value = str(value) + value = unicodedata.normalize('NFKD', value) + value = str(_slugify_strip_re.sub('', value).strip()) + return _slugify_hyphenate_re.sub('-', value) + + +def file_write(filename, data): + if isinstance(data, str): + data = data.encode('utf-8') + dtemp = tempfile.mkdtemp(prefix='tryton_') + if not isinstance(filename, str): + name, ext = filename + else: + name, ext = os.path.splitext(filename) + filename = ''.join([slugify(name), os.extsep, slugify(ext)]) + filepath = os.path.join(dtemp, filename) + with open(filepath, 'wb') as fp: + fp.write(data) + return filepath + + +def file_open(filename, type=None, print_p=False): + def save(): + save_name = file_selection(_('Save As...'), + action=Gtk.FileChooserAction.SAVE) + if save_name: + file_p = open(filename, 'rb') + save_p = open(save_name, 'wb+') + save_p.write(file_p.read()) + save_p.close() + file_p.close() + + if print_p and type == 'zip': + with zipfile.ZipFile(filename, 'r') as zip_file: + for name in zip_file.namelist(): + ztype = os.path.splitext(name) + with zip_file.open(name) as zfile: + zfilename = file_write(name, zfile.read()) + file_open(zfilename, type=ztype, print_p=True) + return + + if os.name == 'nt': + operation = 'open' + if print_p: + operation = 'print' + try: + os.startfile(os.path.normpath(filename), operation) + except WindowsError: + save() + elif print_p: + if type in {'odt', 'odp', 'ods', 'odg'}: + try: + subprocess.Popen(['soffice', '-p', filename]) + except OSError: + save() + else: + try: + subprocess.Popen(['lpr', filename]) + except OSError: + try: + subprocess.Popen(['lp', filename]) + except OSError: + save() + elif sys.platform == 'darwin': + try: + subprocess.Popen(['/usr/bin/open', filename]) + except OSError: + save() + else: + try: + subprocess.Popen(['xdg-open', filename]) + except OSError: + save() + + +def mailto(to=None, cc=None, subject=None, body=None, attachment=None): + if CONFIG['client.email']: + cmd = Template(CONFIG['client.email']).substitute( + to=to or '', + cc=cc or '', + subject=subject or '', + body=body or '', + attachment=attachment or '', + ) + args = shlex.split(str(cmd)) + subprocess.Popen(args) + return + if os.name != 'nt' and sys.platform != 'darwin': + args = ['xdg-email', '--utf8'] + if cc: + args.extend(['--cc', cc]) + if subject: + args.extend(['--subject', subject]) + if body: + args.extend(['--body', body]) + if attachment: + args.extend(['--attach', attachment]) + if to: + args.append(to) + try: + subprocess.Popen(args) + return + except OSError: + pass + # http://www.faqs.org/rfcs/rfc2368.html + url = "mailto:" + if to: + url += urllib.parse.quote(to.strip(), "@,") + url += '?' + if cc: + url += "&cc=" + urllib.parse.quote(cc, "@,") + if subject: + url += "&subject=" + urllib.parse.quote(subject, "") + if body: + body = "\r\n".join(body.splitlines()) + url += "&body=" + urllib.parse.quote(body, "") + if attachment: + url += "&attachment=" + urllib.parse.quote(attachment, "") + webbrowser.open(url, new=1) + + +class UniqueDialog(object): + + def __init__(self): + self.running = False + + def build_dialog(self, *args): + raise NotImplementedError + + def process_response(self, response): + return response + + def __call__(self, *args, **kwargs): + if self.running: + return + + parent = kwargs.pop('parent', None) + if not parent: + parent = get_toplevel_window() + dialog = self.build_dialog(parent, *args, **kwargs) + dialog.set_icon(TRYTON_ICON) + setup_window(dialog) + self.running = True + dialog.show_all() + response = dialog.run() + response = self.process_response(response) + if parent: + parent.present() + dialog.destroy() + self.running = False + return response + + +class MessageDialog(UniqueDialog): + + def build_dialog(self, parent, message, msg_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, secondary=None): + dialog = Gtk.MessageDialog( + transient_for=parent, modal=True, destroy_with_parent=True, + message_type=msg_type, buttons=buttons, text=message) + if secondary: + dialog.format_secondary_text(secondary) + return dialog + + def __call__(self, message, *args, **kwargs): + return super(MessageDialog, self).__call__(message, *args, **kwargs) + + +message = MessageDialog() + + +class WarningDialog(MessageDialog): + + def __call__(self, message, title, buttons=Gtk.ButtonsType.OK, **kwargs): + return super().__call__( + title, Gtk.MessageType.WARNING, buttons, message, **kwargs) + + +warning = WarningDialog() + + +class UserWarningDialog(WarningDialog): + + def build_dialog(self, *args, **kwargs): + dialog = super().build_dialog(*args, **kwargs) + label = Gtk.Label( + label=_('Do you want to proceed?'), + halign=Gtk.Align.FILL, valign=Gtk.Align.END) + dialog.vbox.pack_start(label, expand=True, fill=True, padding=0) + self.always = Gtk.CheckButton( + label=_('Always ignore this warning.'), halign=Gtk.Align.START) + dialog.vbox.pack_start(self.always, expand=True, fill=False, padding=0) + return dialog + + def process_response(self, response): + if response == Gtk.ResponseType.YES: + if self.always.get_active(): + return 'always' + return 'ok' + return 'cancel' + + def __call__(self, message, title): + return super().__call__(message, title, Gtk.ButtonsType.YES_NO) + + +userwarning = UserWarningDialog() + + +class ConfirmationDialog(MessageDialog): + + def __call__(self, message, *args, **kwargs): + return super().__call__( + message, Gtk.MessageType.QUESTION, *args, **kwargs) + + +class SurDialog(ConfirmationDialog): + + def __call__(self, message): + response = super().__call__(message, buttons=Gtk.ButtonsType.YES_NO) + return response == Gtk.ResponseType.YES + + +sur = SurDialog() + + +class Sur3BDialog(ConfirmationDialog): + + response_mapping = { + Gtk.ResponseType.YES: 'ok', + Gtk.ResponseType.NO: 'ko', + Gtk.ResponseType.CANCEL: 'cancel' + } + + def build_dialog(self, *args, **kwargs): + dialog = super().build_dialog(*args, **kwargs) + dialog.add_button(set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + dialog.add_button(set_underline(_("No")), Gtk.ResponseType.NO) + dialog.add_button(set_underline(_("Yes")), Gtk.ResponseType.YES) + dialog.set_default_response(Gtk.ResponseType.YES) + return dialog + + def __call__(self, message): + response = super().__call__(message, buttons=Gtk.ButtonsType.NONE) + return self.response_mapping.get(response, 'cancel') + + +sur_3b = Sur3BDialog() + + +class AskDialog(MessageDialog): + + def build_dialog(self, *args, **kwargs): + visibility = kwargs.pop('visibility') + dialog = super().build_dialog(*args, **kwargs) + dialog.set_default_response(Gtk.ResponseType.OK) + box = dialog.get_message_area() + self.entry = Gtk.Entry() + self.entry.set_activates_default(True) + self.entry.set_visibility(visibility) + box.pack_start(self.entry, False, False, 0) + return dialog + + def process_response(self, response): + if response == Gtk.ResponseType.OK: + return self.entry.get_text() + + def __call__(self, question, visibility=True): + return super().__call__( + question, Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.OK_CANCEL, visibility=visibility) + + +ask = AskDialog() + + +class ConcurrencyDialog(UniqueDialog): + + def build_dialog(self, parent): + tooltips = Tooltips() + dialog = Gtk.MessageDialog( + transient_for=parent, modal=True, destroy_with_parent=True, + message_type=Gtk.MessageType.QUESTION, + buttons=Gtk.ButtonsType.NONE, text=_('Concurrency Exception')) + dialog.format_secondary_text( + _('This record has been modified while you were editing it.')) + cancel_button = dialog.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + tooltips.set_tip(cancel_button, _('Cancel saving')) + compare_button = dialog.add_button( + set_underline(_("Compare")), Gtk.ResponseType.APPLY) + tooltips.set_tip(compare_button, _('See the modified version')) + write_button = dialog.add_button( + set_underline(_("Write Anyway")), Gtk.ResponseType.OK) + tooltips.set_tip(write_button, _('Save your current version')) + dialog.set_default_response(Gtk.ResponseType.CANCEL) + return dialog + + def __call__(self, model, id_, context): + res = super(ConcurrencyDialog, self).__call__() + + if res == Gtk.ResponseType.OK: + return True + if res == Gtk.ResponseType.APPLY: + from tryton.gui.window import Window + name = RPCExecute( + 'model', model, 'read', [id_], ['rec_name'], + context=context)[0]['rec_name'] + with Window(allow_similar=True): + Window.create( + model, + res_id=id_, + name=_("Compare: %s") % name, + domain=[('id', '=', id_)], + context=context, + mode=['form']) + return False + + +concurrency = ConcurrencyDialog() + + +class ErrorDialog(UniqueDialog): + + def build_dialog(self, parent, title, details): + dialog = Gtk.MessageDialog( + transient_for=parent, modal=True, destroy_with_parent=True, + message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.NONE) + dialog.set_default_size(600, 400) + dialog.set_position(Gtk.WindowPosition.CENTER) + + dialog.add_button(set_underline(_("Close")), Gtk.ResponseType.CANCEL) + dialog.set_default_response(Gtk.ResponseType.CANCEL) + + dialog.set_markup( + '%s' % GLib.markup_escape_text(_('Application Error'))) + dialog.format_secondary_markup( + '%s' % GLib.markup_escape_text(title)) + + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + + textview = Gtk.TextView(editable=False, sensitive=True, monospace=True) + buf = Gtk.TextBuffer() + buf.set_text(details) + textview.set_buffer(buf) + + viewport.add(textview) + scrolledwindow.add(viewport) + dialog.vbox.pack_start( + scrolledwindow, expand=True, fill=True, padding=0) + + button_roundup = Gtk.LinkButton.new_with_label( + CONFIG['bug.url'], _("Report Bug")) + button_roundup.get_child().set_halign(Gtk.Align.START) + button_roundup.connect('activate-link', + lambda widget: webbrowser.open(CONFIG['bug.url'], new=2)) + dialog.vbox.pack_start( + button_roundup, expand=False, fill=False, padding=0) + + return dialog + + def __call__(self, title, details): + if isinstance(title, Exception): + title = "%s: %s" % (title.__class__.__name__, title) + details += '\n' + title + log = logging.getLogger(__name__) + log.error(details) + return super(ErrorDialog, self).__call__(title, details) + + +error = ErrorDialog() + + +def check_version(box, version=__version__): + def info_bar_response(info_bar, response, box, url): + if response == Gtk.ResponseType.ACCEPT: + webbrowser.open(url) + box.remove(info_bar) + + class HeadRequest(urllib.request.Request): + def get_method(self): + return 'HEAD' + + version = version.split('.') + series = '.'.join(version[:2]) + version[2] = str(int(version[2]) + 1) + version = '.'.join(version) + filename = 'tryton-%s.tar.gz' % version + if hasattr(sys, 'frozen'): + if sys.platform == 'win32': + bits = platform.architecture()[0] + filename = 'tryton-%s-%s.exe' % (bits, version) + elif sys.platform == 'darwin': + filename = 'tryton-%s.dmg' % version + url = list(urllib.parse.urlparse(CONFIG['download.url'])) + url[2] = '/%s/%s' % (series, filename) + url = urllib.parse.urlunparse(url) + + logger.info(_("Check URL: %s"), url) + try: + urllib.request.urlopen( + HeadRequest(url), timeout=5, cafile=rpc._CA_CERTS) + except (urllib.error.HTTPError, socket.timeout): + return True + except Exception: + logger.error( + _("Unable to check for new version."), exc_info=True) + return True + else: + if check_version(box, version): + info_bar = Gtk.InfoBar() + info_bar.get_content_area().pack_start( + Gtk.Label(label=_("A new version is available!")), + expand=True, fill=True, padding=0) + info_bar.set_show_close_button(True) + info_bar.add_button(_("Download"), Gtk.ResponseType.ACCEPT) + info_bar.connect('response', info_bar_response, box, url) + box.pack_start(info_bar, expand=True, fill=True, padding=0) + info_bar.show_all() + return False + + +def to_xml(string): + return string.replace('&', '&' + ).replace('<', '<').replace('>', '>') + + +PLOCK = Lock() + + +def process_exception(exception, *args, **kwargs): + from .domain_parser import DomainParser + + rpc_execute = kwargs.get('rpc_execute', rpc.execute) + + if isinstance(exception, TrytonServerError): + if exception.faultCode == 'UserWarning': + name, msg, description = exception.args + res = userwarning(description, msg) + if res in ('always', 'ok'): + RPCExecute('model', 'res.user.warning', 'create', [{ + 'user': rpc._USER, + 'name': name, + 'always': (res == 'always'), + }], + process_exception=False) + return rpc_execute(*args) + elif exception.faultCode == 'UserError': + msg, description, domain = exception.args + if domain: + domain, fields = domain + domain_parser = DomainParser(fields) + if domain_parser.stringable(domain): + description += '\n' + domain_parser.string(domain) + warning(description, msg) + elif exception.faultCode == 'ConcurrencyException': + if (len(args) >= 6 + and args[0] == 'model' + and args[2] in {'write', 'delete'} + and len(args[3]) == 1): + if concurrency(args[1], args[3][0], args[5]): + if '_timestamp' in args[5]: + del args[5]['_timestamp'] + return rpc_execute(*args) + else: + message( + _('Concurrency Exception'), msg_type=Gtk.MessageType.ERROR) + elif exception.faultCode == str(int(HTTPStatus.UNAUTHORIZED)): + from tryton.gui.main import Main + if PLOCK.acquire(False): + try: + Login() + except TrytonError as exception: + if exception.faultCode == 'QueryCanceled': + Main().on_quit() + sys.exit() + raise + finally: + PLOCK.release() + if args: + return rpc_execute(*args) + elif exception.faultCode == str(int(HTTPStatus.TOO_MANY_REQUESTS)): + message( + _('Too many requests. Try again later.'), + msg_type=Gtk.MessageType.ERROR) + else: + error(exception, exception.faultString) + else: + error(exception, traceback.format_exc()) + raise RPCException(exception) + + +class Login(object): + def __init__(self, func=rpc.login): + parameters = {} + while True: + try: + func(parameters) + except TrytonServerError as exception: + if exception.faultCode == str(int(HTTPStatus.UNAUTHORIZED)): + parameters.clear() + continue + if (exception.faultCode + == str(int(HTTPStatus.TOO_MANY_REQUESTS))): + message( + _('Too many requests. Try again later.'), + msg_type=Gtk.MessageType.ERROR) + continue + if exception.faultCode != 'LoginException': + raise + name, msg, type = exception.args + value = getattr(self, 'get_%s' % type)(msg) + if value is None: + raise TrytonError('QueryCanceled') + parameters[name] = value + continue + else: + return + + @classmethod + def get_char(cls, message): + return ask(message) + + @classmethod + def get_password(cls, message): + return ask(message, visibility=False) + + +class Logout: + def __init__(self): + try: + rpc.logout() + except TrytonServerError: + pass + + +def node_attributes(node): + result = {} + attrs = node.attributes + if attrs is None: + return {} + for i in range(attrs.length): + result[str(attrs.item(i).localName)] = str(attrs.item(i).nodeValue) + return result + + +def hex2rgb(hexstring, digits=2): + """ + Converts a hexstring color to a rgb tuple. + Example: #ff0000 -> (1.0, 0.0, 0.0) + digits is an integer number telling how many characters should be + interpreted for each component in the hexstring. + """ + if isinstance(hexstring, (tuple, list)): + return hexstring + top = float(int(digits * 'f', 16)) + r = int(hexstring[1:digits + 1], 16) + g = int(hexstring[digits + 1:digits * 2 + 1], 16) + b = int(hexstring[digits * 2 + 1:digits * 3 + 1], 16) + return r / top, g / top, b / top + + +def highlight_rgb(r, g, b, amount=0.1): + h, s, v = colorsys.rgb_to_hsv(r, g, b) + return colorsys.hsv_to_rgb(h, s, (v + amount) % 1) + + +def generateColorscheme(masterColor, keys, light=0.1): + """ + Generates a dictionary where the keys match the keys argument and + the values are colors derivated from the masterColor. + Each color has a value higher then the previous of `light`. + Each color has a hue separated from the previous by the golden angle. + The masterColor is given in a hex string format. + """ + r, g, b = hex2rgb(COLOR_SCHEMES.get(masterColor, masterColor)) + h, s, v = colorsys.rgb_to_hsv(r, g, b) + if keys: + light = min(light, (1. - v) / len(keys)) + golden_angle = 0.618033988749895 + return {key: colorsys.hsv_to_rgb((h + golden_angle * i) % 1, + s, (v + light * i) % 1) for i, key in enumerate(keys)} + + +class RPCException(Exception): + + def __init__(self, exception): + super(RPCException, self).__init__(exception) + self.exception = exception + + +class RPCProgress(object): + + def __init__(self, method, args): + self.method = method + self.args = args + self.parent = None + self.res = None + self.error = False + self.exception = None + + def start(self): + try: + self.res = getattr(rpc, self.method)(*self.args) + except Exception as exception: + self.error = True + self.res = False + self.exception = exception + else: + if not self.res: + self.error = True + if self.callback: + # Post to GTK queue to be run by the main thread + GLib.idle_add(self.process) + return True + + def run(self, process_exception_p=True, callback=None): + self.process_exception_p = process_exception_p + self.callback = callback + + if callback: + # Parent is only useful if it is asynchronous + # otherwise the cursor is not updated. + self.parent = get_toplevel_window() + window = self.parent.get_window() + if window: + display = window.get_display() + watch = Gdk.Cursor.new_for_display( + display, Gdk.CursorType.WATCH) + window.set_cursor(watch) + _thread.start_new_thread(self.start, ()) + return + else: + self.start() + return self.process() + + def process(self): + if self.parent and self.parent.get_window(): + self.parent.get_window().set_cursor(None) + + if self.exception and self.process_exception_p: + def rpc_execute(*args): + return RPCProgress('execute', args).run( + self.process_exception_p, self.callback) + try: + return process_exception( + self.exception, *self.args, rpc_execute=rpc_execute) + except RPCException as exception: + self.exception = exception + + def return_(): + if self.exception: + raise self.exception + else: + return self.res + + if self.callback: + self.callback(return_) + else: + return return_() + + +def RPCExecute(*args, **kwargs): + rpc_context = rpc.CONTEXT.copy() + if kwargs.get('context'): + rpc_context.update(kwargs['context']) + args = args + (rpc_context,) + process_exception = kwargs.get('process_exception', True) + callback = kwargs.get('callback') + return RPCProgress('execute', args).run(process_exception, callback) + + +def RPCContextReload(callback=None): + def update(context): + rpc.context_reset() + try: + rpc.CONTEXT.update(context()) + except RPCException: + pass + if callback: + callback() + context = RPCExecute( + 'model', 'res.user', 'get_preferences', True, + callback=update if callback else None) + if not callback: + rpc.context_reset() + rpc.CONTEXT.update(context) + + +class Tooltips(object): + _tooltips = None + + def set_tip(self, widget, tip_text): + if hasattr(widget, 'set_tooltip_text'): + return widget.set_tooltip_text(tip_text) + if not self._tooltips: + self._tooltips = Gtk.Tooltips() + return self._tooltips.set_tip(widget, tip_text) + + def enable(self): + if self._tooltips: + self._tooltips.enable() + + def disable(self): + if self._tooltips: + self._tooltips.disable() + + +COLOR_SCHEMES = { + 'red': '#cf1d1d', + 'green': '#3fb41b', + 'blue': '#224565', + 'grey': '#444444', + 'black': '#000000', + 'darkcyan': '#305755', +} + + +def filter_domain(domain): + ''' + Return the biggest subset of domain with only AND operator + ''' + res = [] + for arg in domain: + if isinstance(arg, str): + if arg == 'OR': + res = [] + break + continue + if isinstance(arg, tuple): + res.append(arg) + elif isinstance(arg, list): + res.extend(filter_domain(arg)) + return res + + +def timezoned_date(date, reverse=False): + try: + from dateutil.tz.win import tzwinlocal as tzlocal + except ImportError: + from dateutil.tz import tzlocal + from dateutil.tz import tzutc + + lzone = tzlocal() + szone = tzutc() + if reverse: + lzone, szone = szone, lzone + return date.replace(tzinfo=szone).astimezone(lzone).replace(tzinfo=None) + + +def untimezoned_date(date): + return timezoned_date(date, reverse=True) + + +def humanize(size): + for x in ('bytes', 'KB', 'MB', 'GB', 'TB', 'PB'): + if size < 1000: + return '%3.1f%s' % (size, x) + size /= 1000.0 + + +def get_hostname(netloc): + if '[' in netloc and ']' in netloc: + hostname = netloc.split(']')[0][1:] + elif ':' in netloc: + hostname = netloc.split(':')[0] + else: + hostname = netloc + return hostname.strip() + + +def get_port(netloc): + netloc = netloc.split(']')[-1] + if ':' in netloc: + try: + return int(netloc.split(':')[1]) + except ValueError: + pass + return 8000 + + +def resize_pixbuf(pixbuf, width, height): + img_height = pixbuf.get_height() + height = min(img_height, height) if height != -1 else img_height + img_width = pixbuf.get_width() + width = min(img_width, width) if width != -1 else img_width + + if img_width / width < img_height / height: + width = float(img_width) / float(img_height) * float(height) + else: + height = float(img_height) / float(img_width) * float(width) + return pixbuf.scale_simple( + int(width), int(height), GdkPixbuf.InterpType.BILINEAR) + + +def _data2pixbuf(data, width=None, height=None): + loader = GdkPixbuf.PixbufLoader() + if width and height: + loader.set_size(width, height) + loader.write(data) + loader.close() + return loader.get_pixbuf() + + +def data2pixbuf(data, width=None, height=None): + if data: + try: + return _data2pixbuf(data, width, height) + except GLib.GError: + pass + + +def apply_label_attributes(label, readonly, required): + if not readonly: + widget_class(label, 'editable', True) + widget_class(label, 'required', required) + else: + widget_class(label, 'editable', False) + widget_class(label, 'required', False) + + +def ellipsize(string, length): + if len(string) <= length: + return string + ellipsis = _('...') + return string[:length - len(ellipsis)] + ellipsis + + +def get_align(float_, expand=True): + "Convert float align into Gtk.Align" + value = float(float_) + if value < 0.5: + return Gtk.Align.START + elif value == 0.5: + if expand: + return Gtk.Align.FILL + else: + return Gtk.Align.CENTER + else: + return Gtk.Align.END + + +def date_format(format_=None): + return format_ or rpc.CONTEXT.get('locale', {}).get('date', '%x') + + +def idle_add(func): + @wraps(func) + def wrapper(*args, **kwargs): + GLib.idle_add(func, *args, **kwargs) + return wrapper + + +def setup_window(window): + if sys.platform == 'darwin': + window.set_mnemonic_modifier(Gdk.ModifierType.CONTROL_MASK) diff --git a/tryton/tryton/common/completion.py b/tryton/tryton/common/completion.py new file mode 100644 index 0000000000..1860166d4e --- /dev/null +++ b/tryton/tryton/common/completion.py @@ -0,0 +1,74 @@ +# 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 logging +import gettext + +from gi.repository import GLib, Gtk + +from tryton.config import CONFIG +from tryton.common import RPCExecute +from tryton.common.domain_parser import likify +from tryton.exceptions import TrytonServerError, TrytonError + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +def get_completion(search=True, create=True): + "Return a EntryCompletion" + completion = Gtk.EntryCompletion() + completion.set_match_func(lambda *a: True) + completion.set_model(Gtk.ListStore(str, int)) + completion.set_text_column(0) + completion.props.popup_set_width = False + if search: + completion.insert_action_markup(0, _('Search...')) + if create: + completion.insert_action_markup(1, _('Create...')) + return completion + + +def update_completion(entry, record, field, model, domain=None): + "Update entry completion" + def update(search_text, domain): + if not entry.props.window: + return False + if search_text != entry.get_text(): + return False + completion_model = entry.get_completion().get_model() + if not search_text or not model: + completion_model.clear() + completion_model.search_text = search_text + return False + if getattr(completion_model, 'search_text', None) == search_text: + return False + if domain is None: + domain = field.domain_get(record) + context = field.get_search_context(record) + domain = [('rec_name', 'ilike', likify(search_text)), domain] + order = field.get_search_order(record) + + def callback(results): + try: + results = results() + except (TrytonError, TrytonServerError): + results = [] + if search_text != entry.get_text(): + return False + completion_model.clear() + for result in results: + completion_model.append([result['rec_name'], result['id']]) + completion_model.search_text = search_text + # Force display of popup + entry.emit('changed') + try: + RPCExecute('model', model, 'search_read', domain, 0, + CONFIG['client.limit'], order, ['rec_name'], context=context, + process_exception=False, callback=callback) + except Exception: + logger.warning( + _("Unable to search for completion of %s") % model, + exc_info=True) + return False + search_text = entry.get_text() + GLib.timeout_add(300, update, search_text, domain) diff --git a/tryton/tryton/common/datetime_.py b/tryton/tryton/common/datetime_.py new file mode 100644 index 0000000000..41c49a116c --- /dev/null +++ b/tryton/tryton/common/datetime_.py @@ -0,0 +1,650 @@ +# 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 gettext +import datetime + +from gi.repository import Gdk, GObject, Gtk + +from dateutil.relativedelta import relativedelta +from dateutil.parser import parse + +from .common import IconFactory + +__all__ = ['Date', 'CellRendererDate', 'Time', 'CellRendererTime', 'DateTime'] + +_ = gettext.gettext + + +def _fix_format(format_): + if '%Y' in format_: + if (datetime.date.min.strftime('%Y') != '0001' + and datetime.date.min.strftime('%4Y') == '0001'): + format_ = format_.replace('%Y', '%4Y') + return format_ + + +def date_parse(text, format_='%x'): + try: + return datetime.datetime.strptime(text, format_) + except ValueError: + pass + formatted_date = datetime.date(1988, 7, 16).strftime(format_) + try: + dayfirst = formatted_date.index('16') == 0 + except ValueError: + dayfirst = False + try: + monthfirst = formatted_date.index('7') <= 1 + except ValueError: + monthfirst = False + yearfirst = not dayfirst and not monthfirst + return parse(text, dayfirst=dayfirst, yearfirst=yearfirst, ignoretz=True) + + +class Date(Gtk.Entry): + __gtype_name__ = 'Date' + __gproperties__ = { + 'value': (GObject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + 'format': (GObject.TYPE_STRING, + '%x', + _('Format'), + _('Display format'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + } + __gsignals__ = { + 'date-changed': ( + GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, + GObject.TYPE_NONE, ()), + } + + def __init__(self): + self.__date = None + self.__format = '%x' + + Gtk.Entry.__init__(self) + + self.set_width_chars(20) + + self.connect('focus-out-event', self.focus_out) + self.connect('activate', self.activate) + + # Calendar Popup + self.set_icon_from_pixbuf( + Gtk.EntryIconPosition.PRIMARY, + IconFactory.get_pixbuf('tryton-date', Gtk.IconSize.MENU)) + self.set_icon_tooltip_text( + Gtk.EntryIconPosition.PRIMARY, + _('Open the calendar')) + self.connect('icon-press', self.icon_press) + + self.__cal_popup = Gtk.Window(type=Gtk.WindowType.POPUP) + self.__cal_popup.set_events( + self.__cal_popup.get_events() | Gdk.EventMask.KEY_PRESS_MASK) + self.__cal_popup.set_resizable(False) + self.__cal_popup.connect('delete-event', self.cal_popup_closed) + self.__cal_popup.connect('key-press-event', self.cal_popup_key_pressed) + self.__cal_popup.connect('button-press-event', + self.cal_popup_button_pressed) + + self.__calendar = Gtk.Calendar() + cal_options = ( + Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES + | Gtk.CalendarDisplayOptions.SHOW_HEADING + | Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS) + self.__calendar.set_display_options(cal_options) + self.__cal_popup.add(self.__calendar) + self.__calendar.connect('day-selected', self.cal_popup_changed) + self.__calendar.connect('day-selected-double-click', + self.cal_popup_double_click) + self.__calendar.show() + + def parse(self): + text = self.get_text() + date = None + if text: + try: + date = date_parse(text, self.__format).date() + except (ValueError, OverflowError): + pass + + self.__date = date + + def update_label(self): + if not self.__date: + self.set_text('') + return + self.set_text(self.__date.strftime(self.__format)) + + def icon_press(self, entry, icon_pos, event): + self.grab_focus() + if icon_pos == Gtk.EntryIconPosition.PRIMARY: + self.cal_popup_open() + + def cal_popup_open(self): + self.parse() + if self.__date: + self.__calendar.select_month( + self.__date.month - 1, self.__date.year) + self.__calendar.select_day(self.__date.day) + self.__cal_popup.set_transient_for(self.get_toplevel()) + popup_position(self, self.__cal_popup) + popup_show(self.__cal_popup) + + def cal_popup_changed(self, calendar): + year, month, day = self.__calendar.get_date() + self.__date = datetime.date(year, month + 1, day) + + self.update_label() + + self.emit('date-changed') + + def cal_popup_double_click(self, calendar): + self.cal_popup_hide() + + def cal_popup_key_pressed(self, calendar, event): + if event.keyval != Gdk.KEY_Escape: + return False + + self.stop_emission_by_name('key-press-event') + self.cal_popup_hide() + return True + + def cal_popup_button_pressed(self, calendar, event): + child = event.window + window = calendar.get_window() + if child != window: + while child: + if child == window: + return False + child = child.get_parent() + self.cal_popup_hide() + return True + + def cal_popup_closed(self, popup): + self.cal_popup_hide() + return True + + def cal_popup_hide(self): + popup_hide(self.__cal_popup) + self.grab_focus() + self.emit('date-changed') + + def cal_popup_is_visible(self): + return self.__cal_popup.is_visible() + + def focus_out(self, entry, event): + previous_date = self.__date + self.parse() + self.update_label() + if self.__date != previous_date: + self.emit('date-changed') + return False + + def activate(self, entry=None): + self.parse() + self.update_label() + self.emit('date-changed') + return False + + def do_set_property(self, prop, value): + if prop.name == 'value': + if isinstance(value, str): + self.set_text(value) + self.parse() + value = self.__date + if value: + if isinstance(value, datetime.datetime): + value = value.date() + assert isinstance(value, datetime.date), value + self.__date = value + self.update_label() + elif prop.name == 'format': + self.__format = _fix_format(value) + self.update_label() + + def do_get_property(self, prop): + if prop.name == 'value': + return self.__date + elif prop.name == 'format': + return self.__format + + +GObject.type_register(Date) + + +class CellRendererDate(Gtk.CellRendererText): + __gproperties__ = { + 'format': (GObject.TYPE_STRING, + _('Format'), + _('Display format'), + '%x', + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + } + + def __init__(self): + self.__format = '%x' + self.__entry = None + self.__focus_out_id = 0 + + Gtk.CellRendererText.__init__(self) + + def do_set_property(self, prop, value): + if prop.name == 'format': + self.__format = _fix_format(value) + return + Gtk.CellRendererText.set_property(self, prop, value) + + def do_get_property(self, prop): + if prop.name == 'format': + return self.__format + return Gtk.CellRendererText.get_property(self, prop) + + def do_start_editing( + self, event, widget, path, background_area, cell_area, flags): + if not self.props.editable: + return + self.__entry = add_operators(Date()) # TODO add_operators has option + self.__entry.props.format = self.props.format + self.__entry.props.value = self.props.text + self.__entry.set_has_frame(False) + self.__entry.connect('editing-done', self.__editing_done) + self.__focus_out_id = self.__entry.connect( + 'focus-out-event', self.__focus_out_event) + # XXX focus-out-event + self.__entry.show() + return self.__entry + + def __editing_done(self, entry): + self.__entry = None + if self.__focus_out_id: + entry.disconnect(self.__focus_out_id) + canceled = entry.props.editing_canceled + self.stop_editing(canceled) + if canceled: + return + # TODO emit edited + + def __focus_out_event(self, entry, event): + if entry.cal_popup_is_visible(): + return True + entry.props.editing_canceled = True + entry.editing_done() + entry.remove_widget() + return False + + +GObject.type_register(CellRendererDate) + + +class Time(Gtk.ComboBox): + __gtype_name__ = 'Time' + __gproperties__ = { + 'value': (GObject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + 'format': (GObject.TYPE_STRING, + _('Format'), + _('Display format'), + '%X', + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + } + __gsignals__ = { + 'time-changed': ( + GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, + GObject.TYPE_NONE, ()), + } + + def __init__(self): + self.__time = None + self.__format = '%X' + + Gtk.ComboBox.__init__(self, has_entry=True) + + self.__entry = self.get_child() + self.__entry.set_width_chars(10) + + self.__entry.connect('focus-out-event', self.focus_out) + self.__entry.connect('activate', self.activate) + self.connect('changed', self.changed) + + self.__model = Gtk.ListStore( + GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) + self.update_model() + self.set_model(self.__model) + self.set_entry_text_column(0) + + def parse(self): + text = self.__entry.get_text() + time = None + if text: + try: + time = date_parse(text).time() + except (ValueError, OverflowError): + pass + + self.__time = time + + def update_label(self): + if self.__time is None: + self.__entry.set_text('') + return + + self.__entry.set_text(self.__time.strftime(self.__format)) + + def update_model(self): + self.__model.clear() + timelist_set_list( + self.__model, datetime.time(0, 0), datetime.time(23, 59), + self.__format) + + def focus_out(self, entry, event): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def activate(self, entry=None): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def changed(self, combobox): + # "changed" signal is also triggered by text editing + # so only parse when a row is active + if combobox.get_active_iter(): + self.parse() + self.update_label() + self.emit('time-changed') + return False + + def do_set_property(self, prop, value): + if prop.name == 'value': + if isinstance(value, str): + self.__entry.set_text(value) + self.parse() + value = self.__time + if value: + if isinstance(value, datetime.datetime): + value = value.time() + self.__time = value + self.update_label() + elif prop.name == 'format': + self.__format = _fix_format(value) + self.update_label() + self.update_model() + + def do_get_property(self, prop): + if prop.name == 'value': + return self.__time + elif prop.name == 'format': + return self.__format + + +GObject.type_register(Time) + + +class CellRendererTime(Gtk.CellRendererCombo): + __gproperties__ = { + 'format': (GObject.TYPE_STRING, + '%X', + _('Format'), + _('Display format'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + } + + def __init__(self): + self.__format = '%X' + self.__combo = None + self.__focus_out_id = 0 + + Gtk.CellRendererText.__init__(self) + + def do_set_property(self, prop, value): + if prop.name == 'format': + self.__format = _fix_format(value) + return + Gtk.CellRendererText.set_property(self, prop, value) + + def do_get_property(self, prop): + if prop.name == 'format': + return self.__format + return Gtk.CellRendererText.get_property(self, prop) + + def do_start_editing( + self, event, widget, path, background_area, cell_area, flags): + if not self.props.editable: + return + self.__combo = add_operators(Time()) # TODO add_operators has option + self.__combo.props.format = self.props.format + self.__combo.props.value = self.props.text + self.__combo.props.has_frame = False + self.__combo.connect('editing-done', self.__editing_done) + # TODO: connect to changed + self.__focus_out_id = self.__combo.connect( + 'focus-out-event', self.__focus_out_event) + self.__combo.show() + return self.__combo + + def __editing_done(self, combo): + self.__combo = None + canceled = combo.props.editing_canceled + self.stop_editing(canceled) + if canceled: + return + # TODO emit edited + + def __focus_out_event(self, combo, event): + self.__editing_done(combo) + return False + + +GObject.type_register(CellRendererTime) + + +class DateTime(Gtk.HBox): + __gtype_name__ = 'DateTime' + __gproperties__ = { + 'value': (GObject.TYPE_PYOBJECT, + _('Value'), + _('Displayed value'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + 'date-format': (GObject.TYPE_STRING, + '%x', + _('Date Format'), + _('Displayed date format'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + 'time-format': (GObject.TYPE_STRING, + '%X', + _('Date Format'), + _('Displayed date format'), + GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE), + } + __gsignals__ = { + 'datetime-changed': ( + GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION, + GObject.TYPE_NONE, ()), + } + + def __init__(self): + Gtk.HBox.__init__(self, spacing=4) + + self.__date = Date() + self.pack_start(self.__date, expand=True, fill=True, padding=0) + self.__date.show() + self.__date.connect('date-changed', + lambda e: self.emit('datetime-changed')) + + self.__time = Time() + self.pack_start(self.__time, expand=True, fill=True, padding=0) + self.__time.show() + self.__time.connect('time-changed', + lambda e: self.emit('datetime-changed')) + + def parse(self): + self.__date.parse() + self.__time.parse() + + def do_set_property(self, prop, value): + if prop.name == 'value': + self.__date.props.value = value + self.__time.props.value = value + elif prop.name == 'date-format': + self.__date.props.format = value + elif prop.name == 'time-format': + self.__time.props.format = value + + def do_get_property(self, prop): + if prop.name == 'value': + date = self.__date.props.value + time = self.__time.props.value or datetime.time() + if date: + return datetime.datetime.combine(date, time) + else: + return + elif prop.name == 'date-format': + return self.__date.props.format + elif prop.name == 'time-format': + return self.__time.props.format + + def modify_bg(self, state, color): + self.__date.modify_bg(state, color) + self.__time.child.modify_bg(state, color) + + def modify_base(self, state, color): + self.__date.modify_base(state, color) + self.__time.child.modify_base(state, color) + + def modify_fg(self, state, color): + self.__date.modify_fg(state, color) + self.__time.child.modify_fg(state, color) + + def modify_text(self, state, color): + self.__date.modify_text(state, color) + self.__time.child.modify_text(state, color) + + +GObject.type_register(DateTime) + + +def popup_position(widget, popup): + allocation = widget.get_allocation() + x, y = widget.get_window().get_root_coords(allocation.x, allocation.y) + popup.move(x, y + allocation.height) + + +def popup_show(popup): + popup.show() + popup.grab_focus() + popup.grab_add() + + window = popup.get_window() + display = window.get_display() + seat = display.get_default_seat() + seat.grab(window, Gdk.SeatCapabilities.ALL, True, None, None, None, None) + + +def popup_hide(popup): + popup.hide() + popup.grab_remove() + window = popup.get_window() + display = window.get_display() + seat = display.get_default_seat() + seat.ungrab() + + +def timelist_set_list(model, min_, max_, format_): + time = min_ + delta = 30 + while time < max_: + model.append((time.strftime(format_), time)) + hour = time.hour + minute = time.minute + delta + hour, minute = divmod(minute, 60) + hour += time.hour + if hour >= 24: + break + time = datetime.time(hour, minute) + + +def add_operators(widget): + def key_press(editable, event): + if not editable.get_editable(): + return False + if event.keyval in OPERATORS: + value = widget.props.value + if value: + if isinstance(value, datetime.time): + value = datetime.datetime.combine( + datetime.date.today(), value) + try: + widget.props.value = value + OPERATORS[event.keyval] + except TypeError: + return False + return True + elif event.keyval in (Gdk.KEY_KP_Equal, Gdk.KEY_equal): + widget.props.value = datetime.datetime.now() + return True + return False + + if isinstance(widget, DateTime): + for child in widget.get_children(): + add_operators(child) + return widget + if isinstance(widget, Gtk.ComboBox): + editable = widget.get_child() + else: + editable = widget + editable.connect('key-press-event', key_press) + return widget + + +OPERATORS = { + Gdk.KEY_S: relativedelta(seconds=-1), + Gdk.KEY_s: relativedelta(seconds=1), + Gdk.KEY_I: relativedelta(minutes=-1), + Gdk.KEY_i: relativedelta(minutes=1), + Gdk.KEY_H: relativedelta(hours=-1), + Gdk.KEY_h: relativedelta(hours=1), + Gdk.KEY_D: relativedelta(days=-1), + Gdk.KEY_d: relativedelta(days=1), + Gdk.KEY_W: relativedelta(weeks=-1), + Gdk.KEY_w: relativedelta(weeks=1), + Gdk.KEY_M: relativedelta(months=-1), + Gdk.KEY_m: relativedelta(months=1), + Gdk.KEY_Y: relativedelta(years=-1), + Gdk.KEY_y: relativedelta(years=1), + } + +if __name__ == '__main__': + win = Gtk.Window() + win.connect('delete-event', Gtk.main_quit) + + v = Gtk.VBox() + v.show() + + d = add_operators(Date()) + d.show() + v.pack_start(d, expand=False, fill=False, padding=0) + + t = add_operators(Time()) + t.show() + v.pack_start(t, expand=False, fill=False, padding=0) + + t = add_operators(Time()) + t.props.format = '%H:%M' + t.show() + v.pack_start(t, expand=False, fill=False, padding=0) + + dt = add_operators(DateTime()) + dt.show() + v.pack_start(dt, expand=False, fill=False, padding=0) + + win.add(v) + win.show() + + Gtk.main() diff --git a/tryton/tryton/common/domain_inversion.py b/tryton/tryton/common/domain_inversion.py new file mode 100644 index 0000000000..64e59050df --- /dev/null +++ b/tryton/tryton/common/domain_inversion.py @@ -0,0 +1,462 @@ +# 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 re +import operator +import datetime +from collections import defaultdict +from functools import reduce, partial + + +def sql_like(value, pattern, ignore_case=True): + flag = re.IGNORECASE if ignore_case else 0 + + escape = False + chars = [] + for char in re.split(r'(\\|.)', pattern)[1::2]: + if escape: + if char in ('%', '_'): + chars.append(char) + else: + chars.extend(['\\', char]) + escape = False + elif char == '\\': + escape = True + elif char == '_': + chars.append('.') + elif char == '%': + chars.append('.*') + else: + chars.append(re.escape(char)) + + regexp = re.compile(''.join(chars), flag) + return bool(regexp.fullmatch(value)) + + +like = partial(sql_like, ignore_case=False) +ilike = partial(sql_like, ignore_case=True) + + +def in_(a, b): + if isinstance(a, (list, tuple)): + if isinstance(b, (list, tuple)): + return any(operator.contains(b, x) for x in a) + else: + return operator.contains(a, b) + else: + return operator.contains(b, a) + + +OPERATORS = defaultdict(lambda: lambda a, b: True) +OPERATORS.update({ + '=': operator.eq, + '>': operator.gt, + '<': operator.lt, + '<=': operator.le, + '>=': operator.ge, + '!=': operator.ne, + 'in': in_, + 'not in': lambda a, b: not in_(a, b), + 'ilike': ilike, + 'not ilike': lambda a, b: not ilike(a, b), + 'like': like, + 'not like': lambda a, b: not like(a, b), + }) + + +def locale_part(expression, field_name, locale_name='id'): + if expression == field_name: + return locale_name + if '.' in expression: + fieldname, local = expression.split('.', 1) + return local + return expression + + +def is_leaf(expression): + return (isinstance(expression, (list, tuple)) + and len(expression) > 2 + and isinstance(expression[1], str)) + + +def constrained_leaf(part, boolop=operator.and_): + field, operand, value = part[:3] + if operand == '=' and boolop == operator.and_: + # We should consider that other domain inversion will set a correct + # value to this field + return True + return False + + +def eval_leaf(part, context, boolop=operator.and_): + field, operand, value = part[:3] + if '.' in field: + # In the case where the leaf concerns a m2o then having a value in the + # evaluation context is deemed suffisant + return bool(context.get(field.split('.')[0])) + context_field = context.get(field) + if (operand not in {'=', '!='} + and (context_field is None or value is None) + and not (operand in {'in', 'not in'} + and context_field is None + and (isinstance(value, (list, tuple)) and None in value))): + return + if isinstance(context_field, datetime.date) and not value: + if isinstance(context_field, datetime.datetime): + value = datetime.datetime.min + else: + value = datetime.date.min + if isinstance(value, datetime.date) and not context_field: + if isinstance(value, datetime.datetime): + context_field = datetime.datetime.min + else: + context_field = datetime.date.min + if isinstance(context_field, (list, tuple)) and value is None: + value = type(context_field)() + if (isinstance(context_field, str) + and isinstance(value, (list, tuple))): + try: + value = '%s,%s' % tuple(value) + except TypeError: + pass + elif (isinstance(context_field, (list, tuple)) + and isinstance(value, str)): + try: + context_field = '%s,%s' % tuple(context_field) + except TypeError: + pass + elif ((isinstance(context_field, list) and isinstance(value, tuple)) + or (isinstance(context_field, tuple) and isinstance(value, list))): + context_field = list(context_field) + value = list(value) + if (operand in ('=', '!=') + and isinstance(context_field, (list, tuple)) + and isinstance(value, int)): + operand = { + '=': 'in', + '!=': 'not in', + }[operand] + try: + return OPERATORS[operand](context_field, value) + except TypeError: + return False + + +def inverse_leaf(domain): + if domain in ('AND', 'OR'): + return domain + elif is_leaf(domain): + if 'child_of' in domain[1] and '.' not in domain[0]: + if len(domain) == 3: + return domain + else: + return [domain[3]] + list(domain[1:]) + return domain + else: + return list(map(inverse_leaf, domain)) + + +def filter_leaf(domain, field, model): + if domain in ('AND', 'OR'): + return domain + elif is_leaf(domain): + if domain[0].startswith(field) and len(domain) > 3: + if domain[3] != model: + return ('id', '=', None) + return domain + else: + return [filter_leaf(d, field, model) for d in domain] + + +def prepare_reference_domain(domain, reference): + "convert domain to replace reference fields by their local part" + + def value2reference(value): + model, ref_id = None, None + if isinstance(value, str) and ',' in value: + model, ref_id = value.split(',', 1) + if ref_id != '%': + try: + ref_id = int(ref_id) + except ValueError: + model, ref_id = None, value + elif (isinstance(value, (list, tuple)) + and len(value) == 2 + and isinstance(value[0], str) + and (isinstance(value[1], int) or value[1] == '%')): + model, ref_id = value + else: + ref_id = value + return model, ref_id + + if domain in ('AND', 'OR'): + return domain + elif is_leaf(domain): + if domain[0] == reference: + if domain[1] in {'=', '!='}: + model, ref_id = value2reference(domain[2]) + if model is not None: + if ref_id == '%': + if domain[1] == '=': + return [reference + '.id', '!=', None, model] + else: + return [reference, 'not like', domain[2]] + return [reference + '.id', domain[1], ref_id, model] + elif domain[1] in {'in', 'not in'}: + model_values = {} + for value in domain[2]: + model, ref_id = value2reference(value) + if model is None: + break + model_values.setdefault(model, []).append(ref_id) + else: + new_domain = ['OR'] if domain[1] == 'in' else ['AND'] + for model, ref_ids in model_values.items(): + if '%' in ref_ids: + if domain[1] == 'in': + new_domain.append( + [reference + '.id', '!=', None, model]) + else: + new_domain.append( + [reference, 'not like', model + ',%']) + else: + new_domain.append( + [reference + '.id', domain[1], ref_ids, model]) + return new_domain + return [] + return domain + else: + return [prepare_reference_domain(d, reference) for d in domain] + + +def extract_reference_models(domain, field_name): + "returns the set of the models available for field_name" + if domain in ('AND', 'OR'): + return set() + elif is_leaf(domain): + local_part = domain[0].split('.', 1)[0] + if local_part == field_name and len(domain) > 3: + return {domain[3]} + return set() + else: + return reduce(operator.or_, + (extract_reference_models(d, field_name) for d in domain)) + + +def eval_domain(domain, context, boolop=operator.and_): + "compute domain boolean value according to the context" + if is_leaf(domain): + return eval_leaf(domain, context, boolop=boolop) + elif not domain and boolop is operator.and_: + return True + elif not domain and boolop is operator.or_: + return False + elif domain[0] == 'AND': + return eval_domain(domain[1:], context) + elif domain[0] == 'OR': + return eval_domain(domain[1:], context, operator.or_) + else: + return boolop(bool(eval_domain(domain[0], context)), + bool(eval_domain(domain[1:], context, boolop))) + + +def localize_domain(domain, field_name=None, strip_target=False): + "returns only locale part of domain. eg: langage.code -> code" + if domain in ('AND', 'OR', True, False): + return domain + elif is_leaf(domain): + if 'child_of' in domain[1]: + if domain[0].count('.'): + _, target_part = domain[0].split('.', 1) + return [target_part] + list(domain[1:]) + if len(domain) == 3: + return domain + else: + return [domain[3]] + list(domain[1:-1]) + locale_name = 'id' + if isinstance(domain[2], str): + locale_name = 'rec_name' + n = 3 if strip_target else 4 + return [locale_part(domain[0], field_name, locale_name)] \ + + list(domain[1:n]) + list(domain[4:]) + else: + return [localize_domain(part, field_name, strip_target) + for part in domain] + + +def simplify(domain): + "remove unused domain delimiter" + if is_leaf(domain): + return domain + elif domain in ('OR', 'AND'): + return domain + elif domain in (['OR'], ['AND']): + return [] + elif (isinstance(domain, list) and len(domain) == 1 + and not is_leaf(domain[0])): + return simplify(domain[0]) + elif (isinstance(domain, list) and len(domain) == 2 + and domain[0] in ('AND', 'OR')): + return [simplify(domain[1])] + else: + return [simplify(branch) for branch in domain] + + +def merge(domain, domoperator=None): + if not domain or domain in ('AND', 'OR'): + return [] + domain_type = 'OR' if domain[0] == 'OR' else 'AND' + if is_leaf(domain): + return [domain] + elif domoperator is None: + return [domain_type] + reduce(operator.add, + [merge(e, domain_type) for e in domain]) + elif domain_type == domoperator: + return reduce(operator.add, [merge(e, domain_type) for e in domain]) + else: + # without setting the domoperator + return [merge(domain)] + + +def concat(*domains, **kwargs): + domoperator = kwargs.get('domoperator') + result = [] + if domoperator: + result.append(domoperator) + for domain in domains: + if domain: + result.append(domain) + return simplify(merge(result)) + + +def unique_value(domain): + "Return if unique, the field and the value" + if (isinstance(domain, list) + and len(domain) == 1): + domain, = domain + name = domain[0] + value = domain[2] + count = 0 + if len(domain) == 4 and name[-3:] == '.id': + count = 1 + model = domain[3] + value = [model, value] + if name.count('.') == count and domain[1] == '=': + return True, domain[1], value + return False, None, None + + +def parse(domain): + if is_leaf(domain): + return domain + elif not domain: + return And([]) + elif domain[0] == 'OR': + return Or(domain[1:]) + else: + return And(domain[1:] if domain[0] == 'AND' else domain) + + +def domain_inversion(domain, symbol, context=None): + """compute an inversion of the domain eventually the context is used to + simplify the expression""" + if context is None: + context = {} + expression = parse(domain) + if symbol not in expression.variables: + return True + return expression.inverse(symbol, context) + + +class And(object): + + def __init__(self, expressions): + self.branches = list(map(parse, expressions)) + self.variables = set() + for expression in self.branches: + if is_leaf(expression): + self.variables.add(self.base(expression[0])) + elif isinstance(expression, And): + self.variables |= expression.variables + + def base(self, expression): + if '.' not in expression: + return expression + else: + return expression.split('.')[0] + + def inverse(self, symbol, context): + result = [] + for part in self.branches: + if isinstance(part, And): + part_inversion = part.inverse(symbol, context) + evaluated = isinstance(part_inversion, bool) + if symbol not in part.variables: + continue + if not evaluated: + result.append(part_inversion) + elif part_inversion: + continue + else: + return False + elif is_leaf(part) and self.base(part[0]) == symbol: + result.append(part) + else: + field = part[0] + if (field not in context + or field in context + and (eval_leaf(part, context, operator.and_) + or constrained_leaf(part, operator.and_))): + result.append(True) + else: + return False + + result = [e for e in result if e is not True] + if result == []: + return True + else: + return simplify(result) + + +class Or(And): + + def inverse(self, symbol, context): + result = [] + known_variables = set(context.keys()) + if not known_variables >= (self.variables - {symbol}): + # In this case we don't know enough about this OR part, we + # consider it to be True (because people will have the constraint + # on this part later). + return True + for part in self.branches: + if isinstance(part, And): + part_inversion = part.inverse(symbol, context) + evaluated = isinstance(part_inversion, bool) + if symbol not in part.variables: + if evaluated and part_inversion: + return True + continue + if not evaluated: + result.append(part_inversion) + elif part_inversion: + return True + else: + continue + elif is_leaf(part) and self.base(part[0]) == symbol: + result.append(part) + else: + field = part[0] + field = self.base(field) + if (field in context + and (eval_leaf(part, context, operator.or_) + or constrained_leaf(part, operator.or_))): + return True + elif (field in context + and not eval_leaf(part, context, operator.or_)): + result.append(False) + + result = [e for e in result if e is not False] + if result == []: + return False + else: + return simplify(['OR'] + result) diff --git a/tryton/tryton/common/domain_parser.py b/tryton/tryton/common/domain_parser.py new file mode 100644 index 0000000000..f13ad814d1 --- /dev/null +++ b/tryton/tryton/common/domain_parser.py @@ -0,0 +1,877 @@ +# 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 shlex import shlex +from types import GeneratorType +import gettext +import locale +import decimal +from decimal import Decimal +import datetime +import io +from collections import OrderedDict + +from tryton.common import untimezoned_date, timezoned_date, date_format +from tryton.common.datetime_ import date_parse +from tryton.common.timedelta import parse as timedelta_parse +from tryton.common.timedelta import format as timedelta_format +from tryton.pyson import PYSONDecoder + +__all__ = ['DomainParser'] + +_ = gettext.gettext +ListGeneratorType = type(iter([])) +TupleGeneratorType = type(iter(())) +OPERATORS = ( + '!=', + '<=', + '>=', + '=', + '!', + '<', + '>', + ) + + +class udlex(shlex): + "A lexical analyzer class for human domain syntaxes." + def __init__(self, instream=None): + shlex.__init__(self, io.StringIO(instream), posix=True) + self.commenters = '' + self.quotes = '"' + + class DummyWordchars(object): + "Simulate str that contains all chars except somes" + def __contains__(self, item): + return item not in (':', '>', '<', '=', '!', '"', ';', + '(', ')') + + self.wordchars = DummyWordchars() + + +def isgenerator(value): + "Test if value is a generator type" + return isinstance(value, (GeneratorType, ListGeneratorType, + TupleGeneratorType)) + + +def rlist(value): + "Convert recursivly generator into list" + if isgenerator(value) or isinstance(value, list): + return [rlist(x) for x in value] + return value + + +def simplify(value): + "Remove double nested list" + if isinstance(value, list): + if len(value) == 1 and isinstance(value[0], list): + return simplify(value[0]) + elif (len(value) == 2 + and value[0] in ('AND', 'OR') + and isinstance(value[1], list)): + return simplify(value[1]) + elif (len(value) == 3 + and value[0] in ('AND', 'OR') + and isinstance(value[1], list) + and value[0] == value[1][0]): + value = simplify(value[1]) + [value[2]] + return [simplify(x) for x in value] + return value + + +def group_operator(tokens): + "Group token of operators" + try: + cur = next(tokens) + except StopIteration: + return + nex = None + for nex in tokens: + if nex == '=' and cur and cur + nex in OPERATORS: + yield cur + nex + cur = None + else: + if cur is not None: + yield cur + cur = nex + if cur is not None: + yield cur + + +def likify(value, escape='\\'): + "Add % if needed" + if not value: + return '%' + escaped = value.replace(escape + '%', '').replace(escape + '_', '') + if '%' in escaped or '_' in escaped: + return value + else: + return '%' + value + '%' + + +def is_full_text(value, escape='\\'): + escaped = value.strip('%') + escaped = escaped.replace(escape + '%', '').replace(escape + '_', '') + if '%' in escaped or '_' in escaped: + return False + return value.startswith('%') and value.endswith('%') + + +def is_like(value, escape='\\'): + escaped = value.replace(escape + '%', '').replace(escape + '_', '') + return '%' in escaped or '_' in escaped + + +def unescape(value, escape='\\'): + return value.replace(escape + '%', '%').replace(escape + '_', '_') + + +def quote(value): + "Quote string if needed" + if not isinstance(value, str): + return value + if '\\' in value: + value = value.replace('\\', '\\\\') + if '"' in value: + value = value.replace('"', '\\"') + for test in (':', ' ', '(', ')') + OPERATORS: + if test in value: + return '"%s"' % value + return value + + +def ending_clause(domain, deep=0): + "Return the ending clause" + if not domain: + return None, deep + if isinstance(domain[-1], list): + return ending_clause(domain[-1], deep + 1) + return domain[-1], deep + + +def replace_ending_clause(domain, clause): + "Replace the ending clause" + for dom in domain[:-1]: + yield dom + if isinstance(domain[-1], list): + yield replace_ending_clause(domain[-1], clause) + else: + yield clause + + +def append_ending_clause(domain, clause, deep): + "Append clause after the ending clause" + if not domain: + yield clause + return + for dom in domain[:-1]: + yield dom + if isinstance(domain[-1], list): + yield append_ending_clause(domain[-1], clause, deep - 1) + else: + yield domain[-1] + if deep == 0: + yield clause + + +def default_operator(field): + "Return default operator for field" + if field['type'] in ('char', 'text', 'many2one', 'many2many', 'one2many', + 'reference', 'one2one'): + return 'ilike' + elif field['type'] == 'multiselection': + return 'in' + else: + return '=' + + +def negate_operator(operator): + "Return negate operator" + if operator == 'ilike': + return 'not ilike' + elif operator == '=': + return '!=' + elif operator == 'in': + return 'not in' + + +def time_format(field): + return PYSONDecoder({}).decode(field['format']) + + +def split_target_value(field, value): + "Split the reference value into target and value" + assert field['type'] == 'reference' + target = None + if isinstance(value, str): + for key, text in field['selection']: + if value.lower().startswith(text.lower() + ','): + target = key + value = value[len(text) + 1:] + break + return target, value + + +def convert_value(field, value, context=None): + "Convert value for field" + if context is None: + context = {} + + def convert_boolean(): + if isinstance(value, str): + return any(test.lower().startswith(value.lower()) + for test in ( + _('y'), _('Yes'), _('True'), _('t'), '1')) + + def convert_float(): + factor = float(field.get('factor', 1)) + try: + return locale.atof(value) / factor + except (ValueError, AttributeError): + return + + def convert_integer(): + factor = float(field.get('factor', 1)) + try: + return int(locale.atof(value) / factor) + except (ValueError, AttributeError): + return + + def convert_numeric(): + factor = Decimal(field.get('factor', 1)) + try: + return Decimal(locale.delocalize(value)) / factor + except (decimal.InvalidOperation, AttributeError): + return + + def convert_selection(): + if isinstance(value, str): + for key, text in field['selection']: + if value.lower() == text.lower(): + return key + return value + + def convert_datetime(): + if not value: + return + format_ = ( + date_format(context.get('date_format')) + ' ' + time_format(field)) + try: + dt = date_parse(value, format_) + return untimezoned_date(dt) + except ValueError: + return + + def convert_date(): + if not value: + return + format_ = date_format(context.get('date_format')) + try: + return date_parse(value, format_).date() + except (ValueError, TypeError): + return + + def convert_time(): + if not value: + return + try: + return date_parse(value).time() + except (ValueError, TypeError): + return + + def convert_timedelta(): + return timedelta_parse(value, context.get(field.get('converter'))) + + def convert_many2one(): + if value == '': + return None + return value + + converts = { + 'boolean': convert_boolean, + 'float': convert_float, + 'integer': convert_integer, + 'numeric': convert_numeric, + 'selection': convert_selection, + 'multiselection': convert_selection, + 'reference': convert_selection, + 'datetime': convert_datetime, + 'date': convert_date, + 'time': convert_time, + 'timedelta': convert_timedelta, + 'many2one': convert_many2one, + } + return converts.get(field['type'], lambda: value)() + + +def format_value(field, value, target=None, context=None): + "Format value for field" + if context is None: + context = {} + + def format_boolean(): + if value is False: + return _("False") + elif value: + return _("True") + else: + return '' + + def format_integer(): + factor = float(field.get('factor', 1)) + if (value + or (isinstance(value, (int, float)) + and not isinstance(value, bool))): + return str(int(value * factor)) + return '' + + def format_float(): + if (not value + and (not isinstance(value, (int, float, Decimal)) + or isinstance(value, bool))): + return '' + digit = 0 + if isinstance(value, Decimal): + cast = Decimal + else: + cast = float + factor = cast(field.get('factor', 1)) + string_ = str(value * factor) + if 'e' in string_: + string_, exp = string_.split('e') + digit -= int(exp) + if '.' in string_: + digit += len(string_.rstrip('0').split('.')[1]) + return locale.localize( + '{0:.{1}f}'.format(value * factor or 0, digit), True) + + def format_selection(): + if isinstance(field['selection'], (tuple, list)): + selections = dict(field['selection']) + else: + selections = {} + return selections.get(value, value) or '' + + def format_reference(): + if not target: + return format_selection() + selections = dict(field['selection']) + return '%s,%s' % (selections.get(target, target), value) + + def format_datetime(): + if not value: + return '' + if not isinstance(value, datetime.datetime): + time = datetime.datetime.combine(value, datetime.time.min) + else: + time = timezoned_date(value) + format_ = date_format(context.get('date_format')) + if time.time() != datetime.time.min: + format_ += ' ' + time_format(field) + return time.strftime(format_) + + def format_date(): + if not value: + return '' + format_ = date_format(context.get('date_format')) + return value.strftime(format_) + + def format_time(): + if not value: + return '' + return datetime.time.strftime(value, time_format(field)) + + def format_timedelta(): + if not value: + return '' + return timedelta_format(value, context.get(field.get('converter'))) + + def format_many2one(): + if value is None: + return '' + return value + + converts = { + 'boolean': format_boolean, + 'integer': format_integer, + 'float': format_float, + 'numeric': format_float, + 'selection': format_selection, + 'multiselection': format_selection, + 'reference': format_reference, + 'datetime': format_datetime, + 'date': format_date, + 'time': format_time, + 'timedelta': format_timedelta, + 'many2one': format_many2one, + } + if isinstance(value, (list, tuple)): + return ';'.join(format_value(field, x, context=context) for x in value) + return quote(converts.get(field['type'], + lambda: value if value is not None else '')()) + + +def complete_value(field, value): + "Complete value for field" + + def complete_boolean(): + if value is None: + yield True + yield False + elif value: + yield False + else: + yield True + + def complete_selection(): + test_value = value if value is not None else '' + if isinstance(value, list): + test_value = value[-1] or '' + test_value = test_value.strip('%') + for svalue, test in field['selection']: + if test.lower().startswith(test_value.lower()): + if isinstance(value, list): + yield value[:-1] + [svalue] + else: + yield svalue + + def complete_reference(): + test_value = value if value is not None else '' + if isinstance(value, list): + test_value = value[-1] + test_value = test_value.strip('%') + for svalue, test in field['selection']: + if test.lower().startswith(test_value.lower()): + if isinstance(value, list): + yield value[:-1] + [svalue] + else: + yield likify(svalue) + + def complete_datetime(): + yield datetime.date.today() + yield datetime.datetime.utcnow() + + def complete_date(): + yield datetime.date.today() + + def complete_time(): + yield datetime.datetime.now().time() + + completes = { + 'boolean': complete_boolean, + 'selection': complete_selection, + 'multiselection': complete_selection, + 'reference': complete_reference, + 'datetime': complete_datetime, + 'date': complete_date, + 'time': complete_time, + } + return completes.get(field['type'], lambda: [])() + + +def parenthesize(tokens): + "Nest tokens according to parenthesis" + for token in tokens: + if token == '(': + yield iter(list(parenthesize(tokens))) + elif token == ')': + break + else: + yield token + + +def operatorize(tokens, operator='or'): + "Convert operators" + test = (operator, (operator,)) + try: + cur = next(tokens) + while cur in test: + cur = next(tokens) + except StopIteration: + return + if isgenerator(cur): + cur = operatorize(cur, operator) + nex = None + for nex in tokens: + if isgenerator(nex): + nex = operatorize(nex, operator) + if nex in test: + try: + nex = next(tokens) + while nex in test: + nex = next(tokens) + if isgenerator(nex): + nex = operatorize(nex, operator) + cur = iter([operator.upper(), cur, nex]) + except StopIteration: + if cur not in test: + yield iter([operator.upper(), cur]) + cur = None + nex = None + else: + if cur not in test: + yield cur + cur = nex + else: + if nex is not None and nex not in test: + yield nex + elif cur is not None and cur not in test: + yield cur + + +class DomainParser(object): + "A parser for domain" + + def __init__(self, fields, context=None): + self.fields = OrderedDict() + self.strings = OrderedDict() + self.context = context + + def update_fields(fields, prefix='', string_prefix=''): + for name, field in fields.items(): + if not field.get('searchable', True) or name == 'rec_name': + continue + field = field.copy() + fullname = '.'.join(filter(None, [prefix, name])) + string = '.'.join( + filter(None, [string_prefix, field['string']])) + field['string'] = string + field['name'] = fullname + self.fields[fullname] = field + self.strings[string.lower()] = field + rfields = field.get('relation_fields') + if rfields: + update_fields(rfields, fullname, string) + update_fields(fields) + + def parse(self, input_): + "Return domain for the input string" + try: + tokens = udlex(input_) + tokens = group_operator(tokens) + tokens = parenthesize(tokens) + tokens = self.group(tokens) + tokens = operatorize(tokens, 'or') + tokens = operatorize(tokens, 'and') + tokens = self.parse_clause(tokens) + return simplify(rlist(tokens)) + except ValueError as exception: + if str(exception) == 'No closing quotation': + return self.parse(input_ + '"') + + def stringable(self, domain): + + def stringable_(clause): + if not clause: + return True + if (( + (clause[0] in ('AND', 'OR')) + or isinstance(clause[0], (list, tuple))) + and all(isinstance(c, (list, tuple)) for c in clause[1:])): + return self.stringable(clause) + name, _, value = clause[:3] + if name.endswith('.rec_name'): + name = name[:-len('.rec_name')] + if name in self.fields: + field = self.fields[name] + if field['type'] in { + 'many2one', 'one2one', 'one2many', 'many2many'}: + if field['type'] == 'many2one': + types = (str, type(None)) + else: + types = str + if isinstance(value, (list, tuple)): + return all(isinstance(v, types) for v in value) + else: + return isinstance(value, types) + elif field['type'] == 'multiselection': + return not value or isinstance(value, list) + else: + return True + elif name == 'rec_name': + return True + return False + + if not domain: + return True + if domain[0] in ('AND', 'OR'): + domain = domain[1:] + return all(stringable_(clause) for clause in domain) + + def string(self, domain): + "Return string for the domain" + + def string_(clause): + if not clause: + return '' + if (not isinstance(clause[0], str) + or clause[0] in ('AND', 'OR')): + return '(%s)' % self.string(clause) + name, operator, value = clause[:3] + if name.endswith('.rec_name'): + name = name[:-9] + if name not in self.fields: + if is_full_text(value): + value = value[1:-1] + return quote(value) + field = self.fields[name] + + if len(clause) > 3: + target = clause[3] + else: + target = None + + if 'ilike' in operator: + if is_full_text(value): + value = value[1:-1] + elif not is_like(value): + if operator == 'ilike': + operator = '=' + else: + operator = '!' + value = unescape(value) + def_operator = default_operator(field) + if def_operator == operator.strip(): + operator = '' + if value in OPERATORS: + # As the value could be interpreted as an operator, + # the default operator must be forced + operator = '"" ' + elif (def_operator in operator + and ('not' in operator or '!' in operator)): + operator = operator.rstrip(def_operator + ).replace('not', '!').strip() + if operator.endswith('in'): + if operator == 'not in': + operator = '!' + else: + operator = '' + formatted_value = format_value(field, value, target, self.context) + if (operator in OPERATORS + and field['type'] in ('char', 'text', 'selection') + and value == ''): + formatted_value = '""' + return '%s: %s%s' % (quote(field['string']), operator, + formatted_value) + + if not domain: + return '' + if domain[0] in ('AND', 'OR'): + nary = ' ' if domain[0] == 'AND' else ' or ' + domain = domain[1:] + else: + nary = ' ' + return nary.join(string_(clause) for clause in domain) + + def completion(self, input_): + "Return completion for the input string" + domain = self.parse(input_) + closing = 0 + for i in range(1, len(input_)): + if input_[-i] not in (')', ' '): + break + if input_[-i] == ')': + closing += 1 + ending, deep_ending = ending_clause(domain) + deep = deep_ending - closing + if deep: + pslice = slice(-deep) + else: + pslice = slice(None) + if self.string(domain)[pslice] != input_: + yield self.string(domain)[pslice] + complete = None + if ending is not None and closing == 0: + for complete in self.complete(ending): + yield self.string(rlist( + replace_ending_clause(domain, complete)))[pslice] + if input_: + if input_[-1] != ' ': + return + if len(input_) >= 2 and input_[-2] == ':': + return + for field in self.strings.values(): + operator = default_operator(field) + value = '' + if 'ilike' in operator: + value = likify(value) + yield self.string(rlist(append_ending_clause(domain, + (field['name'], operator, value), + deep)))[pslice] + + def complete(self, clause): + "Return all completion for the clause" + if len(clause) == 1: + name, = clause + elif len(clause) == 3: + name, operator, value = clause + else: + name, operator, value, target = clause + if name.endswith('.rec_name'): + name = name[:-9] + value = target + if name == 'rec_name': + if operator == 'ilike': + escaped = value.replace('%%', '__') + if escaped.startswith('%') and escaped.endswith('%'): + value = value[1:-1] + elif '%' not in escaped: + value = value.replace('%%', '%') + operator = None + name = value + value = '' + if not name: + name = '' + if (name.lower() not in self.strings + and name not in self.fields): + for field in self.strings.values(): + if field['string'].lower().startswith(name.lower()): + operator = default_operator(field) + value = '' + if 'ilike' in operator: + value = likify(value) + yield (field['name'], operator, value) + return + if name in self.fields: + field = self.fields[name] + else: + field = self.strings[name.lower()] + if not operator: + operator = default_operator(field) + value = '' + if 'ilike' in operator: + value = likify(value) + yield (field['name'], operator, value) + else: + for comp in complete_value(field, value): + yield (field['name'], operator, comp) + + def group(self, tokens): + "Group tokens by clause" + + def _group(parts): + try: + i = parts.index(':') + except ValueError: + for part in parts: + yield (part,) + return + for j in range(i): + name = ' '.join(parts[j:i]) + if name.lower() in self.strings: + if parts[:j]: + for part in parts[:j]: + yield (part,) + else: + yield (None,) + name = (name,) + # empty string is also the default operator + if (i + 1 < len(parts) + and parts[i + 1] in OPERATORS + ('',)): + name += (parts[i + 1],) + i += 1 + else: + name += (None,) + lvalue = [] + while i + 2 < len(parts): + if parts[i + 2] == ';': + lvalue.append(parts[i + 1]) + i += 2 + else: + break + for part in _group(parts[i + 1:]): + if name: + if lvalue: + if part[0] is not None: + lvalue.append(part[0]) + yield name + (lvalue,) + else: + yield name + part + name = None + else: + yield part + if name: + if lvalue: + yield name + (lvalue,) + else: + yield name + (None,) + break + + parts = [] + for token in tokens: + if isgenerator(token): + for group in _group(parts): + if group != (None,): + yield group + parts = [] + yield self.group(token) + else: + parts.append(token) + for group in _group(parts): + if group != (None,): + yield group + + def parse_clause(self, tokens): + "Parse clause" + for clause in tokens: + if isgenerator(clause): + yield self.parse_clause(clause) + elif clause in ('OR', 'AND'): + yield clause + else: + if len(clause) == 1: + yield ('rec_name', 'ilike', likify(clause[0])) + else: + name, operator, value = clause + field = self.strings[name.lower()] + field_name = field['name'] + + target = None + if field['type'] == 'reference': + target, value = split_target_value(field, value) + if target: + field_name += '.rec_name' + elif field['type'] == 'multiselection': + if value is not None and not isinstance(value, list): + value = [value] + + if not operator: + operator = default_operator(field) + if (isinstance(value, list) + and field['type'] != 'multiselection'): + if operator == '!': + operator = 'not in' + else: + operator = 'in' + if operator == '!': + operator = negate_operator(default_operator(field)) + if value is None and operator.endswith('in'): + if operator.startswith('not'): + operator = '!=' + else: + operator = '=' + if field['type'] in ('integer', 'float', 'numeric', + 'datetime', 'date', 'time'): + if isinstance(value, str) and '..' in value: + lvalue, rvalue = value.split('..', 1) + lvalue = convert_value(field, lvalue, self.context) + rvalue = convert_value(field, rvalue, self.context) + yield iter([ + (field_name, '>=', lvalue), + (field_name, '<=', rvalue), + ]) + continue + if isinstance(value, list): + value = [convert_value(field, v, self.context) + for v in value] + if field['type'] in ('many2one', 'one2many', + 'many2many', 'one2one'): + field_name += '.rec_name' + else: + value = convert_value(field, value, self.context) + if 'like' in operator: + value = likify(value) + if target: + yield (field_name, operator, value, target) + else: + yield field_name, operator, value diff --git a/tryton/tryton/common/entry_position.py b/tryton/tryton/common/entry_position.py new file mode 100644 index 0000000000..93393aca7f --- /dev/null +++ b/tryton/tryton/common/entry_position.py @@ -0,0 +1,9 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +def reset_position(entry): + if entry.get_alignment() <= 0.5: + entry.set_position(0) + else: + entry.set_position(-1) diff --git a/tryton/tryton/common/environment.py b/tryton/tryton/common/environment.py new file mode 100644 index 0000000000..8ccc15b4b7 --- /dev/null +++ b/tryton/tryton/common/environment.py @@ -0,0 +1,60 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +class EvalEnvironment(dict): + + def __init__(self, parent, eval_type='eval'): + super(EvalEnvironment, self).__init__() + self.parent = parent + assert eval_type in ('eval', 'on_change') + self.eval_type = eval_type + + def __getitem__(self, item): + if item == 'id': + return self.parent.id + if item == '_parent_' + self.parent.parent_name and self.parent.parent: + return EvalEnvironment(self.parent.parent, + eval_type=self.eval_type) + if self.eval_type == 'eval': + return self.parent.get_eval()[item] + else: + return self.parent.group.fields[item].get_on_change_value( + self.parent) + + def __getattr__(self, item): + try: + return self.__getitem__(item) + except KeyError: + raise AttributeError(item) + + def get(self, item, default=None): + try: + return self.__getitem__(item) + except KeyError: + pass + return super(EvalEnvironment, self).get(item, default) + + def __bool__(self): + return True + + def __str__(self): + return str(self.parent) + + __repr__ = __str__ + + def __contains__(self, item): + if item == 'id': + return True + if item == '_parent_' + self.parent.parent_name and self.parent.parent: + return True + if self.eval_type == 'eval': + return item in self.parent.get_eval() + else: + return item in self.parent.group.fields + + def keys(self): + if self.eval_type == 'eval': + return self.parent.get_eval().keys() + else: + return self.parent.group.fields.keys() diff --git a/tryton/tryton/common/focus.py b/tryton/tryton/common/focus.py new file mode 100644 index 0000000000..21c8ad0e1a --- /dev/null +++ b/tryton/tryton/common/focus.py @@ -0,0 +1,83 @@ +# 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 functools import cmp_to_key + +from gi.repository import Gtk + + +def get_invisible_ancestor(widget): + if not widget.get_visible(): + return widget + if not widget.props.parent: + return None + return get_invisible_ancestor(widget.props.parent) + + +def find_focused_child(widget): + if widget.has_focus(): + return widget + if not hasattr(widget, 'get_children'): + return None + for child in widget.get_children(): + focused_widget = find_focused_child(child) + if focused_widget: + return focused_widget + + +def tab_compare(a, b): + text_direction = Gtk.Widget.get_default_direction() + a_allocation = a.get_allocation() + b_allocation = b.get_allocation() + y1 = a_allocation.y + a_allocation.height // 2 + y2 = b_allocation.y + b_allocation.height // 2 + + if y1 == y2: + x1 = a_allocation.x + a_allocation.width // 2 + x2 = b_allocation.x + b_allocation.width // 2 + + if text_direction == Gtk.TextDirection.RTL: + return (x2 > x1) - (x2 < x1) + else: + return (x1 > x2) - (x1 < x2) + else: + return (y1 > y2) - (y1 < y2) + + +def get_focus_children(widget): + if hasattr(widget, 'get_children'): + result = sorted(widget.get_children(), key=cmp_to_key(tab_compare)) + return result + else: + return [] + + +def find_focusable_child(widget): + if not widget.get_visible(): + return None + if (widget.get_can_focus() + and (not isinstance(widget, Gtk.Entry) or widget.props.editable)): + return widget + for child in get_focus_children(widget): + focusable = find_focusable_child(child) + if focusable: + return focusable + + +def next_focus_widget(widget): + if not widget.props.parent: + return None + focus_widget = find_focusable_child(widget.props.parent) + if focus_widget: + return focus_widget + else: + return next_focus_widget(widget.props.parent) + + +def find_first_focus_widget(ancestor, widgets): + "Return the widget from widgets which should have first the focus" + if len(widgets) == 1: + return widgets[0] + for child in get_focus_children(ancestor): + common = [w for w in widgets if w.is_ancestor(child)] + if common: + return find_first_focus_widget(child, common) diff --git a/tryton/tryton/common/htmltextbuffer.py b/tryton/tryton/common/htmltextbuffer.py new file mode 100644 index 0000000000..72bbd6ca08 --- /dev/null +++ b/tryton/tryton/common/htmltextbuffer.py @@ -0,0 +1,464 @@ +# 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 sys +from io import StringIO +import xml.etree.ElementTree as ET +from xml.sax.saxutils import escape, unescape +from html.parser import HTMLParser + +from gi.repository import Gtk, Gdk, Pango + + +def guess_decode(bytes, errors='strict'): + for encoding in [sys.getfilesystemencoding(), 'utf-8', 'utf-16', 'utf-32']: + try: + return bytes.decode(encoding) + except UnicodeDecodeError: + continue + else: + return bytes.decode('ascii', errors=errors) + + +MIME = Gdk.Atom.intern('text/html', False) +# Disable serialize/deserialize registration function because it does not work +# on GTK-3, the "guint8 *data" is converted into a Gtk.TextIter +use_serialize_func = False + + +def _reverse_dict(dct): + return {j: i for i, j in dct.items()} + + +SIZE2SCALE = { + '1': 1 / (1.2 * 1.2 * 1.2), + '2': 1 / (1.2 * 1.2), + '3': 1 / 1.2, + '4': 1, + '5': 1.2, + '6': 1.2 * 1.2, + '7': 1.2 * 1.2 * 1.2, + } +SCALE2SIZE = _reverse_dict(SIZE2SCALE) +ALIGN2JUSTIFICATION = { + 'left': Gtk.Justification.LEFT, + 'center': Gtk.Justification.CENTER, + 'right': Gtk.Justification.RIGHT, + 'justify': Gtk.Justification.FILL, + } +JUSTIFICATION2ALIGN = _reverse_dict(ALIGN2JUSTIFICATION) +FAMILIES = ['normal', 'sans', 'serif', 'monospace'] + + +def gdk_to_hex(gdk_color): + "Convert color to 2 digit hex" + colors = [gdk_color.red, gdk_color.green, gdk_color.blue] + return "#" + "".join(["%02x" % (color // 256) for color in colors]) + + +def _markup(text): + return '%s' % text + + +def _strip_markup(text): + return text[len(''):-len('')] + + +def _strip_newline(text): + return ''.join(text.splitlines()) + + +class MarkupHTMLParse(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.root = ET.Element('markup') + self._tags = [self.root] + self.head = False + self.body = True + + def handle_starttag(self, tag, attrs): + if tag in ['b', 'i', 'u', 'div', 'font']: + el = ET.SubElement(self._tags[-1], tag) + for key, value in attrs: + el.set(key, value) + self._tags.append(el) + elif tag == 'br': + ET.SubElement(self._tags[-1], tag) + elif tag == 'head': + self.head = True + + def handle_endtag(self, tag): + if tag in ['b', 'i', 'u', 'div', 'font']: + el = self._tags.pop() + assert el.tag == tag + elif tag == 'head': + self.head = False + elif tag == 'body': + self.body = False + + def handle_data(self, data): + if self.head or not self.body: + return + el = self._tags[-1] + if len(el) == 0: + if el.text is None: + el.text = '' + el.text += data + else: + child = el[-1] + if child.tail is None: + child.tail = '' + child.tail += data + + +def parse_markup(markup_text): + 'Return plain text and a list of start, end TextTag' + markup_text = StringIO(_markup(markup_text)) + plain_text = '' + tag_stack = [] + tags = [] + for event, element in ET.iterparse(markup_text, events=['start', 'end']): + if element.tag == 'markup': + if event == 'start' and element.text: + plain_text += unescape(element.text) + if event == 'end' and element.tail: + plain_text += unescape(element.tail) + continue + if event == 'start': + tag_stack.append((element, len(plain_text))) + if element.text: + plain_text += unescape(element.text) + elif event == 'end': + if element.tag == 'div': + plain_text += '\n' + assert tag_stack[-1][0] == element + _, start = tag_stack.pop() + end = len(plain_text) + tags.append((start, end, element)) + if element.tail: + plain_text += unescape(element.tail) + return plain_text, tags + + +def normalize_markup(markup_text, method='html'): + parser = MarkupHTMLParse() + parser.feed(_strip_newline(markup_text)) + root = parser.root + parent_map = {c: p for p in root.iter() for c in p} + + def order(em): + "Re-order alphabetically tags" + if (len(em) == 1 + and not em.text + and em.tag not in ['div', 'markup']): + child, = em + if ((em.tag > child.tag or child.tag == 'div') + and not child.tail): + em.tag, child.tag = child.tag, em.tag + em.attrib, child.attrib = child.attrib, em.attrib + if em in parent_map: + order(parent_map[em]) + + # Add missing div for the first line + if len(root) > 0 and root[0].tag != 'div': + div = ET.Element('div') + for em in list(root): + if em.tag != 'div': + root.remove(em) + div.append(em) + else: + break + root.insert(0, div) + + for em in root.iter(): + while em.text is None and len(em) == 1: + dup, = em + if dup.tag == em.tag and dup.tail is None: + em.attrib.update(dup.attrib) + em.remove(dup) + em.extend(dup) + em.text = dup.text + else: + break + order(em) + + # Add missing br to empty lines + for em in root.findall('div'): + if em.text is None and len(em) == 0: + em.append(ET.Element('br')) + # TODO order attributes + return _strip_markup( + ET.tostring(root, encoding='utf-8', method=method).decode('utf-8')) + + +def find_list_delta(old, new): + added = [e for e in new if e not in old] + removed = [e for e in reversed(old) if e not in new] + return added, removed + + +def serialize(register, content, start, end, data): + text = '' + if start.compare(end) == 0: + return text + iter_ = start + while True: + # Loop over line per line to get a div per line + tags = [] + active_tags = [] + + end_line = iter_.copy() + if not end_line.ends_line(): + end_line.forward_to_line_end() + if end_line.compare(end) > 0: + end_line = end + + # Open div of the line + align = None + for tag in iter_.get_tags(): + if tag.props.justification_set: + align = JUSTIFICATION2ALIGN[tag.props.justification] + if align: + text += '
' % align + else: + text += '
' + + while True: + new_tags = iter_.get_tags() + + added, removed = find_list_delta(tags, new_tags) + + for tag in removed: + if tag not in active_tags: + continue + + # close all open tags after this one + while active_tags: + atag = active_tags.pop() + text += _get_html(atag, close=True) + if atag == tag: + break + added.insert(0, atag) + + for tag in added: + text += _get_html(tag) + active_tags.append(tag) + + tags = new_tags + + old_iter = iter_.copy() + + # Move to next tag toggle + while True: + iter_.forward_char() + if iter_.compare(end) == 0: + break + if iter_.toggles_tag(): + break + # Might have moved too far + if iter_.compare(end_line) > 0: + iter_ = end_line + + text += escape(old_iter.get_text(iter_)) + + if iter_.compare(end_line) == 0: + break + + # close any open tags + for tag in reversed(active_tags): + text += _get_html(tag, close=True) + + # Close the div of the line + text += '
' + + iter_.forward_char() + if iter_.compare(end) >= 0: + break + + return normalize_markup(text) + + +def deserialize(register, content, iter_, text, create_tags, data): + if not isinstance(text, str): + text = guess_decode(text, errors='replace') + text, tags = parse_markup(normalize_markup(text, method='xml')) + offset = iter_.get_offset() + content.insert(iter_, text) + + def sort_key(tag): + start, end, element = tag + return start, -end, element.tag + + for start, end, element in sorted(tags, key=sort_key): + for tag in _get_tags(content, element): + istart = content.get_iter_at_offset(start + offset) + iend = content.get_iter_at_offset(end + offset) + content.apply_tag(tag, istart, iend) + return True + + +def setup_tags(text_buffer): + for name, props in reversed(_TAGS): + text_buffer.create_tag(name, **props) + + +_TAGS = [ + ('bold', {'weight': Pango.Weight.BOLD}), + ('italic', {'style': Pango.Style.ITALIC}), + ('underline', {'underline': Pango.Underline.SINGLE}), + ] +_TAGS.extend([('family %s' % family, {'family': family}) + for family in FAMILIES]) +_TAGS.extend([('size %s' % size, {'scale': scale}) + for size, scale in SIZE2SCALE.items()]) +_TAGS.extend([('justification %s' % align, {'justification': justification}) + for align, justification in ALIGN2JUSTIFICATION.items()]) + + +def register_foreground(text_buffer, color): + name = 'foreground %s' % color.to_string() + tag_table = text_buffer.get_tag_table() + tag = tag_table.lookup(name) + if not tag: + tag = text_buffer.create_tag(name, foreground_rgba=color) + return tag + + +def _get_tags(content, element): + 'Return tag for the element' + tag_table = content.get_tag_table() + + if element.tag == 'b': + yield tag_table.lookup('bold') + elif element.tag == 'i': + yield tag_table.lookup('italic') + elif element.tag == 'u': + yield tag_table.lookup('underline') + if 'face' in element.attrib: + tag = tag_table.lookup('family %s' % element.attrib['face']) + if tag: + yield tag + size = element.attrib.get('size') + if size in SIZE2SCALE: + yield tag_table.lookup('size %s' % size) + if 'color' in element.attrib: + color = Gdk.RGBA() + color.parse(element.attrib['color']) + yield register_foreground(content, color) + align = element.attrib.get('align') + if align in ALIGN2JUSTIFICATION: + yield tag_table.lookup('justification %s' % align) + # TODO style background-color + + +def _get_html(texttag, close=False, div_only=False): + 'Return the html tag' + # div alignment is managed at line level + tags = [] + attrib = {} + font = attrib['font'] = {} + if texttag.props.weight_set and texttag.props.weight == Pango.Weight.BOLD: + tags.append('b') + if texttag.props.style_set and texttag.props.style == Pango.Style.ITALIC: + tags.append('i') + if (texttag.props.underline_set + and texttag.props.underline == Pango.Underline.SINGLE): + tags.append('u') + if texttag.props.family_set and texttag.props.family: + font['face'] = texttag.props.family + if texttag.props.scale_set and texttag.props.scale != 1: + font['size'] = SCALE2SIZE[texttag.props.scale] + if (texttag.props.foreground_set + and texttag.props.foreground_gdk != Gdk.Color(0, 0, 0)): + font['color'] = gdk_to_hex(texttag.props.foreground_gdk) + # TODO style background-color + if font: + tags.append('font') + + if close: + return ''.join('' % t for t in tags) + return ''.join('<%s %s>' % (t, ' '.join('%s="%s"' % (a, v) + for a, v in attrib.get(t, {}).items())) + for t in tags) + + +def get_tags(content, start, end): + 'Get all tags' + iter_ = start.copy() + tags = set() + while True: + tags.update(iter_.get_tags()) + iter_.forward_char() + if iter_.compare(end) > 0: + iter_ = end + if iter_.compare(end) == 0: + break + tags.update(iter_.get_tags()) + return tags + + +def remove_tags(content, start, end, *names): + 'Remove all tags starting by names' + tags = get_tags(content, start, end) + for tag in sorted(tags, key=lambda t: t.get_priority()): + for name in names: + if tag.props.name and tag.props.name.startswith(name): + content.remove_tag(tag, start, end) + + +if __name__ == '__main__': + html = '''Bold + Italic + Underline +

+
Center
+
Sans6red
+
Title
''' # noqa: E501 + + win = Gtk.Window() + win.set_title('HTMLTextBuffer') + + def cb(window, event): + if use_serialize_func: + print(text_buffer.serialize( + text_buffer, MIME, text_buffer.get_start_iter(), + text_buffer.get_end_iter())) + else: + print(serialize( + text_buffer, text_buffer, text_buffer.get_start_iter(), + text_buffer.get_end_iter(), None)) + Gtk.main_quit() + win.connect('delete-event', cb) + vbox = Gtk.VBox() + win.add(vbox) + + text_buffer = Gtk.TextBuffer() + text_view = Gtk.TextView() + text_view.set_buffer(text_buffer) + vbox.pack_start(text_view, expand=True, fill=True, padding=0) + + setup_tags(text_buffer) + text_buffer.register_serialize_format(str(MIME), serialize, None) + text_buffer.register_deserialize_format(str(MIME), deserialize, None) + + if use_serialize_func: + text_buffer.deserialize( + text_buffer, MIME, text_buffer.get_start_iter(), html) + + result = text_buffer.serialize( + text_buffer, MIME, text_buffer.get_start_iter(), + text_buffer.get_end_iter()) + else: + deserialize( + text_buffer, text_buffer, text_buffer.get_start_iter(), + html, text_buffer.deserialize_get_can_create_tags(MIME), + None) + + result = serialize( + text_buffer, text_buffer, text_buffer.get_start_iter(), + text_buffer.get_end_iter(), None) + + assert normalize_markup(html) == result, (normalize_markup(html), result) + + win.show_all() + Gtk.main() diff --git a/tryton/tryton/common/number_entry.py b/tryton/tryton/common/number_entry.py new file mode 100644 index 0000000000..b56d5a79fc --- /dev/null +++ b/tryton/tryton/common/number_entry.py @@ -0,0 +1,91 @@ +# 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 gettext +import locale + +from gi.repository import Gdk, GObject, Gtk + +__all__ = ['NumberEntry'] + +_ = gettext.gettext + + +class NumberEntry(Gtk.Entry, Gtk.Editable): + # Override Editable to avoid modify the base implementation of Entry + __gtype_name__ = 'NumberEntry' + __digits = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_alignment(1.0) + self.connect('key-press-event', self.__class__.__key_press_event) + + @GObject.Property( + default=None, nick=_("Digits"), blurb=_("The number of decimal")) + def digits(self): + return self.__digits + + @digits.setter + def digits(self, value): + self.__digits = value + + @GObject.Property + def value(self): + text = self.get_text() + if text: + try: + return locale.atof(text) + except ValueError: + pass + return None + + @property + def __decimal_point(self): + return locale.localeconv()['decimal_point'] + + # XXX: Override vfunc because position is inout + # https://gitlab.gnome.org/GNOME/pygobject/issues/12 + def do_insert_text(self, new_text, length, position): + buffer_ = self.get_buffer() + text = self.get_buffer().get_text() + text = text[:position] + new_text + text[position:] + value = None + if text not in ['-', self.__decimal_point]: + try: + value = locale.atof(text) + except ValueError: + return position + if (value and self.__digits is not None + and round(value, self.__digits) != value): + return position + buffer_.insert_text(position, new_text, len(new_text)) + return position + len(new_text) + + def __key_press_event(self, event): + for name in ['KP_Decimal', 'KP_Separator']: + if event.keyval == Gdk.keyval_from_name(name): + text = self.__decimal_point + if self.get_selection_bounds(): + self.delete_text(*self.get_selection_bounds()) + self.do_insert_text( + text, len(text), self.props.cursor_position) + self.set_position(self.props.cursor_position + len(text)) + return True + + +GObject.type_register(NumberEntry) + + +if __name__ == '__main__': + win = Gtk.Window() + win.connect('delete-event', Gtk.main_quit) + vbox = Gtk.VBox() + e = NumberEntry() + vbox.pack_start(NumberEntry(), expand=False, fill=False, padding=0) + vbox.pack_start(NumberEntry(digits=2), expand=False, fill=False, padding=0) + vbox.pack_start(NumberEntry(digits=0), expand=False, fill=False, padding=0) + vbox.pack_start( + NumberEntry(digits=-2), expand=False, fill=False, padding=0) + win.add(vbox) + win.show_all() + Gtk.main() diff --git a/tryton/tryton/common/popup_menu.py b/tryton/tryton/common/popup_menu.py new file mode 100644 index 0000000000..02b702e9e5 --- /dev/null +++ b/tryton/tryton/common/popup_menu.py @@ -0,0 +1,145 @@ +# 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 gettext + +from gi.repository import Gtk, Gdk + +from tryton.common import RPCExecute, RPCException +from tryton.common.common import selection +from tryton.gui.window.view_form.screen import Screen +from tryton.action import Action +from tryton.gui.window import Window +from tryton.gui.window.attachment import Attachment +from tryton.gui.window.email_ import Email +from tryton.gui.window.note import Note + +_ = gettext.gettext + + +def populate(menu, model, record, title='', field=None, context=None): + ''' + Fill menu with the actions of model for the record. + If title is filled, the actions will be put in a submenu. + ''' + if record is None: + return + elif isinstance(record, int): + if record < 0: + return + elif record.id < 0: + return + + def load(record): + if isinstance(record, int): + screen = Screen(model, context=context) + screen.load([record]) + record = screen.current_record + return record + + def id_(record): + if not isinstance(record, int): + return record.id + return record + + def activate(menuitem, action, atype): + rec = load(record) + data = { + 'model': model, + 'id': rec.id, + 'ids': [rec.id], + } + event = Gtk.get_current_event() + allow_similar = False + if (event.state & Gdk.ModifierType.CONTROL_MASK + or event.state & Gdk.ModifierType.MOD1_MASK): + allow_similar = True + with Window(hide_current=True, allow_similar=allow_similar): + Action.execute(action, data, context=rec.get_context()) + + def attachment(menuitem): + Attachment(load(record), None) + + def note(menuitem): + Note(load(record), None) + + def is_report(action): + return action['type'] == 'ir.action.report' + + def email(menuitem, toolbar): + rec = load(record) + prints = filter(is_report, toolbar['print']) + emails = {e['name']: e['id'] for e in toolbar['emails']} + template = selection(_("Template"), emails, alwaysask=True) + if template: + template = template[1] + Email( + '%s: %s' % (title, rec.rec_name()), rec, prints, + template=template) + + def edit(menuitem): + with Window(hide_current=True, allow_similar=True): + Window.create(model, + view_ids=field.attrs.get('view_ids', '').split(','), + res_id=id_(record), + mode=['form'], + name=field.attrs.get('string'), + context=context) + + if title: + if len(menu): + menu.append(Gtk.SeparatorMenuItem()) + title_item = Gtk.MenuItem(label=title) + menu.append(title_item) + submenu = Gtk.Menu() + title_item.set_submenu(submenu) + action_menu = submenu + else: + action_menu = menu + + if len(action_menu): + action_menu.append(Gtk.SeparatorMenuItem()) + if field: + edit_item = Gtk.MenuItem(label=_('Edit...')) + edit_item.connect('activate', edit) + action_menu.append(edit_item) + action_menu.append(Gtk.SeparatorMenuItem()) + attachment_item = Gtk.MenuItem(label=_('Attachments...')) + action_menu.append(attachment_item) + attachment_item.connect('activate', attachment) + note_item = Gtk.MenuItem(label=_('Notes...')) + action_menu.append(note_item) + note_item.connect('activate', note) + + try: + toolbar = RPCExecute('model', model, 'view_toolbar_get') + except RPCException: + pass + else: + for atype, icon, label, flavor in ( + ('action', 'tryton-launch', _('Actions...'), None), + ('relate', 'tryton-link', _('Relate...'), None), + ('print', 'tryton-open', _('Report...'), 'open'), + ('print', 'tryton-print', _('Print...'), 'print'), + ): + if len(action_menu): + action_menu.append(Gtk.SeparatorMenuItem()) + title_item = Gtk.MenuItem(label=label) + action_menu.append(title_item) + if not toolbar[atype]: + title_item.set_sensitive(False) + continue + submenu = Gtk.Menu() + title_item.set_submenu(submenu) + for action in toolbar[atype]: + action = action.copy() + item = Gtk.MenuItem(label=action['name']) + submenu.append(item) + if flavor == 'print': + action['direct_print'] = True + item.connect('activate', activate, action, atype) + menu.show_all() + + email_item = Gtk.MenuItem(label=_('E-Mail...')) + action_menu.append(email_item) + email_item.connect('activate', email, toolbar) + menu.show_all() diff --git a/tryton/tryton/common/richtext.py b/tryton/tryton/common/richtext.py new file mode 100644 index 0000000000..b8c5c070e3 --- /dev/null +++ b/tryton/tryton/common/richtext.py @@ -0,0 +1,346 @@ +# 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 gettext +from contextlib import contextmanager + +from gi.repository import Gtk, Gdk + +from tryton.common import IconFactory +from tryton.common.htmltextbuffer import ( + serialize, deserialize, + setup_tags, register_foreground, remove_tags, + FAMILIES, SIZE2SCALE, MIME, use_serialize_func) + +_ = gettext.gettext +SIZES = sorted(SIZE2SCALE.keys()) + + +def register_format(textview): + buffer_ = textview.get_buffer() + setup_tags(buffer_) + buffer_.register_serialize_format(str(MIME), serialize, None) + buffer_.register_deserialize_format(str(MIME), deserialize, None) + + +def set_content(textview, content): + with disable_text_style(textview): + buffer_ = textview.get_buffer() + start = buffer_.get_start_iter() + end = buffer_.get_end_iter() + buffer_.delete(start, end) + if use_serialize_func: + buffer_.deserialize(buffer_, MIME, start, content) + else: + deserialize( + buffer_, buffer_, start, content, + buffer_.deserialize_get_can_create_tags(MIME), None) + + +def get_content(textview): + buffer_ = textview.get_buffer() + start = buffer_.get_start_iter() + end = buffer_.get_end_iter() + if use_serialize_func: + return buffer_.serialize(buffer_, MIME, start, end) + else: + return serialize(buffer_, buffer_, start, end, None) + + +def add_toolbar(textview): + toolbar = Gtk.Toolbar() + + tag_widgets = {} + colors = {} + + for icon, label in [ + ('bold', _("Bold")), + ('italic', _("Italic")), + ('underline', _("Underline")), + ]: + button = Gtk.ToggleToolButton() + button.set_icon_widget(IconFactory.get_image( + 'tryton-format-%s' % icon, + Gtk.IconSize.SMALL_TOOLBAR)) + button.set_label(label) + button.connect('toggled', _toggle_props, icon, textview) + toolbar.insert(button, -1) + tag_widgets[icon] = button + + toolbar.insert(Gtk.SeparatorToolItem(), -1) + + for name, options, active in [ + ('family', FAMILIES, FAMILIES.index('normal')), + ('size', SIZES, SIZES.index('4')), + ]: + combobox = Gtk.ComboBoxText() + for option in options: + combobox.append_text(option) + combobox.set_active(active) + combobox.set_focus_on_click(False) + combobox.connect('changed', _change_props, name, textview) + tool = Gtk.ToolItem() + tool.add(combobox) + toolbar.insert(tool, -1) + tag_widgets[name] = combobox + + toolbar.insert(Gtk.SeparatorToolItem(), -1) + + button = None + for name, label in [ + ('left', _("Align Left")), + ('center', _("Align Center")), + ('right', _("Align Right")), + ('justify', _("Justify")), + ]: + icon = 'tryton-format-align-%s' % name + button = Gtk.RadioToolButton.new_from_widget(button) + button.set_icon_widget(IconFactory.get_image( + icon, Gtk.IconSize.SMALL_TOOLBAR)) + button.set_active(icon == 'left') + button.set_label(label) + button.connect( + 'toggled', _toggle_justification, name, textview) + toolbar.insert(button, -1) + tag_widgets[name] = button + + toolbar.insert(Gtk.SeparatorToolItem(), -1) + + for icon, label in [ + ('foreground', _("Foreground Color")), + # TODO ('background', _('Background')), + ]: + button = Gtk.ToolButton() + if icon == 'foreground': + button.set_icon_widget(IconFactory.get_image( + 'tryton-format-color-text', + Gtk.IconSize.SMALL_TOOLBAR)) + button.set_label(label) + button.connect('clicked', _toggle_color, icon, textview, colors) + toolbar.insert(button, -1) + tag_widgets[icon] = button + + buffer_ = textview.get_buffer() + buffer_.connect_after( + 'insert-text', _insert_text_style, tag_widgets) + textview.connect_after( + 'move-cursor', + lambda *a: _detect_style(textview, tag_widgets, colors)) + textview.connect_after( + 'button-release-event', + lambda *a: _detect_style(textview, tag_widgets, colors)) + + return toolbar + + +@contextmanager +def disable_text_style(textview): + buffer_ = textview.get_buffer() + try: + buffer_.handler_block_by_func(_insert_text_style) + except TypeError: + pass + yield + try: + buffer_.handler_unblock_by_func(_insert_text_style) + except TypeError: + pass + + +def _toggle_props(toggle, name, textview): + buffer_ = textview.get_buffer() + try: + start, end = buffer_.get_selection_bounds() + except ValueError: + return + _apply_tool(buffer_, name, toggle, start, end) + + +def _change_props(combobox, name, textview): + buffer_ = textview.get_buffer() + try: + start, end = buffer_.get_selection_bounds() + except ValueError: + return + _apply_tool(buffer_, name, combobox, start, end) + + +def _toggle_justification(button, name, textview): + buffer_ = textview.get_buffer() + try: + start, end = buffer_.get_selection_bounds() + except ValueError: + insert = buffer_.get_insert() + start = buffer_.get_iter_at_mark(insert) + end = start.copy() + start.set_line_offset(0) + if not end.ends_line(): + end.forward_to_line_end() + _apply_tool(buffer_, name, button, start, end) + + +def _toggle_color(button, name, textview, colors): + buffer_ = textview.get_buffer() + insert = buffer_.get_insert() + try: + start, end = buffer_.get_selection_bounds() + except ValueError: + start = end = None + else: + # Use offset position to preserve across buffer_ modification + start = start.get_offset() + end = end.get_offset() + + dialog = Gtk.ColorChooserDialog( + title=_('Select a color'), + transient_for=textview.get_toplevel(), + use_alpha=False) + color = Gdk.RGBA() + if name in colors: + color.parse(colors[name]) + dialog.set_rgba(color) + if dialog.run() == Gtk.ResponseType.OK: + color = dialog.get_rgba() + if start is not None and end is not None: + start = buffer_.get_iter_at_offset(start) + end = buffer_.get_iter_at_offset(end) + tag = register_foreground(buffer_, color) + remove_tags(buffer_, start, end, name) + buffer_.apply_tag(tag, start, end) + dialog.destroy() + buffer_.place_cursor(buffer_.get_iter_at_mark(insert)) + + +def _apply_tool(buffer_, name, tool, start, end): + # First test RadioToolButton as they inherit from ToggleToolButton + if isinstance(tool, Gtk.RadioToolButton): + name = 'justification %s' % name + if not tool.get_active(): + remove_tags(buffer_, start, end, name) + else: + remove_tags(buffer_, start, end, 'justification') + buffer_.apply_tag_by_name(name, start, end) + elif isinstance(tool, Gtk.ToggleToolButton): + if tool.get_active(): + buffer_.apply_tag_by_name(name, start, end) + else: + buffer_.remove_tag_by_name(name, start, end) + elif isinstance(tool, Gtk.ComboBoxText): + value = tool.get_active_text() + remove_tags(buffer_, start, end, name) + name = '%s %s' % (name, value) + buffer_.apply_tag_by_name(name, start, end) + + +def _insert_text_style(buffer_, iter_, text, length, tag_widgets): + # Text is already inserted so iter_ points to the end + start = iter_.copy() + start.backward_chars(length) + end = iter_.copy() + # Apply tags activated from the toolbar + for name, widget in tag_widgets.items(): + _apply_tool(buffer_, name, widget, start, end) + + +def _detect_style(textview, tag_widgets, colors): + buffer_ = textview.get_buffer() + try: + start, end = buffer_.get_selection_bounds() + except ValueError: + start = end = buffer_.get_iter_at_mark( + buffer_.get_insert()) + + def toggle_button(name, values): + try: + value, = values + except ValueError: + value = False + button = tag_widgets[name] + button.handler_block_by_func(_toggle_props) + button.set_active(value) + button.handler_unblock_by_func(_toggle_props) + + def set_combobox(name, indexes): + try: + index, = indexes + except ValueError: + index = -1 + combobox = tag_widgets[name] + combobox.handler_block_by_func(_change_props) + combobox.set_active(index) + combobox.handler_unblock_by_func(_change_props) + + def toggle_justification(names, value): + if len(names) != 1: + value = False + for name in names: + button = tag_widgets[name] + button.handler_block_by_func(_toggle_justification) + button.set_active(value) + button.handler_unblock_by_func(_toggle_justification) + + bolds, italics, underlines = set(), set(), set() + families, sizes, justifications = set(), set(), set() + colors['foreground'] = 'black' + + iter_ = start.copy() + while True: + bold, italic, underline = False, False, False + family = FAMILIES.index('normal') + size = SIZES.index('4') + justification = 'left' + + for tag in iter_.get_tags(): + if not tag.props.name: + continue + elif tag.props.name == 'bold': + bold = True + elif tag.props.name == 'italic': + italic = True + elif tag.props.name == 'underline': + underline = True + elif tag.props.name.startswith('family'): + _, family = tag.props.name.split() + family = FAMILIES.index(family) + elif tag.props.name.startswith('size'): + _, size = tag.props.name.split() + size = SIZES.index(size) + elif tag.props.name.startswith('justification'): + _, justification = tag.props.name.split() + elif tag.props.name.startswith('foreground'): + _, colors['foreground'] = tag.props.name.split() + bolds.add(bold) + italics.add(italic) + underlines.add(underline) + families.add(family) + sizes.add(size) + justifications.add(justification) + + iter_.forward_char() + if iter_.compare(end) > 0: + iter_ = end + if iter_.compare(end) == 0: + break + + for name, values in [ + ('bold', bolds), + ('italic', italics), + ('underline', underlines)]: + toggle_button(name, values) + set_combobox('family', families) + set_combobox('size', sizes) + toggle_justification(justifications, True) + + +if __name__ == '__main__': + win = Gtk.Window() + box = Gtk.VBox() + win.add(box) + + textview = Gtk.TextView() + register_format(textview) + toolbar = add_toolbar(textview) + box.pack_start(toolbar, expand=False, fill=True, padding=0) + box.pack_start(textview, expand=True, fill=True, padding=0) + + win.show_all() + Gtk.main() diff --git a/tryton/tryton/common/selection.py b/tryton/tryton/common/selection.py new file mode 100644 index 0000000000..5326b89b6b --- /dev/null +++ b/tryton/tryton/common/selection.py @@ -0,0 +1,230 @@ +# 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 operator +import math + +from gi.repository import Gdk, GLib, GObject, Gtk + +from tryton.common import RPCExecute, RPCException +from tryton.common import eval_domain + + +class SelectionMixin(object): + + def __init__(self, *args, **kwargs): + super(SelectionMixin, self).__init__(*args, **kwargs) + self.nullable_widget = True + self.selection = None + self.inactive_selection = [] + self._last_domain = None + self._values2selection = {} + self._domain_cache = {} + + def init_selection(self, value=None): + if value is None: + value = dict((k, None) + for k in self.attrs.get('selection_change_with') or []) + key = freeze_value(value) + selection = self.attrs.get('selection', [])[:] + help_ = self.attrs.get('help_selection', {}) + if (not isinstance(selection, (list, tuple)) + and key not in self._values2selection): + try: + if self.attrs.get('selection_change_with'): + selection = RPCExecute('model', self.model_name, selection, + value) + else: + selection = RPCExecute('model', self.model_name, selection) + except RPCException: + selection = [] + self._values2selection[key] = selection + elif key in self._values2selection: + selection = self._values2selection[key] + if self.attrs.get('sort', True): + selection.sort(key=operator.itemgetter(1)) + self.selection = selection[:] + self.help = help_ + self.inactive_selection = [] + + def update_selection(self, record, field): + if not field: + return + + domain = field.domain_get(record) + if 'relation' not in self.attrs: + change_with = self.attrs.get('selection_change_with') or [] + value = record._get_on_change_args(change_with) + value.pop('id', None) + self.init_selection(value) + self.filter_selection(domain, record, field) + else: + context = field.get_context(record) + domain_cache_key = (freeze_value(domain), freeze_value(context)) + if domain_cache_key in self._domain_cache: + self.selection = self._domain_cache[domain_cache_key] + self._last_domain = (domain, context) + if (domain, context) == self._last_domain: + return + fields = ['rec_name'] + help_field = self.attrs.get('help_field') + if help_field: + fields.append(help_field) + try: + result = RPCExecute('model', self.attrs['relation'], + 'search_read', domain, 0, None, None, fields, + context=context) + except RPCException: + result = False + if isinstance(result, list): + selection = [(x['id'], x['rec_name']) for x in result] + if self.nullable_widget: + selection.append((None, '')) + if help_field: + help_ = {x['id']: x[help_field] for x in result} + else: + help_ = {} + self._last_domain = (domain, context) + self._domain_cache[domain_cache_key] = selection + else: + selection = [] + help_ = {} + self._last_domain = None + self.selection = selection[:] + self.help = help_ + self.inactive_selection = [] + + def filter_selection(self, domain, record, field): + if not domain: + return + + def _value_evaluator(value): + return eval_domain(domain, { + self.field_name: value[0], + }) + + def _model_evaluator(allowed_models): + def test(value): + return value[0] in allowed_models or not allowed_models + return test + + type_ = field.attrs['type'] + if type_ == 'reference': + allowed_models = field.get_models(record) + evaluator = _model_evaluator(allowed_models) + elif type_ == 'multiselection': + return + else: + evaluator = _value_evaluator + self.selection = list(filter(evaluator, self.selection)) + + def get_inactive_selection(self, value): + if 'relation' not in self.attrs: + return '' + for val, text in self.inactive_selection: + if str(val) == str(value): + return text + else: + try: + result, = RPCExecute('model', self.attrs['relation'], 'read', + [value], ['rec_name']) + self.inactive_selection.append((result['id'], + result['rec_name'])) + return result['rec_name'] + except RPCException: + return '' + + +def selection_shortcuts(entry): + def key_press(widget, event): + if (event.type == Gdk.EventType.KEY_PRESS + and event.state & Gdk.ModifierType.CONTROL_MASK + and event.keyval == Gdk.KEY_space): + widget.popup() + entry.connect('key_press_event', key_press) + return entry + + +def freeze_value(value): + if isinstance(value, dict): + return tuple(sorted((k, freeze_value(v)) + for k, v in value.items())) + elif isinstance(value, (list, set, tuple)): + return tuple(freeze_value(v) for v in value) + else: + return value + + +class PopdownMixin(object): + + def set_popdown(self, selection, entry): + child = entry.get_child() + if not child: # entry is destroyed + return + model, lengths = self.get_popdown_model(selection) + entry.set_model(model) + entry.set_entry_text_column(0) + completion = Gtk.EntryCompletion() + completion.set_inline_selection(True) + completion.set_model(model) + child.set_completion(completion) + if lengths: + pop = sorted(lengths, reverse=True) + average = sum(pop) / len(pop) + deviation = int( + math.sqrt(sum((x - average) ** 2 for x in pop) + / len(pop))) + width = max(next( + (x for x in pop if abs(x - average) < (deviation * 2)), + 10), + 10) + else: + width = 10 + child.set_width_chars(width) + if lengths: + child.set_max_length(max(lengths)) + completion.set_text_column(0) + completion.connect('match-selected', self.match_selected, entry) + + def get_popdown_model(self, selection): + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) + lengths = [] + for (value, name) in selection: + name = str(name) + model.append((name, value)) + lengths.append(len(name)) + return model, lengths + + def match_selected(self, completion, model, iter_, entry): + value, = model.get(iter_, 1) + model = entry.get_model() + for i, values in enumerate(model): + if values[1] == value: + GLib.idle_add(entry.set_active, i) + break + + def get_popdown_value(self, entry, index=1): + active = entry.get_active() + if active < 0: + return None + else: + model = entry.get_model() + return model[active][index] + + def get_popdown_text(self, entry): + return self.get_popdown_value(entry, index=0) + + def set_popdown_value(self, entry, value): + active = -1 + model = entry.get_model() + for i, selection in enumerate(model): + if selection[1] == value: + active = i + break + else: + if value: + return False + entry.set_active(active) + if active == -1: + # When setting no item GTK doesn't clear the entry + entry.get_child().set_text('') + return True diff --git a/tryton/tryton/common/timedelta.py b/tryton/tryton/common/timedelta.py new file mode 100644 index 0000000000..208083b2cb --- /dev/null +++ b/tryton/tryton/common/timedelta.py @@ -0,0 +1,112 @@ +# 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 datetime +import gettext +import locale +from collections import OrderedDict + +__all__ = ['format', 'parse'] + +_ = gettext.gettext + +DEFAULT_CONVERTER = { + 's': 1, + } +DEFAULT_CONVERTER['m'] = DEFAULT_CONVERTER['s'] * 60 +DEFAULT_CONVERTER['h'] = DEFAULT_CONVERTER['m'] * 60 +DEFAULT_CONVERTER['d'] = DEFAULT_CONVERTER['h'] * 24 +DEFAULT_CONVERTER['w'] = DEFAULT_CONVERTER['d'] * 7 +DEFAULT_CONVERTER['M'] = DEFAULT_CONVERTER['d'] * 30 +DEFAULT_CONVERTER['Y'] = DEFAULT_CONVERTER['d'] * 365 + + +def _get_separators(): + return OrderedDict([ + ('Y', _('Y')), + ('M', _('M')), + ('w', _('w')), + ('d', _('d')), + ('h', _('h')), + ('m', _('m')), + ('s', _('s')), + ]) + + +def format(value, converter=None): + 'Convert timedelta to text' + if value is None: + return '' + if not converter: + converter = DEFAULT_CONVERTER + + text = [] + value = value.total_seconds() + sign = '' + if value < 0: + sign = '-' + value = abs(value) + converter = [(k, converter.get(k)) for k in _get_separators()] + values = [] + for k, v in converter: + if v: + part = value // v + value -= part * v + values.append(part) + else: + values.append(0) + + for (k, _), v in zip(converter[:-3], values): + if v: + text.append( + locale.format_string('%d', v, True) + _get_separators()[k]) + if any(values[-3:]) or not text: + time = '%02d:%02d' % tuple(values[-3:-1]) + if values[-1] or value: + time += ':%02d' % values[-1] + text.append(time) + text = sign + ' '.join(text) + if value: + if not any(values[-3:]): + # Add space if no time + text += ' ' + text += locale.format_string('%.6f', value)[1:] + return text + + +def parse(text, converter=None): + if not text: + return + if not converter: + converter = DEFAULT_CONVERTER + + for separator in list(_get_separators().values()): + text = text.replace(separator, separator + ' ') + + seconds = 0 + for part in text.split(): + if ':' in part: + for t, v in zip(part.split(':'), + [converter['h'], converter['m'], converter['s']]): + try: + seconds += abs(locale.atof(t)) * v + except ValueError: + pass + else: + for key, separator in list(_get_separators().items()): + if part.endswith(separator): + part = part[:-len(separator)] + try: + seconds += abs(locale.atof(part)) * converter[key] + except ValueError: + pass + break + else: + try: + seconds += abs(locale.atof(part)) + except ValueError: + pass + + if '-' in text: + seconds *= -1 + return datetime.timedelta(seconds=seconds) diff --git a/tryton/tryton/common/treeviewcontrol.py b/tryton/tryton/common/treeviewcontrol.py new file mode 100644 index 0000000000..caf05e6970 --- /dev/null +++ b/tryton/tryton/common/treeviewcontrol.py @@ -0,0 +1,36 @@ +# 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 gi.repository import Gdk, GObject, Gtk + +MOVEMENT_KEYS = { + Gdk.KEY_Up, + Gdk.KEY_Down, + Gdk.KEY_space, + Gdk.KEY_Left, + Gdk.KEY_KP_Left, + Gdk.KEY_Right, + Gdk.KEY_KP_Right, + Gdk.KEY_Home, + Gdk.KEY_KP_Home, + Gdk.KEY_End, + Gdk.KEY_KP_End, + } + +__all__ = ['TreeViewControl'] + + +class TreeViewControl(Gtk.TreeView): + + def do_button_press_event(self, event): + self.grab_focus() # grab focus because it doesn't whith CONTROL MASK + if event.button == 1: + event.state ^= Gdk.ModifierType.CONTROL_MASK + return Gtk.TreeView.do_button_press_event(self, event) + + def do_key_press_event(self, event): + if event.keyval in MOVEMENT_KEYS: + event.state ^= Gdk.ModifierType.CONTROL_MASK + return Gtk.TreeView.do_key_press_event(self, event) + + +GObject.type_register(TreeViewControl) diff --git a/tryton/tryton/common/underline.py b/tryton/tryton/common/underline.py new file mode 100644 index 0000000000..e62218cbb5 --- /dev/null +++ b/tryton/tryton/common/underline.py @@ -0,0 +1,7 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +def set_underline(label): + "Set underscore for mnemonic accelerator" + return '_' + label.replace('_', '__') diff --git a/tryton/tryton/common/widget_style.py b/tryton/tryton/common/widget_style.py new file mode 100644 index 0000000000..0f2985b7ff --- /dev/null +++ b/tryton/tryton/common/widget_style.py @@ -0,0 +1,10 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +def widget_class(widget, name, value): + style_context = widget.get_style_context() + if value: + style_context.add_class(name) + else: + style_context.remove_class(name) diff --git a/tryton/tryton/config.py b/tryton/tryton/config.py new file mode 100644 index 0000000000..7467568abc --- /dev/null +++ b/tryton/tryton/config.py @@ -0,0 +1,176 @@ +# 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 configparser +import optparse +import os +import gettext +import logging +import sys +import locale + +from gi.repository import GdkPixbuf + +from tryton import __version__ + +_ = gettext.gettext + + +def get_config_dir(): + if os.name == 'nt': + appdata = os.environ['APPDATA'] + if not isinstance(appdata, str): + appdata = str(appdata, sys.getfilesystemencoding()) + return os.path.join(appdata, '.config', 'tryton', + __version__.rsplit('.', 1)[0]) + return os.path.join(os.environ['HOME'], '.config', 'tryton', + __version__.rsplit('.', 1)[0]) + + +if not os.path.isdir(get_config_dir()): + os.makedirs(get_config_dir(), 0o700) + + +class ConfigManager(object): + "Config manager" + + def __init__(self): + short_version = '.'.join(__version__.split('.', 2)[:2]) + demo_server = 'demo%s.tryton.org' % short_version + demo_database = 'demo%s' % short_version + self.defaults = { + 'login.profile': demo_server, + 'login.login': 'demo', + 'login.host': demo_server, + 'login.db': demo_database, + 'login.expanded': False, + 'client.title': 'Tryton', + 'client.modepda': False, + 'client.toolbar': 'default', + 'client.save_tree_width': True, + 'client.save_tree_state': True, + 'client.spellcheck': False, + 'client.lang': locale.getdefaultlocale()[0], + 'client.language_direction': 'ltr', + 'client.email': '', + 'client.limit': 1000, + 'client.check_version': True, + 'client.bus_timeout': 10 * 60, + 'icon.colors': '#3465a4,#555753,#cc0000', + 'tree.colors': '#777,#198754,#ffc107,#dc3545', + 'calendar.colors': '#fff,#3465a4', + 'graph.color': '#3465a4', + 'image.max_size': 10 ** 6, + 'image.cache_size': 1024, + 'bug.url': 'https://bugs.tryton.org/', + 'download.url': 'https://downloads-cdn.tryton.org/', + 'download.frequency': 60 * 60 * 8, + 'menu.pane': 200, + } + self.config = {} + self.options = {} + self.arguments = [] + + def parse(self): + parser = optparse.OptionParser(version=("Tryton %s" % __version__), + usage="Usage: %prog [options] [url]") + parser.add_option("-c", "--config", dest="config", + help=_("specify alternate config file")) + parser.add_option("-d", "--dev", action="store_true", + default=False, dest="dev", + help=_("development mode")) + parser.add_option("-v", "--verbose", action="store_true", + default=False, dest="verbose", + help=_("logging everything at INFO level")) + parser.add_option("-l", "--log-level", dest="log_level", + help=_("specify the log level: " + "DEBUG, INFO, WARNING, ERROR, CRITICAL")) + parser.add_option("-u", "--user", dest="login", + help=_("specify the login user")) + parser.add_option("-s", "--server", dest="host", + help=_("specify the server hostname:port")) + opt, self.arguments = parser.parse_args() + self.rcfile = opt.config or os.path.join( + get_config_dir(), 'tryton.conf') + self.load() + + self.options['dev'] = opt.dev + logging.basicConfig() + loglevels = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, + } + if not opt.log_level: + if opt.verbose: + opt.log_level = 'INFO' + else: + opt.log_level = 'ERROR' + logging.getLogger().setLevel(loglevels[opt.log_level.upper()]) + + for arg in ['login', 'host']: + if getattr(opt, arg): + self.options['login.' + arg] = getattr(opt, arg) + + def save(self): + try: + parser = configparser.ConfigParser() + for entry in list(self.config.keys()): + if not len(entry.split('.')) == 2: + continue + section, name = entry.split('.') + if not parser.has_section(section): + parser.add_section(section) + parser.set(section, name, str(self.config[entry])) + with open(self.rcfile, 'w') as fp: + parser.write(fp) + except IOError: + logging.getLogger(__name__).warn( + _('Unable to write config file %s.') + % (self.rcfile,)) + return False + return True + + def load(self): + parser = configparser.ConfigParser() + parser.read([self.rcfile]) + for section in parser.sections(): + for (name, value) in parser.items(section): + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + if section == 'client' and name == 'limit': + # First convert to float to be backward compatible with old + # configuration + value = int(float(value)) + self.config[section + '.' + name] = value + return True + + def __setitem__(self, key, value, config=True): + self.options[key] = value + if config: + self.config[key] = value + + def __getitem__(self, key): + return self.options.get(key, self.config.get(key, + self.defaults.get(key))) + + +CONFIG = ConfigManager() +CURRENT_DIR = os.path.dirname(__file__) +if hasattr(sys, 'frozen'): + CURRENT_DIR = os.path.dirname(sys.executable) +if not isinstance(CURRENT_DIR, str): + CURRENT_DIR = str(CURRENT_DIR, sys.getfilesystemencoding()) + +PIXMAPS_DIR = os.path.join(CURRENT_DIR, 'data', 'pixmaps', 'tryton') +if not os.path.isdir(PIXMAPS_DIR): + # do not import when frozen + import pkg_resources + PIXMAPS_DIR = pkg_resources.resource_filename( + 'tryton', 'data/pixmaps/tryton') + +TRYTON_ICON = GdkPixbuf.Pixbuf.new_from_file( + os.path.join(PIXMAPS_DIR, 'tryton-icon.png')) diff --git a/tryton/tryton/data/locale/bg/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/bg/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..457a328aea --- /dev/null +++ b/tryton/tryton/data/locale/bg/LC_MESSAGES/tryton.po @@ -0,0 +1,1249 @@ +# Bulgarian (Bulgaria) translations for tryton. +# Copyright (C) 2010 B2CK +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2010. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "укажете друг конфигурационен файл" + +msgid "development mode" +msgstr "" + +msgid "logging everything at INFO level" +msgstr "записване на всичко на ниво INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +msgid "specify the login user" +msgstr "укажете потребителското име" + +#, fuzzy +msgid "specify the server hostname:port" +msgstr "укажете адреса на сървъра" + +#, fuzzy, python-format +msgid "Unable to write config file %s." +msgstr "Не може да запише конфигурационен файл %s!" + +#, python-format +msgid "Unable to set locale %s" +msgstr "Не може да зададе локални настройки %s" + +#, fuzzy +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "Изберете действие" + +#, fuzzy +msgid "No action defined." +msgstr "Не е зададено действие!" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "Избор" + +#, fuzzy +msgid "Cancel" +msgstr "Отказ" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "Вашия избор" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "Съхраняване" + +msgid "Save As..." +msgstr "Запис като ..." + +msgid "Do you want to proceed?" +msgstr "" + +msgid "Always ignore this warning." +msgstr "Винаги игнорирай това предупреждение." + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "Грешка при достъп" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "Сравняване" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "Запис" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Сравняване: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "Грешка в приложението!" + +msgid "Report Bug" +msgstr "Съобщаване за грешка" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +#, fuzzy +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +#, fuzzy +msgid "Value" +msgstr "Лъжа" + +msgid "Displayed value" +msgstr "" + +#, fuzzy +msgid "Format" +msgstr "Нормален" + +msgid "Display format" +msgstr "" + +#, fuzzy +msgid "Open the calendar" +msgstr "Отваряне на календара" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "г" + +msgid "True" +msgstr "Истина" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "Лъжа" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "Свързани" + +#, fuzzy +msgid "Edit..." +msgstr "Изход..." + +msgid "Attachments..." +msgstr "Прикачен файл..." + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "Действия..." + +#, fuzzy +msgid "Relate..." +msgstr "Свързани..." + +msgid "Report..." +msgstr "Справки..." + +msgid "Print..." +msgstr "Печат..." + +msgid "E-Mail..." +msgstr "Email..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "Г" + +msgid "M" +msgstr "М" + +msgid "w" +msgstr "с" + +msgid "d" +msgstr "д" + +msgid "h" +msgstr "ч" + +msgid "m" +msgstr "м" + +msgid "s" +msgstr "" + +#, fuzzy +msgid "Preferences..." +msgstr "Предпочитания..." + +#, fuzzy +msgid "Toolbar" +msgstr "Лента с инструменти" + +#, fuzzy +msgid "Default" +msgstr "По подразбиране" + +#, fuzzy +msgid "Text and Icons" +msgstr "Текс и икони" + +#, fuzzy +msgid "Text" +msgstr "Текст" + +#, fuzzy +msgid "Icons" +msgstr "Икони" + +#, fuzzy +msgid "Form" +msgstr "Форма" + +msgid "Save Column Width" +msgstr "" + +#, fuzzy +msgid "Save Tree State" +msgstr "Запазване на състояние разгърнато дърво" + +msgid "Spell Checking" +msgstr "Проверка на правопис" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "Ограничение при търсене..." + +msgid "Check Version" +msgstr "" + +#, fuzzy +msgid "Options" +msgstr "Настройки" + +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "Бързи клавиши..." + +#, fuzzy +msgid "About..." +msgstr "Относно..." + +#, fuzzy +msgid "Help" +msgstr "Помощ" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "Действие" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Следващите действия изискват затваряне на всички прозорци.\n" +"Искате ли да продължите?" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "Настройки" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "Предишен" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Изрязване на избрания текст" + +msgid "Copy selected text" +msgstr "Копиране на избрания текст" + +msgid "Paste copied text" +msgstr "Поставяне на копирания текст" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "Създаване на нова връзка" + +msgid "Open/Search relation" +msgstr "Отваряне/Търсене на връзка" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "Превключване на изглед" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "Отваряне на връзка" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "Изчистване на ред за изтриване" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "Затваряне на таб" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, fuzzy, python-format +msgid "Attachments (%s)" +msgstr "Прикачен файл(%s)" + +msgid "Profile Editor" +msgstr "Редактор на профили" + +msgid "Profile" +msgstr "Профил" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "Хост:" + +msgid "Database:" +msgstr "База данни:" + +msgid "Fetching databases list" +msgstr "Вземане на списъка с бази данни" + +msgid "Username:" +msgstr "Потребителско име:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Несъвместима версия на сървъра!" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Не може да се свърже със сървъра!" + +msgid "Login" +msgstr "Потребителско име" + +msgid "_Cancel" +msgstr "Отказ" + +msgid "Cancel connection to the Tryton server" +msgstr "Отказ от свръзване с Tryton сървъра" + +msgid "C_onnect" +msgstr "Свързване" + +msgid "Connect the Tryton server" +msgstr "Свързване с Tryton сървъра" + +msgid "Profile:" +msgstr "Профил:" + +msgid "Host / Database information" +msgstr "Информация за Хост / База данни" + +msgid "User name:" +msgstr "Потребителско име:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Не може да зададе локални настройки %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Email %s" + +msgid "To:" +msgstr "До:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Относно:" + +#, fuzzy +msgid "Body" +msgstr "Тяло:" + +#, fuzzy +msgid "Reports" +msgstr "Справки" + +#, fuzzy +msgid "Attachments" +msgstr "Прикачен файл:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Избор" + +#, fuzzy +msgid "Remove File" +msgstr "Изтриване " + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "Предишен" + +msgid "Previous" +msgstr "Предишен" + +msgid "Next" +msgstr "Следващ" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Прикачен файл(%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +#, fuzzy +msgid "You have to select one record." +msgstr "Трябва да изберете един запис!" + +msgid "ID:" +msgstr "ID:" + +#, fuzzy +msgid "Created by:" +msgstr "Дата на създаване:" + +#, fuzzy +msgid "Created at:" +msgstr "Дата на създаване:" + +#, fuzzy +msgid "Edited by:" +msgstr "Редактиране" + +#, fuzzy +msgid "Edited at:" +msgstr "Редактиране" + +msgid "Model:" +msgstr "Модел:" + +msgid "Are you sure to remove this record?" +msgstr "Наистина ли искате да изтриете този запис?" + +msgid "Are you sure to remove those records?" +msgstr "Наистина ли искате да изтриете тези записи?" + +#, fuzzy +msgid "Records not removed." +msgstr "Записа не е изтрит!" + +#, fuzzy +msgid "Records removed." +msgstr "Записите изтрити!" + +#, fuzzy +msgid "Working now on the duplicated record(s)." +msgstr "Работите върху дублиран(и) запис(и)!" + +#, fuzzy +msgid "Record saved." +msgstr "Записа съхранен!" + +msgid " of " +msgstr " от" + +#, fuzzy +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Този запис е променен\n" +"искате ли да го запишете ?" + +msgid "Launch action" +msgstr "Започване на действие" + +msgid "Relate" +msgstr "Свързани" + +msgid "Open related records" +msgstr "Отваряне на свързани записи" + +msgid "Report" +msgstr "Справки" + +msgid "Open report" +msgstr "Отваряне на справка" + +msgid "Print" +msgstr "Печат" + +msgid "Print report" +msgstr "Отпечатване на справка" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "Неизвестен" + +msgid "Limit" +msgstr "Ограничение" + +msgid "Search Limit Settings" +msgstr "Настройки на органичения за търсене" + +msgid "Limit:" +msgstr "Ограничение:" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "Редактиране на потребителски настройки" + +msgid "Preference" +msgstr "Настройки" + +#, fuzzy +msgid "Revision" +msgstr "Предишен" + +#, fuzzy +msgid "Select a revision" +msgstr "Избор:" + +#, fuzzy +msgid "Revision:" +msgstr "Предишен" + +msgid "_Switch View" +msgstr "Превключване на изглед" + +#, fuzzy +msgid "Switch View" +msgstr "Превключване на изглед" + +msgid "_Previous" +msgstr "Предишен" + +msgid "Previous Record" +msgstr "Предишен запис" + +msgid "_Next" +msgstr "Следващ" + +msgid "Next Record" +msgstr "Следващ запис" + +#, fuzzy +msgid "_Search" +msgstr "Търсене" + +msgid "_New" +msgstr "Нов" + +msgid "Create a new record" +msgstr "Създаване на нов запис" + +msgid "_Save" +msgstr "Съхраняване" + +msgid "Save this record" +msgstr "Съхраняване на този запис" + +msgid "_Reload/Undo" +msgstr "Презареждане/Обратно" + +#, fuzzy +msgid "Reload/Undo" +msgstr "Презареждане/Обратно" + +msgid "_Duplicate" +msgstr "Дублиране" + +msgid "_Delete..." +msgstr "Изтриване..." + +msgid "View _Logs..." +msgstr "Преглед на логове" + +msgid "Show revisions..." +msgstr "" + +#, fuzzy +msgid "A_ttachments..." +msgstr "Прикачен файл:" + +msgid "Add an attachment to the record" +msgstr "Добавяне на прикачен файл към записа" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "Действия..." + +msgid "_Relate..." +msgstr "Свързани..." + +msgid "_Report..." +msgstr "Справки..." + +msgid "_Print..." +msgstr "Печат..." + +msgid "_E-Mail..." +msgstr "Email..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Съхраняване на този запис" + +msgid "_Export Data..." +msgstr "Извличане на данни..." + +msgid "_Import Data..." +msgstr "Вмъкване на данни..." + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "Затваряне на таб" + +msgid "All fields" +msgstr "Всички полета" + +msgid "_Add" +msgstr "Добавяне" + +msgid "_Remove" +msgstr "Изтриване" + +#, fuzzy +msgid "_Clear" +msgstr "Изчистване" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "CSV параметри" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "Кодиране:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "Име на поле" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +#, fuzzy +msgid "_Save Export" +msgstr "Запис на извличане" + +#, fuzzy +msgid "_URL Export" +msgstr "Запис на извличане" + +#, fuzzy +msgid "_Delete Export" +msgstr "Изтриване на извличане" + +msgid "Predefined exports" +msgstr "Предварително зададени извличания" + +msgid "Name" +msgstr "Име" + +msgid "Open" +msgstr "Отваряне" + +#, fuzzy +msgid "Listed Records" +msgstr "Редактиране на избрания запис" + +#, fuzzy +msgid "Selected Records" +msgstr "Редактиране на избрания запис" + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Добавяне на имена на полета" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "Какво е името на това извличане?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, fuzzy, python-format +msgid "%d record saved." +msgstr "%d заспис съхранен!" + +#, fuzzy, python-format +msgid "%d records saved." +msgstr "%d записа съхранени!" + +#, fuzzy, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Операцията неуспешна!\n" +"Съобщение за грешка:\n" +"%s" + +msgid "Error" +msgstr "Грешка" + +msgid "Link" +msgstr "Връзка" + +msgid "Delete" +msgstr "Изтриване" + +msgid "New" +msgstr "Нов" + +msgid "Switch" +msgstr "Превключване" + +msgid "Add" +msgstr "Добавяне" + +msgid "Remove " +msgstr "Изтриване " + +msgid "Create a new record " +msgstr "Създаване на нов запис " + +msgid "Delete selected record " +msgstr "Изтриване на избрания запис " + +msgid "Undelete selected record " +msgstr "Възтановяване на избрания запис " + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +#, fuzzy +msgid "_Auto-Detect" +msgstr "Автоматично разпознаване" + +msgid "File to Import:" +msgstr "Файл за вмъкване:" + +msgid "Open..." +msgstr "Отваряне..." + +msgid "Lines to Skip:" +msgstr "Редове за пропускане:" + +#, fuzzy +msgid "You must select an import file first." +msgstr "Първо трябва да изберете файл за вмъкване!" + +msgid "Error opening CSV file" +msgstr "Грешка при отваряне на CSV файл" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Грешка при обработка на файл при поле %s" + +#, fuzzy, python-format +msgid "%d record imported." +msgstr "%d запис вмъкнат!" + +#, fuzzy, python-format +msgid "%d records imported." +msgstr "%d записи вмъкнати!" + +msgid "Search" +msgstr "Търсене" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "Помощник" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +#, fuzzy +msgid "ID" +msgstr "д" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Дата на създаване:" + +#, fuzzy +msgid "Edited by" +msgstr "Редактиране" + +#, fuzzy +msgid "Edited at" +msgstr "Редактиране" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "Не може да зададе локални настройки %s" + +#, fuzzy +msgid "Unable to set view tree state" +msgstr "Не може да зададе локални настройки %s" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Размер на изображение" + +msgid "Width:" +msgstr "Ширина:" + +msgid "Height:" +msgstr "Височина:" + +msgid "PNG image (*.png)" +msgstr "PNG изображение (*.png)" + +msgid "Save As" +msgstr "Запис като" + +#, fuzzy +msgid "Image size too large." +msgstr "Размера на изображението е много голям!" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr "" + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "Търсене" + +#, fuzzy +msgid "Today" +msgstr "Тяло:" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +#, fuzzy +msgid "Week View" +msgstr "Превключване на изглед" + +#, fuzzy +msgid "Month View" +msgstr "Превключване на изглед" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "Изчистване" + +msgid "All files" +msgstr "Всички файлове" + +msgid "Show plain text" +msgstr "Показване само на текст" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "Изображения" + +#, fuzzy +msgid "Add existing record" +msgstr "Съхраняване на този запис" + +#, fuzzy +msgid "Remove selected record" +msgstr "Изтриване на избрания запис " + +#, fuzzy +msgid "Open the record " +msgstr "Отваряне на запис" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "Тъсене на запис" + +#, fuzzy +msgid "Edit selected record" +msgstr "Редактиране на избрания запис" + +#, fuzzy +msgid "Delete selected record" +msgstr "Изтриване на избрания запис " + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +#, fuzzy +msgid "Translation" +msgstr "Добавяне на превод" + +msgid "Edit" +msgstr "Редактиране" + +msgid "Fuzzy" +msgstr "Неясно" + +#, fuzzy +msgid "You need to save the record before adding translations." +msgstr "Трябва да съхраните записа преди да добавите преводи!" + +#, fuzzy +msgid "No other language available." +msgstr "Няма наличен друг език!" + +msgid "Translate view" +msgstr "Превод на изглед" diff --git a/tryton/tryton/data/locale/ca/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/ca/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..8e2c2d295e --- /dev/null +++ b/tryton/tryton/data/locale/ca/LC_MESSAGES/tryton.po @@ -0,0 +1,1171 @@ +# Catalan translations for tryton. +# Copyright (C) 2012 Zikzakmedia SL +# This file is distributed under the same license as the tryton project. +# Sergi Almacellas Abellana 2012. +# Jordi Esteve Cusiné 2012. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "Indica un fitxer de configuració alternatiu" + +msgid "development mode" +msgstr "mode desenvolupament" + +msgid "logging everything at INFO level" +msgstr "Registra tota la informació amb nivell INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "indica el nivell de log: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "Indica l'usuari" + +msgid "specify the server hostname:port" +msgstr "Indica el nom:port del servidor" + +#, python-format +msgid "Unable to write config file %s." +msgstr "No s'ha pogut escriure el fitxer configuració %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "No s'ha pogut establir l'idioma %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Seleccioneu la vostra acció" + +msgid "No action defined." +msgstr "No s'ha definit cap acció." + +msgid "By: " +msgstr "Per: " + +msgid "Selection" +msgstr "Selecció" + +msgid "Cancel" +msgstr "Cancel·la" + +msgid "OK" +msgstr "D'acord" + +msgid "Your selection:" +msgstr "La vostra selecció:" + +msgid "Select" +msgstr "Selecciona" + +msgid "Save" +msgstr "Desa" + +msgid "Save As..." +msgstr "Desa com..." + +msgid "Do you want to proceed?" +msgstr "Voleu continuar?" + +msgid "Always ignore this warning." +msgstr "Ignora sempre aquest advertiment." + +msgid "No" +msgstr "No" + +msgid "Yes" +msgstr "Si" + +msgid "Concurrency Exception" +msgstr "Excepció de concurrència" + +msgid "This record has been modified while you were editing it." +msgstr "Aquest registre ha estat modificat mentre l'editàveu." + +msgid "Cancel saving" +msgstr "No desis els canvis" + +msgid "Compare" +msgstr "Compara" + +msgid "See the modified version" +msgstr "Mostra la versió modificada" + +msgid "Write Anyway" +msgstr "Desa de totes formes" + +msgid "Save your current version" +msgstr "Deseu els vostres canvis" + +#, python-format +msgid "Compare: %s" +msgstr "Compara: %s" + +msgid "Close" +msgstr "Tanca" + +msgid "Application Error" +msgstr "Error d'aplicació" + +msgid "Report Bug" +msgstr "Informa de l'error" + +#, python-format +msgid "Check URL: %s" +msgstr "URL de comprovació: %s" + +msgid "Unable to check for new version." +msgstr "No s'ha pogut comprovar si existeix una nova versió." + +msgid "A new version is available!" +msgstr "Hi ha una nova versió disponible!" + +msgid "Download" +msgstr "Baixa" + +msgid "Too many requests. Try again later." +msgstr "Massa peticions. Proveu-ho de nou més tard." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Cerca..." + +msgid "Create..." +msgstr "Crea..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "No es pot buscar l'autocompletat de %s" + +msgid "Value" +msgstr "Valor" + +msgid "Displayed value" +msgstr "Valor a mostrar" + +msgid "Format" +msgstr "Format" + +msgid "Display format" +msgstr "Format a mostrar" + +msgid "Open the calendar" +msgstr "Obre el calendari" + +msgid "Date Format" +msgstr "Format dates" + +msgid "Displayed date format" +msgstr "Format dates a mostrar" + +msgid "y" +msgstr "a" + +msgid "True" +msgstr "Verdader" + +msgid "t" +msgstr "v" + +msgid "False" +msgstr "Fals" + +msgid "Digits" +msgstr "Dígits" + +msgid "The number of decimal" +msgstr "El número de decimals" + +msgid "Template" +msgstr "Plantilla" + +msgid "Edit..." +msgstr "Edita..." + +msgid "Attachments..." +msgstr "Adjunts..." + +msgid "Notes..." +msgstr "Notes..." + +msgid "Actions..." +msgstr "Accions..." + +msgid "Relate..." +msgstr "Relacionat..." + +msgid "Report..." +msgstr "Informe..." + +msgid "Print..." +msgstr "Imprimeix..." + +msgid "E-Mail..." +msgstr "Correu electrònic..." + +msgid "Bold" +msgstr "Negreta" + +msgid "Italic" +msgstr "Cursiva" + +msgid "Underline" +msgstr "Subratllat" + +msgid "Align Left" +msgstr "Alinear a l'esquerra" + +msgid "Align Center" +msgstr "Alinear al centre" + +msgid "Align Right" +msgstr "Alinear a la dreta" + +msgid "Justify" +msgstr "Justificar" + +msgid "Foreground Color" +msgstr "Color de fons" + +msgid "Select a color" +msgstr "Seleccioneu un color" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "S" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferències..." + +msgid "Toolbar" +msgstr "Barra d'eines" + +msgid "Default" +msgstr "Per defecte" + +msgid "Text and Icons" +msgstr "Text i icones" + +msgid "Text" +msgstr "Text" + +msgid "Icons" +msgstr "Icones" + +msgid "Form" +msgstr "Formulari" + +msgid "Save Column Width" +msgstr "Guarda l'ample de la columna" + +msgid "Save Tree State" +msgstr "Desa l'estat de l'arbre" + +msgid "Spell Checking" +msgstr "Correcció ortogràfica" + +msgid "PDA Mode" +msgstr "Mode PDA" + +msgid "Search Limit..." +msgstr "_Límit de cerca..." + +msgid "Check Version" +msgstr "Comprova versió" + +msgid "Options" +msgstr "Opcions" + +msgid "Keyboard Shortcuts..." +msgstr "Combinacions de tecles..." + +msgid "About..." +msgstr "Quant a..." + +msgid "Help" +msgstr "Ajuda" + +msgid "No result found." +msgstr "No s'han trobat resultats." + +msgid "Favorites" +msgstr "Preferits" + +msgid "Manage..." +msgstr "Gestiona..." + +msgid "Action" +msgstr "Acció" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"L'acció seleccionada requereix tancar totes les pestanyes.\n" +"Voleu continuar?" + +msgid "Application Shortcuts" +msgstr "Dreceres de l'aplicació" + +msgid "Global" +msgstr "Global" + +msgid "Preferences" +msgstr "Preferències" + +msgid "Search menu" +msgstr "Menú de cerca" + +msgid "Toggle menu" +msgstr "Commuta el menú" + +msgid "Previous tab" +msgstr "Pestanya anterior" + +msgid "Next tab" +msgstr "Pestanya següent" + +msgid "Shortcuts" +msgstr "Dreceres" + +msgid "Quit" +msgstr "Sortir" + +msgid "Edition Shortcuts" +msgstr "Accessos directes a l'edició" + +msgid "Text Entries" +msgstr "Entrades de text" + +msgid "Cut selected text" +msgstr "Talla el text seleccionat" + +msgid "Copy selected text" +msgstr "Copia el text seleccionat" + +msgid "Paste copied text" +msgstr "Enganxa el text seleccionat" + +msgid "Next entry" +msgstr "Següent entrada" + +msgid "Previous entry" +msgstr "Entrada anterior" + +msgid "Relation Entries" +msgstr "Entrades de relacions" + +msgid "Create new relation" +msgstr "Crea una nova relació" + +msgid "Open/Search relation" +msgstr "Obre/Cerca una relació" + +msgid "List Entries" +msgstr "Llistat d'entrades" + +msgid "Switch view" +msgstr "Canvia la vista" + +msgid "Create/Select new line" +msgstr "Crear/Seleccionar línia nova" + +msgid "Open relation" +msgstr "Obre una relació" + +msgid "Mark line for deletion/removal" +msgstr "Marcar línia per eliminar/suprimir" + +msgid "Mark line for removal" +msgstr "Marcar línia per eliminar" + +msgid "Unmark line for deletion" +msgstr "Desmarca la línia per ser eliminada" + +msgid "List/Tree Shortcuts" +msgstr "Drecera de llista/arbre" + +msgid "Move Cursor" +msgstr "Mou cursor" + +msgid "Move right" +msgstr "Mou a la dreta" + +msgid "Move left" +msgstr "Mou a l'esquerra" + +msgid "Move up" +msgstr "Mou amunt" + +msgid "Move down" +msgstr "Mou avall" + +msgid "Move up of one page" +msgstr "Mou amunt una pàgina" + +msgid "Move down of one page" +msgstr "Mou avall una pàgina" + +msgid "Move to top" +msgstr "Mou al principi" + +msgid "Move to bottom" +msgstr "Mou al final" + +msgid "Move to parent" +msgstr "Mou al pare" + +msgid "Select all" +msgstr "Selecciona tot" + +msgid "Unselect all" +msgstr "Deselecciona tot" + +msgid "Select parent" +msgstr "Selecciona pare" + +msgid "Select/Activate current row" +msgstr "Selecciona/Activa fila actual" + +msgid "Toggle selection" +msgstr "Commuta selecció" + +msgid "Expand/Collapse" +msgstr "Expandeix/Contrau" + +msgid "Expand row" +msgstr "Expandeix fila" + +msgid "Collapse row" +msgstr "Contrau fila" + +msgid "Toggle row" +msgstr "Commuta fila" + +msgid "Collapse all rows" +msgstr "Contrau totes les files" + +msgid "Expand all rows" +msgstr "Expandeix totes les files" + +msgid "Close Tab" +msgstr "Tanca pestanya" + +msgid "modularity, scalability and security" +msgstr "modularitat, escalabilitat i seguretat" + +msgid "translator-credits" +msgstr "Sergi Almacellas Abellana" + +#, python-format +msgid "Attachments (%s)" +msgstr "Adjunts (%s)" + +msgid "Profile Editor" +msgstr "Editor de perfils" + +msgid "Profile" +msgstr "Perfil" + +msgid "Add new profile" +msgstr "Afegeix un nou perfil" + +msgid "Remove selected profile" +msgstr "Elimina el perfil seleccionat" + +msgid "Host:" +msgstr "Servidor:" + +msgid "Database:" +msgstr "Base de dades:" + +msgid "Fetching databases list" +msgstr "Recuperant llista de bases de dades" + +msgid "Username:" +msgstr "Nom d'usuari:" + +msgid "Incompatible version of the server." +msgstr "El client no és compatible amb la versió del servidor." + +msgid "Could not connect to the server." +msgstr "No s'ha pogut connectar amb el servidor." + +msgid "Login" +msgstr "Accedeix" + +msgid "_Cancel" +msgstr "_Cancel·la" + +msgid "Cancel connection to the Tryton server" +msgstr "Cancel·la connexió amb el servidor Tryton" + +msgid "C_onnect" +msgstr "C_onnecta" + +msgid "Connect the Tryton server" +msgstr "Connecta amb el servidor Tryton" + +msgid "Profile:" +msgstr "Perfil:" + +msgid "Host / Database information" +msgstr "Informació del Servidor / Base de dades" + +msgid "User name:" +msgstr "Nom d'usuari:" + +msgid "Unable to complete email entry" +msgstr "No s'ha pogut completar l'entrada del correu electrònic" + +#, python-format +msgid "E-mail %s" +msgstr "Correu electrònic %s" + +msgid "To:" +msgstr "A:" + +msgid "Cc:" +msgstr "Cc:" + +msgid "Bcc:" +msgstr "Cco:" + +msgid "Subject:" +msgstr "Assumpte:" + +msgid "Body" +msgstr "Missatge" + +msgid "Reports" +msgstr "Informes" + +msgid "Attachments" +msgstr "Adjunts" + +msgid "Files" +msgstr "Fitxers" + +msgid "Send" +msgstr "Envia" + +msgid "Select File" +msgstr "Selecciona un fitxer" + +msgid "Remove File" +msgstr "Elimina fitxer" + +msgid "Add..." +msgstr "Afegeix..." + +msgid "Preview" +msgstr "Previsualitza" + +msgid "Previous" +msgstr "Anterior" + +msgid "Next" +msgstr "Següent" + +#, python-format +msgid "Attachment (%s)" +msgstr "Adjunts (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Nota (%d/%d)" + +msgid "You have to select one record." +msgstr "Ha de seleccionar un registre." + +msgid "ID:" +msgstr "Identificador:" + +msgid "Created by:" +msgstr "Creat per:" + +msgid "Created at:" +msgstr "Creat el:" + +msgid "Edited by:" +msgstr "Editat per:" + +msgid "Edited at:" +msgstr "Editat el:" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Esteu segur que voleu eliminar aquest registre?" + +msgid "Are you sure to remove those records?" +msgstr "Esteu segur que voleu eliminar aquests registres?" + +msgid "Records not removed." +msgstr "Els registres no s'han eliminat." + +msgid "Records removed." +msgstr "Registres eliminats." + +msgid "Working now on the duplicated record(s)." +msgstr "Està treballant amb un registre(s) duplicat." + +msgid "Record saved." +msgstr "Registre desat." + +msgid " of " +msgstr " de " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Aquest registre ha estat modificat.\n" +"Voleu desar-lo?" + +msgid "Launch action" +msgstr "Executa acció" + +msgid "Relate" +msgstr "Relacionat" + +msgid "Open related records" +msgstr "Obre registres relacionats" + +msgid "Report" +msgstr "Informes" + +msgid "Open report" +msgstr "Obre informe" + +msgid "Print" +msgstr "Imprimeix" + +msgid "Print report" +msgstr "Imprimeix informe" + +msgid "_Copy URL" +msgstr "_Copia l'URL" + +msgid "Copy URL into clipboard" +msgstr "Copia l'URL al porta-retalls" + +msgid "Unknown" +msgstr "Desconegut" + +msgid "Limit" +msgstr "Límit" + +msgid "Search Limit Settings" +msgstr "Preferències del límit de cerca" + +msgid "Limit:" +msgstr "Límit:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notes (%s)" + +msgid "Edit User Preferences" +msgstr "Edita preferències d'usuari" + +msgid "Preference" +msgstr "Preferències" + +msgid "Revision" +msgstr "Revisió" + +msgid "Select a revision" +msgstr "Seleccioneu una revisió" + +msgid "Revision:" +msgstr "Revisió:" + +msgid "_Switch View" +msgstr "Canvia la vi_sta" + +msgid "Switch View" +msgstr "Canvia la vista" + +msgid "_Previous" +msgstr "_Anterior" + +msgid "Previous Record" +msgstr "Registre anterior" + +msgid "_Next" +msgstr "Seg_üent" + +msgid "Next Record" +msgstr "Registre següent" + +msgid "_Search" +msgstr "_Cerca" + +msgid "_New" +msgstr "_Nou" + +msgid "Create a new record" +msgstr "Crea un nou registre" + +msgid "_Save" +msgstr "_Desa" + +msgid "Save this record" +msgstr "Desa aquest registre" + +msgid "_Reload/Undo" +msgstr "_Recarrega/Desfés" + +msgid "Reload/Undo" +msgstr "Recarrega/Desfés" + +msgid "_Duplicate" +msgstr "_Duplica" + +msgid "_Delete..." +msgstr "_Elimina..." + +msgid "View _Logs..." +msgstr "Veure _registre..." + +msgid "Show revisions..." +msgstr "Mostra revisions..." + +msgid "A_ttachments..." +msgstr "Adjun_ts..." + +msgid "Add an attachment to the record" +msgstr "Afegeix un adjunt al registre" + +msgid "_Notes..." +msgstr "_Notes..." + +msgid "Add a note to the record" +msgstr "Afegeix una nota al registre" + +msgid "_Actions..." +msgstr "_Accions..." + +msgid "_Relate..." +msgstr "_Relacionat..." + +msgid "_Report..." +msgstr "_Informes..." + +msgid "_Print..." +msgstr "Im_primeix..." + +msgid "_E-Mail..." +msgstr "_Correu electrònic..." + +msgid "Send an e-mail using the record" +msgstr "Envia un correu electrònic utilitzant el registre" + +msgid "_Export Data..." +msgstr "_Exporta dades..." + +msgid "_Import Data..." +msgstr "_Importa dades..." + +msgid "Copy _URL..." +msgstr "Copia l'_URL..." + +msgid "_Close Tab" +msgstr "_Tanca la pestanya" + +msgid "All fields" +msgstr "Tots els camps" + +msgid "_Add" +msgstr "_Afegeix" + +msgid "_Remove" +msgstr "_Elimina" + +msgid "_Clear" +msgstr "_Neteja" + +msgid "Fields selected" +msgstr "Camps seleccionats" + +msgid "CSV Parameters" +msgstr "Paràmetres CSV" + +msgid "Delimiter:" +msgstr "Separador:" + +msgid "Quote char:" +msgstr "Delimitador de text:" + +msgid "Encoding:" +msgstr "Codificació:" + +msgid "Use locale format" +msgstr "Utilitza format local" + +msgid "Field name" +msgstr "Nom del camp" + +#, python-format +msgid "CSV Export: %s" +msgstr "Exportació CSV: %s" + +msgid "_Save Export" +msgstr "_Desa exportació" + +msgid "_URL Export" +msgstr "_URL exportació" + +msgid "_Delete Export" +msgstr "_Elimina exportació" + +msgid "Predefined exports" +msgstr "Exportacions predeterminades" + +msgid "Name" +msgstr "Nom" + +msgid "Open" +msgstr "Obre" + +msgid "Listed Records" +msgstr "Registres llistats" + +msgid "Selected Records" +msgstr "Registres seleccionats" + +msgid "Ignore search limit" +msgstr "Ignora límit de cerca" + +msgid "Add field names" +msgstr "Afegeix noms de camp" + +#, python-format +msgid "%s (string)" +msgstr "%s (text)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nom del model)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (nom del registre)" + +msgid "What is the name of this export?" +msgstr "Quin és el nom d'aquesta exportació?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Voleu sobreescriure la definició de '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "S'ha desat %d registre." + +#, python-format +msgid "%d records saved." +msgstr "S'han desat %d registres." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Ha fallat l'operació.\n" +"Missatge d'error:\n" +"%s" + +msgid "Error" +msgstr "Error" + +msgid "Link" +msgstr "Enllaç" + +msgid "Delete" +msgstr "Elimina" + +msgid "New" +msgstr "Nou" + +msgid "Switch" +msgstr "Canvia la vista" + +msgid "Add" +msgstr "Afegeix" + +msgid "Remove " +msgstr "Elimina " + +msgid "Create a new record " +msgstr "Crea un nou registre " + +msgid "Delete selected record " +msgstr "Elimina el registre seleccionat " + +msgid "Undelete selected record " +msgstr "Recupera el registre seleccionat " + +#, python-format +msgid "CSV Import: %s" +msgstr "Importació CSV: %s" + +msgid "_Auto-Detect" +msgstr "Detecta _automàticament" + +msgid "File to Import:" +msgstr "Fitxer a importar:" + +msgid "Open..." +msgstr "Obre..." + +msgid "Lines to Skip:" +msgstr "Línies a ometre:" + +msgid "You must select an import file first." +msgstr "Primer heu de seleccionar un fitxer a importar." + +msgid "Error opening CSV file" +msgstr "Error a l'obrir el fitxer CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "S'ha produït un error en processar el fitxer en el camp %s." + +#, python-format +msgid "%d record imported." +msgstr "S'ha importat %d registre." + +#, python-format +msgid "%d records imported." +msgstr "S'han importat %d registres." + +msgid "Search" +msgstr "Cerca" + +#, python-format +msgid "Search %s" +msgstr "Cerca %s" + +msgid "Wizard" +msgstr "Assistent" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "No s'ha pogut eliminar l'assistent %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Creat per" + +msgid "Created at" +msgstr "Creat el" + +msgid "Edited by" +msgstr "Editat per" + +msgid "Edited at" +msgstr "Editat el" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "No s'ha pogut obtenir l'estat de la vista d'arbre de %s" + +msgid "Unable to set view tree state" +msgstr "No s'ha pogut desar l'estat de la vista d'arbre" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" no és vàlid segons el seu domini." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" és obligatori." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Els valors de \"%s\" no són vàlids." + +msgid "Pre-validation" +msgstr "Prevalidació" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Mida de la imatge" + +msgid "Width:" +msgstr "Amplada:" + +msgid "Height:" +msgstr "Altura:" + +msgid "PNG image (*.png)" +msgstr "Imatge PNG (*.png)" + +msgid "Save As" +msgstr "Anomena i desa" + +msgid "Image size too large." +msgstr "La mida de la imatge és massa gran." + +msgid "Copy" +msgstr "Copia" + +msgid "Paste" +msgstr "Enganxa" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Obre filtres" + +msgid "Show bookmarks of filters" +msgstr "Mostra les cerques preferides" + +msgid "Remove this bookmark" +msgstr "Elimina de les cerques preferides" + +msgid "Bookmark this filter" +msgstr "Desa com a cerca preferida" + +msgid "Show active records" +msgstr "Mostra registres actius" + +msgid "Show inactive records" +msgstr "Mostra registres inactius" + +msgid "Bookmark Name:" +msgstr "Nom de la cerca preferida:" + +msgid "Find" +msgstr "Cerca" + +msgid "Today" +msgstr "Avui" + +msgid "go back" +msgstr "vés enrere" + +msgid "go forward" +msgstr "vés endavant" + +msgid "previous year" +msgstr "any anterior" + +msgid "next year" +msgstr "l'any següent" + +msgid "Day View" +msgstr "Vista de dia" + +msgid "Week View" +msgstr "Vista setmanal" + +msgid "Month View" +msgstr "Vista mensual" + +msgid "Week" +msgstr "Setmana" + +msgid "Select..." +msgstr "Selecciona..." + +msgid "Clear" +msgstr "Neteja" + +msgid "All files" +msgstr "Tots els fitxers" + +msgid "Show plain text" +msgstr "Mostra com a text pla" + +msgid "Add value" +msgstr "Afegeix un valor" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Eliminar \"%s\"" + +msgid "Images" +msgstr "Imatges" + +msgid "Add existing record" +msgstr "Afegeix un registre existent" + +msgid "Remove selected record" +msgstr "Elimina el registre seleccionat" + +msgid "Open the record " +msgstr "Obre el registre " + +msgid "Clear the field " +msgstr "Eliminar el registre " + +msgid "Search a record " +msgstr "Cerca un registre " + +msgid "Edit selected record" +msgstr "Edita registre seleccionat" + +msgid "Delete selected record" +msgstr "Elimina el registre seleccionat" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Escull un idioma" + +msgid "Translation" +msgstr "Traducció" + +msgid "Edit" +msgstr "Edita" + +msgid "Fuzzy" +msgstr "Dubtosa" + +msgid "You need to save the record before adding translations." +msgstr "Heu de desar el registre abans d'afegir traduccions." + +msgid "No other language available." +msgstr "No hi ha cap altre idioma disponible." + +msgid "Translate view" +msgstr "Tradueix la vista" diff --git a/tryton/tryton/data/locale/cs/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/cs/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..5e9369032f --- /dev/null +++ b/tryton/tryton/data/locale/cs/LC_MESSAGES/tryton.po @@ -0,0 +1,1266 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "zadejte alternativní konfigurační soubor" + +msgid "development mode" +msgstr "" + +msgid "logging everything at INFO level" +msgstr "" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +msgid "specify the login user" +msgstr "zadejte uživatelské jméno" + +#, fuzzy +msgid "specify the server hostname:port" +msgstr "zadejte název hostitele pro server" + +#, fuzzy, python-format +msgid "Unable to write config file %s." +msgstr "Nelze zapsat konfigurační soubor %s!" + +#, python-format +msgid "Unable to set locale %s" +msgstr "Nemohu nastavit lokalizaci %s" + +#, fuzzy +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "Vyberte akci" + +#, fuzzy +msgid "No action defined." +msgstr "Žádná akce není definována!" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "Výběr" + +#, fuzzy +msgid "Cancel" +msgstr "_Zrušit" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "Váš výběr:" + +msgid "Select" +msgstr "" + +#, fuzzy +msgid "Save" +msgstr "_Uložit" + +msgid "Save As..." +msgstr "Uložit jako..." + +msgid "Do you want to proceed?" +msgstr "" + +msgid "Always ignore this warning." +msgstr "zadejte alternativní konfigurační soubor" + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "Výjimka při souběhu" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "Porovnat" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "Přesto zapsat" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Porovnat: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "Chyba aplikace!" + +msgid "Report Bug" +msgstr "Nahlásit chybu" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +#, fuzzy +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +#, fuzzy +msgid "Value" +msgstr "Nepravda" + +msgid "Displayed value" +msgstr "" + +#, fuzzy +msgid "Format" +msgstr "_Normální" + +msgid "Display format" +msgstr "" + +#, fuzzy +msgid "Open the calendar" +msgstr "Otevřít kalendář" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "Pravda" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "Nepravda" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "_Vytvořit" + +#, fuzzy +msgid "Edit..." +msgstr "Konec..." + +#, fuzzy +msgid "Attachments..." +msgstr "Příloha:" + +msgid "Notes..." +msgstr "" + +#, fuzzy +msgid "Actions..." +msgstr "Akce..." + +#, fuzzy +msgid "Relate..." +msgstr "_Odstranit..." + +#, fuzzy +msgid "Report..." +msgstr "Importovat data..." + +#, fuzzy +msgid "Print..." +msgstr "_Tisk..." + +#, fuzzy +msgid "E-Mail..." +msgstr "_Email..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "M" +msgstr "" + +msgid "w" +msgstr "" + +msgid "d" +msgstr "" + +msgid "h" +msgstr "" + +msgid "m" +msgstr "" + +msgid "s" +msgstr "" + +#, fuzzy +msgid "Preferences..." +msgstr "_Vlastnosti..." + +#, fuzzy +msgid "Toolbar" +msgstr "Nástrojová lišta" + +#, fuzzy +msgid "Default" +msgstr "_Výchozí" + +#, fuzzy +msgid "Text and Icons" +msgstr "_Text a ikony" + +#, fuzzy +msgid "Text" +msgstr "_Text" + +#, fuzzy +msgid "Icons" +msgstr "_Ikony" + +#, fuzzy +msgid "Form" +msgstr "Formulář" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "Kontrola pravopisu" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "" + +#, fuzzy +msgid "Options" +msgstr "_Volby" + +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "_Klávesové zkratky..." + +#, fuzzy +msgid "About..." +msgstr "_O programu..." + +#, fuzzy +msgid "Help" +msgstr "Nápo_věda" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "Akce" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "Nastavení" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "_Předchozí" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Vyjmout vybraný text" + +msgid "Copy selected text" +msgstr "Kopírovat vybraný text" + +msgid "Paste copied text" +msgstr "Vložit vybraný text" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "Vytvořit novou relaci" + +msgid "Open/Search relation" +msgstr "Otevřít/prohledat relaci" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "Přepnout pohled" + +msgid "Create/Select new line" +msgstr "" + +#, fuzzy +msgid "Open relation" +msgstr "Otevřít/prohledat relaci" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "Zavřít záložku" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, fuzzy, python-format +msgid "Attachments (%s)" +msgstr "Příloha(%s)" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +#, fuzzy +msgid "Host:" +msgstr "Port:" + +msgid "Database:" +msgstr "Databáze:" + +#, fuzzy +msgid "Fetching databases list" +msgstr "Nastavení nové databáze:" + +#, fuzzy +msgid "Username:" +msgstr "Uživatelské jméno:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Verze serveru není kompatibilní!" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Nelze se spojit se serverem!" + +msgid "Login" +msgstr "Přihlášení" + +msgid "_Cancel" +msgstr "_Zrušit" + +msgid "Cancel connection to the Tryton server" +msgstr "Zrušit spojení k Tryton serveru." + +msgid "C_onnect" +msgstr "_Připojit" + +msgid "Connect the Tryton server" +msgstr "Připojit k Tryton serveru" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "Uživatelské jméno:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Nemohu nastavit lokalizaci %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Email %s" + +msgid "To:" +msgstr "Komu:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Předmět:" + +#, fuzzy +msgid "Body" +msgstr "Tělo:" + +#, fuzzy +msgid "Reports" +msgstr "Sestavy" + +#, fuzzy +msgid "Attachments" +msgstr "Příloha:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Výběr" + +#, fuzzy +msgid "Remove File" +msgstr "_Odstranit" + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "Předchozí" + +msgid "Previous" +msgstr "Předchozí" + +msgid "Next" +msgstr "Další" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Příloha(%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +#, fuzzy +msgid "You have to select one record." +msgstr "Musíte vybrat právě jeden záznam!" + +msgid "ID:" +msgstr "ID:" + +#, fuzzy +msgid "Created by:" +msgstr "Datum vytvoření:" + +#, fuzzy +msgid "Created at:" +msgstr "Datum vytvoření:" + +msgid "Edited by:" +msgstr "" + +#, fuzzy +msgid "Edited at:" +msgstr "Datum vytvoření:" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Opravdu chcete odstranit tento záznam?" + +msgid "Are you sure to remove those records?" +msgstr "Opravdu chcete odstranit tyto záznamy?" + +#, fuzzy +msgid "Records not removed." +msgstr "Záznamy neodstraněny!" + +#, fuzzy +msgid "Records removed." +msgstr "Záznamy odstraněny!" + +#, fuzzy +msgid "Working now on the duplicated record(s)." +msgstr "Nyní pracujete s duplicitním záznamem!" + +#, fuzzy +msgid "Record saved." +msgstr "Záznam uložen!" + +msgid " of " +msgstr " z " + +#, fuzzy +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Tento záznam byl upraven\n" +"chcete ho uložit?" + +msgid "Launch action" +msgstr "" + +#, fuzzy +msgid "Relate" +msgstr "_Vytvořit" + +#, fuzzy +msgid "Open related records" +msgstr "Otevřít záznam" + +#, fuzzy +msgid "Report" +msgstr "Sestavy" + +#, fuzzy +msgid "Open report" +msgstr "Otevřít záznam" + +msgid "Print" +msgstr "Tisk" + +msgid "Print report" +msgstr "" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "Neznámý" + +#, fuzzy +msgid "Limit" +msgstr "Omezit:" + +msgid "Search Limit Settings" +msgstr "" + +#, fuzzy +msgid "Limit:" +msgstr "Omezit:" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "Upravit uživatelské nastavení" + +msgid "Preference" +msgstr "Nastavení" + +#, fuzzy +msgid "Revision" +msgstr "Předchozí" + +#, fuzzy +msgid "Select a revision" +msgstr "Výběr" + +#, fuzzy +msgid "Revision:" +msgstr "Předchozí" + +msgid "_Switch View" +msgstr "Přepnout pohled" + +#, fuzzy +msgid "Switch View" +msgstr "Přepnout pohled" + +msgid "_Previous" +msgstr "_Předchozí" + +msgid "Previous Record" +msgstr "Předchozí záznam" + +msgid "_Next" +msgstr "_Následující" + +msgid "Next Record" +msgstr "Následující záznam" + +#, fuzzy +msgid "_Search" +msgstr "Hledat" + +msgid "_New" +msgstr "_Nový" + +msgid "Create a new record" +msgstr "Vytvořit nový záznam" + +msgid "_Save" +msgstr "_Uložit" + +msgid "Save this record" +msgstr "Uložit tento záznam" + +msgid "_Reload/Undo" +msgstr "Obnovit/Zpět" + +#, fuzzy +msgid "Reload/Undo" +msgstr "Obnovit/Zpět" + +msgid "_Duplicate" +msgstr "_Duplikovat" + +msgid "_Delete..." +msgstr "_Odstranit..." + +msgid "View _Logs..." +msgstr "Zobrazit záznamy..." + +msgid "Show revisions..." +msgstr "" + +#, fuzzy +msgid "A_ttachments..." +msgstr "Příloha:" + +msgid "Add an attachment to the record" +msgstr "Přidat přílohu k záznamu" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "Akce..." + +#, fuzzy +msgid "_Relate..." +msgstr "_Odstranit..." + +#, fuzzy +msgid "_Report..." +msgstr "Importovat data..." + +msgid "_Print..." +msgstr "_Tisk..." + +#, fuzzy +msgid "_E-Mail..." +msgstr "_Email..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Uložit tento záznam" + +msgid "_Export Data..." +msgstr "Exportovat data" + +msgid "_Import Data..." +msgstr "Importovat data..." + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "_Zavřít kartu" + +msgid "All fields" +msgstr "Všechna pole" + +msgid "_Add" +msgstr "_Přidat" + +msgid "_Remove" +msgstr "_Odstranit" + +#, fuzzy +msgid "_Clear" +msgstr "Vymazat" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "Parametry CSV" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "Kódování:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "Jméno pole" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +#, fuzzy +msgid "_Save Export" +msgstr "Uložit exportu" + +#, fuzzy +msgid "_URL Export" +msgstr "Uložit exportu" + +#, fuzzy +msgid "_Delete Export" +msgstr "Smazat exportu" + +msgid "Predefined exports" +msgstr "Předdefinované exporty" + +msgid "Name" +msgstr "" + +msgid "Open" +msgstr "Otevřít" + +#, fuzzy +msgid "Listed Records" +msgstr "Upravit vybraný záznam" + +#, fuzzy +msgid "Selected Records" +msgstr "Upravit vybraný záznam" + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Přidat jména polí" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, fuzzy, python-format +msgid "%d record saved." +msgstr "%d záznam uložen!" + +#, fuzzy, python-format +msgid "%d records saved." +msgstr "%d záznamů uloženo!" + +#, fuzzy, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operace selhala.\n" +"Chybová zpráva:\n" +"%s" + +msgid "Error" +msgstr "Chyba" + +msgid "Link" +msgstr "Odkaz" + +msgid "Delete" +msgstr "Smazat" + +msgid "New" +msgstr "_Nový" + +msgid "Switch" +msgstr "Přepnout" + +msgid "Add" +msgstr "Přidat" + +msgid "Remove " +msgstr "" + +#, fuzzy +msgid "Create a new record " +msgstr "Vytvořit nový záznam" + +#, fuzzy +msgid "Delete selected record " +msgstr "Smazat vybraný záznam" + +#, fuzzy +msgid "Undelete selected record " +msgstr "Smazat vybraný záznam" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +#, fuzzy +msgid "_Auto-Detect" +msgstr "Automatická detekce" + +msgid "File to Import:" +msgstr "Soubor k importu:" + +msgid "Open..." +msgstr "Otevřít..." + +msgid "Lines to Skip:" +msgstr "Vynechané řádky:" + +#, fuzzy +msgid "You must select an import file first." +msgstr "Nejprve je nutné vybrat soubor pro import!" + +msgid "Error opening CSV file" +msgstr "Chyba při otevírání CSV souboru" + +#, fuzzy, python-format +msgid "Error processing the file at field %s." +msgstr "Chyba při zpracování souboru u pole %s.\n" + +#, fuzzy, python-format +msgid "%d record imported." +msgstr "%d záznam načten!" + +#, fuzzy, python-format +msgid "%d records imported." +msgstr "%d záznamů načteno!" + +msgid "Search" +msgstr "Hledat" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "Průvodce" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +#, fuzzy +msgid "ID" +msgstr "Najít" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Datum vytvoření:" + +msgid "Edited by" +msgstr "" + +msgid "Edited at" +msgstr "" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "Nemohu nastavit lokalizaci %s" + +#, fuzzy +msgid "Unable to set view tree state" +msgstr "Nemohu nastavit lokalizaci %s" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Velikost obrázku" + +msgid "Width:" +msgstr "Šířka:" + +msgid "Height:" +msgstr "Výška:" + +msgid "PNG image (*.png)" +msgstr "Obrázek PNG (*.png)" + +msgid "Save As" +msgstr "Uložit jako" + +#, fuzzy +msgid "Image size too large." +msgstr "Rozměry obrázku jsou příliš velké!" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr "" + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "Najít" + +#, fuzzy +msgid "Today" +msgstr "Tělo:" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +#, fuzzy +msgid "Week View" +msgstr "Přepnout pohled" + +#, fuzzy +msgid "Month View" +msgstr "Přepnout pohled" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "Vymazat" + +msgid "All files" +msgstr "Všechny soubory" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "Obrázky" + +#, fuzzy +msgid "Add existing record" +msgstr "Uložit tento záznam" + +#, fuzzy +msgid "Remove selected record" +msgstr "Smazat vybraný záznam" + +#, fuzzy +msgid "Open the record " +msgstr "Otevřít záznam" + +msgid "Clear the field " +msgstr "" + +#, fuzzy +msgid "Search a record " +msgstr "Hledat záznam" + +#, fuzzy +msgid "Edit selected record" +msgstr "Upravit vybraný záznam" + +#, fuzzy +msgid "Delete selected record" +msgstr "Smazat vybraný záznam" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +#, fuzzy +msgid "Translation" +msgstr "Přidat překlad" + +msgid "Edit" +msgstr "" + +msgid "Fuzzy" +msgstr "" + +#, fuzzy +msgid "You need to save the record before adding translations." +msgstr "Musíte uložit záznam před přidáváním překladů!" + +#, fuzzy +msgid "No other language available." +msgstr "Jiný jazyk není k dispozici!" + +msgid "Translate view" +msgstr "Přeložit pohled" diff --git a/tryton/tryton/data/locale/de/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/de/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..23839d2e19 --- /dev/null +++ b/tryton/tryton/data/locale/de/LC_MESSAGES/tryton.po @@ -0,0 +1,1180 @@ +# German (Germany) translations for tryton. +# Copyright (C) 2008-2014 MBSolutions +# This file is distributed under the same license as the tryton project. +# Udo Spallek , 2008. +# Mathias Behrle , 2008-2014. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "Alternative Konfigurationsdatei angeben" + +msgid "development mode" +msgstr "Entwicklungsmodus" + +msgid "logging everything at INFO level" +msgstr "Allgemeiner Log-Level: INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "Log-Level angeben: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "Anmeldename angeben" + +msgid "specify the server hostname:port" +msgstr "Den Server mit Hostname:Port angeben." + +#, python-format +msgid "Unable to write config file %s." +msgstr "Kann Konfigurationsdatei '%s' nicht schreiben." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Kann locale %s nicht setzen." + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Aktion wählen" + +msgid "No action defined." +msgstr "Keine Aktion definiert." + +msgid "By: " +msgstr "Von: " + +msgid "Selection" +msgstr "Auswahl" + +msgid "Cancel" +msgstr "Abbrechen" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Ihre Auswahl:" + +msgid "Select" +msgstr "Auswählen" + +msgid "Save" +msgstr "Speichern" + +msgid "Save As..." +msgstr "Speichern unter..." + +msgid "Do you want to proceed?" +msgstr "Fortfahren?" + +msgid "Always ignore this warning." +msgstr "Diese Warnung künftig nicht mehr anzeigen." + +msgid "No" +msgstr "Nein" + +msgid "Yes" +msgstr "Ja" + +msgid "Concurrency Exception" +msgstr "Aktualisierungskonflikt" + +msgid "This record has been modified while you were editing it." +msgstr "" +"Dieser Datensatz wurde anderweitig geändert, während Sie ihn bearbeitet " +"haben." + +msgid "Cancel saving" +msgstr "Speichern abbrechen" + +msgid "Compare" +msgstr "Vergleichen" + +msgid "See the modified version" +msgstr "Die geänderte Version ansehen" + +msgid "Write Anyway" +msgstr "Überschreiben" + +msgid "Save your current version" +msgstr "Ihre aktuelle Version speichern" + +#, python-format +msgid "Compare: %s" +msgstr "Vergleichen: %s" + +msgid "Close" +msgstr "Schließen" + +msgid "Application Error" +msgstr "Anwendungsfehler" + +msgid "Report Bug" +msgstr "Fehler melden" + +#, python-format +msgid "Check URL: %s" +msgstr "Überprüfung der URL: %s" + +msgid "Unable to check for new version." +msgstr "Es war nicht möglich nach einer neuen Version zu suchen." + +msgid "A new version is available!" +msgstr "Eine neue Version ist verfügbar!" + +msgid "Download" +msgstr "Herunterladen" + +msgid "Too many requests. Try again later." +msgstr "Zu viele Anfragen. Bitte versuchen Sie es später erneut." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Suche..." + +msgid "Create..." +msgstr "Erstelle..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Unmöglich nach einer Vervollständigung von %s zu suchen" + +msgid "Value" +msgstr "Wert" + +msgid "Displayed value" +msgstr "Angezeigter Wert" + +msgid "Format" +msgstr "Format" + +msgid "Display format" +msgstr "Angezeigtes Format" + +msgid "Open the calendar" +msgstr "Kalender öffnen" + +msgid "Date Format" +msgstr "Datumsformat" + +msgid "Displayed date format" +msgstr "Angezeigtes Datumsformat" + +msgid "y" +msgstr "J" + +msgid "True" +msgstr "Wahr" + +msgid "t" +msgstr "w" + +msgid "False" +msgstr "Falsch" + +msgid "Digits" +msgstr "Nachkommastellen" + +msgid "The number of decimal" +msgstr "Anzahl Dezimalstellen" + +msgid "Template" +msgstr "Vorlage" + +msgid "Edit..." +msgstr "Bearbeiten..." + +msgid "Attachments..." +msgstr "Anhänge..." + +msgid "Notes..." +msgstr "Notizen..." + +msgid "Actions..." +msgstr "Aktionen..." + +msgid "Relate..." +msgstr "Beziehungen..." + +msgid "Report..." +msgstr "Berichte..." + +msgid "Print..." +msgstr "Drucken..." + +msgid "E-Mail..." +msgstr "E-Mail..." + +msgid "Bold" +msgstr "Fett" + +msgid "Italic" +msgstr "Kursiv" + +msgid "Underline" +msgstr "Unterstrichen" + +msgid "Align Left" +msgstr "Ausrichtung links" + +msgid "Align Center" +msgstr "Ausrichtung mittig" + +msgid "Align Right" +msgstr "Ausrichtung rechts" + +msgid "Justify" +msgstr "Blocksatz" + +msgid "Foreground Color" +msgstr "Vordergrundfarbe" + +msgid "Select a color" +msgstr "Eine Farbe auswählen" + +msgid "Y" +msgstr "J" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "W" + +msgid "d" +msgstr "t" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Einstellungen..." + +msgid "Toolbar" +msgstr "Werkzeugleiste" + +msgid "Default" +msgstr "Standard" + +msgid "Text and Icons" +msgstr "Text und Symbole" + +msgid "Text" +msgstr "Text" + +msgid "Icons" +msgstr "Symbole" + +msgid "Form" +msgstr "Formular" + +msgid "Save Column Width" +msgstr "Spaltenbreiten speichern" + +msgid "Save Tree State" +msgstr "Menüansicht speichern" + +msgid "Spell Checking" +msgstr "Rechtschreibkorrektur" + +msgid "PDA Mode" +msgstr "Einspaltiger Modus" + +msgid "Search Limit..." +msgstr "Suchlimitierung..." + +msgid "Check Version" +msgstr "Nach neuer Version suchen" + +msgid "Options" +msgstr "Einstellungen" + +msgid "Keyboard Shortcuts..." +msgstr "Tastenkombinationen..." + +msgid "About..." +msgstr "Über..." + +msgid "Help" +msgstr "Hilfe" + +msgid "No result found." +msgstr "Kein Ergebnis." + +msgid "Favorites" +msgstr "Favoriten" + +msgid "Manage..." +msgstr "Verwalten..." + +msgid "Action" +msgstr "Aktion" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Die folgende Aktion erfordert die Schließung aller Registerkarten.\n" +"Fortfahren?" + +msgid "Application Shortcuts" +msgstr "Tastenkombinationen Anwendung" + +msgid "Global" +msgstr "Global" + +msgid "Preferences" +msgstr "Einstellungen" + +msgid "Search menu" +msgstr "Menü durchsuchen" + +msgid "Toggle menu" +msgstr "Menü ein-/ausblenden" + +msgid "Previous tab" +msgstr "Vorherige Registerkarte" + +msgid "Next tab" +msgstr "Nächste Registerkarte" + +msgid "Shortcuts" +msgstr "Tastenkombinationen" + +msgid "Quit" +msgstr "Beenden" + +msgid "Edition Shortcuts" +msgstr "Tastenkombinationen Bearbeitung" + +msgid "Text Entries" +msgstr "Textfelder" + +msgid "Cut selected text" +msgstr "Ausgewählten Text ausschneiden" + +msgid "Copy selected text" +msgstr "Ausgewählten Text kopieren" + +msgid "Paste copied text" +msgstr "Ausgewählten Text einfügen" + +msgid "Next entry" +msgstr "Nächstes Feld" + +msgid "Previous entry" +msgstr "Vorheriges Feld" + +msgid "Relation Entries" +msgstr "Beziehungsfelder" + +msgid "Create new relation" +msgstr "Neue Beziehung erstellen" + +msgid "Open/Search relation" +msgstr "Beziehung suchen/öffnen" + +msgid "List Entries" +msgstr "Listenfelder" + +msgid "Switch view" +msgstr "Ansicht wechseln" + +msgid "Create/Select new line" +msgstr "Neue Zeile erstellen/auswählen" + +msgid "Open relation" +msgstr "Beziehung öffnen" + +msgid "Mark line for deletion/removal" +msgstr "Zeile zum Löschen/Entfernen markieren" + +msgid "Mark line for removal" +msgstr "Zeile zum Entfernen markieren" + +msgid "Unmark line for deletion" +msgstr "Löschmarkierung für Zeile aufheben" + +msgid "List/Tree Shortcuts" +msgstr "Tastenkombinationen Listen-/Baumansicht" + +msgid "Move Cursor" +msgstr "Cursor bewegen" + +msgid "Move right" +msgstr "Nach rechts bewegen" + +msgid "Move left" +msgstr "Nach links bewegen" + +msgid "Move up" +msgstr "Nach oben bewegen" + +msgid "Move down" +msgstr "Nach unten bewegen" + +msgid "Move up of one page" +msgstr "Eine Seite nach oben bewegen" + +msgid "Move down of one page" +msgstr "Eine Seite nach unten bewegen" + +msgid "Move to top" +msgstr "An den Anfang bewegen" + +msgid "Move to bottom" +msgstr "Zum Ende bewegen" + +msgid "Move to parent" +msgstr "Zum übergeordneten Datensatz bewegen" + +msgid "Select all" +msgstr "Alle auswählen" + +msgid "Unselect all" +msgstr "Auswahl aufheben" + +msgid "Select parent" +msgstr "Übergeordneten Datensatz auswählen" + +msgid "Select/Activate current row" +msgstr "Aktuelle Zeile auswählen/aktivieren" + +msgid "Toggle selection" +msgstr "Auswahl umkehren" + +msgid "Expand/Collapse" +msgstr "Ausklappen/Einklappen" + +msgid "Expand row" +msgstr "Zeile ausklappen" + +msgid "Collapse row" +msgstr "Zeile einklappen" + +msgid "Toggle row" +msgstr "Auswahl für Zeile umkehren" + +msgid "Collapse all rows" +msgstr "Alle Zeilen einklappen" + +msgid "Expand all rows" +msgstr "Alle Zeilen ausklappen" + +msgid "Close Tab" +msgstr "Registerkarte schließen" + +msgid "modularity, scalability and security" +msgstr "Modularität, Skalierbarkeit und Sicherheit" + +msgid "translator-credits" +msgstr "" +"Mathias Behrle\n" +"Clemens Hupka\n" +"Korbinian Preisler\n" +"Udo Spallek" + +#, python-format +msgid "Attachments (%s)" +msgstr "Anhänge (%s)" + +msgid "Profile Editor" +msgstr "Profilbearbeitung" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "Neues Profil hinzufügen" + +msgid "Remove selected profile" +msgstr "Ausgewähltes Profil löschen" + +msgid "Host:" +msgstr "Host:" + +msgid "Database:" +msgstr "Datenbank:" + +msgid "Fetching databases list" +msgstr "Datenbankliste wird abgefragt..." + +msgid "Username:" +msgstr "Anmeldename:" + +msgid "Incompatible version of the server." +msgstr "Inkompatible Serverversion." + +msgid "Could not connect to the server." +msgstr "Verbindung zum Server nicht möglich." + +msgid "Login" +msgstr "Log-in" + +msgid "_Cancel" +msgstr "_Abbrechen" + +msgid "Cancel connection to the Tryton server" +msgstr "Abbrechen" + +msgid "C_onnect" +msgstr "_Verbinden" + +msgid "Connect the Tryton server" +msgstr "Verbindung zum Tryton Server herstellen" + +msgid "Profile:" +msgstr "Profil:" + +msgid "Host / Database information" +msgstr "Host-/Datenbankdetails" + +msgid "User name:" +msgstr "Anmeldename:" + +msgid "Unable to complete email entry" +msgstr "" +"Die automatische Vervollständigung des E-Mail Eintrags ist fehlgeschlagen" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "An:" + +msgid "Cc:" +msgstr "Cc:" + +msgid "Bcc:" +msgstr "Bcc:" + +msgid "Subject:" +msgstr "Betreff:" + +msgid "Body" +msgstr "Textkörper" + +msgid "Reports" +msgstr "Berichte" + +msgid "Attachments" +msgstr "Anhänge" + +msgid "Files" +msgstr "Dateien" + +msgid "Send" +msgstr "Senden" + +msgid "Select File" +msgstr "Datei auswählen" + +msgid "Remove File" +msgstr "Datei entfernen" + +msgid "Add..." +msgstr "Hinzufügen..." + +msgid "Preview" +msgstr "Vorschau" + +msgid "Previous" +msgstr "Vorheriger" + +msgid "Next" +msgstr "Nächster" + +#, python-format +msgid "Attachment (%s)" +msgstr "Anhang (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Notiz (%d/%d)" + +msgid "You have to select one record." +msgstr "Ein Datensatz muss ausgewählt werden." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Erstellt von:" + +msgid "Created at:" +msgstr "Erstellt am:" + +msgid "Edited by:" +msgstr "Bearbeitet von:" + +msgid "Edited at:" +msgstr "Zuletzt bearbeitet am:" + +msgid "Model:" +msgstr "Modell:" + +msgid "Are you sure to remove this record?" +msgstr "Möchten Sie diesen Datensatz wirklich löschen?" + +msgid "Are you sure to remove those records?" +msgstr "Möchten Sie diese Datensätze wirklich löschen?" + +msgid "Records not removed." +msgstr "Die Datensätze wurden nicht gelöscht." + +msgid "Records removed." +msgstr "Die Datensätze wurden gelöscht." + +msgid "Working now on the duplicated record(s)." +msgstr "Sie arbeiten nun an dem/den duplizierten Datensatz/Datensätzen." + +msgid "Record saved." +msgstr "Der Datensatz wurde gespeichert." + +msgid " of " +msgstr " von " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Datensatz geändert.\n" +"Soll der Datensatz gespeichert werden?" + +msgid "Launch action" +msgstr "Aktion ausführen" + +msgid "Relate" +msgstr "Beziehung" + +msgid "Open related records" +msgstr "Datensätze einer Beziehung öffnen" + +msgid "Report" +msgstr "Bericht" + +msgid "Open report" +msgstr "Bericht öffnen" + +msgid "Print" +msgstr "Drucken" + +msgid "Print report" +msgstr "Bericht drucken" + +msgid "_Copy URL" +msgstr "URL kopieren" + +msgid "Copy URL into clipboard" +msgstr "URL in Zwischenablage kopieren" + +msgid "Unknown" +msgstr "Unbekannt" + +msgid "Limit" +msgstr "Limit" + +msgid "Search Limit Settings" +msgstr "Einstellung Suchlimit" + +msgid "Limit:" +msgstr "Limit:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notizen (%s)" + +msgid "Edit User Preferences" +msgstr "Benutzereinstellungen bearbeiten" + +msgid "Preference" +msgstr "Einstellung" + +msgid "Revision" +msgstr "Bearbeitung" + +msgid "Select a revision" +msgstr "Bearbeitung auswählen" + +msgid "Revision:" +msgstr "Bearbeitung:" + +msgid "_Switch View" +msgstr "_Ansicht wechseln" + +msgid "Switch View" +msgstr "Ansicht wechseln" + +msgid "_Previous" +msgstr "_Vorheriger" + +msgid "Previous Record" +msgstr "Vorheriger Datensatz" + +msgid "_Next" +msgstr "_Nächster" + +msgid "Next Record" +msgstr "Nächster Datensatz" + +msgid "_Search" +msgstr "_Suche" + +msgid "_New" +msgstr "_Neu" + +msgid "Create a new record" +msgstr "Neuen Datensatz erstellen" + +msgid "_Save" +msgstr "_Speichern" + +msgid "Save this record" +msgstr "Diesen Datensatz speichern" + +msgid "_Reload/Undo" +msgstr "Neu laden/_Rückgängig" + +msgid "Reload/Undo" +msgstr "Neu laden/Rückgängig" + +msgid "_Duplicate" +msgstr "_Duplizieren" + +msgid "_Delete..." +msgstr "_Löschen..." + +msgid "View _Logs..." +msgstr "_Protokoll ansehen..." + +msgid "Show revisions..." +msgstr "Bearbeitungen anzeigen..." + +msgid "A_ttachments..." +msgstr "A_nhänge..." + +msgid "Add an attachment to the record" +msgstr "Einen Anhang zum Datensatz hinzufügen" + +msgid "_Notes..." +msgstr "_Notizen..." + +msgid "Add a note to the record" +msgstr "Eine Notiz zu einem Datensatz hinzufügen" + +msgid "_Actions..." +msgstr "_Aktion ausführen..." + +msgid "_Relate..." +msgstr "_Beziehung öffnen..." + +msgid "_Report..." +msgstr "_Bericht öffnen..." + +msgid "_Print..." +msgstr "_Bericht drucken..." + +msgid "_E-Mail..." +msgstr "_Bericht per E-Mail..." + +msgid "Send an e-mail using the record" +msgstr "Eine E-Mail basierend auf diesem Datensatz versenden" + +msgid "_Export Data..." +msgstr "Daten _exportieren..." + +msgid "_Import Data..." +msgstr "Daten _importieren..." + +msgid "Copy _URL..." +msgstr "_URL kopieren..." + +msgid "_Close Tab" +msgstr "Registerkarte schließen" + +msgid "All fields" +msgstr "Alle Felder" + +msgid "_Add" +msgstr "_Hinzufügen" + +msgid "_Remove" +msgstr "_Entfernen" + +msgid "_Clear" +msgstr "_Leeren" + +msgid "Fields selected" +msgstr "Ausgewählte Felder" + +msgid "CSV Parameters" +msgstr "CSV-Parameter" + +msgid "Delimiter:" +msgstr "Feldtrenner:" + +msgid "Quote char:" +msgstr "Anführungszeichen:" + +msgid "Encoding:" +msgstr "Zeichenkodierung:" + +msgid "Use locale format" +msgstr "Lokales Format verwenden" + +msgid "Field name" +msgstr "Feldname" + +#, python-format +msgid "CSV Export: %s" +msgstr "CSV-Export: %s" + +msgid "_Save Export" +msgstr "Exportkonfiguration _speichern" + +msgid "_URL Export" +msgstr "_URL-Export" + +msgid "_Delete Export" +msgstr "Exportkonfiguration _löschen" + +msgid "Predefined exports" +msgstr "Vordefinierte Exporte" + +msgid "Name" +msgstr "Name" + +msgid "Open" +msgstr "Öffnen" + +msgid "Listed Records" +msgstr "Angezeigte Datensätze" + +msgid "Selected Records" +msgstr "Ausgewählte Datensätze" + +msgid "Ignore search limit" +msgstr "Suchlimit ignorieren" + +msgid "Add field names" +msgstr "Feldnamen hinzufügen" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (Bezeichnung des Modells)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (Bezeichnung des Datensatzes)" + +msgid "What is the name of this export?" +msgstr "Wie soll der Name des Exports lauten?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Definition von '%s' nicht berücksichtigen?" + +#, python-format +msgid "%d record saved." +msgstr "%d Datensatz gespeichert." + +#, python-format +msgid "%d records saved." +msgstr "%d Datensätze gespeichert." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operation fehlgeschlagen.\n" +"Fehlermeldung:\n" +"%s" + +msgid "Error" +msgstr "Fehler" + +msgid "Link" +msgstr "Verknüpfung" + +msgid "Delete" +msgstr "Löschen" + +msgid "New" +msgstr "Neu" + +msgid "Switch" +msgstr "Ansicht wechseln" + +msgid "Add" +msgstr "Hinzufügen" + +msgid "Remove " +msgstr "Entfernen " + +msgid "Create a new record " +msgstr "Neuen Datensatz erstellen " + +msgid "Delete selected record " +msgstr "Ausgewählte Datensätze löschen " + +msgid "Undelete selected record " +msgstr "Löschung der ausgewählten Datensätze rückgängig machen " + +#, python-format +msgid "CSV Import: %s" +msgstr "CSV-Import: %s" + +msgid "_Auto-Detect" +msgstr "_Automatische Erkennung" + +msgid "File to Import:" +msgstr "Importdatei:" + +msgid "Open..." +msgstr "Öffnen..." + +msgid "Lines to Skip:" +msgstr "Zu überspringende Zeilen:" + +msgid "You must select an import file first." +msgstr "Es muss zuerst eine Importdatei ausgewählt werden." + +msgid "Error opening CSV file" +msgstr "Fehler beim Öffnen der CSV-Datei" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Fehler bei der Verarbeitung der Datei in Feld %s." + +#, python-format +msgid "%d record imported." +msgstr "%d Datensatz importiert." + +#, python-format +msgid "%d records imported." +msgstr "%d Datensätze importiert." + +msgid "Search" +msgstr "Suchen" + +#, python-format +msgid "Search %s" +msgstr "Suche %s" + +msgid "Wizard" +msgstr "Assistent" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Der Assistent %s kann nicht gelöscht werden." + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Erstellt von" + +msgid "Created at" +msgstr "Erstellt am" + +msgid "Edited by" +msgstr "Bearbeitet von" + +msgid "Edited at" +msgstr "Bearbeitet um" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Unmöglich den Status für die Baumansicht von %s zu holen." + +msgid "Unable to set view tree state" +msgstr "Kann Sicht für den Menübaum nicht anwenden." + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" liegt nicht im gültigen Wertebereich (Domain)." + +#, python-format +msgid "\"%s\" is required." +msgstr "In Feld \"%s\" ist ein Eintrag erforderlich." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Die Werte von \"%s\" sind ungültig." + +msgid "Pre-validation" +msgstr "Vorvalidierung" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Bildgröße" + +msgid "Width:" +msgstr "Breite:" + +msgid "Height:" +msgstr "Höhe:" + +msgid "PNG image (*.png)" +msgstr "PNG-Bild (*.png)" + +msgid "Save As" +msgstr "Speichern unter…" + +msgid "Image size too large." +msgstr "Bild ist zu groß." + +msgid "Copy" +msgstr "Kopieren" + +msgid "Paste" +msgstr "Einfügen" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Filter öffnen" + +msgid "Show bookmarks of filters" +msgstr "Filterlesezeichen anzeigen" + +msgid "Remove this bookmark" +msgstr "Dieses Lesezeichen entfernen" + +msgid "Bookmark this filter" +msgstr "Diesen Filter als Lesezeichen hinzufügen" + +msgid "Show active records" +msgstr "Aktive Datensätze anzeigen" + +msgid "Show inactive records" +msgstr "Inaktive Datensätze anzeigen" + +msgid "Bookmark Name:" +msgstr "Lesezeichenname:" + +msgid "Find" +msgstr "Suchen" + +msgid "Today" +msgstr "Heute" + +msgid "go back" +msgstr "Rückwärts bewegen" + +msgid "go forward" +msgstr "Vorwärts bewegen" + +msgid "previous year" +msgstr "Vorheriges Jahr" + +msgid "next year" +msgstr "Nächstes Jahr" + +msgid "Day View" +msgstr "Tagesansicht" + +msgid "Week View" +msgstr "Wochenansicht" + +msgid "Month View" +msgstr "Monatsansicht" + +msgid "Week" +msgstr "Woche" + +msgid "Select..." +msgstr "Auswählen..." + +msgid "Clear" +msgstr "Leeren" + +msgid "All files" +msgstr "Alle Dateien" + +msgid "Show plain text" +msgstr "Klartext anzeigen" + +msgid "Add value" +msgstr "Wert hinzufügen" + +#, python-format +msgid "Remove \"%s\"" +msgstr "\"%s\" entfernen" + +msgid "Images" +msgstr "Bilder" + +msgid "Add existing record" +msgstr "Bestehenden Datensatz hinzufügen" + +msgid "Remove selected record" +msgstr "Ausgewählten Datensatz löschen " + +msgid "Open the record " +msgstr "Datensatz öffnen " + +msgid "Clear the field " +msgstr "Feld löschen " + +msgid "Search a record " +msgstr "Datensatz suchen " + +msgid "Edit selected record" +msgstr "Ausgewählten Datensatz bearbeiten" + +msgid "Delete selected record" +msgstr "Ausgewählten Datensatz löschen" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Sprache auswählen" + +msgid "Translation" +msgstr "Übersetzung" + +msgid "Edit" +msgstr "Bearbeiten" + +msgid "Fuzzy" +msgstr "Fuzzy" + +msgid "You need to save the record before adding translations." +msgstr "" +"Der Datensatz muss gespeichert werden, bevor eine Übersetzung hinzugefügt " +"werden kann." + +msgid "No other language available." +msgstr "Keine weitere Sprache verfügbar." + +msgid "Translate view" +msgstr "Aktuelle Sicht übersetzen" diff --git a/tryton/tryton/data/locale/es/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/es/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..b7cd68e4e3 --- /dev/null +++ b/tryton/tryton/data/locale/es/LC_MESSAGES/tryton.po @@ -0,0 +1,1175 @@ +# Spanish (Spain) translations for tryton. +# Copyright (C) 2008 igor@tamarapatino.org +# Copyright (C) 2009 PEMAS Servicios Profesionales, S.L. +# Copyright (C) 2012 Zikzakmedia SL +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2008. +# Carlos Perelló Marín , 2008-2009. +# Sergi Almacellas Abellana 2012. +# Jordi Esteve Cusiné 2012. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "Indica un archivo de configuración alternativo" + +msgid "development mode" +msgstr "Modo desarrollo" + +msgid "logging everything at INFO level" +msgstr "Registra toda la información con nivel INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "Indica el nivel de log: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "Indica el usuario" + +msgid "specify the server hostname:port" +msgstr "Indica el nombre:puerto del servidor" + +#, python-format +msgid "Unable to write config file %s." +msgstr "No se ha podido escribir el archivo de configuración %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "No se ha podido establecer el idioma %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Seleccione su acción" + +msgid "No action defined." +msgstr "No se ha definido ninguna acción." + +msgid "By: " +msgstr "Por: " + +msgid "Selection" +msgstr "Selección" + +msgid "Cancel" +msgstr "Cancelar" + +msgid "OK" +msgstr "Aceptar" + +msgid "Your selection:" +msgstr "Su selección:" + +msgid "Select" +msgstr "Seleccionar" + +msgid "Save" +msgstr "Guardar" + +msgid "Save As..." +msgstr "Guardar como..." + +msgid "Do you want to proceed?" +msgstr "¿Desea continuar?" + +msgid "Always ignore this warning." +msgstr "Ignorar siempre esta advertencia." + +msgid "No" +msgstr "No" + +msgid "Yes" +msgstr "Sí" + +msgid "Concurrency Exception" +msgstr "Excepción de concurrencia" + +msgid "This record has been modified while you were editing it." +msgstr "Este registro ha sido modificado mientras lo editaba." + +msgid "Cancel saving" +msgstr "No guardes los cambios" + +msgid "Compare" +msgstr "Comparar" + +msgid "See the modified version" +msgstr "Mostrar la versión modificada" + +msgid "Write Anyway" +msgstr "Guardar de todas formas" + +msgid "Save your current version" +msgstr "Guardar vuestros cambios" + +#, python-format +msgid "Compare: %s" +msgstr "Comparar: %s" + +msgid "Close" +msgstr "Cerrar" + +msgid "Application Error" +msgstr "Error de aplicación" + +msgid "Report Bug" +msgstr "Informar del error" + +#, python-format +msgid "Check URL: %s" +msgstr "URL de comprobación: %s" + +msgid "Unable to check for new version." +msgstr "No se ha podido comprobar si existe una nueva versión." + +msgid "A new version is available!" +msgstr "Esta disponible una nueva versión!" + +msgid "Download" +msgstr "Descargar" + +msgid "Too many requests. Try again later." +msgstr "Demasiadas peticiones. Inténtelo de nuevo más tarde." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Buscar..." + +msgid "Create..." +msgstr "Crear..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "No se ha podido buscar el autocompletado de %s" + +msgid "Value" +msgstr "Valor" + +msgid "Displayed value" +msgstr "Valor a mostrar" + +msgid "Format" +msgstr "Formato" + +msgid "Display format" +msgstr "Format a mostrar" + +msgid "Open the calendar" +msgstr "Abrir el calendario" + +msgid "Date Format" +msgstr "Formato fecha" + +msgid "Displayed date format" +msgstr "Formato fecha actual" + +msgid "y" +msgstr "a" + +msgid "True" +msgstr "Verdadero" + +msgid "t" +msgstr "v" + +msgid "False" +msgstr "Falso" + +msgid "Digits" +msgstr "Dígitos" + +msgid "The number of decimal" +msgstr "El numero de decimales" + +msgid "Template" +msgstr "Plantilla" + +msgid "Edit..." +msgstr "Editar..." + +msgid "Attachments..." +msgstr "Adjuntos..." + +msgid "Notes..." +msgstr "Notas..." + +msgid "Actions..." +msgstr "Acciones..." + +msgid "Relate..." +msgstr "Relacionado..." + +msgid "Report..." +msgstr "Informe..." + +msgid "Print..." +msgstr "Imprimir..." + +msgid "E-Mail..." +msgstr "Correo electrónico..." + +msgid "Bold" +msgstr "Negrita" + +msgid "Italic" +msgstr "Cursiva" + +msgid "Underline" +msgstr "Subrayado" + +msgid "Align Left" +msgstr "Alinear a la izquierda" + +msgid "Align Center" +msgstr "Alinear en el centro" + +msgid "Align Right" +msgstr "Alinear a la derecha" + +msgid "Justify" +msgstr "Justificar" + +msgid "Foreground Color" +msgstr "Color de fondo" + +msgid "Select a color" +msgstr "Seleccione un color" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "S" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferencias..." + +msgid "Toolbar" +msgstr "Barra de herramientas" + +msgid "Default" +msgstr "Por defecto" + +msgid "Text and Icons" +msgstr "Texto e iconos" + +msgid "Text" +msgstr "Texto" + +msgid "Icons" +msgstr "Iconos" + +msgid "Form" +msgstr "Formulario" + +msgid "Save Column Width" +msgstr "Guardar ancho de columna" + +msgid "Save Tree State" +msgstr "Guardar estado de expansión del árbol" + +msgid "Spell Checking" +msgstr "Corrección ortográfica" + +msgid "PDA Mode" +msgstr "Modo PDA" + +msgid "Search Limit..." +msgstr "_Límite de búsqueda..." + +msgid "Check Version" +msgstr "Comprobar versión" + +msgid "Options" +msgstr "Opciones" + +msgid "Keyboard Shortcuts..." +msgstr "Combinaciones de teclas..." + +msgid "About..." +msgstr "Acerca de..." + +msgid "Help" +msgstr "Ayuda" + +msgid "No result found." +msgstr "No se han encontrado resultados." + +msgid "Favorites" +msgstr "Favoritos" + +msgid "Manage..." +msgstr "Gestionar..." + +msgid "Action" +msgstr "Acción" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"La acción seleccionada requiere cerrar todas las pestañas.\n" +"¿Desea continuar?" + +msgid "Application Shortcuts" +msgstr "Atajos de la aplicación" + +msgid "Global" +msgstr "Global" + +msgid "Preferences" +msgstr "Preferencias" + +msgid "Search menu" +msgstr "Menú de búsqueda" + +msgid "Toggle menu" +msgstr "Commutar el menu" + +msgid "Previous tab" +msgstr "Pestaña anterior" + +msgid "Next tab" +msgstr "Pestaña siguiente" + +msgid "Shortcuts" +msgstr "Atajos" + +msgid "Quit" +msgstr "Salir" + +msgid "Edition Shortcuts" +msgstr "Atajos de edición" + +msgid "Text Entries" +msgstr "Entradas de texto" + +msgid "Cut selected text" +msgstr "Cortar texto seleccionado" + +msgid "Copy selected text" +msgstr "Copiar texto seleccionado" + +msgid "Paste copied text" +msgstr "Pegar texto seleccionado" + +msgid "Next entry" +msgstr "Siguiente entrada" + +msgid "Previous entry" +msgstr "Entrada anterior" + +msgid "Relation Entries" +msgstr "Entradas relacionadas" + +msgid "Create new relation" +msgstr "Crear nueva relación" + +msgid "Open/Search relation" +msgstr "Abrir/Buscar relación" + +msgid "List Entries" +msgstr "Lista de entradas" + +msgid "Switch view" +msgstr "Cambiar de vista" + +msgid "Create/Select new line" +msgstr "Crear/Seleccionar línea nueva" + +msgid "Open relation" +msgstr "Abrir relación" + +msgid "Mark line for deletion/removal" +msgstr "Marcar linea para eliminar/suprimir" + +msgid "Mark line for removal" +msgstr "Marcar para eliminar" + +msgid "Unmark line for deletion" +msgstr "Desmarcar para eliminación" + +msgid "List/Tree Shortcuts" +msgstr "Atajos de lista/árbol" + +msgid "Move Cursor" +msgstr "Mover cursor" + +msgid "Move right" +msgstr "Mover a la derecha" + +msgid "Move left" +msgstr "Mover a la izquierda" + +msgid "Move up" +msgstr "Mover arriba" + +msgid "Move down" +msgstr "Mover abajo" + +msgid "Move up of one page" +msgstr "Mover arriba una página" + +msgid "Move down of one page" +msgstr "Mover abajo una página" + +msgid "Move to top" +msgstr "Mover al principio" + +msgid "Move to bottom" +msgstr "Mover al final" + +msgid "Move to parent" +msgstr "Mover al padre" + +msgid "Select all" +msgstr "Seleccionar todo" + +msgid "Unselect all" +msgstr "Deseleccionar todo" + +msgid "Select parent" +msgstr "Seleccionar padre" + +msgid "Select/Activate current row" +msgstr "Seleccionar/Activar fila actual" + +msgid "Toggle selection" +msgstr "Conmutar selección" + +msgid "Expand/Collapse" +msgstr "Expandir/Contraer" + +msgid "Expand row" +msgstr "Expandir fila" + +msgid "Collapse row" +msgstr "Contraer fila" + +msgid "Toggle row" +msgstr "Conmutar fila" + +msgid "Collapse all rows" +msgstr "Contraer todas las filas" + +msgid "Expand all rows" +msgstr "Expandir todas las filas" + +msgid "Close Tab" +msgstr "Cerrar pestaña" + +msgid "modularity, scalability and security" +msgstr "modularidad, escalabilidad y seguridad" + +msgid "translator-credits" +msgstr "Sergi Almacellas Abellana" + +#, python-format +msgid "Attachments (%s)" +msgstr "Adjuntos (%s)" + +msgid "Profile Editor" +msgstr "Editor de perfiles" + +msgid "Profile" +msgstr "Perfil" + +msgid "Add new profile" +msgstr "Añadir un nuevo perfil" + +msgid "Remove selected profile" +msgstr "Eliminar el perfil seleccionado" + +msgid "Host:" +msgstr "Servidor:" + +msgid "Database:" +msgstr "Base de datos:" + +msgid "Fetching databases list" +msgstr "Recuperando lista de bases de datos" + +msgid "Username:" +msgstr "Nombre de usuario:" + +msgid "Incompatible version of the server." +msgstr "El cliente no es compatible con la versión del servidor." + +msgid "Could not connect to the server." +msgstr "No se ha podido conectar con el servidor." + +msgid "Login" +msgstr "Acceder" + +msgid "_Cancel" +msgstr "_Cancelar" + +msgid "Cancel connection to the Tryton server" +msgstr "Cancelar conexión con el servidor Tryton" + +msgid "C_onnect" +msgstr "C_onectar" + +msgid "Connect the Tryton server" +msgstr "Conectar con el servidor Tryton" + +msgid "Profile:" +msgstr "Perfil:" + +msgid "Host / Database information" +msgstr "Información del Servidor / Base de datos" + +msgid "User name:" +msgstr "Nombre de usuario:" + +msgid "Unable to complete email entry" +msgstr "No se ha podido completar la entrada de correo electrónico" + +#, python-format +msgid "E-mail %s" +msgstr "Correo electrónico %s" + +msgid "To:" +msgstr "A:" + +msgid "Cc:" +msgstr "Cc:" + +msgid "Bcc:" +msgstr "CCo:" + +msgid "Subject:" +msgstr "Asunto:" + +msgid "Body" +msgstr "Mensaje" + +msgid "Reports" +msgstr "Informes" + +msgid "Attachments" +msgstr "Adjuntos" + +msgid "Files" +msgstr "Archivos" + +msgid "Send" +msgstr "Enviar" + +msgid "Select File" +msgstr "Seleccionar un archivo" + +msgid "Remove File" +msgstr "Eliminar archivo" + +msgid "Add..." +msgstr "Añadir..." + +msgid "Preview" +msgstr "Previsualizar" + +msgid "Previous" +msgstr "Anterior" + +msgid "Next" +msgstr "Siguiente" + +#, python-format +msgid "Attachment (%s)" +msgstr "Adjuntos (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Nota (%d/%d)" + +msgid "You have to select one record." +msgstr "Debe elegir un registro." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Creado por:" + +msgid "Created at:" +msgstr "Creado el:" + +msgid "Edited by:" +msgstr "Editado por:" + +msgid "Edited at:" +msgstr "Editado el:" + +msgid "Model:" +msgstr "Modelo:" + +msgid "Are you sure to remove this record?" +msgstr "¿Está seguro que quiere eliminar este registro?" + +msgid "Are you sure to remove those records?" +msgstr "¿Está seguro que quiere eliminar estos registros?" + +msgid "Records not removed." +msgstr "Los registros no se han eliminado." + +msgid "Records removed." +msgstr "Registros eliminados." + +msgid "Working now on the duplicated record(s)." +msgstr "Ahora está trabajando en el/los registro(s) duplicado(s)." + +msgid "Record saved." +msgstr "Registro guardado." + +msgid " of " +msgstr " de " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Este registro ha sido modificado.\n" +"¿Desea guardarlo?" + +msgid "Launch action" +msgstr "Ejecutar acción" + +msgid "Relate" +msgstr "Relacionado" + +msgid "Open related records" +msgstr "Abrir registros relacionados" + +msgid "Report" +msgstr "Informes" + +msgid "Open report" +msgstr "Abrir informe" + +msgid "Print" +msgstr "Imprimir" + +msgid "Print report" +msgstr "Imprimir informe" + +msgid "_Copy URL" +msgstr "_Copiar la URL" + +msgid "Copy URL into clipboard" +msgstr "Copiar la URL al portapapeles" + +msgid "Unknown" +msgstr "Desconocido" + +msgid "Limit" +msgstr "Límite" + +msgid "Search Limit Settings" +msgstr "Preferencias del límite de búsqueda" + +msgid "Limit:" +msgstr "Límite:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notas (%s)" + +msgid "Edit User Preferences" +msgstr "Editar preferencias de usuario" + +msgid "Preference" +msgstr "Preferencias" + +msgid "Revision" +msgstr "Revisión" + +msgid "Select a revision" +msgstr "Seleccionar una versión" + +msgid "Revision:" +msgstr "Versión:" + +msgid "_Switch View" +msgstr "Cambiar vi_sta" + +msgid "Switch View" +msgstr "Cambiar vista" + +msgid "_Previous" +msgstr "_Anterior" + +msgid "Previous Record" +msgstr "Registro anterior" + +msgid "_Next" +msgstr "Sig_uiente" + +msgid "Next Record" +msgstr "Registro siguiente" + +msgid "_Search" +msgstr "_Buscar" + +msgid "_New" +msgstr "_Nuevo" + +msgid "Create a new record" +msgstr "Crear un nuevo registro" + +msgid "_Save" +msgstr "_Guardar" + +msgid "Save this record" +msgstr "Guardar este registro" + +msgid "_Reload/Undo" +msgstr "_Recargar/Deshacer" + +msgid "Reload/Undo" +msgstr "Recargar/Deshacer" + +msgid "_Duplicate" +msgstr "_Duplicar" + +msgid "_Delete..." +msgstr "_Eliminar..." + +msgid "View _Logs..." +msgstr "Ver _registro..." + +msgid "Show revisions..." +msgstr "Mostrar revisiones..." + +msgid "A_ttachments..." +msgstr "Adjun_tos..." + +msgid "Add an attachment to the record" +msgstr "Añadir un adjunto al registro" + +msgid "_Notes..." +msgstr "_Notas..." + +msgid "Add a note to the record" +msgstr "Añadir una nota al registro" + +msgid "_Actions..." +msgstr "_Acciones..." + +msgid "_Relate..." +msgstr "_Relacionado..." + +msgid "_Report..." +msgstr "_Informes..." + +msgid "_Print..." +msgstr "Im_primir..." + +msgid "_E-Mail..." +msgstr "_Correo electrónico..." + +msgid "Send an e-mail using the record" +msgstr "Enviar un correo electrónico usando el registro" + +msgid "_Export Data..." +msgstr "_Exportar datos..." + +msgid "_Import Data..." +msgstr "_Importar datos..." + +msgid "Copy _URL..." +msgstr "Copiar _URL..." + +msgid "_Close Tab" +msgstr "_Cerrar pestaña" + +msgid "All fields" +msgstr "Todos los campos" + +msgid "_Add" +msgstr "_Añadir" + +msgid "_Remove" +msgstr "_Eliminar" + +msgid "_Clear" +msgstr "_Limpiar" + +msgid "Fields selected" +msgstr "Campos seleccionados" + +msgid "CSV Parameters" +msgstr "Parámetros CSV" + +msgid "Delimiter:" +msgstr "Separador:" + +msgid "Quote char:" +msgstr "Delimitador de texto:" + +msgid "Encoding:" +msgstr "Codificación:" + +msgid "Use locale format" +msgstr "Usar formato local" + +msgid "Field name" +msgstr "Nombre del campo" + +#, python-format +msgid "CSV Export: %s" +msgstr "Exportación CSV: %s" + +msgid "_Save Export" +msgstr "_Guardar exportación" + +msgid "_URL Export" +msgstr "_URL Exportación" + +msgid "_Delete Export" +msgstr "_Eliminar exportación" + +msgid "Predefined exports" +msgstr "Exportaciones predeterminadas" + +msgid "Name" +msgstr "Nombre" + +msgid "Open" +msgstr "Abrir" + +msgid "Listed Records" +msgstr "Registros listados" + +msgid "Selected Records" +msgstr "Registros seleccionados" + +msgid "Ignore search limit" +msgstr "Ignorar límite de búsqueda" + +msgid "Add field names" +msgstr "Añadir nombres de campo" + +#, python-format +msgid "%s (string)" +msgstr "%s (cadena)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nombre del modelo)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (nombre del registro)" + +msgid "What is the name of this export?" +msgstr "¿Cuál es el nombre de esta exportación?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Sobrescribir la definición de '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "Se ha guardado %d registro." + +#, python-format +msgid "%d records saved." +msgstr "Se han guardado %d registros." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Ha fallado la operación.\n" +"Mensaje de error:\n" +"%s" + +msgid "Error" +msgstr "Error" + +msgid "Link" +msgstr "Enlace" + +msgid "Delete" +msgstr "Eliminar" + +msgid "New" +msgstr "Nuevo" + +msgid "Switch" +msgstr "Cambiar" + +msgid "Add" +msgstr "Añadir" + +msgid "Remove " +msgstr "Eliminar " + +msgid "Create a new record " +msgstr "Crear un nuevo registro " + +msgid "Delete selected record " +msgstr "Eliminar registro seleccionado " + +msgid "Undelete selected record " +msgstr "Recuperar registro seleccionado " + +#, python-format +msgid "CSV Import: %s" +msgstr "Importación CSV: %s" + +msgid "_Auto-Detect" +msgstr "_Auto-detectar" + +msgid "File to Import:" +msgstr "Archivo a importar:" + +msgid "Open..." +msgstr "Abrir..." + +msgid "Lines to Skip:" +msgstr "Líneas a omitir:" + +msgid "You must select an import file first." +msgstr "Primero debe elegir un archivo a importar." + +msgid "Error opening CSV file" +msgstr "Error al abrir el archivo CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Se ha producido un error al procesar el archivo en el campo %s." + +#, python-format +msgid "%d record imported." +msgstr "Se ha importado %d registro." + +#, python-format +msgid "%d records imported." +msgstr "Se han importado %d registros." + +msgid "Search" +msgstr "Buscar" + +#, python-format +msgid "Search %s" +msgstr "Buscar %s" + +msgid "Wizard" +msgstr "Asistente" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "No se ha podido eliminar el asistente %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Creado por" + +msgid "Created at" +msgstr "Creado el" + +msgid "Edited by" +msgstr "Editado por" + +msgid "Edited at" +msgstr "Editado el" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "No se ha podido obtener el estado de expansión del árbol de %s" + +msgid "Unable to set view tree state" +msgstr "No se ha podido establecer el estado de expansión del árbol" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" no es válido según su dominio." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" es obligatorio." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Los valores de \"%s\" no son válidos." + +msgid "Pre-validation" +msgstr "Prevalidación" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Tamaño de la imagen" + +msgid "Width:" +msgstr "Anchura:" + +msgid "Height:" +msgstr "Altura:" + +msgid "PNG image (*.png)" +msgstr "Imagen PNG (*.png)" + +msgid "Save As" +msgstr "Guardar como" + +msgid "Image size too large." +msgstr "El tamaño de la imagen es muy grande." + +msgid "Copy" +msgstr "Copiar" + +msgid "Paste" +msgstr "Pegar" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Abrir filtros" + +msgid "Show bookmarks of filters" +msgstr "Muestra las búsquedas favoritas" + +msgid "Remove this bookmark" +msgstr "Eliminar de las búsquedas favoritas" + +msgid "Bookmark this filter" +msgstr "Guardar como búsqueda favorita" + +msgid "Show active records" +msgstr "Mostrar registros activos" + +msgid "Show inactive records" +msgstr "Mostrar registros inactivos" + +msgid "Bookmark Name:" +msgstr "Nombre de la búsqueda favorita:" + +msgid "Find" +msgstr "Buscar" + +msgid "Today" +msgstr "Hoy" + +msgid "go back" +msgstr "retroceder" + +msgid "go forward" +msgstr "ir adelante" + +msgid "previous year" +msgstr "año anterior" + +msgid "next year" +msgstr "año próximo" + +msgid "Day View" +msgstr "Vista de día" + +msgid "Week View" +msgstr "Vista semanal" + +msgid "Month View" +msgstr "Vista mensual" + +msgid "Week" +msgstr "Semana" + +msgid "Select..." +msgstr "Seleccionar…" + +msgid "Clear" +msgstr "Limpiar" + +msgid "All files" +msgstr "Todos los archivos" + +msgid "Show plain text" +msgstr "Mostrar como texto plano" + +msgid "Add value" +msgstr "Añadir un valor" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Eliminar \"%s\"" + +msgid "Images" +msgstr "Imágenes" + +msgid "Add existing record" +msgstr "Añadir un registro existente" + +msgid "Remove selected record" +msgstr "Eliminar registro seleccionado" + +msgid "Open the record " +msgstr "Abrir registro " + +msgid "Clear the field " +msgstr "Eliminar el registro " + +msgid "Search a record " +msgstr "Buscar registro " + +msgid "Edit selected record" +msgstr "Editar registro seleccionado" + +msgid "Delete selected record" +msgstr "Eliminar registro seleccionado" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Escoja un lenguaje" + +msgid "Translation" +msgstr "Traducción" + +msgid "Edit" +msgstr "Editar" + +msgid "Fuzzy" +msgstr "Dudoso" + +msgid "You need to save the record before adding translations." +msgstr "Debe guardar el registro antes de añadir traducciones." + +msgid "No other language available." +msgstr "No hay otro idioma disponible." + +msgid "Translate view" +msgstr "Traducir vista" diff --git a/tryton/tryton/data/locale/es_419/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/es_419/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..9f255636e0 --- /dev/null +++ b/tryton/tryton/data/locale/es_419/LC_MESSAGES/tryton.po @@ -0,0 +1,1170 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "" + +msgid "development mode" +msgstr "" + +msgid "logging everything at INFO level" +msgstr "" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +msgid "specify the login user" +msgstr "" + +msgid "specify the server hostname:port" +msgstr "" + +#, python-format +msgid "Unable to write config file %s." +msgstr "" + +#, python-format +msgid "Unable to set locale %s" +msgstr "" + +msgid ", " +msgstr "" + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "" + +msgid "No action defined." +msgstr "" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save As..." +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + +msgid "Always ignore this warning." +msgstr "" + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "" + +msgid "Save your current version" +msgstr "Guardar tu versión actual" + +#, python-format +msgid "Compare: %s" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Application Error" +msgstr "" + +msgid "Report Bug" +msgstr "" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "" + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Displayed value" +msgstr "" + +msgid "Format" +msgstr "" + +msgid "Display format" +msgstr "" + +msgid "Open the calendar" +msgstr "" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "" + +msgid "Edit..." +msgstr "" + +msgid "Attachments..." +msgstr "" + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "" + +msgid "Relate..." +msgstr "" + +msgid "Report..." +msgstr "" + +msgid "Print..." +msgstr "" + +msgid "E-Mail..." +msgstr "" + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "M" +msgstr "" + +msgid "w" +msgstr "" + +msgid "d" +msgstr "" + +msgid "h" +msgstr "" + +msgid "m" +msgstr "" + +msgid "s" +msgstr "" + +msgid "Preferences..." +msgstr "" + +msgid "Toolbar" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Text and Icons" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Icons" +msgstr "" + +msgid "Form" +msgstr "" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "" + +msgid "Options" +msgstr "" + +msgid "Keyboard Shortcuts..." +msgstr "" + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "" + +msgid "Copy selected text" +msgstr "" + +msgid "Paste copied text" +msgstr "" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "Sebastian Marro" + +#, python-format +msgid "Attachments (%s)" +msgstr "" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "" + +msgid "Database:" +msgstr "" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "" + +msgid "Incompatible version of the server." +msgstr "" + +msgid "Could not connect to the server." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "_Cancel" +msgstr "" + +msgid "Cancel connection to the Tryton server" +msgstr "" + +msgid "C_onnect" +msgstr "" + +msgid "Connect the Tryton server" +msgstr "" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "" + +msgid "Unable to complete email entry" +msgstr "" + +#, python-format +msgid "E-mail %s" +msgstr "" + +msgid "To:" +msgstr "" + +msgid "Cc:" +msgstr "" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "" + +msgid "Body" +msgstr "" + +msgid "Reports" +msgstr "" + +msgid "Attachments" +msgstr "" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Select File" +msgstr "" + +msgid "Remove File" +msgstr "" + +msgid "Add..." +msgstr "" + +msgid "Preview" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" + +#, python-format +msgid "Attachment (%s)" +msgstr "" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "" + +msgid "ID:" +msgstr "" + +#, fuzzy +msgid "Created by:" +msgstr "Fecha creación:" + +#, fuzzy +msgid "Created at:" +msgstr "Fecha creación:" + +#, fuzzy +msgid "Edited by:" +msgstr "Editar" + +#, fuzzy +msgid "Edited at:" +msgstr "Editar" + +msgid "Model:" +msgstr "" + +msgid "Are you sure to remove this record?" +msgstr "" + +msgid "Are you sure to remove those records?" +msgstr "" + +msgid "Records not removed." +msgstr "" + +msgid "Records removed." +msgstr "" + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "" + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" + +msgid "Launch action" +msgstr "" + +msgid "Relate" +msgstr "" + +msgid "Open related records" +msgstr "" + +msgid "Report" +msgstr "" + +msgid "Open report" +msgstr "" + +msgid "Print" +msgstr "" + +msgid "Print report" +msgstr "" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Search Limit Settings" +msgstr "" + +msgid "Limit:" +msgstr "" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "" + +msgid "Preference" +msgstr "" + +msgid "Revision" +msgstr "" + +msgid "Select a revision" +msgstr "" + +msgid "Revision:" +msgstr "" + +msgid "_Switch View" +msgstr "" + +msgid "Switch View" +msgstr "" + +msgid "_Previous" +msgstr "" + +msgid "Previous Record" +msgstr "" + +msgid "_Next" +msgstr "" + +msgid "Next Record" +msgstr "" + +msgid "_Search" +msgstr "" + +msgid "_New" +msgstr "" + +msgid "Create a new record" +msgstr "" + +msgid "_Save" +msgstr "" + +msgid "Save this record" +msgstr "" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "" + +msgid "_Delete..." +msgstr "" + +msgid "View _Logs..." +msgstr "" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "" + +msgid "Add an attachment to the record" +msgstr "" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "" + +msgid "_Relate..." +msgstr "" + +msgid "_Report..." +msgstr "" + +msgid "_Print..." +msgstr "" + +msgid "_E-Mail..." +msgstr "" + +msgid "Send an e-mail using the record" +msgstr "" + +msgid "_Export Data..." +msgstr "" + +msgid "_Import Data..." +msgstr "" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "" + +msgid "All fields" +msgstr "" + +msgid "_Add" +msgstr "" + +msgid "_Remove" +msgstr "" + +msgid "_Clear" +msgstr "" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "" + +msgid "_URL Export" +msgstr "" + +msgid "_Delete Export" +msgstr "" + +msgid "Predefined exports" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Listed Records" +msgstr "" + +msgid "Selected Records" +msgstr "" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "" + +#, python-format +msgid "%d records saved." +msgstr "" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Link" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "New" +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Remove " +msgstr "" + +msgid "Create a new record " +msgstr "" + +msgid "Delete selected record " +msgstr "" + +msgid "Undelete selected record " +msgstr "" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "" + +msgid "File to Import:" +msgstr "" + +msgid "Open..." +msgstr "" + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "" + +msgid "Error opening CSV file" +msgstr "" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "" + +#, python-format +msgid "%d record imported." +msgstr "" + +#, python-format +msgid "%d records imported." +msgstr "" + +msgid "Search" +msgstr "" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Fecha creación:" + +#, fuzzy +msgid "Edited by" +msgstr "Editar" + +#, fuzzy +msgid "Edited at" +msgstr "Editar" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr "" + +msgid "Image Size" +msgstr "" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "" + +msgid "Image size too large." +msgstr "" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr "" + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "" + +msgid "Month View" +msgstr "" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "" + +msgid "All files" +msgstr "" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Add existing record" +msgstr "" + +msgid "Remove selected record" +msgstr "" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +msgid "Edit selected record" +msgstr "" + +msgid "Delete selected record" +msgstr "" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Fuzzy" +msgstr "Revisión" + +msgid "You need to save the record before adding translations." +msgstr "" + +msgid "No other language available." +msgstr "" + +msgid "Translate view" +msgstr "" diff --git a/tryton/tryton/data/locale/et/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/et/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..966c8e0701 --- /dev/null +++ b/tryton/tryton/data/locale/et/LC_MESSAGES/tryton.po @@ -0,0 +1,1193 @@ +# Translations template for tryton. +# Copyright (C) 2018 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2018. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "määra alternatiivne konfiguratsioonifail" + +msgid "development mode" +msgstr "arendusreziim" + +msgid "logging everything at INFO level" +msgstr "logi kõik INFO tasemel" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "määra logimise tase: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "määra kasutaja sisselogimise tunnus" + +msgid "specify the server hostname:port" +msgstr "määra server:port" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Ei saa kirjutada konfiguratsioonifaili %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Ei saa seadistada lokaati %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Vali toiming" + +msgid "No action defined." +msgstr "Toiming on määramata." + +msgid "By: " +msgstr "Poolt:" + +msgid "Selection" +msgstr "Valik" + +msgid "Cancel" +msgstr "Tühista" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Teie valik:" + +msgid "Select" +msgstr "Vali" + +msgid "Save" +msgstr "Salvesta" + +msgid "Save As..." +msgstr "Salvesta nimega..." + +msgid "Do you want to proceed?" +msgstr "Kas soovid jätkata?" + +msgid "Always ignore this warning." +msgstr "Eira alati seda hoiatust." + +msgid "No" +msgstr "Ei" + +msgid "Yes" +msgstr "Jah" + +msgid "Concurrency Exception" +msgstr "Konkurentsi erand" + +msgid "This record has been modified while you were editing it." +msgstr "Seda kirjet on muudetud." + +msgid "Cancel saving" +msgstr "Tühista salvestamine" + +msgid "Compare" +msgstr "Võrdle" + +msgid "See the modified version" +msgstr "Vaata muudetud versiooni" + +msgid "Write Anyway" +msgstr "Kirjuta ikkagi" + +msgid "Save your current version" +msgstr "Salvesta praegune versioon" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Võrdle: %s" + +msgid "Close" +msgstr "Sulge" + +msgid "Application Error" +msgstr "Rakenduse viga" + +msgid "Report Bug" +msgstr "Teatvita veast" + +#, python-format +msgid "Check URL: %s" +msgstr "Kontrolli URLi: %s" + +#, fuzzy +msgid "Unable to check for new version." +msgstr "Ei saa kontrollida uut versiooni" + +msgid "A new version is available!" +msgstr "Uus versioon on saadaval!" + +msgid "Download" +msgstr "Allalaadimine" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Otsi..." + +msgid "Create..." +msgstr "Loo..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "Väärtus" + +msgid "Displayed value" +msgstr "Kuvatud väärtus" + +msgid "Format" +msgstr "Formaat" + +msgid "Display format" +msgstr "Kuvatav formaat" + +msgid "Open the calendar" +msgstr "Ava kalender" + +msgid "Date Format" +msgstr "Kuupäevaformaat" + +msgid "Displayed date format" +msgstr "Kuvatav kuupäevaformaat" + +msgid "y" +msgstr "j" + +msgid "True" +msgstr "Tõene" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "Väär" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "Seo" + +msgid "Edit..." +msgstr "Muuda..." + +msgid "Attachments..." +msgstr "Manused..." + +msgid "Notes..." +msgstr "Märkused..." + +msgid "Actions..." +msgstr "Toimingud..." + +msgid "Relate..." +msgstr "Seosta..." + +msgid "Report..." +msgstr "Aruanne..." + +msgid "Print..." +msgstr "Prindi..." + +msgid "E-Mail..." +msgstr "E-kiri..." + +msgid "Bold" +msgstr "Paks" + +msgid "Italic" +msgstr "Kursiiv" + +msgid "Underline" +msgstr "Allajoonitud" + +msgid "Align Left" +msgstr "Joonda vasakule" + +msgid "Align Center" +msgstr "Joonda keskele" + +msgid "Align Right" +msgstr "Joonda paremale" + +msgid "Justify" +msgstr "Ühtlusta" + +msgid "Foreground Color" +msgstr "Esiplaani värv" + +msgid "Select a color" +msgstr "Vali värv" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "K" + +msgid "w" +msgstr "n" + +msgid "d" +msgstr "p" + +msgid "h" +msgstr "t" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Eelistused..." + +msgid "Toolbar" +msgstr "Tööriistariba" + +msgid "Default" +msgstr "Vaikimisi" + +msgid "Text and Icons" +msgstr "Tekst ja ikoonid" + +msgid "Text" +msgstr "Tekst" + +msgid "Icons" +msgstr "Ikoonid" + +msgid "Form" +msgstr "Vorm" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "Salvesta puu olek" + +msgid "Spell Checking" +msgstr "Õigekirja kontroll" + +msgid "PDA Mode" +msgstr "PDA reziim" + +msgid "Search Limit..." +msgstr "Otsingupiirang..." + +msgid "Check Version" +msgstr "Kontrolli versiooni" + +msgid "Options" +msgstr "Valikud" + +msgid "Keyboard Shortcuts..." +msgstr "Kiirklahvid..." + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "Abi" + +msgid "No result found." +msgstr "Tulemusi ei leitud." + +msgid "Favorites" +msgstr "Lemmikud" + +msgid "Manage..." +msgstr "Halda..." + +msgid "Action" +msgstr "Toiming" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Järgneva toimingu teostamiseks on vaja sulgeda kõik vahelehed. \n" +"Kas jätkata?" + +msgid "Application Shortcuts" +msgstr "Rakenduse otseteed" + +msgid "Global" +msgstr "Globaalne" + +msgid "Preferences" +msgstr "Eelistused" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "Eelmine vaheleht" + +msgid "Next tab" +msgstr "Järgmine vaheleht" + +msgid "Shortcuts" +msgstr "Otseteed" + +msgid "Quit" +msgstr "Välju" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Lõika valitud tekst" + +msgid "Copy selected text" +msgstr "Kopeeri valitud tekst" + +msgid "Paste copied text" +msgstr "Aseta valitud tekst" + +msgid "Next entry" +msgstr "Järgmine" + +msgid "Previous entry" +msgstr "Eelmine" + +msgid "Relation Entries" +msgstr "Seotud kirjed" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +#, fuzzy +msgid "Switch view" +msgstr "Vaheta vaadet" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "Liigu paremale" + +msgid "Move left" +msgstr "Liigu vasakule" + +msgid "Move up" +msgstr "Liigu üles" + +msgid "Move down" +msgstr "Liigu alla" + +msgid "Move up of one page" +msgstr "Liigu lehekülg üles" + +msgid "Move down of one page" +msgstr "Liigu lehekülg alla" + +msgid "Move to top" +msgstr "Liigu algusesse" + +msgid "Move to bottom" +msgstr "Liigu lõppu" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "Vali kõik" + +msgid "Unselect all" +msgstr "Tühista valik" + +msgid "Select parent" +msgstr "Vali ülem" + +msgid "Select/Activate current row" +msgstr "Vali/aktiveeri rida" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "Ava/sule" + +msgid "Expand row" +msgstr "Ava rida" + +msgid "Collapse row" +msgstr "Sulge rida" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "Sulge kõik read" + +msgid "Expand all rows" +msgstr "Ava kõik read" + +msgid "Close Tab" +msgstr "Sulge vaheleht" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "Manused (%s)" + +msgid "Profile Editor" +msgstr "Profiili muutmine" + +msgid "Profile" +msgstr "Profiil" + +msgid "Add new profile" +msgstr "Lisa uus profiil" + +msgid "Remove selected profile" +msgstr "Eemalda valitud profiil" + +msgid "Host:" +msgstr "Server:" + +msgid "Database:" +msgstr "Andmebaas:" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "Kasutajanimi:" + +msgid "Incompatible version of the server." +msgstr "" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Ei saanud serveriga ühendust" + +msgid "Login" +msgstr "Logi sisse" + +msgid "_Cancel" +msgstr "_Tühista" + +msgid "Cancel connection to the Tryton server" +msgstr "Katkesta ühendus Tryton serveriga" + +msgid "C_onnect" +msgstr "Ü_hendu" + +msgid "Connect the Tryton server" +msgstr "Ühendu Tryton serveriga" + +msgid "Profile:" +msgstr "Profiil:" + +msgid "Host / Database information" +msgstr "Serveri / andmebaasi info" + +msgid "User name:" +msgstr "Kasutaja nimi:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Ei saa kustutada nõustajat %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "E-kiri %s" + +msgid "To:" +msgstr "Saaja:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Pealkiri:" + +#, fuzzy +msgid "Body" +msgstr "Sisu:" + +#, fuzzy +msgid "Reports" +msgstr "Aruanne" + +#, fuzzy +msgid "Attachments" +msgstr "Manus:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Vali kõik" + +#, fuzzy +msgid "Remove File" +msgstr "Eemalda " + +msgid "Add..." +msgstr "Lisa..." + +#, fuzzy +msgid "Preview" +msgstr "Eelmine" + +msgid "Previous" +msgstr "Eelmine" + +msgid "Next" +msgstr "Edasi" + +#, python-format +msgid "Attachment (%s)" +msgstr "Manus (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Märkus (%d%d)" + +msgid "You have to select one record." +msgstr "Pead valima vähemasti ühe rea." + +msgid "ID:" +msgstr "ID" + +#, fuzzy +msgid "Created by:" +msgstr "Loomise kuupäev" + +#, fuzzy +msgid "Created at:" +msgstr "Loomise kuupäev" + +#, fuzzy +msgid "Edited by:" +msgstr "Muuda" + +#, fuzzy +msgid "Edited at:" +msgstr "Muuda" + +msgid "Model:" +msgstr "Mudel:" + +msgid "Are you sure to remove this record?" +msgstr "Oled kindel, et kustutada see kirje?" + +msgid "Are you sure to remove those records?" +msgstr "Oled kindel, et kustutada need kirjed?" + +msgid "Records not removed." +msgstr "Kirjeid ei kustutatud." + +msgid "Records removed." +msgstr "Kirjed kustutatud." + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "Kirje salvestatud." + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Seda kirjet on muudetud,\n" +"kas soovid salvestada?" + +msgid "Launch action" +msgstr "Käivita toiming" + +msgid "Relate" +msgstr "Seo" + +msgid "Open related records" +msgstr "Ava seotud kirjed" + +msgid "Report" +msgstr "Aruanne" + +msgid "Open report" +msgstr "Ava aruanne" + +msgid "Print" +msgstr "Prindi" + +msgid "Print report" +msgstr "Prindi aruanne" + +msgid "_Copy URL" +msgstr "_Kopeeri URL" + +msgid "Copy URL into clipboard" +msgstr "Kopeeri URL lõikelauale" + +msgid "Unknown" +msgstr "Tundmatu" + +msgid "Limit" +msgstr "Limiit" + +msgid "Search Limit Settings" +msgstr "Otsingupiirangu seaded" + +msgid "Limit:" +msgstr "Limiit" + +#, python-format +msgid "Notes (%s)" +msgstr "Märkused (%s)" + +msgid "Edit User Preferences" +msgstr "Muuda kasutaja eelistusi" + +msgid "Preference" +msgstr "Eelistus" + +msgid "Revision" +msgstr "Läbivaatus" + +msgid "Select a revision" +msgstr "Vali läbivaatus" + +msgid "Revision:" +msgstr "Läbivaatus:" + +msgid "_Switch View" +msgstr "_Vaheta vaadet" + +msgid "Switch View" +msgstr "Vaheta vaadet" + +msgid "_Previous" +msgstr "_Eelmine" + +msgid "Previous Record" +msgstr "Eelmine kirje" + +msgid "_Next" +msgstr "_Edasi" + +msgid "Next Record" +msgstr "Järgmine kirje" + +msgid "_Search" +msgstr "_Otsi" + +msgid "_New" +msgstr "_Uus" + +msgid "Create a new record" +msgstr "Loo uus kirje" + +msgid "_Save" +msgstr "_Salvesta" + +msgid "Save this record" +msgstr "Salvesta kirje" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "_Duplikaat" + +msgid "_Delete..." +msgstr "_Kustuta..." + +msgid "View _Logs..." +msgstr "Vaata _logisid" + +msgid "Show revisions..." +msgstr "Näita läbivaatust..." + +msgid "A_ttachments..." +msgstr "M_anused..." + +msgid "Add an attachment to the record" +msgstr "Lisa kirjele manus" + +msgid "_Notes..." +msgstr "_Märkused..." + +msgid "Add a note to the record" +msgstr "Lisa kirjele manus" + +msgid "_Actions..." +msgstr "_Toimingud..." + +msgid "_Relate..." +msgstr "_Seosta..." + +msgid "_Report..." +msgstr "_Aruanne..." + +msgid "_Print..." +msgstr "_Prindi..." + +msgid "_E-Mail..." +msgstr "_E-kiri..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Lisa kirjele manus" + +msgid "_Export Data..." +msgstr "_Ekspordi andmed..." + +msgid "_Import Data..." +msgstr "_Impordi andmed..." + +msgid "Copy _URL..." +msgstr "Kopeeri _URL..." + +msgid "_Close Tab" +msgstr "_Sulge vaheleht" + +msgid "All fields" +msgstr "Kõik väljad" + +msgid "_Add" +msgstr "_Lisa" + +msgid "_Remove" +msgstr "_Eemalda" + +msgid "_Clear" +msgstr "_Tühjenda" + +msgid "Fields selected" +msgstr "Valitud väljad" + +msgid "CSV Parameters" +msgstr "CSV parameetrid" + +msgid "Delimiter:" +msgstr "Eralaja:" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "Kasuta piirkonna formaati" + +msgid "Field name" +msgstr "Välja nimi" + +#, python-format +msgid "CSV Export: %s" +msgstr "CSV eksport: %s" + +msgid "_Save Export" +msgstr "_Salvesta eksport" + +#, fuzzy +msgid "_URL Export" +msgstr "_Salvesta eksport" + +msgid "_Delete Export" +msgstr "_Kustuta eksport" + +msgid "Predefined exports" +msgstr "Eelnevalt määratletud ekspordid" + +msgid "Name" +msgstr "Nimi" + +msgid "Open" +msgstr "Ava" + +#, fuzzy +msgid "Listed Records" +msgstr "Muuda valitud kirjet" + +#, fuzzy +msgid "Selected Records" +msgstr "Muuda valitud kirjet" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "Lisa välja nimed" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (model name)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (record name)" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "%d kirje salvestatud." + +#, python-format +msgid "%d records saved." +msgstr "%d kirjed salvestatud." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Toiming ebaõnnestus.\n" +"Veateade:\n" +"%s" + +msgid "Error" +msgstr "Viga" + +msgid "Link" +msgstr "Link" + +msgid "Delete" +msgstr "Kustuta" + +msgid "New" +msgstr "Uus" + +msgid "Switch" +msgstr "Vaheta" + +msgid "Add" +msgstr "Lisa" + +msgid "Remove " +msgstr "Eemalda " + +msgid "Create a new record " +msgstr "Uus kirje " + +msgid "Delete selected record " +msgstr "Kustuta " + +msgid "Undelete selected record " +msgstr "Taasta valitud kirje " + +#, python-format +msgid "CSV Import: %s" +msgstr "CSV import: %s" + +msgid "_Auto-Detect" +msgstr "_Automaatne tuvastus" + +msgid "File to Import:" +msgstr "Imporditav fail:" + +msgid "Open..." +msgstr "Ava..." + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "Esmalt tuleb valida imporditav fail." + +msgid "Error opening CSV file" +msgstr "CSV faili avamisel tekkis viga" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Faili töötlemisel tekkis väljal %s viga." + +#, python-format +msgid "%d record imported." +msgstr "%d kirje imporditud." + +#, python-format +msgid "%d records imported." +msgstr "%d kirjed imporditud." + +msgid "Search" +msgstr "Otsi" + +#, python-format +msgid "Search %s" +msgstr "Otsi %s" + +msgid "Wizard" +msgstr "Nõustaja" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Ei saa kustutada nõustajat %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Loomise kuupäev" + +#, fuzzy +msgid "Edited by" +msgstr "Muuda" + +#, fuzzy +msgid "Edited at" +msgstr "Muuda" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" ei ole selle domeeni jaoks lubatud" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" on nõutud" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "\"%s\" väärtused ei ole lubatud" + +msgid "Pre-validation" +msgstr "Eelkontroll" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Pildi suurus" + +msgid "Width:" +msgstr "Laius:" + +msgid "Height:" +msgstr "Kõrgus:" + +msgid "PNG image (*.png)" +msgstr "PNG pilt (*.png)" + +msgid "Save As" +msgstr "Salvesta nimega" + +msgid "Image size too large." +msgstr "Pilt on liiga suur." + +msgid "Copy" +msgstr "Kopeeri" + +msgid "Paste" +msgstr "Aseta" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Ava filtrid" + +msgid "Show bookmarks of filters" +msgstr "Näita filtrite järjehoidjaid" + +msgid "Remove this bookmark" +msgstr "Eemalda järjehoidja" + +msgid "Bookmark this filter" +msgstr "Lisa filter järjehoidjatesse" + +msgid "Show active records" +msgstr "Näita aktiivseid kirjeid" + +msgid "Show inactive records" +msgstr "Näita mitteaktiivseid filtreid" + +msgid "Bookmark Name:" +msgstr "Järjehoidja nimi:" + +msgid "Find" +msgstr "Otsi" + +msgid "Today" +msgstr "Täna" + +msgid "go back" +msgstr "mine tagasi" + +msgid "go forward" +msgstr "mine edasi" + +msgid "previous year" +msgstr "eelmine aasta" + +msgid "next year" +msgstr "järgmine aasta" + +msgid "Day View" +msgstr "Päeva vaade" + +msgid "Week View" +msgstr "Nädala vaade" + +msgid "Month View" +msgstr "Kuu vaade" + +msgid "Week" +msgstr "Nädal" + +msgid "Select..." +msgstr "Vali..." + +msgid "Clear" +msgstr "Tühjenda" + +msgid "All files" +msgstr "Kõik failid" + +msgid "Show plain text" +msgstr "Näita tekstina" + +msgid "Add value" +msgstr "Lisa väärtus" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Eemalda \"%s\"" + +msgid "Images" +msgstr "Pildid" + +msgid "Add existing record" +msgstr "Lisa olemasolev kirje" + +msgid "Remove selected record" +msgstr "Eemalda valitud kirje" + +msgid "Open the record " +msgstr "Ava see kirje " + +msgid "Clear the field " +msgstr "Tühjenda väli " + +msgid "Search a record " +msgstr "Otsi kirjet " + +msgid "Edit selected record" +msgstr "Muuda valitud kirjet" + +msgid "Delete selected record" +msgstr "Kustuta valitud kirje" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Vali keel" + +msgid "Translation" +msgstr "Tõlge" + +msgid "Edit" +msgstr "Muuda" + +msgid "Fuzzy" +msgstr "Hägune" + +msgid "You need to save the record before adding translations." +msgstr "Kirje tuleb salvestada enne tõlke lisamist." + +msgid "No other language available." +msgstr "Teisi keeli pole saadaval." + +msgid "Translate view" +msgstr "Tõlgi vaade" diff --git a/tryton/tryton/data/locale/fa/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/fa/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..493409bdc9 --- /dev/null +++ b/tryton/tryton/data/locale/fa/LC_MESSAGES/tryton.po @@ -0,0 +1,1201 @@ +# Translations template for tryton. +# Copyright (C) 2018 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2018. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "فایل پیکربندی جایگزین را مشخص کنید" + +msgid "development mode" +msgstr "حالت پیشرفته" + +msgid "logging everything at INFO level" +msgstr "گزارشگیری همه چیز در سطح اطلاعات" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "تعیین سطح گزارش : اشکال زدایی، اطلاعات، هشدار، خطا، بحرانی" + +msgid "specify the login user" +msgstr "تعیین ورود به سیستم کاربر" + +msgid "specify the server hostname:port" +msgstr "تعیین نام میزبان سرور : درگاه" + +#, python-format +msgid "Unable to write config file %s." +msgstr "قادر به نوشتن در فایل پیکربندی \"%s\" نیست." + +#, python-format +msgid "Unable to set locale %s" +msgstr "قادر به تنظیم محلی: \"%s\" نیست" + +msgid ", " +msgstr "، " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "\"%s\" (%s)" + +msgid "Select your action" +msgstr "انتخاب اقدام شما" + +msgid "No action defined." +msgstr "اقدامی تعریف نشده." + +msgid "By: " +msgstr "توسط : " + +msgid "Selection" +msgstr "انتخاب" + +msgid "Cancel" +msgstr "انصراف" + +msgid "OK" +msgstr "بلی" + +msgid "Your selection:" +msgstr "انتخاب شما:" + +msgid "Select" +msgstr "انتخاب" + +msgid "Save" +msgstr "ذخیره" + +msgid "Save As..." +msgstr "ذخیره با نام..." + +msgid "Do you want to proceed?" +msgstr "آیا می خواهید پردازش شود؟" + +msgid "Always ignore this warning." +msgstr "همیشه این هشدار را نادیده بگیر." + +msgid "No" +msgstr "خیر" + +msgid "Yes" +msgstr "بله" + +msgid "Concurrency Exception" +msgstr "همزمانی استثنا" + +msgid "This record has been modified while you were editing it." +msgstr "این پرونده در حالی که شما در حال ویرایش آن بوید، اصلاح شد." + +msgid "Cancel saving" +msgstr "لغو ذخیره کردن" + +msgid "Compare" +msgstr "مقایسه" + +msgid "See the modified version" +msgstr "نسخه اصلاح شده را ببینید" + +msgid "Write Anyway" +msgstr "نوشتن بهرحال" + +msgid "Save your current version" +msgstr "نسخه فعلی خود را ذخیره کنید" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "مقایسه : %s" + +msgid "Close" +msgstr "بستن" + +msgid "Application Error" +msgstr "خطای برنامه" + +msgid "Report Bug" +msgstr "گزارش اشکال(باگ)" + +#, python-format +msgid "Check URL: %s" +msgstr "بررسی URL : %s" + +#, fuzzy +msgid "Unable to check for new version." +msgstr "قادر به بررسی نسخه جدید نیست" + +msgid "A new version is available!" +msgstr "نسخه جدید در دسترس می باشد!" + +msgid "Download" +msgstr "دانلود" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "جستجو..." + +msgid "Create..." +msgstr "ایجاد..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "قادر به جستجو برای تکمیل : \"%s\" نیست" + +msgid "Value" +msgstr "مقدار" + +msgid "Displayed value" +msgstr "مقدار نمایش داده شده" + +msgid "Format" +msgstr "قالب" + +msgid "Display format" +msgstr "فرمت نمایش" + +msgid "Open the calendar" +msgstr "بازکردن تقویم" + +msgid "Date Format" +msgstr "فرمت تاریخ" + +msgid "Displayed date format" +msgstr "فرمت تاریخ تمایش داده شده" + +msgid "y" +msgstr "y" + +msgid "True" +msgstr "درست" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "غلط" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "مرتبط" + +msgid "Edit..." +msgstr "ویرایش..." + +msgid "Attachments..." +msgstr "پیوست ها..." + +msgid "Notes..." +msgstr "یادداشت ها..." + +msgid "Actions..." +msgstr "اقدامات..." + +msgid "Relate..." +msgstr "مرتبط..." + +msgid "Report..." +msgstr "گزارش..." + +msgid "Print..." +msgstr "چاپ..." + +msgid "E-Mail..." +msgstr "ایمیل..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "انتخاب یک رنگ" + +msgid "Y" +msgstr "Y" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "w" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "ظاهر برنامه..." + +msgid "Toolbar" +msgstr "نوارابزار" + +msgid "Default" +msgstr "پیش فرض" + +msgid "Text and Icons" +msgstr "متن و آیکون ها" + +msgid "Text" +msgstr "متن" + +msgid "Icons" +msgstr "آیکون ها" + +msgid "Form" +msgstr "فرم" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "ذخیره حالت درختی" + +msgid "Spell Checking" +msgstr "غلط ‌یاب املایی" + +msgid "PDA Mode" +msgstr "حالت PDA" + +msgid "Search Limit..." +msgstr "جستجو محدود..." + +msgid "Check Version" +msgstr "بررسی نسخه" + +msgid "Options" +msgstr "گزینه‌ها" + +msgid "Keyboard Shortcuts..." +msgstr "میانبرهای صفحه کلید..." + +msgid "About..." +msgstr "درباره..." + +msgid "Help" +msgstr "راهنما" + +msgid "No result found." +msgstr "نتیجه ای پیدا نشد." + +msgid "Favorites" +msgstr "علاقمندی ها" + +msgid "Manage..." +msgstr "مدیریت..." + +msgid "Action" +msgstr "اقدام" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"اقدام زیر نیاز به بستن همه برگه ها دارد.\n" +"می خواهید ادامه دهید؟" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "ظاهر برنامه" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "_برگه قبلی" + +#, fuzzy +msgid "Next tab" +msgstr "_برگه بعدی" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "خروج" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "برش متن انتخابی" + +msgid "Copy selected text" +msgstr "کپی متن انتخابی" + +msgid "Paste copied text" +msgstr "چسباندن متن کپی شده" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "ایجاد رابطه جدید" + +msgid "Open/Search relation" +msgstr "بازکردن / ارتباط جستجو" + +msgid "List Entries" +msgstr "" + +#, fuzzy +msgid "Switch view" +msgstr "تعویض نمایه" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "بازکردن رابطه" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "برای حذف علامت سطر را بردارید" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "حرکت مکان نما" + +#, fuzzy +msgid "Move right" +msgstr "حرکت به راست" + +#, fuzzy +msgid "Move left" +msgstr "حرکت به چپ" + +msgid "Move up" +msgstr "حرکت به بالا" + +msgid "Move down" +msgstr "حرکت به پایین" + +msgid "Move up of one page" +msgstr "حرکت به بالای یک صفحه" + +msgid "Move down of one page" +msgstr "حرکت به پایین یک صفحه" + +msgid "Move to top" +msgstr "حرکت به بالا" + +msgid "Move to bottom" +msgstr "حرکت به انتها" + +msgid "Move to parent" +msgstr "انتقال به منبع" + +msgid "Select all" +msgstr "انتخاب همه" + +msgid "Unselect all" +msgstr "انتخاب نکردن همه" + +msgid "Select parent" +msgstr "انتخاب منبع" + +msgid "Select/Activate current row" +msgstr "انتخاب/فعال کردن ردیف جاری" + +msgid "Toggle selection" +msgstr "انتخاب زنجیر" + +msgid "Expand/Collapse" +msgstr "گشودن - بستن" + +msgid "Expand row" +msgstr "گسترش ردیف" + +msgid "Collapse row" +msgstr "بستن ردیف" + +msgid "Toggle row" +msgstr "زنجیر ردیف" + +msgid "Collapse all rows" +msgstr "بستن همه ردیف ها" + +msgid "Expand all rows" +msgstr "گشودن همه ردیف ها" + +msgid "Close Tab" +msgstr "بستن برگه" + +msgid "modularity, scalability and security" +msgstr "مدولاربودن، مقیاس پذیری و امنیت" + +msgid "translator-credits" +msgstr "پرویز همایون نژاد" + +#, python-format +msgid "Attachments (%s)" +msgstr "پیوستها (%s)" + +msgid "Profile Editor" +msgstr "ویرایشگر پروفایل" + +msgid "Profile" +msgstr "پروفایل" + +msgid "Add new profile" +msgstr "افزودن پرفایل جدید" + +msgid "Remove selected profile" +msgstr "حذف پرفایل انتخاب شده" + +msgid "Host:" +msgstr "هاست:" + +msgid "Database:" +msgstr "پایگاه داده:" + +msgid "Fetching databases list" +msgstr "وارد کردن فهرست های پایگاه داده" + +msgid "Username:" +msgstr "نام کاربری:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "نسخه ناسازگار سرور" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "نمی تواند به سرور متصل شود" + +msgid "Login" +msgstr "ورود به سیستم" + +msgid "_Cancel" +msgstr "_انصراف" + +msgid "Cancel connection to the Tryton server" +msgstr "لغو اتصال به سرور تریتون" + +msgid "C_onnect" +msgstr "_ارتباط" + +msgid "Connect the Tryton server" +msgstr "اتصال سرور تریتون" + +msgid "Profile:" +msgstr "پروفایل:" + +msgid "Host / Database information" +msgstr "اطلاعات میزبان / پایگاه داده" + +msgid "User name:" +msgstr "نام کاربر:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "قادر به حذف دستیار \"%s\" نیست" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "ایمیل %s" + +msgid "To:" +msgstr "به:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "موضوع :" + +#, fuzzy +msgid "Body" +msgstr "بدنه:" + +#, fuzzy +msgid "Reports" +msgstr "گزارش" + +#, fuzzy +msgid "Attachments" +msgstr "پیوست:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "انتخاب همه" + +#, fuzzy +msgid "Remove File" +msgstr "حذف " + +msgid "Add..." +msgstr "افزودن..." + +#, fuzzy +msgid "Preview" +msgstr "قبلی" + +msgid "Previous" +msgstr "قبلی" + +msgid "Next" +msgstr "بعدی" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "پیوستها (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "شما باید یک پرونده را انتخاب کنید." + +msgid "ID:" +msgstr "شناسه:" + +#, fuzzy +msgid "Created by:" +msgstr "تاریخ ایجاد" + +#, fuzzy +msgid "Created at:" +msgstr "تاریخ ایجاد" + +#, fuzzy +msgid "Edited by:" +msgstr "ویرایش" + +#, fuzzy +msgid "Edited at:" +msgstr "ویرایش" + +msgid "Model:" +msgstr "مدل:" + +msgid "Are you sure to remove this record?" +msgstr "برای حذف این پرونده اطمینان کامل دارید؟" + +msgid "Are you sure to remove those records?" +msgstr "برای حذف این پرونده ها اطمینان کامل دارید؟" + +msgid "Records not removed." +msgstr "پرونده ها حذف نشدند." + +msgid "Records removed." +msgstr "پرونده ها حذف شدند." + +msgid "Working now on the duplicated record(s)." +msgstr "هم اکنون کار بر روی پرونده(ها) تکثیر شده انجام می شود." + +msgid "Record saved." +msgstr "پرونده ذخیره شد." + +msgid " of " +msgstr " از " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"این پرونده هنوز اصلاح نشده است.\n" +"می‌خواهید آن را ذخیره کنید ؟" + +msgid "Launch action" +msgstr "انجام اقدام" + +msgid "Relate" +msgstr "مرتبط" + +msgid "Open related records" +msgstr "بازکردن پرونده های مرتبط" + +msgid "Report" +msgstr "گزارش" + +msgid "Open report" +msgstr "باز کردن گزارش" + +msgid "Print" +msgstr "چاپ" + +msgid "Print report" +msgstr "چاپ گزارش" + +msgid "_Copy URL" +msgstr "_رونوشت URL" + +msgid "Copy URL into clipboard" +msgstr "کپی URL درون کلیپبورد" + +msgid "Unknown" +msgstr "نامعلوم" + +msgid "Limit" +msgstr "محدود" + +msgid "Search Limit Settings" +msgstr "تنظیمات جستجو محدود" + +msgid "Limit:" +msgstr "محدودیت:" + +#, python-format +msgid "Notes (%s)" +msgstr "یادداشت ها(%s)" + +msgid "Edit User Preferences" +msgstr "ویرایش ظاهر برنامه کاربر" + +msgid "Preference" +msgstr "ظاهر" + +msgid "Revision" +msgstr "تجدید نظر" + +msgid "Select a revision" +msgstr "انتخاب یک تجدید نظر" + +msgid "Revision:" +msgstr "تجدید نظر:" + +msgid "_Switch View" +msgstr "_تعویض نمایه" + +msgid "Switch View" +msgstr "تعویض نمایه" + +msgid "_Previous" +msgstr "_قبلی" + +msgid "Previous Record" +msgstr "پرونده قبلی" + +msgid "_Next" +msgstr "_بعدی" + +msgid "Next Record" +msgstr "پرونده بعدی" + +msgid "_Search" +msgstr "_جستجو" + +msgid "_New" +msgstr "_جدید" + +msgid "Create a new record" +msgstr "ایجاد پرونده جدید" + +msgid "_Save" +msgstr "_ذخیره" + +msgid "Save this record" +msgstr "ذخیره این پرونده" + +msgid "_Reload/Undo" +msgstr "_بارگیری مجدد / واگرد" + +msgid "Reload/Undo" +msgstr "بارگیری مجدد / واگرد" + +msgid "_Duplicate" +msgstr "_تکثیر" + +msgid "_Delete..." +msgstr "_حذف..." + +msgid "View _Logs..." +msgstr "_مشاهده گزارشات..." + +msgid "Show revisions..." +msgstr "مشاهده تجدیدنظرها..." + +msgid "A_ttachments..." +msgstr "_پیوست..." + +msgid "Add an attachment to the record" +msgstr "پیوست را به پرونده اضافه کنید" + +msgid "_Notes..." +msgstr "_یادداشت ها..." + +msgid "Add a note to the record" +msgstr "یک یادداشت به پرونده اضافه کنید" + +msgid "_Actions..." +msgstr "_اقدامات..." + +msgid "_Relate..." +msgstr "_مرتبط..." + +msgid "_Report..." +msgstr "_گزارش..." + +msgid "_Print..." +msgstr "_چاپ..." + +msgid "_E-Mail..." +msgstr "_ایمیل..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "یک یادداشت به پرونده اضافه کنید" + +msgid "_Export Data..." +msgstr "_صادرات داده ها..." + +msgid "_Import Data..." +msgstr "_واردات داده ها..." + +msgid "Copy _URL..." +msgstr "_رونوشت URL..." + +msgid "_Close Tab" +msgstr "_بستن برگه" + +msgid "All fields" +msgstr "همه فیلد ها" + +msgid "_Add" +msgstr "_افزودن" + +msgid "_Remove" +msgstr "_حذف" + +msgid "_Clear" +msgstr "_پاکسازی" + +msgid "Fields selected" +msgstr "فیلد های انتخابی" + +msgid "CSV Parameters" +msgstr "مولفه های CSV" + +msgid "Delimiter:" +msgstr "جدا کننده:" + +msgid "Quote char:" +msgstr "کاراکترنقل قول:" + +msgid "Encoding:" +msgstr "رمزگذاری:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "نام فیلد" + +#, python-format +msgid "CSV Export: %s" +msgstr "صادرکردن(CSV) : %s" + +msgid "_Save Export" +msgstr "_ذخیره استخراج" + +#, fuzzy +msgid "_URL Export" +msgstr "_ذخیره استخراج" + +msgid "_Delete Export" +msgstr "_حذف استخراج" + +msgid "Predefined exports" +msgstr "استخراج از پیش تعریف شده" + +msgid "Name" +msgstr "نام" + +msgid "Open" +msgstr "باز کردن" + +#, fuzzy +msgid "Listed Records" +msgstr "ویرایش پرونده انتخابی " + +#, fuzzy +msgid "Selected Records" +msgstr "ویرایش پرونده انتخابی " + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "_اضافه کردن نام فیلدها" + +#, python-format +msgid "%s (string)" +msgstr "متنی %s" + +#, python-format +msgid "%s (model name)" +msgstr "نام مدل %s" + +#, python-format +msgid "%s (record name)" +msgstr "نام پرونده %s" + +msgid "What is the name of this export?" +msgstr "نام این فایل استخراجی چیست؟" + +#, python-format +msgid "Override '%s' definition?" +msgstr "بازنویسی %s تعاریف؟" + +#, python-format +msgid "%d record saved." +msgstr "%d پرونده ذخیره شد." + +#, python-format +msgid "%d records saved." +msgstr "%d پرونده های ذخیره شده." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"عملیات ناموفق بود.\n" +"پیام خطا:\n" +"%s" + +msgid "Error" +msgstr "خطا" + +msgid "Link" +msgstr "پیوند" + +msgid "Delete" +msgstr "حذف" + +msgid "New" +msgstr "جدید" + +msgid "Switch" +msgstr "تعویض" + +msgid "Add" +msgstr "افزودن" + +msgid "Remove " +msgstr "حذف " + +msgid "Create a new record " +msgstr "ایجاد یک پرونده جدید " + +msgid "Delete selected record " +msgstr "حذف پرونده انتخابی " + +msgid "Undelete selected record " +msgstr "بازگردانی فایل انتخابی " + +#, python-format +msgid "CSV Import: %s" +msgstr "وارد کردن (CSV) : %s" + +msgid "_Auto-Detect" +msgstr "_شناسایی - خودکار" + +msgid "File to Import:" +msgstr "فایل برای واردکردن:" + +msgid "Open..." +msgstr "باز کردن..." + +msgid "Lines to Skip:" +msgstr "چشم پوشی از سطرهای:" + +msgid "You must select an import file first." +msgstr "شما باید ابتدا فایل ورودی را انتخاب کنید." + +msgid "Error opening CSV file" +msgstr "خطا در بازگشایی فایل CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "خطای پردازش فایل واقع در فیلد : \"%s\"." + +#, python-format +msgid "%d record imported." +msgstr "%d پرونده وارد شد." + +#, python-format +msgid "%d records imported." +msgstr "%d پرونده ها وارد شدند." + +msgid "Search" +msgstr "جستجو" + +#, python-format +msgid "Search %s" +msgstr "جستجو %s" + +msgid "Wizard" +msgstr "دستیار" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "قادر به حذف دستیار \"%s\" نیست" + +msgid "ID" +msgstr "شناسه" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "تاریخ ایجاد" + +#, fuzzy +msgid "Edited by" +msgstr "ویرایش" + +#, fuzzy +msgid "Edited at" +msgstr "ویرایش" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "قادربه دریافت حالت نمایش درختی نیست برای %s" + +msgid "Unable to set view tree state" +msgstr "قادر به تنظیم حالت نمایش درختی نیست" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "با توجه به دامنه : \"%s\" معتبر نیست" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" مورد نیاز است" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "مقادیر \"%s\" معتبر نیستند" + +msgid "Pre-validation" +msgstr "پیش-اعتبارسنجی" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "اندازه تصویر" + +msgid "Width:" +msgstr "عرض:" + +msgid "Height:" +msgstr "ارتفاع:" + +msgid "PNG image (*.png)" +msgstr "PNG image (*.png)" + +msgid "Save As" +msgstr "ذخیره با نام" + +msgid "Image size too large." +msgstr "اندازه تصویر بیش از حد بزرگ است." + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "بازکردن فیلترها" + +msgid "Show bookmarks of filters" +msgstr "نمایش بوک مارک های فیلترها" + +msgid "Remove this bookmark" +msgstr "حذف این بوک مارک" + +msgid "Bookmark this filter" +msgstr "بوک مارک این فیلتر" + +msgid "Show active records" +msgstr "نمایش پرونده های فعال" + +msgid "Show inactive records" +msgstr "نمایش پرونده های غیرفعال" + +msgid "Bookmark Name:" +msgstr "نام بوک مارک :" + +msgid "Find" +msgstr "یافتن" + +msgid "Today" +msgstr "امروز" + +msgid "go back" +msgstr "بازگشت" + +msgid "go forward" +msgstr "برو جلو" + +msgid "previous year" +msgstr "سال قبل" + +msgid "next year" +msgstr "سال بعد" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "نمایش هفته" + +msgid "Month View" +msgstr "نمایش ماه" + +msgid "Week" +msgstr "هفته" + +msgid "Select..." +msgstr "انتخاب..." + +msgid "Clear" +msgstr "پاک کردن" + +msgid "All files" +msgstr "همه فایل ها" + +msgid "Show plain text" +msgstr "نمایش متن ساده" + +msgid "Add value" +msgstr "افزودن مقدار" + +#, python-format +msgid "Remove \"%s\"" +msgstr "حذف \"%s\"" + +msgid "Images" +msgstr "تصاویر" + +msgid "Add existing record" +msgstr "افزودن پرونده موجود" + +msgid "Remove selected record" +msgstr "حذف پرونده انتخابی" + +msgid "Open the record " +msgstr "بازکردن پرونده " + +msgid "Clear the field " +msgstr "پاکسازی فیلد " + +msgid "Search a record " +msgstr "جستجوی یک پرونده " + +#, fuzzy +msgid "Edit selected record" +msgstr "ویرایش پرونده انتخابی " + +#, fuzzy +msgid "Delete selected record" +msgstr "حذف پرونده انتخابی" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "ترجمه" + +msgid "Edit" +msgstr "ویرایش" + +msgid "Fuzzy" +msgstr "نامانوس" + +msgid "You need to save the record before adding translations." +msgstr "قبل از اضافه کردن ترجمه ها شما باید پرونده را ذخیره کنید." + +msgid "No other language available." +msgstr "هیچ زبان دیگری در دسترس نیست." + +msgid "Translate view" +msgstr "نمایش ترجمه" diff --git a/tryton/tryton/data/locale/fi/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/fi/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..c4d4470363 --- /dev/null +++ b/tryton/tryton/data/locale/fi/LC_MESSAGES/tryton.po @@ -0,0 +1,1167 @@ +# Translations template for tryton. +# Copyright (C) 2018 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2018. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "" + +msgid "development mode" +msgstr "" + +msgid "logging everything at INFO level" +msgstr "" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +msgid "specify the login user" +msgstr "" + +msgid "specify the server hostname:port" +msgstr "" + +#, python-format +msgid "Unable to write config file %s." +msgstr "" + +#, python-format +msgid "Unable to set locale %s" +msgstr "" + +#, fuzzy +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "" + +msgid "No action defined." +msgstr "" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save As..." +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + +msgid "Always ignore this warning." +msgstr "" + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "" + +msgid "Save your current version" +msgstr "" + +#, python-format +msgid "Compare: %s" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Application Error" +msgstr "" + +msgid "Report Bug" +msgstr "" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +#, fuzzy +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Displayed value" +msgstr "" + +msgid "Format" +msgstr "" + +msgid "Display format" +msgstr "" + +msgid "Open the calendar" +msgstr "" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "" + +msgid "Edit..." +msgstr "" + +msgid "Attachments..." +msgstr "" + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "" + +msgid "Relate..." +msgstr "" + +msgid "Report..." +msgstr "" + +msgid "Print..." +msgstr "" + +msgid "E-Mail..." +msgstr "" + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "M" +msgstr "" + +msgid "w" +msgstr "" + +msgid "d" +msgstr "" + +msgid "h" +msgstr "" + +msgid "m" +msgstr "" + +msgid "s" +msgstr "" + +msgid "Preferences..." +msgstr "" + +msgid "Toolbar" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Text and Icons" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Icons" +msgstr "" + +msgid "Form" +msgstr "" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "" + +msgid "Options" +msgstr "" + +msgid "Keyboard Shortcuts..." +msgstr "" + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "" + +msgid "Copy selected text" +msgstr "" + +msgid "Paste copied text" +msgstr "" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "" + +msgid "Database:" +msgstr "" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "" + +msgid "Incompatible version of the server." +msgstr "" + +msgid "Could not connect to the server." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "_Cancel" +msgstr "" + +msgid "Cancel connection to the Tryton server" +msgstr "" + +msgid "C_onnect" +msgstr "" + +msgid "Connect the Tryton server" +msgstr "" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "" + +msgid "Unable to complete email entry" +msgstr "" + +#, python-format +msgid "E-mail %s" +msgstr "" + +msgid "To:" +msgstr "" + +msgid "Cc:" +msgstr "" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "" + +msgid "Body" +msgstr "" + +msgid "Reports" +msgstr "" + +msgid "Attachments" +msgstr "" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Select File" +msgstr "" + +msgid "Remove File" +msgstr "" + +msgid "Add..." +msgstr "" + +msgid "Preview" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" + +#, python-format +msgid "Attachment (%s)" +msgstr "" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "" + +msgid "ID:" +msgstr "" + +msgid "Created by:" +msgstr "" + +msgid "Created at:" +msgstr "" + +msgid "Edited by:" +msgstr "" + +msgid "Edited at:" +msgstr "" + +msgid "Model:" +msgstr "" + +msgid "Are you sure to remove this record?" +msgstr "" + +msgid "Are you sure to remove those records?" +msgstr "" + +msgid "Records not removed." +msgstr "" + +msgid "Records removed." +msgstr "" + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "" + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" + +msgid "Launch action" +msgstr "" + +msgid "Relate" +msgstr "" + +msgid "Open related records" +msgstr "" + +msgid "Report" +msgstr "" + +msgid "Open report" +msgstr "" + +msgid "Print" +msgstr "" + +msgid "Print report" +msgstr "" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Search Limit Settings" +msgstr "" + +msgid "Limit:" +msgstr "" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "" + +msgid "Preference" +msgstr "" + +msgid "Revision" +msgstr "" + +msgid "Select a revision" +msgstr "" + +msgid "Revision:" +msgstr "" + +msgid "_Switch View" +msgstr "" + +msgid "Switch View" +msgstr "" + +msgid "_Previous" +msgstr "" + +msgid "Previous Record" +msgstr "" + +msgid "_Next" +msgstr "" + +msgid "Next Record" +msgstr "" + +msgid "_Search" +msgstr "" + +msgid "_New" +msgstr "" + +msgid "Create a new record" +msgstr "" + +msgid "_Save" +msgstr "" + +msgid "Save this record" +msgstr "" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "" + +msgid "_Delete..." +msgstr "" + +msgid "View _Logs..." +msgstr "" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "" + +msgid "Add an attachment to the record" +msgstr "" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "" + +msgid "_Relate..." +msgstr "" + +msgid "_Report..." +msgstr "" + +msgid "_Print..." +msgstr "" + +msgid "_E-Mail..." +msgstr "" + +msgid "Send an e-mail using the record" +msgstr "" + +msgid "_Export Data..." +msgstr "" + +msgid "_Import Data..." +msgstr "" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "" + +msgid "All fields" +msgstr "" + +msgid "_Add" +msgstr "" + +msgid "_Remove" +msgstr "" + +msgid "_Clear" +msgstr "" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "" + +msgid "_URL Export" +msgstr "" + +msgid "_Delete Export" +msgstr "" + +msgid "Predefined exports" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Listed Records" +msgstr "" + +msgid "Selected Records" +msgstr "" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "" + +#, python-format +msgid "%d records saved." +msgstr "" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Link" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "New" +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Remove " +msgstr "" + +msgid "Create a new record " +msgstr "" + +msgid "Delete selected record " +msgstr "" + +msgid "Undelete selected record " +msgstr "" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "" + +msgid "File to Import:" +msgstr "" + +msgid "Open..." +msgstr "" + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "" + +msgid "Error opening CSV file" +msgstr "" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "" + +#, python-format +msgid "%d record imported." +msgstr "" + +#, python-format +msgid "%d records imported." +msgstr "" + +msgid "Search" +msgstr "" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Create by" +msgstr "" + +msgid "Created at" +msgstr "" + +msgid "Edited by" +msgstr "" + +msgid "Edited at" +msgstr "" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +#, fuzzy +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "" + +msgid "Image size too large." +msgstr "" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +#, fuzzy +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "" + +msgid "Month View" +msgstr "" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "" + +msgid "All files" +msgstr "" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Add existing record" +msgstr "" + +msgid "Remove selected record" +msgstr "" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +msgid "Edit selected record" +msgstr "" + +msgid "Delete selected record" +msgstr "" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Fuzzy" +msgstr "" + +msgid "You need to save the record before adding translations." +msgstr "" + +msgid "No other language available." +msgstr "" + +msgid "Translate view" +msgstr "" diff --git a/tryton/tryton/data/locale/fr/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/fr/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..d00d7a8b18 --- /dev/null +++ b/tryton/tryton/data/locale/fr/LC_MESSAGES/tryton.po @@ -0,0 +1,1175 @@ +# French (France) translations for tryton. +# Copyright (C) 2008-2013 B2CK +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2008-2013. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "spécifier un fichier de configuration alternatif" + +msgid "development mode" +msgstr "mode développement" + +msgid "logging everything at INFO level" +msgstr "logger tout au niveau INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "spécifie le niveau de log : DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "spécifier l'identifiant de l'utilisateur" + +msgid "specify the server hostname:port" +msgstr "spécifier le nom du serveur:port" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Impossible d'écrire le fichier de configuration %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Impossible de sélectionner la locale %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Sélectionnez votre action" + +msgid "No action defined." +msgstr "Pas d'action définie." + +msgid "By: " +msgstr "Par : " + +msgid "Selection" +msgstr "Sélection" + +msgid "Cancel" +msgstr "Annuler" + +msgid "OK" +msgstr "Valider" + +msgid "Your selection:" +msgstr "Votre sélection :" + +msgid "Select" +msgstr "Sélectionner" + +msgid "Save" +msgstr "Sauver" + +msgid "Save As..." +msgstr "Enregistrer sous..." + +msgid "Do you want to proceed?" +msgstr "Souhaitez-vous continuer ?" + +msgid "Always ignore this warning." +msgstr "Toujours ignorer cet avertissement." + +msgid "No" +msgstr "Non" + +msgid "Yes" +msgstr "Oui" + +msgid "Concurrency Exception" +msgstr "Erreur d'accès concurrent" + +msgid "This record has been modified while you were editing it." +msgstr "" +"Cet enregistrement a été modifié pendant que vous étiez en train de " +"l'éditer." + +msgid "Cancel saving" +msgstr "Annuler la sauvegarde" + +msgid "Compare" +msgstr "Comparer" + +msgid "See the modified version" +msgstr "Voir la version modifiée" + +msgid "Write Anyway" +msgstr "Écraser" + +msgid "Save your current version" +msgstr "Sauver votre version actuelle" + +#, python-format +msgid "Compare: %s" +msgstr "Comparer : %s" + +msgid "Close" +msgstr "Fermer" + +msgid "Application Error" +msgstr "Erreur applicative" + +msgid "Report Bug" +msgstr "Rapporter un bogue" + +#, python-format +msgid "Check URL: %s" +msgstr "Vérification de l'URL : %s" + +msgid "Unable to check for new version." +msgstr "Impossible de vérifier les nouvelles versions." + +msgid "A new version is available!" +msgstr "Un nouvelle version est disponible !" + +msgid "Download" +msgstr "Télécharger" + +msgid "Too many requests. Try again later." +msgstr "Trop de demandes. Réessayez plus tard." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Recherche..." + +msgid "Create..." +msgstr "Créer..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Impossible de rechercher la complétion de %s" + +msgid "Value" +msgstr "Valeur" + +msgid "Displayed value" +msgstr "Valeur affichée" + +msgid "Format" +msgstr "Format" + +msgid "Display format" +msgstr "Format d'affichage" + +msgid "Open the calendar" +msgstr "Ouvrir le calendrier" + +msgid "Date Format" +msgstr "Format de date" + +msgid "Displayed date format" +msgstr "Format de date affichée" + +msgid "y" +msgstr "y" + +msgid "True" +msgstr "Vrai" + +msgid "t" +msgstr "v" + +msgid "False" +msgstr "Faux" + +msgid "Digits" +msgstr "Chiffres" + +msgid "The number of decimal" +msgstr "Le nombre de décimal" + +msgid "Template" +msgstr "Modèle" + +msgid "Edit..." +msgstr "Éditer..." + +msgid "Attachments..." +msgstr "Attachements..." + +msgid "Notes..." +msgstr "Notes..." + +msgid "Actions..." +msgstr "Actions..." + +msgid "Relate..." +msgstr "Relation..." + +msgid "Report..." +msgstr "Rapport..." + +msgid "Print..." +msgstr "Imprimer..." + +msgid "E-Mail..." +msgstr "E-mail..." + +msgid "Bold" +msgstr "Gras" + +msgid "Italic" +msgstr "Italique" + +msgid "Underline" +msgstr "Souligné" + +msgid "Align Left" +msgstr "Aligner à gauche" + +msgid "Align Center" +msgstr "Centrer" + +msgid "Align Right" +msgstr "Aligner à droite" + +msgid "Justify" +msgstr "Justifier" + +msgid "Foreground Color" +msgstr "Couleur de premier plan" + +msgid "Select a color" +msgstr "Sélectionner une couleur" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "S" + +msgid "d" +msgstr "j" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Préférences..." + +msgid "Toolbar" +msgstr "Barre d'outils" + +msgid "Default" +msgstr "Défaut" + +msgid "Text and Icons" +msgstr "Textes et Icônes" + +msgid "Text" +msgstr "Textes" + +msgid "Icons" +msgstr "Icônes" + +msgid "Form" +msgstr "Formulaire" + +msgid "Save Column Width" +msgstr "Enregistrer la largeur de colonne" + +msgid "Save Tree State" +msgstr "Sauver l'état des arbres" + +msgid "Spell Checking" +msgstr "Vérifier l'orthographe" + +msgid "PDA Mode" +msgstr "Mode PDA" + +msgid "Search Limit..." +msgstr "Limite la recherche à..." + +msgid "Check Version" +msgstr "Vérifier la version" + +msgid "Options" +msgstr "Options" + +msgid "Keyboard Shortcuts..." +msgstr "Raccourcis clavier..." + +msgid "About..." +msgstr "À Propos..." + +msgid "Help" +msgstr "Aide" + +msgid "No result found." +msgstr "Aucun résultat trouvé." + +msgid "Favorites" +msgstr "Favoris" + +msgid "Manage..." +msgstr "Gérer..." + +msgid "Action" +msgstr "Actions" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"L'action suivante nécessite de fermer tous les onglets.\n" +"Voulez-vous continuer ?" + +msgid "Application Shortcuts" +msgstr "Raccourcis de l'application" + +msgid "Global" +msgstr "Global" + +msgid "Preferences" +msgstr "Préférences" + +msgid "Search menu" +msgstr "Rechercher dans le menu" + +msgid "Toggle menu" +msgstr "Basculer le menu" + +msgid "Previous tab" +msgstr "Onglet précédent" + +msgid "Next tab" +msgstr "Onglet suivant" + +msgid "Shortcuts" +msgstr "Raccourcis" + +msgid "Quit" +msgstr "Quitter" + +msgid "Edition Shortcuts" +msgstr "Éditer les raccourcis" + +msgid "Text Entries" +msgstr "Entrées de texte" + +msgid "Cut selected text" +msgstr "Couper le texte sélectionné" + +msgid "Copy selected text" +msgstr "Copier le texte sélectionné" + +msgid "Paste copied text" +msgstr "Coller le texte copié" + +msgid "Next entry" +msgstr "Entrée suivante" + +msgid "Previous entry" +msgstr "Entrée précédente" + +msgid "Relation Entries" +msgstr "Entrées de relation" + +msgid "Create new relation" +msgstr "Créer une nouvelle relation" + +msgid "Open/Search relation" +msgstr "Ouvrir/Chercher une relation" + +msgid "List Entries" +msgstr "Entrées de liste" + +msgid "Switch view" +msgstr "Basculer la vue" + +msgid "Create/Select new line" +msgstr "Créer/Sélectionner une nouvelle ligne" + +msgid "Open relation" +msgstr "Ouvrir une relation" + +msgid "Mark line for deletion/removal" +msgstr "Marquer la ligne pour suppression/enlèvement" + +msgid "Mark line for removal" +msgstr "Marquer la ligne pour enlèvement" + +msgid "Unmark line for deletion" +msgstr "Annuler le marquage de la ligne pour suppression" + +msgid "List/Tree Shortcuts" +msgstr "Raccourcis de liste/arbre" + +msgid "Move Cursor" +msgstr "Déplacer le curseur" + +msgid "Move right" +msgstr "Déplacer vers la droite" + +msgid "Move left" +msgstr "Déplacer vers la gauche" + +msgid "Move up" +msgstr "Déplacer vers le haut" + +msgid "Move down" +msgstr "Déplacer vers le bas" + +msgid "Move up of one page" +msgstr "Déplacer vers le haut d'une page" + +msgid "Move down of one page" +msgstr "Déplacer vers le bas d'une page" + +msgid "Move to top" +msgstr "Déplacer au dessus" + +msgid "Move to bottom" +msgstr "Déplacer en bas" + +msgid "Move to parent" +msgstr "Déplacer au parent" + +msgid "Select all" +msgstr "Tout sélectionner" + +msgid "Unselect all" +msgstr "Désélectionner tout" + +msgid "Select parent" +msgstr "Sélectionner le parent" + +msgid "Select/Activate current row" +msgstr "Sélectionner/Activer la ligne courante" + +msgid "Toggle selection" +msgstr "Basculer la sélection" + +msgid "Expand/Collapse" +msgstr "Déplier/Replier" + +msgid "Expand row" +msgstr "Déplier la ligne" + +msgid "Collapse row" +msgstr "Replier la ligne" + +msgid "Toggle row" +msgstr "Basculer la ligne" + +msgid "Collapse all rows" +msgstr "Replier toutes les lignes" + +msgid "Expand all rows" +msgstr "Déplier toutes les lignes" + +msgid "Close Tab" +msgstr "Fermer l'onglet" + +msgid "modularity, scalability and security" +msgstr "modularité, évolutivité et sécurité" + +msgid "translator-credits" +msgstr "" +"Cédric Krier\n" +"Nicolas Évrard" + +#, python-format +msgid "Attachments (%s)" +msgstr "Attachements (%s)" + +msgid "Profile Editor" +msgstr "Éditeur de profil" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "Ajouter un nouveau profile" + +msgid "Remove selected profile" +msgstr "Supprimer le profile sélectionné" + +msgid "Host:" +msgstr "Hôte :" + +msgid "Database:" +msgstr "Base de données :" + +msgid "Fetching databases list" +msgstr "Récupération de la liste des bases de données" + +msgid "Username:" +msgstr "Nom d'utilisateur :" + +msgid "Incompatible version of the server." +msgstr "Version du serveur incompatible." + +msgid "Could not connect to the server." +msgstr "Impossible de se connecter au serveur." + +msgid "Login" +msgstr "Connexion" + +msgid "_Cancel" +msgstr "A_nnuler" + +msgid "Cancel connection to the Tryton server" +msgstr "Annuler la connexion au serveur Tryton" + +msgid "C_onnect" +msgstr "C_onnecter" + +msgid "Connect the Tryton server" +msgstr "Se connecter au serveur Tryton" + +msgid "Profile:" +msgstr "Profil :" + +msgid "Host / Database information" +msgstr "Hôte / Port" + +msgid "User name:" +msgstr "Nom d'utilisateur :" + +msgid "Unable to complete email entry" +msgstr "Impossible de compléter l'entrée d'e-mail" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "À :" + +msgid "Cc:" +msgstr "Cc :" + +msgid "Bcc:" +msgstr "Cci :" + +msgid "Subject:" +msgstr "Objet :" + +msgid "Body" +msgstr "Corps" + +msgid "Reports" +msgstr "Rapports" + +msgid "Attachments" +msgstr "Pièces jointes" + +msgid "Files" +msgstr "Fichiers" + +msgid "Send" +msgstr "Envoyer" + +msgid "Select File" +msgstr "Choisir un fichier" + +msgid "Remove File" +msgstr "Supprimer le fichier" + +msgid "Add..." +msgstr "Ajouter..." + +msgid "Preview" +msgstr "Aperçu" + +msgid "Previous" +msgstr "Précédent" + +msgid "Next" +msgstr "Suivant" + +#, python-format +msgid "Attachment (%s)" +msgstr "Attachement (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Note (%d/%d)" + +msgid "You have to select one record." +msgstr "Vous devez sélectionner un enregistrement." + +msgid "ID:" +msgstr "ID :" + +msgid "Created by:" +msgstr "Crée par :" + +msgid "Created at:" +msgstr "Crée le :" + +msgid "Edited by:" +msgstr "Édité par :" + +msgid "Edited at:" +msgstr "Édité le :" + +msgid "Model:" +msgstr "Modèle :" + +msgid "Are you sure to remove this record?" +msgstr "Êtes-vous sûr de vouloir supprimer cet enregistrement ?" + +msgid "Are you sure to remove those records?" +msgstr "Êtes-vous sûr de vouloir supprimer ces enregistrements ?" + +msgid "Records not removed." +msgstr "Enregistrements non supprimés." + +msgid "Records removed." +msgstr "Enregistrements supprimés." + +msgid "Working now on the duplicated record(s)." +msgstr "Sur les enregistrement(s) dupliqué(s) maintenant." + +msgid "Record saved." +msgstr "Enregistrement sauvegardé." + +msgid " of " +msgstr " de " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Cet enregistrement a été modifié\n" +"voulez-vous le sauvegarder ?" + +msgid "Launch action" +msgstr "Lancer une action" + +msgid "Relate" +msgstr "Relation" + +msgid "Open related records" +msgstr "Ouvrir les enregistrements liés" + +msgid "Report" +msgstr "Rapport" + +msgid "Open report" +msgstr "Ouvrir un rapport" + +msgid "Print" +msgstr "Imprimer" + +msgid "Print report" +msgstr "Imprimer un rapport" + +msgid "_Copy URL" +msgstr "_Copier l'URL" + +msgid "Copy URL into clipboard" +msgstr "Copier l'URL dans le presse-papiers" + +msgid "Unknown" +msgstr "Inconnu" + +msgid "Limit" +msgstr "Limite" + +msgid "Search Limit Settings" +msgstr "Limite de recherche" + +msgid "Limit:" +msgstr "Limite :" + +#, python-format +msgid "Notes (%s)" +msgstr "Notes (%s)" + +msgid "Edit User Preferences" +msgstr "Éditer les préférences utilisateur" + +msgid "Preference" +msgstr "Préférence" + +msgid "Revision" +msgstr "Révision" + +msgid "Select a revision" +msgstr "Sélectionner une révision" + +msgid "Revision:" +msgstr "Révision :" + +msgid "_Switch View" +msgstr "_Changer de vue" + +msgid "Switch View" +msgstr "Changer de vue" + +msgid "_Previous" +msgstr "_Précédent" + +msgid "Previous Record" +msgstr "Enregistrement précédent" + +msgid "_Next" +msgstr "Sui_vant" + +msgid "Next Record" +msgstr "Enregistrement suivant" + +msgid "_Search" +msgstr "_Recherche" + +msgid "_New" +msgstr "_Nouveau" + +msgid "Create a new record" +msgstr "Créer un nouvel enregistrement" + +msgid "_Save" +msgstr "_Sauver" + +msgid "Save this record" +msgstr "Sauver cet enregistrement" + +msgid "_Reload/Undo" +msgstr "_Recharger/Annuler" + +msgid "Reload/Undo" +msgstr "Recharger/Annuler" + +msgid "_Duplicate" +msgstr "_Dupliquer" + +msgid "_Delete..." +msgstr "Su_pprimer..." + +msgid "View _Logs..." +msgstr "Voir les _Logs..." + +msgid "Show revisions..." +msgstr "Afficher les révisions..." + +msgid "A_ttachments..." +msgstr "_Pièce jointes..." + +msgid "Add an attachment to the record" +msgstr "Ajouter un attachement à l'enregistrement" + +msgid "_Notes..." +msgstr "_Notes..." + +msgid "Add a note to the record" +msgstr "Ajouter une note à l'enregistrement" + +msgid "_Actions..." +msgstr "_Actions..." + +msgid "_Relate..." +msgstr "_Relation..." + +msgid "_Report..." +msgstr "_Rapport..." + +msgid "_Print..." +msgstr "_Imprimer..." + +msgid "_E-Mail..." +msgstr "_E-mail..." + +msgid "Send an e-mail using the record" +msgstr "Envoyer un e-mail en utilisant l'enregistrement" + +msgid "_Export Data..." +msgstr "_Export de données..." + +msgid "_Import Data..." +msgstr "_Import de données..." + +msgid "Copy _URL..." +msgstr "Copier l'_URL..." + +msgid "_Close Tab" +msgstr "_Fermer l'onglet" + +msgid "All fields" +msgstr "Tout les champs" + +msgid "_Add" +msgstr "_Ajouter" + +msgid "_Remove" +msgstr "_Enlever" + +msgid "_Clear" +msgstr "_Effacer" + +msgid "Fields selected" +msgstr "Champs sélectionnés" + +msgid "CSV Parameters" +msgstr "Paramètres CSV" + +msgid "Delimiter:" +msgstr "Délimiteur :" + +msgid "Quote char:" +msgstr "Caractère de guillemet :" + +msgid "Encoding:" +msgstr "Codage :" + +msgid "Use locale format" +msgstr "Utiliser le format local" + +msgid "Field name" +msgstr "Nom du champ" + +#, python-format +msgid "CSV Export: %s" +msgstr "Export CSV : %s" + +msgid "_Save Export" +msgstr "_Sauver l'exportation" + +msgid "_URL Export" +msgstr "_URL d'exportation" + +msgid "_Delete Export" +msgstr "_Supprimer l'exportation" + +msgid "Predefined exports" +msgstr "Exportations prédéfinies" + +msgid "Name" +msgstr "Nom" + +msgid "Open" +msgstr "Ouvrir" + +msgid "Listed Records" +msgstr "Enregistrements listés" + +msgid "Selected Records" +msgstr "Enregistrements sélectionnés" + +msgid "Ignore search limit" +msgstr "Ignorer la limite de recherche" + +msgid "Add field names" +msgstr "Ajouter les noms des champs" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nom de l'enregistrement)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (nom de l'enregistrement)" + +msgid "What is the name of this export?" +msgstr "Quel est le nom de cet export ?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Surcharger la définition « %s » ?" + +#, python-format +msgid "%d record saved." +msgstr "%d enregistrement sauvé." + +#, python-format +msgid "%d records saved." +msgstr "%d enregistrements sauvés." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Échec de l'opération.\n" +"Message d'erreur :\n" +"%s" + +msgid "Error" +msgstr "Erreur" + +msgid "Link" +msgstr "Lien" + +msgid "Delete" +msgstr "Supprimer" + +msgid "New" +msgstr "Nouveau" + +msgid "Switch" +msgstr "Basculer" + +msgid "Add" +msgstr "Ajouter" + +msgid "Remove " +msgstr "Supprimer " + +msgid "Create a new record " +msgstr "Créer un nouvel enregistrement" + +msgid "Delete selected record " +msgstr "Supprimer l'enregistrement sélectionné" + +msgid "Undelete selected record " +msgstr "Restaurer l'enregistrement sélectionné" + +#, python-format +msgid "CSV Import: %s" +msgstr "Import CSV : %s" + +msgid "_Auto-Detect" +msgstr "Détection _automatique" + +msgid "File to Import:" +msgstr "Fichier à importer :" + +msgid "Open..." +msgstr "Ouvrir..." + +msgid "Lines to Skip:" +msgstr "Lignes à ignorer :" + +msgid "You must select an import file first." +msgstr "Vous devez d'abord sélectionner un fichier d'import." + +msgid "Error opening CSV file" +msgstr "Erreur d'ouverture du fichier CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Erreur lors du traitement du fichier du champ %s." + +#, python-format +msgid "%d record imported." +msgstr "%d enregistrement importé." + +#, python-format +msgid "%d records imported." +msgstr "%d enregistrements importés." + +msgid "Search" +msgstr "Rechercher" + +#, python-format +msgid "Search %s" +msgstr "Rechercher %s" + +msgid "Wizard" +msgstr "Assistant" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Impossible de supprimer l'assistant %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Crée par" + +msgid "Created at" +msgstr "Crée le" + +msgid "Edited by" +msgstr "Édité par" + +msgid "Edited at" +msgstr "Édité le" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Impossible d'obtenir l'état de la vue arbre de %s" + +msgid "Unable to set view tree state" +msgstr "Impossible de définir l'état de la vue arbre" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "« %s » n'est pas valide selon son domaine." + +#, python-format +msgid "\"%s\" is required." +msgstr "« %s » est requis." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Les valeurs de « %s » ne sont pas valides." + +msgid "Pre-validation" +msgstr "Pré-validation" + +msgid ":" +msgstr " :" + +msgid "Image Size" +msgstr "Taille de l'image" + +msgid "Width:" +msgstr "Largeur :" + +msgid "Height:" +msgstr "Hauteur :" + +msgid "PNG image (*.png)" +msgstr "image PNG (*.png)" + +msgid "Save As" +msgstr "Enregistrer sous" + +msgid "Image size too large." +msgstr "Taille de l'image trop grande." + +msgid "Copy" +msgstr "Copier" + +msgid "Paste" +msgstr "Coller" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Ouvrir les filtres" + +msgid "Show bookmarks of filters" +msgstr "Montrer les marques-pages des filtres" + +msgid "Remove this bookmark" +msgstr "Supprimer ce marque-page" + +msgid "Bookmark this filter" +msgstr "Sauver ce filtre" + +msgid "Show active records" +msgstr "Montrer les enregistrements actifs" + +msgid "Show inactive records" +msgstr "Montrer les enregistrements inactifs" + +msgid "Bookmark Name:" +msgstr "Nom du marque-page :" + +msgid "Find" +msgstr "Chercher" + +msgid "Today" +msgstr "Aujourd'hui" + +msgid "go back" +msgstr "revenir" + +msgid "go forward" +msgstr "avancer" + +msgid "previous year" +msgstr "année précédente" + +msgid "next year" +msgstr "année suivante" + +msgid "Day View" +msgstr "Vue jour" + +msgid "Week View" +msgstr "Vue semaine" + +msgid "Month View" +msgstr "Vue mois" + +msgid "Week" +msgstr "Semaine" + +msgid "Select..." +msgstr "Sélectionner..." + +msgid "Clear" +msgstr "Effacer" + +msgid "All files" +msgstr "Tous les fichiers" + +msgid "Show plain text" +msgstr "Montrer en text clair" + +msgid "Add value" +msgstr "Ajouter une valeur" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Supprimer « %s »" + +msgid "Images" +msgstr "Images" + +msgid "Add existing record" +msgstr "Ajouter un enregistrement existant" + +msgid "Remove selected record" +msgstr "Supprimer l'enregistrement sélectionné" + +msgid "Open the record " +msgstr "Ouvrir l'enregistrement " + +msgid "Clear the field " +msgstr "Effacer le champ " + +msgid "Search a record " +msgstr "Chercher un enregistrement" + +msgid "Edit selected record" +msgstr "Éditer l'enregistrement sélectionné" + +msgid "Delete selected record" +msgstr "Supprimer l'enregistrement sélectionné" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Choisir une langue" + +msgid "Translation" +msgstr "Traduction" + +msgid "Edit" +msgstr "Éditer" + +msgid "Fuzzy" +msgstr "Floue" + +msgid "You need to save the record before adding translations." +msgstr "" +"Vous devez sauvegarder l'enregistrement avant d'ajouter des traductions." + +msgid "No other language available." +msgstr "Pas d'autre langue disponible." + +msgid "Translate view" +msgstr "Traduire la vue" diff --git a/tryton/tryton/data/locale/hu/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/hu/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..edbdb803e5 --- /dev/null +++ b/tryton/tryton/data/locale/hu/LC_MESSAGES/tryton.po @@ -0,0 +1,1176 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "adjon meg alternatív konfigurációs fájlt" + +msgid "development mode" +msgstr "fejlesztői mód" + +msgid "logging everything at INFO level" +msgstr "minden INFO szintű üzenet naplózva" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "naplózási szint megadása: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "adja meg a felhasználónevet" + +msgid "specify the server hostname:port" +msgstr "adja meg a kiszolgálónév:portot" + +#, fuzzy, python-format +msgid "Unable to write config file %s." +msgstr "Nem lehet a következő konfigurációs fájlba írni: „%s”" + +#, python-format +msgid "Unable to set locale %s" +msgstr "Nem lehet beállítani a %s nyelvet" + +#, fuzzy +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "Válasszon műveletet" + +#, fuzzy +msgid "No action defined." +msgstr "Nincs megadva művelet" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "Kijelölés" + +msgid "Cancel" +msgstr "Mégse" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Kiválasztva" + +msgid "Select" +msgstr "Kiválaszt" + +msgid "Save" +msgstr "Mentés" + +msgid "Save As..." +msgstr "Mentés másként…" + +msgid "Do you want to proceed?" +msgstr "Akarja folyatni?" + +msgid "Always ignore this warning." +msgstr "Ne mutassa többet ezt a figyelmeztetést" + +msgid "No" +msgstr "Nem" + +msgid "Yes" +msgstr "Igen" + +msgid "Concurrency Exception" +msgstr "Egyidejű módosítási hiba" + +msgid "This record has been modified while you were editing it." +msgstr "" +"Ezt a rekordot valaki más módosította az idő alatt, míg Ön dolgozott rajta." + +msgid "Cancel saving" +msgstr "Ne mentse el" + +msgid "Compare" +msgstr "Összehasonlítás" + +msgid "See the modified version" +msgstr "A másik felhasználó által módosított változat megtekintése" + +msgid "Write Anyway" +msgstr "Felülírás" + +msgid "Save your current version" +msgstr "Írja felül a másikét az Ön verziójával" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Összehasonlítás: %s" + +msgid "Close" +msgstr "Bezárás" + +msgid "Application Error" +msgstr "Alkalmazáshiba" + +msgid "Report Bug" +msgstr "A hiba jelentése" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "Új verzió elérhető!" + +msgid "Download" +msgstr "Letöltés" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Keresés..." + +msgid "Create..." +msgstr "Új rekord..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "Érték" + +msgid "Displayed value" +msgstr "Megjelenített érték" + +msgid "Format" +msgstr "Formátum" + +msgid "Display format" +msgstr "Megjelenítési formátum" + +msgid "Open the calendar" +msgstr "Naptár megnyitása" + +msgid "Date Format" +msgstr "Dátumformátum" + +msgid "Displayed date format" +msgstr "Mutatott dátumformátum" + +msgid "y" +msgstr "J" + +msgid "True" +msgstr "Igaz" + +msgid "t" +msgstr "w" + +msgid "False" +msgstr "Hamis" + +msgid "Digits" +msgstr "Tizedesjegyek" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "Sablon" + +msgid "Edit..." +msgstr "Módosítás…" + +msgid "Attachments..." +msgstr "Mellékletek..." + +msgid "Notes..." +msgstr "Jegyzetek..." + +msgid "Actions..." +msgstr "Műveletek" + +msgid "Relate..." +msgstr "Kapcsolódó elemek..." + +msgid "Report..." +msgstr "Jelentés..." + +msgid "Print..." +msgstr "Nyomtatás..." + +msgid "E-Mail..." +msgstr "E-Mail..." + +msgid "Bold" +msgstr "Félkövér" + +msgid "Italic" +msgstr "Dőlt" + +msgid "Underline" +msgstr "Aláhúzott" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "É" + +msgid "M" +msgstr "H" + +msgid "w" +msgstr "h" + +msgid "d" +msgstr "t" + +msgid "h" +msgstr "ó" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Felhasználó..." + +msgid "Toolbar" +msgstr "Eszköztár" + +msgid "Default" +msgstr "Alapértelmezett" + +msgid "Text and Icons" +msgstr "Szöveg és ikonok" + +msgid "Text" +msgstr "Szöveg" + +msgid "Icons" +msgstr "Ikonok" + +msgid "Form" +msgstr "Űrlap" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "Listák testreszabásának mentése" + +msgid "Spell Checking" +msgstr "Helyesírás ellenőrzés" + +msgid "PDA Mode" +msgstr "Tablet mód" + +msgid "Search Limit..." +msgstr "Keresési korlát..." + +msgid "Check Version" +msgstr "Verzió ellenőrzése" + +msgid "Options" +msgstr "Testreszabás" + +msgid "Keyboard Shortcuts..." +msgstr "Gyorsbillentyűk..." + +msgid "About..." +msgstr "Névjegy..." + +msgid "Help" +msgstr "Súgó" + +msgid "No result found." +msgstr "Nincs találat" + +msgid "Favorites" +msgstr "Könyvjelzők" + +msgid "Manage..." +msgstr "Módosítás..." + +msgid "Action" +msgstr "Művelet" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"A továbblépéshez be kell zárnia minden nyitott lapot.\n" +"Biztosan tovább akar lépni?" + +msgid "Application Shortcuts" +msgstr "Általános gyorsbillentyűk" + +msgid "Global" +msgstr "A teljes alkalmazásban érvényes" + +msgid "Preferences" +msgstr "Felhasználói beállítások" + +msgid "Search menu" +msgstr "Keresősáv" + +msgid "Toggle menu" +msgstr "Oldalsáv ki/be" + +msgid "Previous tab" +msgstr "Előző lap" + +msgid "Next tab" +msgstr "Következő lap" + +msgid "Shortcuts" +msgstr "Gyorsbillentyűk" + +msgid "Quit" +msgstr "Kilépés" + +msgid "Edition Shortcuts" +msgstr "Szerkesztés gyorsbillentyűi" + +msgid "Text Entries" +msgstr "Szövegmezők" + +msgid "Cut selected text" +msgstr "Kijelölt szöveg kivágása" + +msgid "Copy selected text" +msgstr "Kijelölt szöveg másolása" + +msgid "Paste copied text" +msgstr "Kijelölt szöveg beillesztése" + +msgid "Next entry" +msgstr "Következő mező" + +msgid "Previous entry" +msgstr "Előző mező" + +msgid "Relation Entries" +msgstr "Kapcsolatmezők" + +msgid "Create new relation" +msgstr "Új kapcsolódó rekord létrehozása" + +msgid "Open/Search relation" +msgstr "Kapcsolódó rekordok keresése/megnyitása" + +msgid "List Entries" +msgstr "Listamezők" + +msgid "Switch view" +msgstr "Nézet váltása" + +msgid "Create/Select new line" +msgstr "Új/meglévő rekord hozzáadása" + +msgid "Open relation" +msgstr "Kapcsolódó rekord megnyitása" + +msgid "Mark line for deletion/removal" +msgstr "Sor törlésre/eltávolításra jelölése" + +msgid "Mark line for removal" +msgstr "Sor törlésre jelölése" + +msgid "Unmark line for deletion" +msgstr "A törlésre jelölés visszavonása" + +msgid "List/Tree Shortcuts" +msgstr "Lista-/fanézet gyorsbillentyűi" + +msgid "Move Cursor" +msgstr "Lépkedés a listaelemek között" + +msgid "Move right" +msgstr "Jobbra" + +msgid "Move left" +msgstr "Balra" + +msgid "Move up" +msgstr "Fel" + +msgid "Move down" +msgstr "Le" + +msgid "Move up of one page" +msgstr "Fel egy oldalt" + +msgid "Move down of one page" +msgstr "Le egy oldalt" + +msgid "Move to top" +msgstr "Első rekordra ugrás" + +msgid "Move to bottom" +msgstr "Utolsó rekordra ugrás" + +msgid "Move to parent" +msgstr "Szülőrekordra ugrás" + +msgid "Select all" +msgstr "Mind kijelölése" + +msgid "Unselect all" +msgstr "Kijelölés megszüntetése" + +msgid "Select parent" +msgstr "Szülőrekord kijelölése" + +msgid "Select/Activate current row" +msgstr "Aktuális sor kijelölése/aktiválása" + +msgid "Toggle selection" +msgstr "Aktuális sor kijelölése ki/be" + +msgid "Expand/Collapse" +msgstr "Felnyitás/összecsukás" + +msgid "Expand row" +msgstr "Sor felnyitása" + +msgid "Collapse row" +msgstr "Sor összecsukása" + +msgid "Toggle row" +msgstr "Sor felnyitás ki/be" + +msgid "Collapse all rows" +msgstr "Minden sor összecsukása" + +msgid "Expand all rows" +msgstr "Minden sor felnyitása" + +msgid "Close Tab" +msgstr "Lap bezárása" + +msgid "modularity, scalability and security" +msgstr "modularitás, skálázhatóság és biztonság" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "Mellékletek (%s)" + +msgid "Profile Editor" +msgstr "Profilszerkesztő" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "Új profil hozzáadása" + +msgid "Remove selected profile" +msgstr "A kiválasztott profil törlése" + +msgid "Host:" +msgstr "Kiszolgáló:" + +msgid "Database:" +msgstr "Adatbázis:" + +msgid "Fetching databases list" +msgstr "Adatbázislista lekérése" + +msgid "Username:" +msgstr "Felhasználónév:" + +msgid "Incompatible version of the server." +msgstr "A kiszolgálónak más verziója van, mint a kliensnek." + +msgid "Could not connect to the server." +msgstr "Nem sikerült csatlakozni a kiszolgálóhoz." + +msgid "Login" +msgstr "Bejelentkezés" + +msgid "_Cancel" +msgstr "_Mégse" + +msgid "Cancel connection to the Tryton server" +msgstr "Bejelentkezőablak bezárása" + +msgid "C_onnect" +msgstr "Bejelentkezés" + +msgid "Connect the Tryton server" +msgstr "Kapcsolódás a Tryton kiszolgálóhoz" + +msgid "Profile:" +msgstr "Profil:" + +msgid "Host / Database information" +msgstr "Kiszolgáló / adatbázis információ" + +msgid "User name:" +msgstr "Felhasználónév:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Nem lehet beállítani a csoportot: %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "Címzett:" + +msgid "Cc:" +msgstr "Másolatot kap:" + +msgid "Bcc:" +msgstr "Rejtett másolat:" + +msgid "Subject:" +msgstr "Tárgy:" + +msgid "Body" +msgstr "Tartalom" + +msgid "Reports" +msgstr "Nyomtatványok" + +msgid "Attachments" +msgstr "Mellékletek" + +msgid "Files" +msgstr "Fájlok" + +msgid "Send" +msgstr "Küldés" + +#, fuzzy +msgid "Select File" +msgstr "Mind kijelölése" + +msgid "Remove File" +msgstr "Fájl eltávolítása" + +msgid "Add..." +msgstr "Hozzáadás..." + +msgid "Preview" +msgstr "Előnézet" + +msgid "Previous" +msgstr "Előző" + +msgid "Next" +msgstr "Következő" + +#, python-format +msgid "Attachment (%s)" +msgstr "Melléklet(%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Jegyzet (%d/%d)" + +msgid "You have to select one record." +msgstr "Ki kell választania egy rekordot." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Létrehozta:" + +msgid "Created at:" +msgstr "Létrehozás dátuma:" + +msgid "Edited by:" +msgstr "Módosította:" + +msgid "Edited at:" +msgstr "Módosítás dátuma:" + +msgid "Model:" +msgstr "Elem:" + +msgid "Are you sure to remove this record?" +msgstr "Biztos törölni szeretné?" + +msgid "Are you sure to remove those records?" +msgstr "Biztos törölni szeretné?" + +msgid "Records not removed." +msgstr "A rekordok nem lettek törölve." + +msgid "Records removed." +msgstr "Rekordok törölve." + +msgid "Working now on the duplicated record(s)." +msgstr "Most az új másolaton(okon) dolgozik." + +msgid "Record saved." +msgstr "Rekord elmentve." + +msgid " of " +msgstr " , összesen " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Módosította ezt a rekordot,\n" +"el akarja menteni?" + +msgid "Launch action" +msgstr "Művelet indítása" + +msgid "Relate" +msgstr "Kapcsolat" + +msgid "Open related records" +msgstr "Kapcsolódó rekordok megnyitása" + +msgid "Report" +msgstr "Nyomtatvány" + +msgid "Open report" +msgstr "Nyomtatvány megnyitása" + +msgid "Print" +msgstr "Nyomtatás" + +msgid "Print report" +msgstr "Nyomtatás" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "URL vágólapra másolása" + +msgid "Unknown" +msgstr "Ismeretlen" + +msgid "Limit" +msgstr "Korlát" + +msgid "Search Limit Settings" +msgstr "Keresett rekordok számának korlátozása" + +msgid "Limit:" +msgstr "Korlát:" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "Felhasználói beállítások szerkesztése" + +msgid "Preference" +msgstr "Felhasználói beállítások" + +msgid "Revision" +msgstr "Szerkesztés" + +msgid "Select a revision" +msgstr "Szerkesztés kiválasztása" + +msgid "Revision:" +msgstr "Szerkesztés:" + +msgid "_Switch View" +msgstr "Nézet váltá_sa" + +msgid "Switch View" +msgstr "Nézet váltása" + +msgid "_Previous" +msgstr "_Előző" + +msgid "Previous Record" +msgstr "Előző rekord" + +msgid "_Next" +msgstr "_Következő" + +msgid "Next Record" +msgstr "Következő rekord" + +msgid "_Search" +msgstr "Kere_sés" + +msgid "_New" +msgstr "Ú_j" + +msgid "Create a new record" +msgstr "Új rekord létrehozása" + +msgid "_Save" +msgstr "Menté_s" + +msgid "Save this record" +msgstr "Rekord mentése" + +msgid "_Reload/Undo" +msgstr "Új_ratöltés / Visszavonás" + +msgid "Reload/Undo" +msgstr "Újratöltés / Visszavonás" + +msgid "_Duplicate" +msgstr "_Duplázás" + +msgid "_Delete..." +msgstr "Törlés..." + +msgid "View _Logs..." +msgstr "Nap_ló megtekintése…" + +msgid "Show revisions..." +msgstr "Feldolgozások megtekintése" + +msgid "A_ttachments..." +msgstr "Mellékle_t" + +msgid "Add an attachment to the record" +msgstr "Melléklet hozzáadása a rekordhoz" + +msgid "_Notes..." +msgstr "Jegyzetek..." + +msgid "Add a note to the record" +msgstr "Jegyzet hozzáadása a rekordhoz" + +msgid "_Actions..." +msgstr "Műveletek..." + +msgid "_Relate..." +msgstr "Kapcsolat megnyitása" + +msgid "_Report..." +msgstr "Nyomtatvány megnyitása..." + +msgid "_Print..." +msgstr "Nyomtatás…" + +msgid "_E-Mail..." +msgstr "E-mail..." + +msgid "Send an e-mail using the record" +msgstr "E-mail küldése a rekord adataival" + +msgid "_Export Data..." +msgstr "Adat _exportálás..." + +msgid "_Import Data..." +msgstr "Adat _importálás..." + +msgid "Copy _URL..." +msgstr "_URL másolása..." + +msgid "_Close Tab" +msgstr "Lap bezárása" + +msgid "All fields" +msgstr "Összes mező" + +msgid "_Add" +msgstr "Hozzá_adás" + +msgid "_Remove" +msgstr "_Eltávolítás (_R)" + +msgid "_Clear" +msgstr "Összes eltávolítása (_C)" + +msgid "Fields selected" +msgstr "Kiválasztott mezők" + +msgid "CSV Parameters" +msgstr "CSV paraméterek" + +msgid "Delimiter:" +msgstr "Elválasztójel:" + +msgid "Quote char:" +msgstr "Karakterlánc-elválasztó:" + +msgid "Encoding:" +msgstr "Kódolás:" + +msgid "Use locale format" +msgstr "A felhasználó nyelvén" + +msgid "Field name" +msgstr "Mezőnév" + +#, python-format +msgid "CSV Export: %s" +msgstr "Exportálás CSV-be: %s" + +msgid "_Save Export" +msgstr "Oszloplista elmenté_se" + +msgid "_URL Export" +msgstr "Exportálás _URL-je" + +msgid "_Delete Export" +msgstr "Oszloplista törlése (_D)" + +msgid "Predefined exports" +msgstr "Elmentett oszloplisták" + +msgid "Name" +msgstr "Név" + +msgid "Open" +msgstr "Megnyitás" + +msgid "Listed Records" +msgstr "A listán szereplő összes rekordot" + +msgid "Selected Records" +msgstr "Csak a kiválasztott rekordokat" + +msgid "Ignore search limit" +msgstr "Keresési korlát nélkül" + +msgid "Add field names" +msgstr "Mezőnevek hozzáadása" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "Mi legyen az oszloplista neve?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "'%s' definiálásának figyelembe vétele?" + +#, python-format +msgid "%d record saved." +msgstr "%d rekord elmentve." + +#, python-format +msgid "%d records saved." +msgstr "%d rekord elmentve." + +#, fuzzy, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Sikertelen művelet!\n" +"Error message:\n" +"%s" + +msgid "Error" +msgstr "Hiba" + +msgid "Link" +msgstr "Hivatkozás" + +msgid "Delete" +msgstr "Töröl" + +msgid "New" +msgstr "Új" + +msgid "Switch" +msgstr "Nézet váltása" + +msgid "Add" +msgstr "Hozzáad" + +msgid "Remove " +msgstr "Mező eltávolítása" + +msgid "Create a new record " +msgstr "Új rekord létrehozása " + +msgid "Delete selected record " +msgstr "Kiválasztott rekord törlése " + +msgid "Undelete selected record " +msgstr "Rekord törlésének visszavonása " + +#, python-format +msgid "CSV Import: %s" +msgstr "Importálás CSV-ből: %s" + +msgid "_Auto-Detect" +msgstr "_Automatikus felismerés" + +msgid "File to Import:" +msgstr "Importadatok:" + +msgid "Open..." +msgstr "Megnyitás..." + +msgid "Lines to Skip:" +msgstr "Átugrandó sorok:" + +msgid "You must select an import file first." +msgstr "Először adja meg a betöltendő fájlt." + +msgid "Error opening CSV file" +msgstr "Hiba a CSV fájl megnyitásakor" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Nem sikerült feldolgozni a fájlt, mert a %s mezőnél hiba van." + +#, python-format +msgid "%d record imported." +msgstr "%d rekord betöltve." + +#, python-format +msgid "%d records imported." +msgstr "%d rekord betöltve." + +msgid "Search" +msgstr "Keresés" + +#, python-format +msgid "Search %s" +msgstr "%s keresése" + +msgid "Wizard" +msgstr "Wizard" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Létrehozta" + +msgid "Created at" +msgstr "Létrehozás dátuma" + +msgid "Edited by" +msgstr "Módosította" + +msgid "Edited at" +msgstr "Módosítás dátuma" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "A menü láthatósága nem állítható be %s" + +msgid "Unable to set view tree state" +msgstr "A menü láthatósága nem állítható be" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "A „%s” mezőben helytelen értéket adott meg." + +#, python-format +msgid "\"%s\" is required." +msgstr "A „%s” mezőt meg kell adni." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "A „%s” mező értékei között van érvénytelen." + +msgid "Pre-validation" +msgstr "Adatkitöltés ellenőrzése" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Kép mérete" + +msgid "Width:" +msgstr "Szélesség:" + +msgid "Height:" +msgstr "Magasság:" + +msgid "PNG image (*.png)" +msgstr "PNG kép (*.png)" + +msgid "Save As" +msgstr "Mentés másként" + +#, fuzzy +msgid "Image size too large." +msgstr "Kép túl nagy!" + +msgid "Copy" +msgstr "Másolás" + +msgid "Paste" +msgstr "Beillesztés" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Szűrők megnyitása" + +msgid "Show bookmarks of filters" +msgstr "Elmentett szűrők megjelenítése" + +msgid "Remove this bookmark" +msgstr "Ez az elmentett szűrő eltávolítása" + +msgid "Bookmark this filter" +msgstr "Ez a szűrő elmentése" + +msgid "Show active records" +msgstr "Aktív rekordok megjelenítése" + +msgid "Show inactive records" +msgstr "Inaktív rekordok megjelenítése" + +msgid "Bookmark Name:" +msgstr "Könyvjelző neve:" + +msgid "Find" +msgstr "Keresés" + +msgid "Today" +msgstr "Ma" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "Heti nézet" + +msgid "Month View" +msgstr "Havi nézet" + +msgid "Week" +msgstr "Hét" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "Törlés" + +msgid "All files" +msgstr "Összes fájl" + +msgid "Show plain text" +msgstr "Mutassa meg az elrejtett szöveget" + +msgid "Add value" +msgstr "Érték hozzáadása" + +#, python-format +msgid "Remove \"%s\"" +msgstr "„%s” eltávolítása" + +msgid "Images" +msgstr "Képek" + +msgid "Add existing record" +msgstr "Meglévő rekord hozzáadása" + +msgid "Remove selected record" +msgstr "A kiválasztott rekord eltávolítása" + +msgid "Open the record " +msgstr "Rekord megnyitása " + +msgid "Clear the field " +msgstr "A mező kiürítése" + +msgid "Search a record " +msgstr "Rekord keresése" + +msgid "Edit selected record" +msgstr "Kiválasztott rekord szerkesztése" + +msgid "Delete selected record" +msgstr "Kiválasztott rekord törlése" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "Fordítás" + +msgid "Edit" +msgstr "Módosítás" + +msgid "Fuzzy" +msgstr "Pontatlan" + +msgid "You need to save the record before adding translations." +msgstr "El kell menteni a rekordot mielőtt hozzáadja a fordítást." + +msgid "No other language available." +msgstr "Másik nyelv nem elérhető." + +msgid "Translate view" +msgstr "Nézet lefordítása" diff --git a/tryton/tryton/data/locale/id/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/id/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..7d15ee44cd --- /dev/null +++ b/tryton/tryton/data/locale/id/LC_MESSAGES/tryton.po @@ -0,0 +1,1167 @@ +# +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "tentukan file konfigurasi alternatif" + +msgid "development mode" +msgstr "mode pengembangan" + +msgid "logging everything at INFO level" +msgstr "mencatat semua pada level INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "tentukan tingkatan log: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "tentukan user login" + +msgid "specify the server hostname:port" +msgstr "tentukan nama host server:port" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Tidak dapat menulis file konfigurasi %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Pilih tindakan Anda" + +msgid "No action defined." +msgstr "Tidak ada tindakan yang ditentukan." + +msgid "By: " +msgstr "Oleh: " + +msgid "Selection" +msgstr "" + +msgid "Cancel" +msgstr "Batal" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Pilihan anda:" + +msgid "Select" +msgstr "Pilih" + +msgid "Save" +msgstr "Simpan" + +msgid "Save As..." +msgstr "Simpan Sebagai..." + +msgid "Do you want to proceed?" +msgstr "Apakah Anda ingin melanjutkan?" + +msgid "Always ignore this warning." +msgstr "Selalu abaikan peringatan ini." + +msgid "No" +msgstr "Tidak" + +msgid "Yes" +msgstr "Ya" + +msgid "Concurrency Exception" +msgstr "" + +msgid "This record has been modified while you were editing it." +msgstr "Rekaman ini telah diubah selagi anda menyuntingnya." + +msgid "Cancel saving" +msgstr "Batalkan penyimpanan" + +msgid "Compare" +msgstr "Bandingkan" + +msgid "See the modified version" +msgstr "Lihat versi yang diubah" + +msgid "Write Anyway" +msgstr "Tetap Menulis" + +msgid "Save your current version" +msgstr "Simpan versi anda saat ini" + +#, python-format +msgid "Compare: %s" +msgstr "Bandingkan: %s" + +msgid "Close" +msgstr "Tutup" + +msgid "Application Error" +msgstr "" + +msgid "Report Bug" +msgstr "Melaporkan Kesalahan" + +#, python-format +msgid "Check URL: %s" +msgstr "Periksa URL: %s" + +msgid "Unable to check for new version." +msgstr "Tidak dapat memeriksa versi baru." + +msgid "A new version is available!" +msgstr "Versi baru tersedia!" + +msgid "Download" +msgstr "Unduh" + +msgid "Too many requests. Try again later." +msgstr "Terlalu banyak permintaan. Coba lagi nanti." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Cari..." + +msgid "Create..." +msgstr "Buat..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Tidak dapat mencari penyelesaian %s" + +msgid "Value" +msgstr "Nilai" + +msgid "Displayed value" +msgstr "Nilai yang ditampilkan" + +msgid "Format" +msgstr "" + +msgid "Display format" +msgstr "" + +msgid "Open the calendar" +msgstr "Buka kalender" + +msgid "Date Format" +msgstr "Format Tanggal" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "y" + +msgid "True" +msgstr "Benar" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "Salah" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "Templat" + +msgid "Edit..." +msgstr "Sunting..." + +msgid "Attachments..." +msgstr "" + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "" + +msgid "Relate..." +msgstr "" + +msgid "Report..." +msgstr "" + +msgid "Print..." +msgstr "Cetak..." + +msgid "E-Mail..." +msgstr "E-Mail..." + +msgid "Bold" +msgstr "Tulisan Tebal" + +msgid "Italic" +msgstr "Tulisan Miring" + +msgid "Underline" +msgstr "Tulisan Garis Bawah" + +msgid "Align Left" +msgstr "Rata Kiri" + +msgid "Align Center" +msgstr "Rata Tengah" + +msgid "Align Right" +msgstr "Rata Kanan" + +msgid "Justify" +msgstr "Sama Rata" + +msgid "Foreground Color" +msgstr "Warna Latar Depan" + +msgid "Select a color" +msgstr "Pilih satu warna" + +msgid "Y" +msgstr "Y" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "w" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "" + +msgid "Toolbar" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Text and Icons" +msgstr "" + +msgid "Text" +msgstr "Teks" + +msgid "Icons" +msgstr "Ikon" + +msgid "Form" +msgstr "" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "Periksa Versi" + +msgid "Options" +msgstr "" + +msgid "Keyboard Shortcuts..." +msgstr "" + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "Bantuan" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "Tindakan" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Tindakan berikut ini perlu untuk menutup semua tab.\n" +"Apakah Anda ingin melanjutkan?" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "Keluar" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "" + +msgid "Copy selected text" +msgstr "" + +msgid "Paste copied text" +msgstr "" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "Ferry" + +#, python-format +msgid "Attachments (%s)" +msgstr "" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "" + +msgid "Database:" +msgstr "" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "" + +msgid "Incompatible version of the server." +msgstr "" + +msgid "Could not connect to the server." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "_Cancel" +msgstr "" + +msgid "Cancel connection to the Tryton server" +msgstr "" + +msgid "C_onnect" +msgstr "" + +msgid "Connect the Tryton server" +msgstr "" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "" + +msgid "Unable to complete email entry" +msgstr "" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "Kepada:" + +msgid "Cc:" +msgstr "" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Subject:" + +msgid "Body" +msgstr "Body" + +#, fuzzy +msgid "Reports" +msgstr "Laporan" + +msgid "Attachments" +msgstr "" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "Kirim" + +#, fuzzy +msgid "Select File" +msgstr "Pilih" + +#, fuzzy +msgid "Remove File" +msgstr "Hapus " + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "Sebelumnya" + +msgid "Previous" +msgstr "Sebelumnya" + +msgid "Next" +msgstr "Berikutnya" + +#, python-format +msgid "Attachment (%s)" +msgstr "Lampiran (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "" + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Dibuat oleh:" + +msgid "Created at:" +msgstr "Dibuat pada:" + +msgid "Edited by:" +msgstr "Disunting oleh:" + +msgid "Edited at:" +msgstr "Disunting pada:" + +msgid "Model:" +msgstr "" + +msgid "Are you sure to remove this record?" +msgstr "" + +msgid "Are you sure to remove those records?" +msgstr "" + +msgid "Records not removed." +msgstr "" + +msgid "Records removed." +msgstr "" + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "" + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" + +msgid "Launch action" +msgstr "" + +msgid "Relate" +msgstr "" + +msgid "Open related records" +msgstr "" + +msgid "Report" +msgstr "Laporan" + +msgid "Open report" +msgstr "" + +msgid "Print" +msgstr "Cetak" + +msgid "Print report" +msgstr "Cetak laporan" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Limit" +msgstr "Batasan" + +msgid "Search Limit Settings" +msgstr "" + +msgid "Limit:" +msgstr "" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "" + +msgid "Preference" +msgstr "" + +msgid "Revision" +msgstr "Revisi" + +msgid "Select a revision" +msgstr "" + +msgid "Revision:" +msgstr "" + +msgid "_Switch View" +msgstr "" + +msgid "Switch View" +msgstr "" + +msgid "_Previous" +msgstr "" + +msgid "Previous Record" +msgstr "" + +msgid "_Next" +msgstr "" + +msgid "Next Record" +msgstr "" + +msgid "_Search" +msgstr "" + +msgid "_New" +msgstr "" + +msgid "Create a new record" +msgstr "" + +msgid "_Save" +msgstr "" + +msgid "Save this record" +msgstr "" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "" + +msgid "_Delete..." +msgstr "" + +msgid "View _Logs..." +msgstr "" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "" + +msgid "Add an attachment to the record" +msgstr "" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "" + +msgid "_Relate..." +msgstr "" + +msgid "_Report..." +msgstr "" + +msgid "_Print..." +msgstr "" + +msgid "_E-Mail..." +msgstr "" + +msgid "Send an e-mail using the record" +msgstr "" + +msgid "_Export Data..." +msgstr "" + +msgid "_Import Data..." +msgstr "" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "" + +msgid "All fields" +msgstr "" + +msgid "_Add" +msgstr "" + +msgid "_Remove" +msgstr "" + +msgid "_Clear" +msgstr "" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "" + +msgid "_URL Export" +msgstr "" + +msgid "_Delete Export" +msgstr "" + +msgid "Predefined exports" +msgstr "" + +msgid "Name" +msgstr "Nama" + +msgid "Open" +msgstr "Buka" + +msgid "Listed Records" +msgstr "" + +#, fuzzy +msgid "Selected Records" +msgstr "Pilih satu warna" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nama model)" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "" + +#, python-format +msgid "%d records saved." +msgstr "" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Link" +msgstr "Tautan" + +msgid "Delete" +msgstr "Hapus" + +msgid "New" +msgstr "Baru" + +msgid "Switch" +msgstr "" + +msgid "Add" +msgstr "Tambahkan" + +msgid "Remove " +msgstr "Hapus " + +msgid "Create a new record " +msgstr "" + +msgid "Delete selected record " +msgstr "" + +msgid "Undelete selected record " +msgstr "" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "" + +msgid "File to Import:" +msgstr "" + +msgid "Open..." +msgstr "" + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "" + +msgid "Error opening CSV file" +msgstr "" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "" + +#, python-format +msgid "%d record imported." +msgstr "" + +#, python-format +msgid "%d records imported." +msgstr "" + +msgid "Search" +msgstr "" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "Wisaya" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +msgid "Created at" +msgstr "Dibuat pada" + +msgid "Edited by" +msgstr "" + +msgid "Edited at" +msgstr "" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" diperlukan." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Ukuran Gambar" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "Simpan Sebagai" + +msgid "Image size too large." +msgstr "Ukuran gambar terlalu besar." + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "" + +msgid "Today" +msgstr "Hari ini" + +msgid "go back" +msgstr "kembali" + +msgid "go forward" +msgstr "maju" + +msgid "previous year" +msgstr "tahun sebelumnya" + +msgid "next year" +msgstr "tahun berikutnya" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "" + +msgid "Month View" +msgstr "" + +msgid "Week" +msgstr "Pekan" + +msgid "Select..." +msgstr "Pilih..." + +msgid "Clear" +msgstr "Bersihkan" + +msgid "All files" +msgstr "" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Add existing record" +msgstr "" + +msgid "Remove selected record" +msgstr "" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +msgid "Edit selected record" +msgstr "" + +msgid "Delete selected record" +msgstr "" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Pilih bahasa" + +msgid "Translation" +msgstr "Terjemahan" + +msgid "Edit" +msgstr "Sunting" + +msgid "Fuzzy" +msgstr "" + +msgid "You need to save the record before adding translations." +msgstr "" + +msgid "No other language available." +msgstr "Tidak ada bahasa lain yang tersedia." + +msgid "Translate view" +msgstr "" diff --git a/tryton/tryton/data/locale/it/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/it/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..93602a6590 --- /dev/null +++ b/tryton/tryton/data/locale/it/LC_MESSAGES/tryton.po @@ -0,0 +1,1192 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "indica un altro file di configurazione" + +msgid "development mode" +msgstr "modo sviluppatore" + +msgid "logging everything at INFO level" +msgstr "registrare tutto a livello INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" +"specifica il livello di registrazione: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "specifica l'utente di accesso" + +msgid "specify the server hostname:port" +msgstr "specifica hostname:porta del server" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Impossibile scrivere il file di configurazione %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Impossibile impostare la localizzazione %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Seleziona l'azione" + +msgid "No action defined." +msgstr "Nessuna azione definita." + +msgid "By: " +msgstr "per: " + +msgid "Selection" +msgstr "Selezione" + +msgid "Cancel" +msgstr "Annulla" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Selezione:" + +msgid "Select" +msgstr "Seleziona" + +msgid "Save" +msgstr "Salva" + +msgid "Save As..." +msgstr "Salva come..." + +msgid "Do you want to proceed?" +msgstr "Procedere?" + +msgid "Always ignore this warning." +msgstr "Ignorare sempre l'avviso" + +msgid "No" +msgstr "No" + +msgid "Yes" +msgstr "Sì" + +msgid "Concurrency Exception" +msgstr "Eccezione di concorrenza" + +msgid "This record has been modified while you were editing it." +msgstr "Questo record è stato modificato mentre lo stavi compilando." + +msgid "Cancel saving" +msgstr "Annulla il salvataggio" + +msgid "Compare" +msgstr "Confronta" + +msgid "See the modified version" +msgstr "Vedi la versione modificata" + +msgid "Write Anyway" +msgstr "Scrivi comunque" + +msgid "Save your current version" +msgstr "Salva la tua versione attuale" + +#, python-format +msgid "Compare: %s" +msgstr "Confronta: %s" + +msgid "Close" +msgstr "Chiudi" + +msgid "Application Error" +msgstr "Errore dell'applicazione" + +msgid "Report Bug" +msgstr "Segnala l'errore" + +#, python-format +msgid "Check URL: %s" +msgstr "Controlla URL: %s" + +msgid "Unable to check for new version." +msgstr "Impossibile verificare la presenza di una nuova versione." + +msgid "A new version is available!" +msgstr "Una nuova versione è disponibile!" + +msgid "Download" +msgstr "Scarica" + +msgid "Too many requests. Try again later." +msgstr "Troppe richieste. Riprovare più tardi." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Cerca..." + +msgid "Create..." +msgstr "Crea..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Impossibile cercare per il completamento di %s" + +msgid "Value" +msgstr "Valore" + +msgid "Displayed value" +msgstr "Valore visualizzato" + +msgid "Format" +msgstr "Formato" + +msgid "Display format" +msgstr "Formato di visualizzazione" + +msgid "Open the calendar" +msgstr "Apertura calendario" + +msgid "Date Format" +msgstr "Formato data" + +msgid "Displayed date format" +msgstr "Formato data visualizzato" + +msgid "y" +msgstr "a" + +msgid "True" +msgstr "True" + +msgid "t" +msgstr "o" + +msgid "False" +msgstr "False" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "Il numero di decimali" + +#, fuzzy +msgid "Template" +msgstr "Collegato" + +msgid "Edit..." +msgstr "Modifica..." + +msgid "Attachments..." +msgstr "Allegati..." + +msgid "Notes..." +msgstr "Appunti..." + +msgid "Actions..." +msgstr "Azioni..." + +msgid "Relate..." +msgstr "Riferire..." + +msgid "Report..." +msgstr "Segnala..." + +msgid "Print..." +msgstr "Stampa..." + +msgid "E-Mail..." +msgstr "E-Mail" + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "S" + +msgid "d" +msgstr "g" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferenze..." + +msgid "Toolbar" +msgstr "Barra degli strumenti" + +msgid "Default" +msgstr "Predefinito" + +msgid "Text and Icons" +msgstr "Testo e icone" + +msgid "Text" +msgstr "Testo" + +msgid "Icons" +msgstr "Icone" + +msgid "Form" +msgstr "Modulo" + +msgid "Save Column Width" +msgstr "Salva larghezza colonna" + +msgid "Save Tree State" +msgstr "Salva diagramma ad albero" + +msgid "Spell Checking" +msgstr "Controllo ortogbrafico" + +msgid "PDA Mode" +msgstr "Modalità PDA" + +msgid "Search Limit..." +msgstr "Limite di ricerca..." + +msgid "Check Version" +msgstr "Controlla la versione" + +msgid "Options" +msgstr "Opzioni" + +msgid "Keyboard Shortcuts..." +msgstr "Scorciatoie da tastiera..." + +msgid "About..." +msgstr "Informazioni su..." + +msgid "Help" +msgstr "Aiuto" + +msgid "No result found." +msgstr "Nessun risultato trovato." + +msgid "Favorites" +msgstr "Preferiti" + +msgid "Manage..." +msgstr "Gestisci..." + +msgid "Action" +msgstr "Azione" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"L'azione seguente richiede la chiusura di tutte le finestre.\n" +"Vuoi continuare?" + +msgid "Application Shortcuts" +msgstr "Scorciatoie per le applicazioni" + +msgid "Global" +msgstr "Globale" + +msgid "Preferences" +msgstr "Preferenze" + +msgid "Search menu" +msgstr "Menu di ricerca" + +msgid "Toggle menu" +msgstr "Attiva/disattiva menu" + +msgid "Previous tab" +msgstr "Scheda precedente" + +msgid "Next tab" +msgstr "Scheda successiva" + +msgid "Shortcuts" +msgstr "Scorciatoie da tastiera" + +msgid "Quit" +msgstr "Esci" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "Voci di testo" + +msgid "Cut selected text" +msgstr "Taglia testo selezionato" + +msgid "Copy selected text" +msgstr "Copia il testo selezionato" + +msgid "Paste copied text" +msgstr "Incolla il testo copiato" + +msgid "Next entry" +msgstr "Voce successiva" + +msgid "Previous entry" +msgstr "Voce precedente" + +msgid "Relation Entries" +msgstr "Voci di relazione" + +msgid "Create new relation" +msgstr "Crea nuova relazione" + +msgid "Open/Search relation" +msgstr "Apri/Cerca relazione" + +msgid "List Entries" +msgstr "Elenca voci" + +msgid "Switch view" +msgstr "Cambia vista" + +msgid "Create/Select new line" +msgstr "Crea/Seleziona nuova riga" + +msgid "Open relation" +msgstr "Apri relazione" + +msgid "Mark line for deletion/removal" +msgstr "Contrassegna la riga per la cancellazione/rimozione" + +msgid "Mark line for removal" +msgstr "Contrassegnare la linea per la rimozione" + +msgid "Unmark line for deletion" +msgstr "Deseleziona riga da eliminare" + +msgid "List/Tree Shortcuts" +msgstr "Scorciatoie Elenco/Albero" + +msgid "Move Cursor" +msgstr "Sposta cursore" + +msgid "Move right" +msgstr "Spostati a destra" + +msgid "Move left" +msgstr "Spostati a sinistra" + +msgid "Move up" +msgstr "Spostati su" + +msgid "Move down" +msgstr "Spostati giù" + +msgid "Move up of one page" +msgstr "Spostati alto di una pagina" + +msgid "Move down of one page" +msgstr "Spostati in basso di una pagina" + +msgid "Move to top" +msgstr "Vai in cima" + +msgid "Move to bottom" +msgstr "Vai alla fine" + +msgid "Move to parent" +msgstr "Muoviti sul padre" + +msgid "Select all" +msgstr "Seleziona tutto" + +msgid "Unselect all" +msgstr "Deseleziona tutto" + +msgid "Select parent" +msgstr "Seleziona padre" + +msgid "Select/Activate current row" +msgstr "Seleziona/Attiva riga corrente" + +msgid "Toggle selection" +msgstr "Attiva/disattiva la selezione" + +msgid "Expand/Collapse" +msgstr "Espandi/Riduci" + +msgid "Expand row" +msgstr "Espandi riga" + +msgid "Collapse row" +msgstr "Comprimi riga" + +msgid "Toggle row" +msgstr "Attiva/disattiva riga" + +msgid "Collapse all rows" +msgstr "Comprimi tutte le righe" + +msgid "Expand all rows" +msgstr "Espandi tutte le righe" + +msgid "Close Tab" +msgstr "Chiudere la scheda" + +msgid "modularity, scalability and security" +msgstr "modularità, scalabilità e sicurezza" + +msgid "translator-credits" +msgstr "Luca Cristaldi" + +#, python-format +msgid "Attachments (%s)" +msgstr "Allegati (%s)" + +msgid "Profile Editor" +msgstr "Editor del profilo" + +msgid "Profile" +msgstr "Profilo" + +msgid "Add new profile" +msgstr "Aggiungi nuovo profilo" + +msgid "Remove selected profile" +msgstr "Rimuovi i profili selezionati" + +msgid "Host:" +msgstr "Host:" + +msgid "Database:" +msgstr "Database:" + +msgid "Fetching databases list" +msgstr "Preleva la lista dei database" + +msgid "Username:" +msgstr "Nome utente:" + +msgid "Incompatible version of the server." +msgstr "Versione del server incompatibile." + +msgid "Could not connect to the server." +msgstr "Impossibile connettersi al server." + +msgid "Login" +msgstr "Accedi" + +msgid "_Cancel" +msgstr "_Annulla" + +msgid "Cancel connection to the Tryton server" +msgstr "Annulla la connessione al server Tryton" + +msgid "C_onnect" +msgstr "C_onnetti" + +msgid "Connect the Tryton server" +msgstr "Connetti il server Tryton" + +msgid "Profile:" +msgstr "Profilo:" + +msgid "Host / Database information" +msgstr "Informazione su Host/ database" + +msgid "User name:" +msgstr "nome utente" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Impossibile impostare la localizzazione %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Email %s" + +msgid "To:" +msgstr "a:" + +#, fuzzy +msgid "Cc:" +msgstr ".CC" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Oggetto:" + +#, fuzzy +msgid "Body" +msgstr "Corpo:" + +#, fuzzy +msgid "Reports" +msgstr "Rapporto" + +#, fuzzy +msgid "Attachments" +msgstr "Allegato:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Seleziona tutto" + +#, fuzzy +msgid "Remove File" +msgstr "_Rimuovi" + +msgid "Add..." +msgstr "Inserisci..." + +#, fuzzy +msgid "Preview" +msgstr "Precedente" + +msgid "Previous" +msgstr "Precedente" + +msgid "Next" +msgstr "Prossimo" + +#, python-format +msgid "Attachment (%s)" +msgstr "Allegato (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Appunto (%d/%d)" + +msgid "You have to select one record." +msgstr "Devi selezionare un record." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Creato da:" + +msgid "Created at:" +msgstr "Creato il:" + +msgid "Edited by:" +msgstr "Modificato da:" + +msgid "Edited at:" +msgstr "Modifica il:" + +msgid "Model:" +msgstr "Modello:" + +msgid "Are you sure to remove this record?" +msgstr "Sei sicuro di rimuovere questo record?" + +msgid "Are you sure to remove those records?" +msgstr "Sei sicuro di rimuovere questi records?" + +msgid "Records not removed." +msgstr "Record non rimossi." + +msgid "Records removed." +msgstr "Record rimossi." + +msgid "Working now on the duplicated record(s)." +msgstr "Stai ora lavorando nel record duplicato." + +msgid "Record saved." +msgstr "Record salvato." + +msgid " of " +msgstr " di " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Questo record è stato modificato\n" +"intendi salvarlo?" + +msgid "Launch action" +msgstr "Lanciare l'azione" + +msgid "Relate" +msgstr "Collegato" + +msgid "Open related records" +msgstr "Apri record relazionati" + +msgid "Report" +msgstr "Rapporto" + +msgid "Open report" +msgstr "Apri rapporto" + +msgid "Print" +msgstr "Stampa" + +msgid "Print report" +msgstr "Stampa report" + +msgid "_Copy URL" +msgstr "_Copia URL" + +msgid "Copy URL into clipboard" +msgstr "Copia l'URL negli appunti" + +msgid "Unknown" +msgstr "Sconosciuto" + +msgid "Limit" +msgstr "Limite" + +msgid "Search Limit Settings" +msgstr "Impostazione dei limiti di ricerca" + +msgid "Limit:" +msgstr "Limite:" + +#, python-format +msgid "Notes (%s)" +msgstr "Appunti (%s)" + +msgid "Edit User Preferences" +msgstr "Modifica le preferenze dell'utente" + +msgid "Preference" +msgstr "Preferenza" + +msgid "Revision" +msgstr "Revisione" + +msgid "Select a revision" +msgstr "Scegli una revisione" + +msgid "Revision:" +msgstr "Revisione:" + +msgid "_Switch View" +msgstr "_Cambia vista" + +msgid "Switch View" +msgstr "Cambia vista" + +msgid "_Previous" +msgstr "_Precedente" + +msgid "Previous Record" +msgstr "Record Precedente" + +msgid "_Next" +msgstr "_Successivo" + +msgid "Next Record" +msgstr "Prossimo Record" + +msgid "_Search" +msgstr "_Cerca" + +msgid "_New" +msgstr "_Nuovo" + +msgid "Create a new record" +msgstr "Crea nuovo record" + +msgid "_Save" +msgstr "_Salva" + +msgid "Save this record" +msgstr "Salva questo record." + +msgid "_Reload/Undo" +msgstr "_Ricarica/Ripristina" + +msgid "Reload/Undo" +msgstr "Ricarica/Ripristina" + +msgid "_Duplicate" +msgstr "_Duplicato" + +msgid "_Delete..." +msgstr "_Eliimina" + +msgid "View _Logs..." +msgstr "Visualizza_Log..." + +msgid "Show revisions..." +msgstr "Mostra revisioni..." + +msgid "A_ttachments..." +msgstr "A_llegati..." + +msgid "Add an attachment to the record" +msgstr "Aggiungi un allegato al record" + +msgid "_Notes..." +msgstr "_Appunti..." + +msgid "Add a note to the record" +msgstr "Aggiungi un appunto sul record" + +msgid "_Actions..." +msgstr "_Azioni..." + +#, fuzzy +msgid "_Relate..." +msgstr "Riferire..." + +msgid "_Report..." +msgstr "_Rapporto..." + +msgid "_Print..." +msgstr "_Stampa..." + +msgid "_E-Mail..." +msgstr "_E-Mail..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Aggiungi un appunto sul record" + +msgid "_Export Data..." +msgstr "_Esporta dati" + +msgid "_Import Data..." +msgstr "_Importa dati" + +msgid "Copy _URL..." +msgstr "Copia _URL..." + +msgid "_Close Tab" +msgstr "_Chiudi scheda" + +msgid "All fields" +msgstr "Tutti i campi" + +msgid "_Add" +msgstr "Profilo" + +msgid "_Remove" +msgstr "_Rimuovi" + +msgid "_Clear" +msgstr "_Pulisci" + +msgid "Fields selected" +msgstr "Campi selezionati" + +msgid "CSV Parameters" +msgstr "Parametri CSV" + +msgid "Delimiter:" +msgstr "Delimitatore:" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "Codifica:" + +msgid "Use locale format" +msgstr "Usa il formato locale" + +msgid "Field name" +msgstr "Nome di campo" + +#, python-format +msgid "CSV Export: %s" +msgstr "Esportazione CSV: %s" + +msgid "_Save Export" +msgstr "_Salva esportazione" + +msgid "_URL Export" +msgstr "_URL Esportazione" + +msgid "_Delete Export" +msgstr "_Elimina Esportazione" + +msgid "Predefined exports" +msgstr "esportazioni predefinite" + +msgid "Name" +msgstr "Nome" + +msgid "Open" +msgstr "Apri" + +msgid "Listed Records" +msgstr "Record elencati" + +msgid "Selected Records" +msgstr "Record selezionati" + +msgid "Ignore search limit" +msgstr "Ignora limite di ricerca" + +msgid "Add field names" +msgstr "Aggiungi nomi dei campo" + +#, python-format +msgid "%s (string)" +msgstr "%s (stringa)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nome modello)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (nome record)" + +msgid "What is the name of this export?" +msgstr "Qual è il nome di questa esportazione?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Sostituisci definizione '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "%d record salvato." + +#, python-format +msgid "%d records saved." +msgstr "%d record salvato." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "Errore" + +msgid "Link" +msgstr "" + +msgid "Delete" +msgstr "Elimina" + +msgid "New" +msgstr "Nuovo" + +msgid "Switch" +msgstr "Cambia" + +#, fuzzy +msgid "Add" +msgstr "Profilo" + +msgid "Remove " +msgstr "" + +#, fuzzy +msgid "Create a new record " +msgstr "Crea nuovo record" + +msgid "Delete selected record " +msgstr "Elimina il record selezionato " + +msgid "Undelete selected record " +msgstr "Non eliminare il record selezionato " + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +#, fuzzy +msgid "_Auto-Detect" +msgstr "Autorilevamento" + +msgid "File to Import:" +msgstr "File da importare:" + +msgid "Open..." +msgstr "Apri..." + +msgid "Lines to Skip:" +msgstr "Righe da saltare." + +#, fuzzy +msgid "You must select an import file first." +msgstr "Devi prima selezionare un file di import!" + +msgid "Error opening CSV file" +msgstr "Errore nell'apertura del file CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Errore nel processare il file nel campo %s." + +#, python-format +msgid "%d record imported." +msgstr "%d record importati." + +#, python-format +msgid "%d records imported." +msgstr "%d record importati." + +msgid "Search" +msgstr "Ricerca" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "Wizard" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +#, fuzzy +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Data di creazione" + +#, fuzzy +msgid "Edited by" +msgstr "Modifica" + +#, fuzzy +msgid "Edited at" +msgstr "Modifica" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "Impossibile configurare la vista ad albero %s" + +msgid "Unable to set view tree state" +msgstr "Impossibile configurare la vista ad albero" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Dimensione immagine" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "Salva come" + +msgid "Image size too large." +msgstr "" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr "" + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "Mostra segnalibri dei filtri" + +msgid "Remove this bookmark" +msgstr "Rimuovi questo segnalibro" + +msgid "Bookmark this filter" +msgstr "contrassegnare questo filtro" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "Nome segnalibro:" + +msgid "Find" +msgstr "Trova" + +msgid "Today" +msgstr "Oggi" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "Vista settimanale" + +msgid "Month View" +msgstr "Vista mensile" + +msgid "Week" +msgstr "Settimana" + +msgid "Select..." +msgstr "Seleziona..." + +msgid "Clear" +msgstr "Pulisci" + +msgid "All files" +msgstr "Tutti i files" + +msgid "Show plain text" +msgstr "Mostra testo semplice" + +msgid "Add value" +msgstr "Aggiungi valore" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Rimuovi \"%s\"" + +msgid "Images" +msgstr "Immagini" + +msgid "Add existing record" +msgstr "Aggiungi un record esistente" + +msgid "Remove selected record" +msgstr "Rimuovi i record selezionati" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +#, fuzzy +msgid "Edit selected record" +msgstr "Modifica il record selezionato " + +#, fuzzy +msgid "Delete selected record" +msgstr "Rimuovi i record selezionati" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +#, fuzzy +msgid "Translation" +msgstr "Traduzione" + +msgid "Edit" +msgstr "Modifica" + +msgid "Fuzzy" +msgstr "Vago" + +#, fuzzy +msgid "You need to save the record before adding translations." +msgstr "E' necessario salvare il record prima di aggiungere le traduzioni!" + +#, fuzzy +msgid "No other language available." +msgstr "Nessun altra lingua disponibile!" + +msgid "Translate view" +msgstr "Traduci vista" diff --git a/tryton/tryton/data/locale/ja_JP/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/ja_JP/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..a0f8e45068 --- /dev/null +++ b/tryton/tryton/data/locale/ja_JP/LC_MESSAGES/tryton.po @@ -0,0 +1,1605 @@ +# Japanese (Japan) translations for tryton. +# Copyright (C) 2010 B2CK +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2010. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +#: tryton/config.py:74 +msgid "specify alternate config file" +msgstr "代替設定ファイルを指定" + +#: tryton/config.py:77 +msgid "development mode" +msgstr "" + +#: tryton/config.py:80 +msgid "logging everything at INFO level" +msgstr "全てをINFOレベルでログを取る" + +#: tryton/config.py:82 +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +#: tryton/config.py:85 +msgid "specify the login user" +msgstr "ログインユーザーを指定" + +#: tryton/config.py:87 +#, fuzzy +msgid "specify the server hostname:port" +msgstr "サーバーホスト名を指定" + +#: tryton/config.py:127 +#, python-format, python-format, fuzzy, python-format +msgid "Unable to write config file %s." +msgstr "設定ファイル %s が書きこめません!" + +#: tryton/translate.py:185 +#, python-format +msgid "Unable to set locale %s" +msgstr "%s ロケールに設定できません" + +#: tryton/action/main.py:89 tryton/common/button.py:54 +#, fuzzy +msgid ", " +msgstr ", " + +#: tryton/action/main.py:91 +#, fuzzy +msgid ",…" +msgstr ",…" + +#: tryton/action/main.py:92 +#, python-format +msgid "%s (%s)" +msgstr "" + +#: tryton/action/main.py:187 +msgid "Select your action" +msgstr "動作を選択する" + +#: tryton/action/main.py:193 +#, fuzzy +msgid "No action defined." +msgstr "動作が定義されていません!" + +#: tryton/common/button.py:54 +msgid "By: " +msgstr "" + +#: tryton/common/common.py:307 tryton/gui/window/shortcuts.py:64 +msgid "Selection" +msgstr "選択" + +#: tryton/common/common.py:309 tryton/common/common.py:365 +#: tryton/common/common.py:368 tryton/common/common.py:642 +#: tryton/common/common.py:693 tryton/common/common.py:796 +#: tryton/gui/window/email.py:23 tryton/gui/window/limit.py:24 +#: tryton/gui/window/preference.py:35 tryton/gui/window/revision.py:25 +#: tryton/gui/window/view_form/view/form_gtk/widget.py:149 +#: tryton/gui/window/view_form/view/graph.py:106 +#: tryton/gui/window/win_csv.py:173 tryton/gui/window/win_form.py:68 +#: tryton/gui/window/win_search.py:54 +#, fuzzy +msgid "Cancel" +msgstr "キャンセル(_C)" + +#: tryton/common/common.py:310 tryton/common/common.py:797 +#: tryton/gui/window/email.py:28 tryton/gui/window/limit.py:29 +#: tryton/gui/window/preference.py:40 tryton/gui/window/revision.py:30 +#: tryton/gui/window/shortcuts.py:25 +#: tryton/gui/window/view_form/view/form_gtk/widget.py:154 +#: tryton/gui/window/view_form/view/graph.py:111 +#: tryton/gui/window/win_csv.py:178 tryton/gui/window/win_form.py:97 +#: tryton/gui/window/win_search.py:72 +msgid "OK" +msgstr "" + +#: tryton/common/common.py:315 +msgid "Your selection:" +msgstr "あなたの選択:" + +#: tryton/common/common.py:366 tryton/gui/window/form.py:114 +#: tryton/gui/window/view_form/view/form_gtk/binary.py:83 +msgid "Select" +msgstr "" + +#: tryton/common/common.py:369 tryton/gui/window/win_export.py:83 +msgid "Save" +msgstr "保存" + +#: tryton/common/common.py:447 +#: tryton/gui/window/view_form/view/form_gtk/binary.py:31 +#: tryton/gui/window/view_form/view/form_gtk/binary.py:116 +#: tryton/gui/window/view_form/view/graph.py:171 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:493 +#: tryton/gui/window/win_export.py:305 +msgid "Save As..." +msgstr "名前を付けて保存" + +#: tryton/common/common.py:592 +msgid "Always ignore this warning." +msgstr "常にこの警告を無視する。" + +#: tryton/common/common.py:597 +msgid "Do you want to proceed?" +msgstr "" + +#: tryton/common/common.py:643 +msgid "No" +msgstr "" + +#: tryton/common/common.py:644 tryton/common/domain_parser.py:239 +msgid "Yes" +msgstr "" + +#: tryton/common/common.py:689 tryton/common/common.py:983 +msgid "Concurrency Exception" +msgstr "並列性例外" + +#: tryton/common/common.py:691 +msgid "This record has been modified while you were editing it." +msgstr "" + +#: tryton/common/common.py:694 +msgid "Cancel saving" +msgstr "" + +#: tryton/common/common.py:696 +msgid "Compare" +msgstr "比較" + +#: tryton/common/common.py:697 +msgid "See the modified version" +msgstr "" + +#: tryton/common/common.py:699 +msgid "Write Anyway" +msgstr "とにかく書き込む" + +#: tryton/common/common.py:700 +msgid "Save your current version" +msgstr "" + +#: tryton/common/common.py:729 +#, fuzzy +msgid "Application Error" +msgstr "アプリケーションエラー!" + +#: tryton/common/common.py:732 +msgid "Report Bug" +msgstr "バグ報告" + +#: tryton/common/common.py:733 tryton/gui/window/dblogin.py:36 +msgid "Close" +msgstr "" + +#: tryton/common/common.py:751 +msgid "Error: " +msgstr "エラー:" + +#: tryton/common/common.py:768 +#, python-format, fuzzy +msgid "To report bugs you must have an account on %s" +msgstr "記録しておきたいバグを報告%s" + +#: tryton/common/common.py:794 +msgid "Bug Tracker" +msgstr "バグトラッカー" + +#: tryton/common/common.py:811 +msgid "User:" +msgstr "ユーザー:" + +#: tryton/common/common.py:819 +msgid "Password:" +msgstr "パスワード:" + +#: tryton/common/common.py:875 +msgid "" +"The same bug was already reported by another user.\n" +"To keep you informed your username is added to the nosy-list of this issue" +msgstr "" +"別のユーザーに同じバグが報告されています。\n" +"お知らせするためにこの問題の通知リストにあなたのユーザー名を追加しました。" + +#: tryton/common/common.py:886 +msgid "Created new bug with ID " +msgstr "新しいバグがID番号と一緒に作成されました" + +#: tryton/common/common.py:894 +#, fuzzy +msgid "" +"Connection error.\n" +"Bad username or password." +msgstr "" +"接続エラー!\n" +"ユーザー名またはパスワードが違います!" + +#: tryton/common/common.py:899 +msgid "Exception:" +msgstr "例外:" + +#: tryton/common/common.py:926 +#, python-format +msgid "Check URL: %s" +msgstr "" + +#: tryton/common/common.py:934 +msgid "Unable to check for new version" +msgstr "" + +#: tryton/common/common.py:940 +msgid "A new version is available!" +msgstr "" + +#: tryton/common/common.py:942 +msgid "Download" +msgstr "" + +#: tryton/common/common.py:1323 +#, fuzzy +msgid "..." +msgstr "..." + +#: tryton/common/completion.py:25 +msgid "Search..." +msgstr "" + +#: tryton/common/completion.py:27 +msgid "Create..." +msgstr "" + +#: tryton/common/completion.py:70 +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +#: tryton/common/datetime_.py:32 tryton/common/datetime_.py:238 +#: tryton/common/datetime_.py:391 +#, fuzzy +msgid "Value" +msgstr "偽" + +#: tryton/common/datetime_.py:33 tryton/common/datetime_.py:239 +#: tryton/common/datetime_.py:392 +msgid "Displayed value" +msgstr "" + +#: tryton/common/datetime_.py:37 tryton/common/datetime_.py:195 +#: tryton/common/datetime_.py:242 tryton/common/datetime_.py:347 +#, fuzzy +msgid "Format" +msgstr "ノーマル(_N)" + +#: tryton/common/datetime_.py:38 tryton/common/datetime_.py:196 +#: tryton/common/datetime_.py:243 tryton/common/datetime_.py:348 +msgid "Display format" +msgstr "" + +#: tryton/common/datetime_.py:63 +#, fuzzy +msgid "Open the calendar" +msgstr "カレンダーを開く" + +#: tryton/common/datetime_.py:396 tryton/common/datetime_.py:401 +msgid "Date Format" +msgstr "" + +#: tryton/common/datetime_.py:397 tryton/common/datetime_.py:402 +msgid "Displayed date format" +msgstr "" + +#: tryton/common/domain_parser.py:239 +msgid "y" +msgstr "" + +#: tryton/common/domain_parser.py:239 tryton/common/domain_parser.py:490 +#: tryton/gui/window/view_form/view/screen_container.py:567 +msgid "True" +msgstr "真" + +#: tryton/common/domain_parser.py:239 +msgid "t" +msgstr "" + +#: tryton/common/domain_parser.py:490 +#: tryton/gui/window/view_form/view/screen_container.py:567 +msgid "False" +msgstr "偽" + +#: tryton/common/popup_menu.py:84 +#, fuzzy +msgid "Edit..." +msgstr "終了(_Q)" + +#: tryton/common/popup_menu.py:89 +#, fuzzy +msgid "Attachments..." +msgstr "添付:" + +#: tryton/common/popup_menu.py:95 +msgid "Notes..." +msgstr "" + +#: tryton/common/popup_menu.py:107 +#, fuzzy +msgid "Actions..." +msgstr "実行(_A)" + +#: tryton/common/popup_menu.py:108 +#, fuzzy +msgid "Relate..." +msgstr "削除(_D)" + +#: tryton/common/popup_menu.py:109 +#, fuzzy +msgid "Report..." +msgstr "データのインポート(_I)" + +#: tryton/common/popup_menu.py:110 +#, fuzzy +msgid "E-Mail..." +msgstr "Eメール(_E)" + +#: tryton/common/popup_menu.py:111 +#, fuzzy +msgid "Print..." +msgstr "印刷(_P)" + +#: tryton/common/timedelta.py:26 +msgid "Y" +msgstr "年" + +#: tryton/common/timedelta.py:27 +msgid "M" +msgstr "月" + +#: tryton/common/timedelta.py:28 +msgid "w" +msgstr "週" + +#: tryton/common/timedelta.py:29 +msgid "d" +msgstr "日" + +#: tryton/common/timedelta.py:30 +msgid "h" +msgstr "時" + +#: tryton/common/timedelta.py:31 +msgid "m" +msgstr "分" + +#: tryton/common/timedelta.py:32 +msgid "s" +msgstr "" + +#: tryton/gui/main.py:123 +#, fuzzy +msgid "Preferences..." +msgstr "設定(_P)" + +#: tryton/gui/main.py:127 +#, fuzzy +msgid "Toolbar" +msgstr "ツールバー(_T)" + +#: tryton/gui/main.py:128 +#, fuzzy +msgid "Default" +msgstr "デフォルト(_D)" + +#: tryton/gui/main.py:129 +#, fuzzy +msgid "Text and Icons" +msgstr "テキストとアイコン(_T)" + +#: tryton/gui/main.py:130 +#, fuzzy +msgid "Text" +msgstr "テキスト(_T)" + +#: tryton/gui/main.py:131 +#, fuzzy +msgid "Icons" +msgstr "アイコン(_I)" + +#: tryton/gui/main.py:134 +#, fuzzy +msgid "Form" +msgstr "フォーム(_F)" + +#: tryton/gui/main.py:135 +msgid "Save Width/Height" +msgstr "幅/高さを保存" + +#: tryton/gui/main.py:136 +msgid "Save Tree State" +msgstr "" + +#: tryton/gui/main.py:137 +msgid "Fast Tabbing" +msgstr "" + +#: tryton/gui/main.py:138 +msgid "Spell Checking" +msgstr "スペルチェック" + +#: tryton/gui/main.py:140 +msgid "PDA Mode" +msgstr "" + +#: tryton/gui/main.py:141 +msgid "Search Limit..." +msgstr "" + +#: tryton/gui/main.py:142 +#, fuzzy +msgid "Email..." +msgstr "Eメール(_E)" + +#: tryton/gui/main.py:143 +msgid "Check Version" +msgstr "" + +#: tryton/gui/main.py:145 +#, fuzzy +msgid "Options" +msgstr "オプション(_O)" + +#: tryton/gui/main.py:148 +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "キーボードショートカット(_K)" + +#: tryton/gui/main.py:149 +#, fuzzy +msgid "About..." +msgstr "Trytonについて" + +#: tryton/gui/main.py:150 +#, fuzzy +msgid "Help" +msgstr "ヘルプ(_H)" + +#: tryton/gui/main.py:153 +msgid "Quit" +msgstr "" + +#: tryton/gui/main.py:395 +msgid "No result found." +msgstr "" + +#: tryton/gui/main.py:447 +msgid "Favorites" +msgstr "" + +#: tryton/gui/main.py:464 tryton/gui/window/dblogin.py:438 +#: tryton/gui/window/form.py:139 +msgid "Manage..." +msgstr "" + +#: tryton/gui/main.py:541 tryton/gui/window/form.py:559 +msgid "Action" +msgstr "実行" + +#: tryton/gui/main.py:563 +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +#: tryton/gui/main.py:747 +msgid "Close Tab" +msgstr "タブを閉じる" + +#: tryton/gui/window/about.py:37 +msgid "modularity, scalability and security" +msgstr "" + +#: tryton/gui/window/about.py:43 +msgid "translator-credits" +msgstr "" + +#: tryton/gui/window/attachment.py:24 +#, python-format, python-format, fuzzy, python-format +msgid "Attachments (%s)" +msgstr "添付(%s)" + +#: tryton/gui/window/dblogin.py:33 +msgid "Profile Editor" +msgstr "" + +#: tryton/gui/window/dblogin.py:49 +msgid "Profile" +msgstr "" + +#: tryton/gui/window/dblogin.py:58 +msgid "Add new profile" +msgstr "" + +#: tryton/gui/window/dblogin.py:63 +msgid "Remove selected profile" +msgstr "" + +#: tryton/gui/window/dblogin.py:77 tryton/gui/window/dblogin.py:450 +#, fuzzy +msgid "Host:" +msgstr "ポート番号:" + +#: tryton/gui/window/dblogin.py:88 tryton/gui/window/dblogin.py:462 +msgid "Database:" +msgstr "データベース:" + +#: tryton/gui/window/dblogin.py:108 +#, fuzzy +msgid "Fetching databases list" +msgstr "新規データベースセットアップ:" + +#: tryton/gui/window/dblogin.py:122 +#, fuzzy +msgid "Username:" +msgstr "ユーザー名:" + +#: tryton/gui/window/dblogin.py:309 tryton/gui/window/dblogin.py:619 +#, fuzzy +msgid "Incompatible version of the server" +msgstr "サーバのバージョンが非互換です!" + +#: tryton/gui/window/dblogin.py:311 tryton/gui/window/dblogin.py:623 +#, fuzzy +msgid "Could not connect to the server" +msgstr "サーバーに接続できませんでした!" + +#: tryton/gui/window/dblogin.py:390 +msgid "Login" +msgstr "ログイン" + +#: tryton/gui/window/dblogin.py:396 +msgid "_Cancel" +msgstr "キャンセル(_C)" + +#: tryton/gui/window/dblogin.py:398 +msgid "Cancel connection to the Tryton server" +msgstr "Trytonサーバへの接続を中止する" + +#: tryton/gui/window/dblogin.py:400 +msgid "C_onnect" +msgstr "接続(_N)" + +#: tryton/gui/window/dblogin.py:404 +msgid "Connect the Tryton server" +msgstr "Trytonサーバーへ接続する" + +#: tryton/gui/window/dblogin.py:432 +msgid "Profile:" +msgstr "" + +#: tryton/gui/window/dblogin.py:447 +msgid "Host / Database information" +msgstr "" + +#: tryton/gui/window/dblogin.py:478 +msgid "User name:" +msgstr "ユーザー名:" + +#: tryton/gui/window/email.py:19 +msgid "Email" +msgstr "Eメール" + +#: tryton/gui/window/email.py:36 +msgid "Email Program Settings" +msgstr "Emailプログラムの設定" + +#: tryton/gui/window/email.py:39 +msgid "Command Line:" +msgstr "コマンドライン:" + +#: tryton/gui/window/email.py:49 +msgid "Legend of Available Placeholders:" +msgstr "利用できるプレースホルダーの凡例:" + +#: tryton/gui/window/email.py:56 +msgid "To:" +msgstr "宛先:" + +#: tryton/gui/window/email.py:60 +msgid "CC:" +msgstr "CC:" + +#: tryton/gui/window/email.py:64 +msgid "Subject:" +msgstr "題名:" + +#: tryton/gui/window/email.py:68 +msgid "Body:" +msgstr "本文:" + +#: tryton/gui/window/email.py:72 +msgid "Attachment:" +msgstr "添付:" + +#: tryton/gui/window/form.py:135 +msgid "Add..." +msgstr "" + +#: tryton/gui/window/form.py:155 +#, python-format +msgid "Attachment(%d)" +msgstr "添付(%d)" + +#: tryton/gui/window/form.py:185 +#, python-format +msgid "Note(%d)" +msgstr "" + +#: tryton/gui/window/form.py:207 +#, fuzzy +msgid "You have to select one record." +msgstr "レコードを一つ選択して下さい!" + +#: tryton/gui/window/form.py:211 +msgid "ID:" +msgstr "ID:" + +#: tryton/gui/window/form.py:212 +msgid "Creation User:" +msgstr "作成ユーザー:" + +#: tryton/gui/window/form.py:213 +msgid "Creation Date:" +msgstr "作成日:" + +#: tryton/gui/window/form.py:214 +msgid "Latest Modification by:" +msgstr "最終更新ユーザー:" + +#: tryton/gui/window/form.py:215 +msgid "Latest Modification Date:" +msgstr "最終更新日:" + +#: tryton/gui/window/form.py:234 +msgid "Model:" +msgstr "モデル:" + +#: tryton/gui/window/form.py:312 +msgid "Are you sure to remove this record?" +msgstr "このレコードを削除しますか?" + +#: tryton/gui/window/form.py:314 +msgid "Are you sure to remove those records?" +msgstr "このレコードを削除しますか?" + +#: tryton/gui/window/form.py:317 +#, fuzzy +msgid "Records not removed." +msgstr "レコードが削除されませんでした!" + +#: tryton/gui/window/form.py:319 +#, fuzzy +msgid "Records removed." +msgstr "レコードが削除されました!" + +#: tryton/gui/window/form.py:356 +#, fuzzy +msgid "Working now on the duplicated record(s)." +msgstr "重複したレコードで作業中です!" + +#: tryton/gui/window/form.py:368 +#, fuzzy +msgid "Record saved." +msgstr "レコードが保存されました!" + +#: tryton/gui/window/form.py:485 +msgid " of " +msgstr " 中 " + +#: tryton/gui/window/form.py:505 +#, fuzzy +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"レコードが変更されています\n" +"保存しますか?" + +#: tryton/gui/window/form.py:559 +msgid "Launch action" +msgstr "" + +#: tryton/gui/window/form.py:560 +#, fuzzy +msgid "Relate" +msgstr "作成(_C)" + +#: tryton/gui/window/form.py:560 +#, fuzzy +msgid "Open related records" +msgstr "レコードを開く" + +#: tryton/gui/window/form.py:562 +#, fuzzy +msgid "Report" +msgstr "レポート" + +#: tryton/gui/window/form.py:562 +#, fuzzy +msgid "Open report" +msgstr "レコードを開く" + +#: tryton/gui/window/form.py:563 +#, fuzzy +msgid "E-Mail" +msgstr "Eメール" + +#: tryton/gui/window/form.py:563 +msgid "E-Mail report" +msgstr "" + +#: tryton/gui/window/form.py:564 +msgid "Print" +msgstr "印刷" + +#: tryton/gui/window/form.py:564 +msgid "Print report" +msgstr "" + +#: tryton/gui/window/form.py:590 +msgid "_Copy URL" +msgstr "" + +#: tryton/gui/window/form.py:593 +msgid "Copy URL into clipboard" +msgstr "" + +#: tryton/gui/window/form.py:652 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:932 +msgid "Unknown" +msgstr "未知" + +#: tryton/gui/window/limit.py:20 +#, fuzzy +msgid "Limit" +msgstr "リミット:" + +#: tryton/gui/window/limit.py:37 +msgid "Search Limit Settings" +msgstr "" + +#: tryton/gui/window/limit.py:40 +msgid "Limit:" +msgstr "リミット:" + +#: tryton/gui/window/note.py:17 +#, python-format +msgid "Notes (%s)" +msgstr "" + +#: tryton/gui/window/preference.py:25 +msgid "Preferences" +msgstr "設定" + +#: tryton/gui/window/preference.py:58 +msgid "Edit User Preferences" +msgstr "個人設定の編集" + +#: tryton/gui/window/preference.py:85 +msgid "Preference" +msgstr "設定" + +#: tryton/gui/window/revision.py:21 +#, fuzzy +msgid "Revision" +msgstr "前" + +#: tryton/gui/window/revision.py:38 +#, fuzzy +msgid "Select a revision" +msgstr "選択" + +#: tryton/gui/window/revision.py:41 +#, fuzzy +msgid "Revision:" +msgstr "前" + +#: tryton/gui/window/shortcuts.py:20 +msgid "Keyboard Shortcuts" +msgstr "キーボードショートカット" + +#: tryton/gui/window/shortcuts.py:35 +msgid "Text Entries Shortcuts" +msgstr "テキストエントリショートカット" + +#: tryton/gui/window/shortcuts.py:36 +msgid "Cut selected text" +msgstr "選択されたテキストを切り取り" + +#: tryton/gui/window/shortcuts.py:37 +msgid "Copy selected text" +msgstr "選択されたテキストをコピー" + +#: tryton/gui/window/shortcuts.py:38 +msgid "Paste copied text" +msgstr "コピーされたテキストを貼り付け" + +#: tryton/gui/window/shortcuts.py:39 +msgid "Next widget" +msgstr "次のウィジェット" + +#: tryton/gui/window/shortcuts.py:40 +msgid "Previous widget" +msgstr "前のウィジェット" + +#: tryton/gui/window/shortcuts.py:41 +msgid "Relation Entries Shortcuts" +msgstr "リレーションエントリショートカット" + +#: tryton/gui/window/shortcuts.py:42 +msgid "Create new relation" +msgstr "新しいリレーションを作る" + +#: tryton/gui/window/shortcuts.py:43 +msgid "Open/Search relation" +msgstr "リレーションを開く/検索" + +#: tryton/gui/window/shortcuts.py:44 +#, fuzzy +msgid "List Entries Shortcuts" +msgstr "テキストエントリショートカット" + +#: tryton/gui/window/shortcuts.py:45 +#, fuzzy +msgid "Create new line" +msgstr "新しいリレーションを作る" + +#: tryton/gui/window/shortcuts.py:46 +#, fuzzy +msgid "Open relation" +msgstr "リレーションを開く/検索" + +#: tryton/gui/window/shortcuts.py:47 +msgid "Mark line for deletion" +msgstr "" + +#: tryton/gui/window/shortcuts.py:48 +msgid "Unmark line for deletion" +msgstr "" + +#: tryton/gui/window/shortcuts.py:51 +msgid "Edition Widgets" +msgstr "エディションウィジェット" + +#: tryton/gui/window/shortcuts.py:54 +msgid "Move Cursor" +msgstr "" + +#: tryton/gui/window/shortcuts.py:55 +msgid "Move to right" +msgstr "" + +#: tryton/gui/window/shortcuts.py:56 +msgid "Move to left" +msgstr "" + +#: tryton/gui/window/shortcuts.py:57 +msgid "Move up" +msgstr "" + +#: tryton/gui/window/shortcuts.py:58 +msgid "Move down" +msgstr "" + +#: tryton/gui/window/shortcuts.py:59 +msgid "Move up of one page" +msgstr "" + +#: tryton/gui/window/shortcuts.py:60 +msgid "Move down of one page" +msgstr "" + +#: tryton/gui/window/shortcuts.py:61 +msgid "Move to top" +msgstr "" + +#: tryton/gui/window/shortcuts.py:62 +msgid "Move to bottom" +msgstr "" + +#: tryton/gui/window/shortcuts.py:63 +msgid "Move to parent" +msgstr "" + +#: tryton/gui/window/shortcuts.py:65 tryton/gui/window/shortcuts.py:66 +msgid "Select all" +msgstr "" + +#: tryton/gui/window/shortcuts.py:67 tryton/gui/window/shortcuts.py:68 +msgid "Unselect all" +msgstr "" + +#: tryton/gui/window/shortcuts.py:69 +msgid "Select parent" +msgstr "" + +#: tryton/gui/window/shortcuts.py:70 tryton/gui/window/shortcuts.py:71 +#: tryton/gui/window/shortcuts.py:72 tryton/gui/window/shortcuts.py:73 +msgid "Select/Activate current row" +msgstr "" + +#: tryton/gui/window/shortcuts.py:74 +msgid "Toggle selection" +msgstr "" + +#: tryton/gui/window/shortcuts.py:75 +msgid "Expand/Collapse" +msgstr "" + +#: tryton/gui/window/shortcuts.py:76 +msgid "Expand row" +msgstr "" + +#: tryton/gui/window/shortcuts.py:77 +msgid "Collapse row" +msgstr "" + +#: tryton/gui/window/shortcuts.py:78 +msgid "Toggle row" +msgstr "" + +#: tryton/gui/window/shortcuts.py:79 +msgid "Collapse all rows" +msgstr "" + +#: tryton/gui/window/shortcuts.py:80 +msgid "Expand all rows" +msgstr "" + +#: tryton/gui/window/shortcuts.py:83 +msgid "Tree view" +msgstr "" + +#: tryton/gui/window/tabcontent.py:43 +msgid "_Switch View" +msgstr "ビューの切替(_S)" + +#: tryton/gui/window/tabcontent.py:44 +#, fuzzy +msgid "Switch View" +msgstr "ビューの切替(_S)" + +#: tryton/gui/window/tabcontent.py:49 +msgid "_Previous" +msgstr "前(_P)" + +#: tryton/gui/window/tabcontent.py:50 +msgid "Previous Record" +msgstr "前のレコード" + +#: tryton/gui/window/tabcontent.py:55 +msgid "_Next" +msgstr "次(_N)" + +#: tryton/gui/window/tabcontent.py:56 +msgid "Next Record" +msgstr "次のレコード" + +#: tryton/gui/window/tabcontent.py:61 +#, fuzzy +msgid "_Search" +msgstr "検索" + +#: tryton/gui/window/tabcontent.py:67 +msgid "_New" +msgstr "新規(_N)" + +#: tryton/gui/window/tabcontent.py:68 +msgid "Create a new record" +msgstr "レコードの新規作成" + +#: tryton/gui/window/tabcontent.py:73 tryton/gui/window/win_form.py:85 +msgid "_Save" +msgstr "保存(_S)" + +#: tryton/gui/window/tabcontent.py:74 +msgid "Save this record" +msgstr "レコードの保存" + +#: tryton/gui/window/tabcontent.py:79 +msgid "_Reload/Undo" +msgstr "再読み込み/やり直し(_R)" + +#: tryton/gui/window/tabcontent.py:80 +#, fuzzy +msgid "Reload/Undo" +msgstr "再読み込み/やり直し(_R)" + +#: tryton/gui/window/tabcontent.py:85 +msgid "_Duplicate" +msgstr "複製(_D)" + +#: tryton/gui/window/tabcontent.py:90 +msgid "_Delete..." +msgstr "削除(_D)" + +#: tryton/gui/window/tabcontent.py:96 +msgid "View _Logs..." +msgstr "ログの閲覧(_L)" + +#: tryton/gui/window/tabcontent.py:100 +msgid "Show revisions..." +msgstr "" + +#: tryton/gui/window/tabcontent.py:105 +#, fuzzy +msgid "A_ttachments..." +msgstr "添付:" + +#: tryton/gui/window/tabcontent.py:106 +msgid "Add an attachment to the record" +msgstr "レコードに添付を追加する" + +#: tryton/gui/window/tabcontent.py:112 +msgid "_Notes..." +msgstr "" + +#: tryton/gui/window/tabcontent.py:113 +msgid "Add a note to the record" +msgstr "" + +#: tryton/gui/window/tabcontent.py:118 +msgid "_Actions..." +msgstr "実行(_A)" + +#: tryton/gui/window/tabcontent.py:123 +#, fuzzy +msgid "_Relate..." +msgstr "削除(_D)" + +#: tryton/gui/window/tabcontent.py:129 +#, fuzzy +msgid "_Report..." +msgstr "データのインポート(_I)" + +#: tryton/gui/window/tabcontent.py:134 +#, fuzzy +msgid "_E-Mail..." +msgstr "Eメール(_E)" + +#: tryton/gui/window/tabcontent.py:139 +msgid "_Print..." +msgstr "印刷(_P)" + +#: tryton/gui/window/tabcontent.py:145 +msgid "_Export Data..." +msgstr "データのエクスポート(_E)" + +#: tryton/gui/window/tabcontent.py:150 +msgid "_Import Data..." +msgstr "データのインポート(_I)" + +#: tryton/gui/window/tabcontent.py:155 +msgid "Copy _URL..." +msgstr "" + +#: tryton/gui/window/tabcontent.py:161 +msgid "_Close Tab" +msgstr "タブを閉じる(_C)" + +#: tryton/gui/window/win_csv.py:60 +msgid "All fields" +msgstr "全てのフィールド" + +#: tryton/gui/window/win_csv.py:69 +msgid "_Add" +msgstr "追加(_A)" + +#: tryton/gui/window/win_csv.py:77 +msgid "_Remove" +msgstr "削除(_R)" + +#: tryton/gui/window/win_csv.py:85 +#, fuzzy +msgid "_Clear" +msgstr "クリア" + +#: tryton/gui/window/win_csv.py:105 +msgid "Fields selected" +msgstr "" + +#: tryton/gui/window/win_csv.py:124 +msgid "CSV Parameters" +msgstr "CSVパラメータ" + +#: tryton/gui/window/win_csv.py:132 +msgid "Delimiter:" +msgstr "" + +#: tryton/gui/window/win_csv.py:146 +msgid "Quote char:" +msgstr "" + +#: tryton/gui/window/win_csv.py:155 +msgid "Encoding:" +msgstr "エンコーディング:" + +#: tryton/gui/window/win_csv.py:195 tryton/gui/window/win_csv.py:199 +msgid "Field name" +msgstr "フィールド名" + +#: tryton/gui/window/win_export.py:28 +#, python-format +msgid "CSV Export: %s" +msgstr "" + +#: tryton/gui/window/win_export.py:32 +#, fuzzy +msgid "_Save Export" +msgstr "エクスポートを保存" + +#: tryton/gui/window/win_export.py:40 +#, fuzzy +msgid "_Delete Export" +msgstr "エクスポートを削除" + +#: tryton/gui/window/win_export.py:55 +msgid "Predefined exports" +msgstr "定義済みエキスポート" + +#: tryton/gui/window/win_export.py:62 +msgid "Name" +msgstr "名前" + +#: tryton/gui/window/win_export.py:82 +msgid "Open" +msgstr "開く" + +#: tryton/gui/window/win_export.py:87 +msgid "Add _field names" +msgstr "フィールド名を追加(_F)" + +#: tryton/gui/window/win_export.py:103 +#, python-format +msgid "%s (string)" +msgstr "" + +#: tryton/gui/window/win_export.py:106 +#, python-format +msgid "%s (model name)" +msgstr "" + +#: tryton/gui/window/win_export.py:108 +#, python-format +msgid "%s (record name)" +msgstr "" + +#: tryton/gui/window/win_export.py:207 +msgid "What is the name of this export?" +msgstr "このエクスポートの名前は何ですか?" + +#: tryton/gui/window/win_export.py:213 +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#: tryton/gui/window/win_export.py:328 +#, python-format, python-format, fuzzy, python-format +msgid "%d record saved." +msgstr "%d レコード保存されました!" + +#: tryton/gui/window/win_export.py:330 +#, python-format, python-format, fuzzy, python-format +msgid "%d records saved." +msgstr "%d レコード保存されました!" + +#: tryton/gui/window/win_export.py:333 +#, python-format, python-format, fuzzy, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"操作が失敗しました!\n" +"エラーメッセージ:\n" +"%s" + +#: tryton/gui/window/win_export.py:334 tryton/gui/window/win_import.py:112 +#: tryton/gui/window/win_import.py:139 +msgid "Error" +msgstr "エラー" + +#: tryton/gui/window/win_form.py:38 +msgid "Link" +msgstr "リンク" + +#: tryton/gui/window/win_form.py:66 +msgid "Delete" +msgstr "削除" + +#: tryton/gui/window/win_form.py:78 tryton/gui/window/win_search.py:65 +msgid "New" +msgstr "新規(_N)" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:48 +#: tryton/gui/window/win_form.py:140 +msgid "Switch" +msgstr "切替" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:56 +#: tryton/gui/window/view_form/view/screen_container.py:222 +#: tryton/gui/window/win_form.py:148 +msgid "Previous" +msgstr "前" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:67 +#: tryton/gui/window/view_form/view/screen_container.py:231 +#: tryton/gui/window/win_form.py:159 +msgid "Next" +msgstr "次" + +#: tryton/gui/window/win_form.py:176 +msgid "Add" +msgstr "追加" + +#: tryton/gui/window/win_form.py:186 +msgid "Remove " +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:118 +#: tryton/gui/window/win_form.py:198 +#, fuzzy +msgid "Create a new record " +msgstr "レコードの新規作成" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:134 +#: tryton/gui/window/win_form.py:208 +#, fuzzy +msgid "Delete selected record " +msgstr "選択したレコードを削除" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:142 +#: tryton/gui/window/win_form.py:219 +#, fuzzy +msgid "Undelete selected record " +msgstr "選択したレコードを削除" + +#: tryton/gui/window/win_import.py:26 +#, python-format +msgid "CSV Import: %s" +msgstr "" + +#: tryton/gui/window/win_import.py:30 +#, fuzzy +msgid "_Auto-Detect" +msgstr "自動検出" + +#: tryton/gui/window/win_import.py:41 +msgid "File to Import:" +msgstr "インポートするファイル:" + +#: tryton/gui/window/view_form/view/form_gtk/binary.py:200 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:466 +#: tryton/gui/window/win_import.py:43 +msgid "Open..." +msgstr "開く" + +#: tryton/gui/window/win_import.py:48 +msgid "Lines to Skip:" +msgstr "スキップする行:" + +#: tryton/gui/window/win_import.py:101 +#, fuzzy +msgid "You must select an import file first." +msgstr "最初にインポートするファイルを選択して下さい!" + +#: tryton/gui/window/win_import.py:112 +msgid "Error opening CSV file" +msgstr "CSVファイルを開くときにエラーが起こりました" + +#: tryton/gui/window/win_import.py:138 +#, python-format, python-format, fuzzy, python-format +msgid "Error processing the file at field %s." +msgstr "ファイルの%sフィールドの処理でエラーが起こりました。" + +#: tryton/gui/window/win_import.py:198 +#, python-format, python-format, fuzzy, python-format +msgid "%d record imported." +msgstr "%d レコードをインポートしました!" + +#: tryton/gui/window/win_import.py:200 +#, python-format, python-format, fuzzy, python-format +msgid "%d records imported." +msgstr "%d レコードをインポートしました。" + +#: tryton/gui/window/view_form/view/form_gtk/dictionary.py:344 +#: tryton/gui/window/view_form/view/form_gtk/many2many.py:46 +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:81 +#: tryton/gui/window/view_form/view/screen_container.py:146 +#: tryton/gui/window/win_search.py:36 tryton/gui/window/win_search.py:59 +msgid "Search" +msgstr "検索" + +#: tryton/gui/window/win_search.py:91 +#, python-format +msgid "Search %s" +msgstr "" + +#: tryton/gui/window/wizard.py:160 +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +#: tryton/gui/window/wizard.py:317 +msgid "Wizard" +msgstr "ウィザード" + +#: tryton/gui/window/view_form/screen/screen.py:206 +#, fuzzy +msgid "ID" +msgstr "日" + +#: tryton/gui/window/view_form/screen/screen.py:207 +#, fuzzy +msgid "Creation User" +msgstr "作成ユーザー:" + +#: tryton/gui/window/view_form/screen/screen.py:208 +#, fuzzy +msgid "Creation Date" +msgstr "作成日:" + +#: tryton/gui/window/view_form/screen/screen.py:209 +#, fuzzy +msgid "Modification User" +msgstr "最終更新日:" + +#: tryton/gui/window/view_form/screen/screen.py:210 +#, fuzzy +msgid "Modification Date" +msgstr "最終更新日:" + +#: tryton/gui/window/view_form/screen/screen.py:806 +#, python-format, python-format, fuzzy +msgid "Unable to get view tree state for %s" +msgstr "%s ロケールに設定できません" + +#: tryton/gui/window/view_form/screen/screen.py:867 +#, fuzzy +msgid "Unable to set view tree state" +msgstr "%s ロケールに設定できません" + +#: tryton/gui/window/view_form/screen/screen.py:1054 +#, python-format +msgid "\"%s\" is not valid according to its domain" +msgstr "" + +#: tryton/gui/window/view_form/screen/screen.py:1061 +#, python-format +msgid "\"%s\" is required" +msgstr "" + +#: tryton/gui/window/view_form/screen/screen.py:1065 +#, python-format +msgid "The values of \"%s\" are not valid" +msgstr "" + +#: tryton/gui/window/view_form/screen/screen.py:1114 +msgid "Pre-validation" +msgstr "" + +#: tryton/gui/window/view_form/view/form.py:261 +#: tryton/gui/window/view_form/view/form.py:263 +#: tryton/gui/window/view_form/view/form_gtk/dictionary.py:476 +#: tryton/gui/window/view_form/view/form_gtk/dictionary.py:478 +#: tryton/gui/window/view_form/view/form_gtk/widget.py:172 +#: tryton/gui/window/view_form/view/form_gtk/widget.py:174 +#: tryton/gui/window/view_form/view/list.py:530 +#: tryton/gui/window/view_form/view/list.py:532 +msgid ":" +msgstr ":" + +#: tryton/gui/window/view_form/view/graph.py:102 +msgid "Image Size" +msgstr "画像サイズ" + +#: tryton/gui/window/view_form/view/graph.py:121 +msgid "Width:" +msgstr "幅:" + +#: tryton/gui/window/view_form/view/graph.py:129 +msgid "Height:" +msgstr "高さ:" + +#: tryton/gui/window/view_form/view/graph.py:140 +msgid "PNG image (*.png)" +msgstr "PNG画像 (*.png)" + +#: tryton/gui/window/view_form/view/graph.py:149 +msgid "Save As" +msgstr "名前をつけて保存" + +#: tryton/gui/window/view_form/view/graph.py:161 +#, fuzzy +msgid "Image size too large." +msgstr "画像サイズが大きすぎます!" + +#: tryton/gui/window/view_form/view/screen_container.py:23 +msgid ".." +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:152 +msgid "Open filters" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:209 +msgid "Show bookmarks of filters" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:371 +msgid "Remove this bookmark" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:379 +msgid "Bookmark this filter" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:396 +msgid "Show active records" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:398 +msgid "Show inactive records" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:479 +msgid "Bookmark Name:" +msgstr "" + +#: tryton/gui/window/view_form/view/screen_container.py:599 +msgid "Find" +msgstr "検索" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:22 +#, fuzzy +msgid "Today" +msgstr "本文:" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:32 +msgid "go back" +msgstr "" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:73 +msgid "go forward" +msgstr "" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:82 +msgid "previous year" +msgstr "" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:96 +msgid "next year" +msgstr "" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:107 +#, fuzzy +msgid "Week View" +msgstr "ビューの切替" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:117 +#, fuzzy +msgid "Month View" +msgstr "ビューの切替" + +#: tryton/gui/window/view_form/view/calendar_gtk/toolbar.py:142 +msgid "Week" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/binary.py:39 +msgid "Select..." +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/binary.py:47 +msgid "Clear" +msgstr "クリア" + +#: tryton/gui/window/view_form/view/form_gtk/binary.py:60 +msgid "All files" +msgstr "全てのファイル" + +#: tryton/gui/window/view_form/view/form_gtk/char.py:169 +msgid "Show plain text" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/dictionary.py:369 +msgid "Add value" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/dictionary.py:490 +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/image.py:50 +msgid "Images" +msgstr "画像" + +#: tryton/gui/window/view_form/view/form_gtk/many2many.py:66 +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:99 +#, fuzzy +msgid "Add existing record" +msgstr "レコードの保存" + +#: tryton/gui/window/view_form/view/form_gtk/many2many.py:74 +#, fuzzy +msgid "Remove selected record " +msgstr "選択したレコードを削除" + +#: tryton/gui/window/view_form/view/form_gtk/many2one.py:305 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:582 +#, fuzzy +msgid "Open the record " +msgstr "レコードを開く" + +#: tryton/gui/window/view_form/view/form_gtk/many2one.py:306 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:583 +msgid "Clear the field " +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/many2one.py:309 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:586 +#, fuzzy +msgid "Search a record " +msgstr "レコードを検索する" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:108 +#, fuzzy +msgid "Remove selected record" +msgstr "選択したレコードを削除" + +#: tryton/gui/window/view_form/view/form_gtk/one2many.py:126 +#, fuzzy +msgid "Edit selected record " +msgstr "選択したレコードを編集" + +#: tryton/gui/window/view_form/view/form_gtk/progressbar.py:35 +#: tryton/gui/window/view_form/view/list_gtk/widget.py:908 +#, python-format +msgid "%s%%" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/richtextbox.py:85 +msgid "Foreground" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/richtextbox.py:328 +msgid "Select a color" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/widget.py:136 +#, fuzzy +msgid "Translation" +msgstr "翻訳を追加" + +#: tryton/gui/window/view_form/view/form_gtk/widget.py:209 +msgid "Edit" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/widget.py:214 +msgid "Fuzzy" +msgstr "" + +#: tryton/gui/window/view_form/view/form_gtk/widget.py:285 +#, fuzzy +msgid "You need to save the record before adding translations." +msgstr "翻訳を追加する前にレコードを保存して下さい!" + +#: tryton/gui/window/view_form/view/form_gtk/widget.py:296 +#, fuzzy +msgid "No other language available." +msgstr "他の利用できる言語はありません" + +#: tryton/plugins/translation/__init__.py:17 +#: tryton/plugins/translation/__init__.py:24 +msgid "Translate view" +msgstr "翻訳ビュー" diff --git a/tryton/tryton/data/locale/lo/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/lo/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..ae2fe6b184 --- /dev/null +++ b/tryton/tryton/data/locale/lo/LC_MESSAGES/tryton.po @@ -0,0 +1,1216 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "ລະບຸ config file ທີ່ສະຫຼັບກັນ" + +msgid "development mode" +msgstr "ຮູບແບບການພັດທະນາ" + +msgid "logging everything at INFO level" +msgstr "ເຂົ້າໃຊ້ທຸກຢ່າງໃນລະດັບຂໍ້ມູນ" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" +"ລະບຸ ລະດັບ ການບັນທຶກ: ແກ້ໄຂຂໍ້ຜິດພາດ, ຂໍ້ມູນ, ຄຳເຕືອນ, ຂໍ້ຜິດພາດ, ທີ່ສຳຄັນ" + +msgid "specify the login user" +msgstr "ລະບຸ ຜູ້ເຂົ້າໃຊ້ງານ" + +#, fuzzy +msgid "specify the server hostname:port" +msgstr "ລະບຸ ທີ່ຢູ່ ຂອງ ເຄື່ອງແມ່ຂ່າຍ" + +#, python-format +msgid "Unable to write config file %s." +msgstr "ບໍ່ສາມາດ ຂຽນ ຟາຍລ໌ ຕັ້ງຄ່າ (config file) %s ນີ້ໄດ້." + +#, python-format +msgid "Unable to set locale %s" +msgstr "ບໍ່ສາມາດຕັ້ງຄ່າ ທ້ອງຖິ່ນ %s ໄດ້." + +msgid ", " +msgstr "," + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "ເລືອກການດຳເນີນການຂອງທ່ານ" + +msgid "No action defined." +msgstr "ບໍ່ມີ ການດຳເນີນການ ທີ່ລະບຸໄວ້!" + +msgid "By: " +msgstr "ໂດຍ:" + +msgid "Selection" +msgstr "ການເລືອກ" + +#, fuzzy +msgid "Cancel" +msgstr "_ຍົກເລີກ" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "ການເລືອກຂອງທ່ານ:" + +msgid "Select" +msgstr "ເລືອກ..." + +msgid "Save" +msgstr "ບັນທຶກ" + +msgid "Save As..." +msgstr "ບັນທຶກເປັນ..." + +msgid "Do you want to proceed?" +msgstr "ທ່ານຕ້ອງການດຳເນີນການຕໍ່ໄປບໍ່?" + +msgid "Always ignore this warning." +msgstr "ບໍ່ສົນກັບຄຳເຕືອນນີ້ເລີຍ." + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "ແມ່ນ" + +msgid "Concurrency Exception" +msgstr "ຍົກເວັ້ນການເຂົ້າເຖິງພ້ອມກັນ" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "ສົມທຽບ" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "ຂຽນລົງເລີຍ" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "ສົມທຽບ: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "ໂປຣແກຣມ ມີຂໍ້ຜິດພາດ!" + +msgid "Report Bug" +msgstr "ລາຍງານຂໍ້ຜິດພາດ" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "ຊອກຫາ..." + +msgid "Create..." +msgstr "ສ້າງ..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "ຄ່າ" + +msgid "Displayed value" +msgstr "ຄ່າສະແດງອອກ" + +msgid "Format" +msgstr "ຮູບແບບ" + +msgid "Display format" +msgstr "ຮູບແບບສະແດງອອກ" + +msgid "Open the calendar" +msgstr "ໄຂປະຕິທິນ" + +msgid "Date Format" +msgstr "ຮູບແບບວັນທີ" + +msgid "Displayed date format" +msgstr "ຮູບແບບວັນທີທີ່ສະແດງອອກ" + +msgid "y" +msgstr "ມ" + +msgid "True" +msgstr "ຈິງ" + +msgid "t" +msgstr "ຈ" + +msgid "False" +msgstr "ຜິດ" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "ທີ່ກ່ຽວພັນ" + +msgid "Edit..." +msgstr "ແກ້ໄຂ..." + +msgid "Attachments..." +msgstr "ຄັດຕິດ..." + +msgid "Notes..." +msgstr "ໝາຍເຫດ..." + +msgid "Actions..." +msgstr "ດຳເນີນການ..." + +msgid "Relate..." +msgstr "ທີ່ກ່ຽວພັນ..." + +msgid "Report..." +msgstr "ລາຍງານ..." + +msgid "Print..." +msgstr "ພິມອອກ..." + +msgid "E-Mail..." +msgstr "ອີ-ເມວລ໌..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "ເລືອກສີ" + +msgid "Y" +msgstr "ປ" + +msgid "M" +msgstr "ດ" + +msgid "w" +msgstr "ອ" + +msgid "d" +msgstr "ມ" + +msgid "h" +msgstr "ມ." + +msgid "m" +msgstr "ນ" + +msgid "s" +msgstr "ວ" + +#, fuzzy +msgid "Preferences..." +msgstr "_ການຕັ້ງຄ່າຂອງ..." + +#, fuzzy +msgid "Toolbar" +msgstr "_ແຖບເຄື່ອງມື" + +#, fuzzy +msgid "Default" +msgstr "_ຄ່າເດີມ" + +#, fuzzy +msgid "Text and Icons" +msgstr "_ຂໍ້ຄວາມ ແລະ ປຸ່ມລັດ" + +#, fuzzy +msgid "Text" +msgstr "_ຂໍ້ຄວາມ" + +#, fuzzy +msgid "Icons" +msgstr "_ປຸ່ມລັດ" + +#, fuzzy +msgid "Form" +msgstr "_ແບບຟອມ" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "ບັນທຶກ ສະຖານະ ໂຄງສ້າງ" + +msgid "Spell Checking" +msgstr "ກວດສອບການສະກົດຄຳ" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "ຈຳກັດການຊອກຫາ..." + +msgid "Check Version" +msgstr "" + +#, fuzzy +msgid "Options" +msgstr "_ທາງເລືອກ" + +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "_ປຸ່ມລັດທາງແປ້ນພິມ..." + +#, fuzzy +msgid "About..." +msgstr "_ກ່ຽວກັບ..." + +#, fuzzy +msgid "Help" +msgstr "_ຊ່ອຍເຫຼືອ" + +msgid "No result found." +msgstr "ບໍ່ພົບຜົນຊອກຫາ" + +#, fuzzy +msgid "Favorites" +msgstr "ທີ່_ມັກຫຼາຍ" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "ການດຳເນີນການ" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"ການດຳເນີນການດັ່ງຕໍ່ໄປນີ້ ຕ້ອງການໃຫ້ ອັດແຖບງານທັງໝົດລົງ\n" +"ທ່ານຕ້ອງການສືບຕໍ່ບໍ່?" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "ການຕັ້ງຄ່າ" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "_ແຖບງານກ່ອນໜ້າ" + +#, fuzzy +msgid "Next tab" +msgstr "_ແຖບງານ ຕໍ່ໄປ" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "ຕັດຂໍ້ຄວາມທີ່ເລືອກໄວ້" + +msgid "Copy selected text" +msgstr "ກ່າຍເອົາຂໍ້ຄວາມທີ່ເລືອກໄວ້" + +msgid "Paste copied text" +msgstr "ວາງຂໍ້ຄວາມທີ່ກ່າຍເອົາໃສ່" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "ສ້າງການກ່ຽວພັນໃໝ່" + +msgid "Open/Search relation" +msgstr "ໄຂ/ຊອກຫາການກ່ຽວພັນ" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "ສະຫຼັບປ່ຽນການເບິ່ງ" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "ໄຂການກ່ຽວພັນ" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "ບໍ່ໝາຍແຖວທີ່ຈະລຶບ" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "ຍ້າຍ ເຄິກເຊີຣ໌" + +#, fuzzy +msgid "Move right" +msgstr "ຍ້າຍໄປຂວາ" + +#, fuzzy +msgid "Move left" +msgstr "ຍ້າຍໄປຊ້າຍ" + +msgid "Move up" +msgstr "ຍ້າຍຂຶ້ນ" + +msgid "Move down" +msgstr "ຍ້າຍລົງ" + +msgid "Move up of one page" +msgstr "ຫຍັບຂຶ້ນໜຶ່ງໜ້າ" + +msgid "Move down of one page" +msgstr "ຍ້າຍລົງລຸ່ມໜຶ່ງໜ້າ" + +msgid "Move to top" +msgstr "ຍ້າຍຂຶ້ນເທິງສຸດ" + +msgid "Move to bottom" +msgstr "ຍ້າຍລົງລຸ່ມ" + +msgid "Move to parent" +msgstr "ຍ້າຍໄປຫາບັນຊີແມ່" + +msgid "Select all" +msgstr "ເລືອກທັງໝົດ" + +msgid "Unselect all" +msgstr "ບໍ່ເລືອກທັງໝົດ" + +msgid "Select parent" +msgstr "ເລືອກ ບັນຊີແມ່" + +msgid "Select/Activate current row" +msgstr "ເລືອກ/ໃຊ້ງານແຖວປັດຈຸບັນ" + +msgid "Toggle selection" +msgstr "ສະຫຼັບການເລືອກ" + +msgid "Expand/Collapse" +msgstr "ຂະຫຽາຍອອກ/ຍຸບເຂົ້າກັນ" + +msgid "Expand row" +msgstr "ຂະຫຽາຍແຖວອອກ" + +msgid "Collapse row" +msgstr "ຍຸບແຖວເຂົ້າກັນ" + +msgid "Toggle row" +msgstr "ສະຫຼັບແຖວ" + +msgid "Collapse all rows" +msgstr "ຍຸບແຖວທັງໝົດ" + +msgid "Expand all rows" +msgstr "ຂະຫຽາຍແຖວທັງໝົດ" + +msgid "Close Tab" +msgstr "ອັດແຖບງານ" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "ເອກະສານຄັດຕິດ (%s)" + +msgid "Profile Editor" +msgstr "ຂໍ້ມູນລາຍລະອຽດບັນນາທິການ" + +msgid "Profile" +msgstr "ຂໍ້ມູນລາຍລະອຽດ" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "ແມ່ຂ່າຍ:" + +msgid "Database:" +msgstr "ຖານຂໍ້ມູນ:" + +msgid "Fetching databases list" +msgstr "ລາຍການດຶງເອົາຖານຂໍ້ມູນ" + +msgid "Username:" +msgstr "ຜູ້ໃຊ້:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "ບໍ່ຖືກລຸ່ນກັບແມ່ຂ່າຍ" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບແມ່ຂ່າຍໄດ້" + +msgid "Login" +msgstr "ເຂົ້າສູ່ລະບົບ" + +msgid "_Cancel" +msgstr "_ຍົກເລີກ" + +msgid "Cancel connection to the Tryton server" +msgstr "ຍົກເລີກການເຊື່ອມຕໍ່ກັບແມ່ຂ່າຍ ໄຕຣຕັອນ" + +msgid "C_onnect" +msgstr "ເ_ຊື່ອມຕໍ່" + +msgid "Connect the Tryton server" +msgstr "ເຊື່ອມຕໍ່ກັບແມ່ຂ່າຍ ໄຕຣຕັອນ" + +msgid "Profile:" +msgstr "ຂໍ້ມູນລາຍລະອຽດ:" + +msgid "Host / Database information" +msgstr "ແມ່ຂ່າຍ / ຂໍ້ມູນກ່ຽວກັບຖານຂໍ້ມູນ" + +msgid "User name:" +msgstr "ຊື່ຜູ້ໃຊ້:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "ບໍ່ສາມາດຕັ້ງຄ່າ ທ້ອງຖິ່ນ %s ໄດ້." + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "ອີເມວລ໌ %s" + +msgid "To:" +msgstr "ເຖິງ:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "ຫົວເລື່ອງ:" + +#, fuzzy +msgid "Body" +msgstr "ເນື້ອຄວາມ:" + +#, fuzzy +msgid "Reports" +msgstr "ລາຍງານ..." + +#, fuzzy +msgid "Attachments" +msgstr "ເອກະສານຄັດຕິດ:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "ເລືອກທັງໝົດ" + +#, fuzzy +msgid "Remove File" +msgstr "ເອົາອອກ" + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "ກ່ອນໜ້າ" + +msgid "Previous" +msgstr "ກ່ອນໜ້າ" + +msgid "Next" +msgstr "ຕໍ່ໄປ" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "ເອກະສານຄັດຕິດ (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "ທ່ານຕ້ອງໄດ້ເລືອກ ໜຶ່ງແຖວຂໍ້ມູນ." + +msgid "ID:" +msgstr "ເລກລໍາດັບ:" + +#, fuzzy +msgid "Created by:" +msgstr "ວັນທີສ້າງ" + +#, fuzzy +msgid "Created at:" +msgstr "ວັນທີສ້າງ" + +#, fuzzy +msgid "Edited by:" +msgstr "ແກ້ໄຂ" + +#, fuzzy +msgid "Edited at:" +msgstr "ແກ້ໄຂ" + +msgid "Model:" +msgstr "ແບບ:" + +msgid "Are you sure to remove this record?" +msgstr "ທ່ານແນ່ໃຈບໍ່ທີ່ຈະລຶບແຖວຂໍ້ມູນນີ້?" + +msgid "Are you sure to remove those records?" +msgstr "ທ່ານແນ່ໃຈບໍ່ທີ່ຈະລຶບແຖວຂໍ້ມູນເຫຼົ່ານີ້?" + +msgid "Records not removed." +msgstr "ແຖວຂໍ້ມູນບໍ່ທັນຖືກເອົາອອກ." + +msgid "Records removed." +msgstr "ແຖວຂໍ້ມູນຖືກເອົາອອກແລ້ວ." + +msgid "Working now on the duplicated record(s)." +msgstr "ດຽວນີ້ກຳລັງເຮັດວຽກກັບແຖວຂໍ້ມູນທີ່ສ້າງຂຶ້ນຊ້ອນກັນ." + +msgid "Record saved." +msgstr "ແຖວຂໍ້ມູນບັນທຶກແລ້ວ" + +msgid " of " +msgstr " ຂອງ " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "ແຖວຂໍ້ມູນນີ້ ຖືກແກ້ໄຂແລ້ວ ທ່ານຕ້ອງການບັນທຶກໄວ້ບໍ່?" + +msgid "Launch action" +msgstr "ເປີດການດຳເນີນການ" + +msgid "Relate" +msgstr "ທີ່ກ່ຽວພັນ" + +msgid "Open related records" +msgstr "ໄຂແຖວຂໍ້ມູນທີ່ກ່ຽວພັນ" + +msgid "Report" +msgstr "ລາຍງານ..." + +msgid "Open report" +msgstr "ໄຂບົດລາຍງານ" + +msgid "Print" +msgstr "ພິມອອກ" + +msgid "Print report" +msgstr "ພິມລາຍງານອອກ" + +msgid "_Copy URL" +msgstr "_ກ່າຍ URL" + +msgid "Copy URL into clipboard" +msgstr "ກ່າຍເອົາ URL ໃສ່ ຄລິບບອດ" + +msgid "Unknown" +msgstr "ບໍ່ຮູ້" + +msgid "Limit" +msgstr "ຂີດຈຳກັດ:" + +msgid "Search Limit Settings" +msgstr "ການຕັ້ງຄ່າຂີດຈຳກັດການຊອກຫາ" + +msgid "Limit:" +msgstr "ຂີດຈຳກັດ:" + +#, python-format +msgid "Notes (%s)" +msgstr "ບັນທຶກ (%s)" + +msgid "Edit User Preferences" +msgstr "ແກ້ໄຂການຕັ້ງຄ່າຜູ້ໃຊ້ງານ" + +msgid "Preference" +msgstr "ການຕັ້ງຄ່າ" + +msgid "Revision" +msgstr "ການກວດແກ້ຄືນ" + +msgid "Select a revision" +msgstr "ເລືອກໜຶ່ງການກວດແກ້ຄືນ" + +msgid "Revision:" +msgstr "ການກວດແກ້ຄືນ:" + +msgid "_Switch View" +msgstr "_ສະຫຼັບໜ້າເບິ່ງ" + +#, fuzzy +msgid "Switch View" +msgstr "_ສະຫຼັບໜ້າເບິ່ງ" + +msgid "_Previous" +msgstr "_ກ່ອນໜ້າ" + +msgid "Previous Record" +msgstr "ແຖວຂໍ້ມູນກ່ອນນີ້" + +msgid "_Next" +msgstr "_ຕໍ່ໄປ" + +msgid "Next Record" +msgstr "ແຖວຂໍ້ມູນ ຕໍ່ໄປ" + +msgid "_Search" +msgstr "_ຊອກຫາ" + +msgid "_New" +msgstr "_ສ້າງໃໝ່" + +msgid "Create a new record" +msgstr "ສ້າງແຖວຂໍ້ມູນໃໝ່" + +msgid "_Save" +msgstr "_ບັນທຶກ" + +msgid "Save this record" +msgstr "ບັນທຶກແຖວຂໍ້ມູນນີ້" + +msgid "_Reload/Undo" +msgstr "_ໂຫຼດຄືນໃໝ່/ຍົກເລີກ" + +#, fuzzy +msgid "Reload/Undo" +msgstr "_ໂຫຼດຄືນໃໝ່/ຍົກເລີກ" + +msgid "_Duplicate" +msgstr "_ເຮັດຊໍ້າກັນ" + +msgid "_Delete..." +msgstr "_ລຶບ..." + +msgid "View _Logs..." +msgstr "ເບິ່ງ_ບັນທຶກ..." + +msgid "Show revisions..." +msgstr "ສະແດງການກວດແກ້ຄືນ..." + +msgid "A_ttachments..." +msgstr "ຄ_ັດຕິດ..." + +msgid "Add an attachment to the record" +msgstr "ເພີ່ມເອກະສານຄັດຕິດໃສ່ແຖວຂໍ້ມູນ" + +msgid "_Notes..." +msgstr "_ຈົດບັນທຶກ..." + +msgid "Add a note to the record" +msgstr "ເພີ່ມບັນທຶກຂໍ້ຄວາມໃສ່ແຖວຂໍ້ມູນ" + +msgid "_Actions..." +msgstr "_ດຳເນີນການ..." + +msgid "_Relate..." +msgstr "_ທີ່ກ່ຽວພັນ..." + +msgid "_Report..." +msgstr "_ລາຍງານ..." + +msgid "_Print..." +msgstr "_ພິມອອກ..." + +msgid "_E-Mail..." +msgstr "_ອີ-ແມວລ໌.." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "ເພີ່ມບັນທຶກຂໍ້ຄວາມໃສ່ແຖວຂໍ້ມູນ" + +msgid "_Export Data..." +msgstr "_ສົ່ງອອກຂໍ້ມູນ..." + +msgid "_Import Data..." +msgstr "_ນໍາເຂົ້າຂໍ້ມູນ..." + +msgid "Copy _URL..." +msgstr "ກ່າຍ_URL..." + +msgid "_Close Tab" +msgstr "_ອັດແຖບງານ" + +msgid "All fields" +msgstr "ຊ່ອງຂໍ້ມູນທັງໝົດ" + +msgid "_Add" +msgstr "_ເພີ່ມເຂົ້າ" + +msgid "_Remove" +msgstr "_ເອົາອອກ" + +msgid "_Clear" +msgstr "_ໃຫ້ເປົາ" + +msgid "Fields selected" +msgstr "ຊ່ອງຂໍ້ມູນທີ່ເລືອກໄວ້" + +msgid "CSV Parameters" +msgstr "ຂອບເຂດການໃຊ້ (parameters) ຂອງ ຟາຍລ໌ CSV" + +msgid "Delimiter:" +msgstr "ຕົວຂັ້ນ:" + +msgid "Quote char:" +msgstr "ຂໍ້ຄວາມອ້າງອີງ:" + +msgid "Encoding:" +msgstr "ເຂົ້າລະຫັດ:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "ຊື່ຊ່ອງຂໍ້ມູນ" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "_ບັນທຶກການສົ່ງອອກ" + +#, fuzzy +msgid "_URL Export" +msgstr "_ບັນທຶກການສົ່ງອອກ" + +msgid "_Delete Export" +msgstr "_ລຶບການສົ່ງອອກ" + +msgid "Predefined exports" +msgstr "ການສົ່ງອອກທີ່ຈັດຕຽມໄວ້ກ່ອນ" + +msgid "Name" +msgstr "ຊື່" + +msgid "Open" +msgstr "ໄຂ" + +#, fuzzy +msgid "Listed Records" +msgstr "ແກ້ໄຂ ແຖວຂໍ້ມູນ ທີ່ເລືອກ " + +#, fuzzy +msgid "Selected Records" +msgstr "ແກ້ໄຂ ແຖວຂໍ້ມູນ ທີ່ເລືອກ " + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "ເພີ່ມ_ຊ່ອງຂໍ້ມູນ" + +#, python-format +msgid "%s (string)" +msgstr "%s (ສະຕຣິງ)" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "ຊື່ຂອງການສົ່ງອອກນີ້ແມ່ນຫຍັງ?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "ຂຽນທັບ ຄຳອະທິບາຍ '%s' ນີ້ບໍ່?" + +#, python-format +msgid "%d record saved." +msgstr "ແຖວຂໍ້ມູນ %d ຖືກບັນທຶກແລ້ວ!" + +#, python-format +msgid "%d records saved." +msgstr "ບັນດາແຖວຂໍ້ມູນ %d ຖືກບັນທຶກແລ້ວ!" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"ການດຳເນີນການລົ້ມເຫຼວ!\n" +"ຂໍ້ຄວາມເຕືອນຂໍ້ຜິດພາດ:\n" +"%s" + +msgid "Error" +msgstr "ຜິດພາດ" + +msgid "Link" +msgstr "ເຊື່ອມໂຍງ" + +msgid "Delete" +msgstr "ລືບ" + +msgid "New" +msgstr "ສ້າງໃໝ່" + +msgid "Switch" +msgstr "ສະຫຼັບ" + +msgid "Add" +msgstr "ເພີ່ມ" + +msgid "Remove " +msgstr "ເອົາອອກ" + +msgid "Create a new record " +msgstr "ສ້າງແຖວຂໍ້ມູນໃໝ່ " + +msgid "Delete selected record " +msgstr "ລຶບແຖວຂໍ້ມູນທີ່ເລືອກ " + +msgid "Undelete selected record " +msgstr "ຍົກເລີກການລຶບແຖວທີ່ເລືອກ" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "_ກວດຫາ-ອັດຕະໂນມັດ" + +msgid "File to Import:" +msgstr "ຟາຍລ໌ທີ່ຈະນໍາເຂົ້າ:" + +msgid "Open..." +msgstr "ໄຂ..." + +msgid "Lines to Skip:" +msgstr "ແຖວທີ່ຈະຂວ້າມ:" + +msgid "You must select an import file first." +msgstr "ກ່ອນອື່ນ ທ່ານຕ້ອງໄດ້ເລືອກ ຟາຍລ໌ ທີ່ຈະນໍາເຂົ້າກ່ອນ." + +msgid "Error opening CSV file" +msgstr "ຜິດພາດໃນການໄຂ ຟາຍລ໌ CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "ປະມວນຜົນ ຟາຍລ໌ ຜິດພາດ ໃນຊ່ອງຂໍ້ມູນ %s." + +#, python-format +msgid "%d record imported." +msgstr "ແຖວຂໍ້ມູນ %d ຖືກນໍາເຂົ້າແລ້ວ!" + +#, python-format +msgid "%d records imported." +msgstr "ບັນດາແຖວຂໍ້ມູນ %d ຖືກນໍາເຂົ້າແລ້ວ!" + +msgid "Search" +msgstr "ຊອກຫາ" + +#, python-format +msgid "Search %s" +msgstr "ຊອກຫາ %s" + +msgid "Wizard" +msgstr "ໂຕນໍາພາສ້າງ" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "ເລກລໍາດັບ" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "ວັນທີສ້າງ" + +#, fuzzy +msgid "Edited by" +msgstr "ແກ້ໄຂ" + +#, fuzzy +msgid "Edited at" +msgstr "ແກ້ໄຂ" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "ບໍ່ສາມາດຕັ້ງຄ່າສະຖານະມູມມອງແບບຕົ້ນໄມ້ໄດ້ %s" + +msgid "Unable to set view tree state" +msgstr "ບໍ່ສາມາດຕັ້ງຄ່າສະຖານະມູມມອງແບບຕົ້ນໄມ້ໄດ້" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" ບໍ່ຖືກຕ້ອງ ກັບ ໂດເມນຂອງມັນ" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "ຕ້ອງໃຫ້ມີ \"%s\"" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "ຄ່າຂອງ \"%s\" ນີ້ ບໍ່ຖືກຕ້ອງ" + +msgid "Pre-validation" +msgstr "ກ່ອນ-ການກວດສອບ" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "ຂະໜາດຮູບ" + +msgid "Width:" +msgstr "ຄວາມກວ້າງ:" + +msgid "Height:" +msgstr "ຄວາມສູງ:" + +msgid "PNG image (*.png)" +msgstr "ຮູບແບບ PNG (*.png)" + +msgid "Save As" +msgstr "ບັນທຶກເປັນ" + +msgid "Image size too large." +msgstr "ຂະໜາດຮູບ ໃຫຍ່ໂພດ." + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "ໄຂໂຕກັ່ນຕອງ" + +msgid "Show bookmarks of filters" +msgstr "ສະແດງໝາຍໜ້າຂອງການກັ່ນຕອງ" + +msgid "Remove this bookmark" +msgstr "ເອົາໝາຍໜ້ານີ້ອອກ" + +msgid "Bookmark this filter" +msgstr "ໝາຍໜ້າກັ່ນຕອງນີ້" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "ຊື່ໝາຍໜ້າ:" + +msgid "Find" +msgstr "ຊອກພົບ" + +msgid "Today" +msgstr "ມື້ນີ້" + +msgid "go back" +msgstr "ກັບຄືນ" + +msgid "go forward" +msgstr "ໄປໜ້າ" + +msgid "previous year" +msgstr "ປີກ່ອນ" + +msgid "next year" +msgstr "ປີຕໍ່ໄປ" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "ເບິ່ງເປັນລາຍອາທິດ" + +msgid "Month View" +msgstr "ເບິ່ງເປັນເດືອນ" + +msgid "Week" +msgstr "ອາທິດ" + +msgid "Select..." +msgstr "ເລືອກ..." + +msgid "Clear" +msgstr "ໃຫ້ເປົ່າ" + +msgid "All files" +msgstr "ຟາຍລ໌ ທັງໝົດ" + +msgid "Show plain text" +msgstr "ສະແດງເປັນຂໍ້ຄວາມທຳມະດາ" + +msgid "Add value" +msgstr "ເພີ່ມຄ່າ" + +#, python-format +msgid "Remove \"%s\"" +msgstr "ເອົາ \"%s\" ອອກ" + +msgid "Images" +msgstr "ຮູບ" + +msgid "Add existing record" +msgstr "ເພີ່ມແຖວຂໍ້ມູນທີ່ມີຢູ່" + +msgid "Remove selected record" +msgstr "ເອົາແຖວຂໍ້ມູນທີ່ເລືອກນີ້ອອກ" + +msgid "Open the record " +msgstr "ໄຂແຖວຂໍ້ມູນ " + +#, fuzzy +msgid "Clear the field " +msgstr "ອະນາໄມແຖວຂໍ້ມູນ " + +msgid "Search a record " +msgstr "ຊອກຫາແຖວຂໍ້ມູນໜຶ່ງ " + +#, fuzzy +msgid "Edit selected record" +msgstr "ແກ້ໄຂ ແຖວຂໍ້ມູນ ທີ່ເລືອກ " + +#, fuzzy +msgid "Delete selected record" +msgstr "ເອົາແຖວຂໍ້ມູນທີ່ເລືອກນີ້ອອກ" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "ການແປ" + +msgid "Edit" +msgstr "ແກ້ໄຂ" + +msgid "Fuzzy" +msgstr "ເລືອນລາງ" + +msgid "You need to save the record before adding translations." +msgstr "ທ່ານຕ້ອງບັນທຶກຂໍ້ມູນໄວ້ກ່ອນຈຶ່ງເພີ່ມການແປເຂົ້າໄປໄດ້." + +msgid "No other language available." +msgstr "ບໍ່ມີພາສາອື່ນໃຫ້ໃຊ້." + +msgid "Translate view" +msgstr "ມູມມອງການແປ" diff --git a/tryton/tryton/data/locale/lt/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/lt/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..43afc71d36 --- /dev/null +++ b/tryton/tryton/data/locale/lt/LC_MESSAGES/tryton.po @@ -0,0 +1,1191 @@ +# Lithuanian (Lithuania) translations for tryton. +# Copyright (C) 2011 B2CK +# This file is distributed under the same license as the tryton project. +# Giedrius Slavinskas , 2011. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "nurodyti alternatyvų konfigūravimo failą" + +msgid "development mode" +msgstr "kūrimo veiksena" + +msgid "logging everything at INFO level" +msgstr "rašyti į žurnalą INFO lygiu" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "nurodyti rašymo į žurnalą lygmenį: DEBUG, INFO, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "nurodyti naudotoją" + +msgid "specify the server hostname:port" +msgstr "nurodyti tarnybinę stotį pavadinimas:prievadas" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Negalima įrašyti konfigūracinį failą %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Nepavyko nustatyti lokalės %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Pasirinkite veiksmą" + +msgid "No action defined." +msgstr "Nėra apibrėžtų veiksmų." + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "Parinktis" + +msgid "Cancel" +msgstr "Atsisakyti" + +msgid "OK" +msgstr "Gerai" + +msgid "Your selection:" +msgstr "Jūsų pasirinkimas:" + +msgid "Select" +msgstr "Pasirinkti" + +msgid "Save" +msgstr "Išsaugoti" + +msgid "Save As..." +msgstr "Įrašyti kaip..." + +msgid "Do you want to proceed?" +msgstr "Ar tikrai norite vykdyti?" + +msgid "Always ignore this warning." +msgstr "Daugiau nepaisyti šio pranešimo." + +msgid "No" +msgstr "Ne" + +msgid "Yes" +msgstr "Taip" + +msgid "Concurrency Exception" +msgstr "Lygiagretaus duomenų rašymo klaida" + +msgid "This record has been modified while you were editing it." +msgstr "Kol jūs taisėte šį įrašą, jis buvo pakeistas." + +msgid "Cancel saving" +msgstr "Nutraukti įrašymą" + +msgid "Compare" +msgstr "Palyginti" + +msgid "See the modified version" +msgstr "Žiūrėti pakeistą versiją" + +msgid "Write Anyway" +msgstr "Rašyti visvien" + +msgid "Save your current version" +msgstr "Išsaugoti dabartinę jūsų versiją" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Palyginti: %s" + +msgid "Close" +msgstr "Užverti" + +msgid "Application Error" +msgstr "Programos klaida" + +msgid "Report Bug" +msgstr "Pranešti apie klaidą" + +#, python-format +msgid "Check URL: %s" +msgstr "Pasirinkti URL: %s" + +#, fuzzy +msgid "Unable to check for new version." +msgstr "Negalima patikrinti naujos versijos" + +msgid "A new version is available!" +msgstr "Yra prieinama nauja versija!" + +msgid "Download" +msgstr "Atsisiųsti" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Ieškoti..." + +msgid "Create..." +msgstr "Sukurti..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Negalima rasti %s papildymo" + +msgid "Value" +msgstr "Reikšmė" + +msgid "Displayed value" +msgstr "Rodoma reikšmė" + +msgid "Format" +msgstr "Formatas" + +msgid "Display format" +msgstr "Rodyti formatą" + +msgid "Open the calendar" +msgstr "Atverti kalendorių" + +msgid "Date Format" +msgstr "Datos formatas" + +msgid "Displayed date format" +msgstr "Rodomas datos formatas" + +msgid "y" +msgstr "t" + +msgid "True" +msgstr "Tiesa" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "Netiesa" + +msgid "Digits" +msgstr "Skaitmenys" + +msgid "The number of decimal" +msgstr "Dešimtainių skaitmenų skaičius" + +#, fuzzy +msgid "Template" +msgstr "Susijęs" + +msgid "Edit..." +msgstr "Keisti..." + +msgid "Attachments..." +msgstr "Priedai..." + +msgid "Notes..." +msgstr "Pastabos..." + +msgid "Actions..." +msgstr "Veiksmai..." + +msgid "Relate..." +msgstr "Susiję įrašai..." + +msgid "Report..." +msgstr "Ataskaita..." + +msgid "Print..." +msgstr "Spausdinti..." + +msgid "E-Mail..." +msgstr "El. paštas..." + +msgid "Bold" +msgstr "Pusjuodis" + +msgid "Italic" +msgstr "Kursyvas" + +msgid "Underline" +msgstr "Pabrauktas" + +msgid "Align Left" +msgstr "Kairinė lygiuotė" + +msgid "Align Center" +msgstr "Centrinė lygiuotė" + +msgid "Align Right" +msgstr "Dešininė lygiuotė" + +msgid "Justify" +msgstr "Išlyginti" + +msgid "Foreground Color" +msgstr "Priekinio plano spalva" + +msgid "Select a color" +msgstr "Pasirinkti spalvą" + +msgid "Y" +msgstr "m." + +msgid "M" +msgstr "mėn." + +msgid "w" +msgstr "sav." + +msgid "d" +msgstr "d." + +msgid "h" +msgstr "val." + +msgid "m" +msgstr "min." + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Nustatymai..." + +msgid "Toolbar" +msgstr "Įrankių juosta" + +msgid "Default" +msgstr "Numatytasis" + +msgid "Text and Icons" +msgstr "Tekstas ir ikonos" + +msgid "Text" +msgstr "Tekstas" + +msgid "Icons" +msgstr "Ikonos" + +msgid "Form" +msgstr "Forma" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "Įsiminti hierarchinio sąrašo būseną" + +msgid "Spell Checking" +msgstr "Rašybos klaidų tikrinimas" + +msgid "PDA Mode" +msgstr "PDA režimas" + +msgid "Search Limit..." +msgstr "Paieškos ribojimas..." + +msgid "Check Version" +msgstr "Tikrinti versiją" + +msgid "Options" +msgstr "Parinktys" + +msgid "Keyboard Shortcuts..." +msgstr "Trumpiniai..." + +msgid "About..." +msgstr "Apie..." + +msgid "Help" +msgstr "Pagalba" + +msgid "No result found." +msgstr "Nerasta rezultatų." + +msgid "Favorites" +msgstr "Mėgstami" + +msgid "Manage..." +msgstr "Valdyti..." + +msgid "Action" +msgstr "Veiksmai" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Atliekant šį veiksmą bus uždarytos visos kortelės.\n" +"Ar norite tęsti?" + +msgid "Application Shortcuts" +msgstr "Programų šaukiniai" + +msgid "Global" +msgstr "Visuotinis" + +msgid "Preferences" +msgstr "Nustatymai" + +msgid "Search menu" +msgstr "Paieškos meniu" + +msgid "Toggle menu" +msgstr "Perjungti meniu" + +#, fuzzy +msgid "Previous tab" +msgstr "_Buvusi kortelė" + +#, fuzzy +msgid "Next tab" +msgstr "Kita kortelė" + +msgid "Shortcuts" +msgstr "Šaukiniai" + +msgid "Quit" +msgstr "Išeiti" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Iškirpti pažymėtą tekstą" + +msgid "Copy selected text" +msgstr "Kopijuoti pažymėtą tekstą" + +msgid "Paste copied text" +msgstr "Įdėti nukopijuotą tekstą" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "Kurti naują sąryšį" + +msgid "Open/Search relation" +msgstr "Atverti/ieškoti sąryšinio įrašo" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "Perjungti rodymą" + +msgid "Create/Select new line" +msgstr "Sukurti/pasirinkti naują eilutę" + +msgid "Open relation" +msgstr "Atverti sąryšį" + +msgid "Mark line for deletion/removal" +msgstr "Pažymėti eilutę trynimui/pašalinimui" + +msgid "Mark line for removal" +msgstr "Pažymėti eilutę pašalinimui" + +msgid "Unmark line for deletion" +msgstr "Nuimti eilutės pažymėjimą ištrynimui" + +msgid "List/Tree Shortcuts" +msgstr "Sąrašo/Hierarchinio sąrašo šaukiniai" + +msgid "Move Cursor" +msgstr "Paslinkti žymeklį" + +msgid "Move right" +msgstr "Paslinkti į dešinę" + +msgid "Move left" +msgstr "Paslinkti į kairę" + +msgid "Move up" +msgstr "Paslinkti į viršų" + +msgid "Move down" +msgstr "Paslinkti į apačią" + +msgid "Move up of one page" +msgstr "Perkelti į viršų per vieną puslapį" + +msgid "Move down of one page" +msgstr "Perkelti į apačią per vieną puslapį" + +msgid "Move to top" +msgstr "Perkelti į viršų" + +msgid "Move to bottom" +msgstr "Perkelti į apačią" + +msgid "Move to parent" +msgstr "Perkelti į motininį" + +msgid "Select all" +msgstr "Pasirinkti viską" + +msgid "Unselect all" +msgstr "Panaikinti visą žymėjimą" + +msgid "Select parent" +msgstr "Pasirinkti motininį" + +msgid "Select/Activate current row" +msgstr "Pasirinkti/Aktyvuoti esamą eilutę" + +msgid "Toggle selection" +msgstr "Sukeisti atranką" + +msgid "Expand/Collapse" +msgstr "Išplėsti/Suspausti" + +msgid "Expand row" +msgstr "Išplėsti eilutę" + +msgid "Collapse row" +msgstr "Suspausti eilutę" + +msgid "Toggle row" +msgstr "Sukeisti eilutę" + +msgid "Collapse all rows" +msgstr "Suspausti visas eilutes" + +msgid "Expand all rows" +msgstr "Išplėsti visas eilutes" + +msgid "Close Tab" +msgstr "Uždaryti kortelę" + +msgid "modularity, scalability and security" +msgstr "moduliškumas, išplečiamumas ir saugumas" + +msgid "translator-credits" +msgstr "Mindaugas" + +#, python-format +msgid "Attachments (%s)" +msgstr "Priedai (%s)" + +msgid "Profile Editor" +msgstr "Profilių tvarkymas" + +msgid "Profile" +msgstr "Profilis" + +msgid "Add new profile" +msgstr "Sukurti naują profilį" + +msgid "Remove selected profile" +msgstr "Pašalinti pasirinktą profilį" + +msgid "Host:" +msgstr "Serveris:" + +msgid "Database:" +msgstr "Duomenų bazė:" + +msgid "Fetching databases list" +msgstr "Gaunamas duomenų bazių sąrašas" + +msgid "Username:" +msgstr "Naudotojas:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Nesuderinama serverio versija" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Nepavyko prisijungti prie serverio" + +msgid "Login" +msgstr "Prisijungti" + +msgid "_Cancel" +msgstr "_Atsisakyti" + +msgid "Cancel connection to the Tryton server" +msgstr "Atšaukti prisijungimą prie Tryton serverio" + +msgid "C_onnect" +msgstr "_Prisijungti" + +msgid "Connect the Tryton server" +msgstr "Prisijungti prie Tryton serverio" + +msgid "Profile:" +msgstr "Profilis:" + +msgid "Host / Database information" +msgstr "Serverio / duomenų bazės duomenys" + +msgid "User name:" +msgstr "Naudotojas:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Negalima ištrinti %s vedlio" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "El. paštas %s" + +msgid "To:" +msgstr "Kam:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Tema:" + +#, fuzzy +msgid "Body" +msgstr "Turinys:" + +#, fuzzy +msgid "Reports" +msgstr "Ataskaita" + +#, fuzzy +msgid "Attachments" +msgstr "Priedas:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Pasirinkti viską" + +#, fuzzy +msgid "Remove File" +msgstr "Šalinti " + +msgid "Add..." +msgstr "Sukurti naują..." + +#, fuzzy +msgid "Preview" +msgstr "Buvęs" + +msgid "Previous" +msgstr "Buvęs" + +msgid "Next" +msgstr "Kitas" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Priedai (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Pastaba (%d/%d)" + +msgid "You have to select one record." +msgstr "Pasirinkite bent vieną įrašą." + +msgid "ID:" +msgstr "ID:" + +#, fuzzy +msgid "Created by:" +msgstr "Sukūręs naudotojas" + +#, fuzzy +msgid "Created at:" +msgstr "Sukūrimo data" + +msgid "Edited by:" +msgstr "Pakeitęs naudotojas:" + +msgid "Edited at:" +msgstr "Keitimo data:" + +msgid "Model:" +msgstr "Modelis:" + +msgid "Are you sure to remove this record?" +msgstr "Ar tikrai norite ištrinti šį įrašą?" + +msgid "Are you sure to remove those records?" +msgstr "Ar tikrai norite ištrinti šiuos įrašus?" + +msgid "Records not removed." +msgstr "Įrašai nebuvo ištrinti." + +msgid "Records removed." +msgstr "Įrašai ištrinti." + +msgid "Working now on the duplicated record(s)." +msgstr "Dabar dirbate su įrašo(-ų) kopija(-omis)." + +msgid "Record saved." +msgstr "Įrašas išsaugotas." + +msgid " of " +msgstr " iš " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Šis įrašas buvo pakeistas\n" +"Ar norite jį išsaugoti?" + +msgid "Launch action" +msgstr "Vykdyti veiksmą" + +msgid "Relate" +msgstr "Susijęs" + +msgid "Open related records" +msgstr "Atverti susijusius įrašus" + +msgid "Report" +msgstr "Ataskaita" + +msgid "Open report" +msgstr "Atverti ataskaitą" + +msgid "Print" +msgstr "Spausdinimas" + +msgid "Print report" +msgstr "Spausdinti ataskaitą" + +msgid "_Copy URL" +msgstr "_Kopijuoti URL" + +msgid "Copy URL into clipboard" +msgstr "Kopijuoti URL į iškarpinę" + +msgid "Unknown" +msgstr "Nežinomas" + +msgid "Limit" +msgstr "Riba" + +msgid "Search Limit Settings" +msgstr "Paieškos ribojimo nustatymai" + +msgid "Limit:" +msgstr "Riba:" + +#, python-format +msgid "Notes (%s)" +msgstr "Pastabos (%s)" + +msgid "Edit User Preferences" +msgstr "Keisti naudotojo nustatymus" + +msgid "Preference" +msgstr "Nustatymai" + +msgid "Revision" +msgstr "Taisymas" + +msgid "Select a revision" +msgstr "Pasirinkite taisymą" + +msgid "Revision:" +msgstr "Taisymas:" + +msgid "_Switch View" +msgstr "_Perjungti rodymą" + +msgid "Switch View" +msgstr "Perjungti rodinį" + +msgid "_Previous" +msgstr "_Buvęs" + +msgid "Previous Record" +msgstr "Buvęs įrašas" + +msgid "_Next" +msgstr "_Kitas" + +msgid "Next Record" +msgstr "Kitas įrašas" + +msgid "_Search" +msgstr "I_eškoti" + +msgid "_New" +msgstr "_Naujas" + +msgid "Create a new record" +msgstr "Sukurti naują įrašą" + +msgid "_Save" +msgstr "_Saugoti" + +msgid "Save this record" +msgstr "Išsaugoti šį įrašą" + +msgid "_Reload/Undo" +msgstr "Pe_rkrauti/atšaukti" + +msgid "Reload/Undo" +msgstr "Perkrauti/Atšaukti" + +msgid "_Duplicate" +msgstr "Sukurti ko_piją" + +msgid "_Delete..." +msgstr "_Ištrinti..." + +msgid "View _Logs..." +msgstr "Žiūrėti _žurnalus..." + +msgid "Show revisions..." +msgstr "Rodyti pakeitimus..." + +msgid "A_ttachments..." +msgstr "P_riedas..." + +msgid "Add an attachment to the record" +msgstr "Prisegti failą prie įrašo" + +msgid "_Notes..." +msgstr "Pastabos..." + +msgid "Add a note to the record" +msgstr "Prisegti pastabą prie įrašo" + +msgid "_Actions..." +msgstr "_Veiksmai..." + +msgid "_Relate..." +msgstr "_Susiję įrašai..." + +msgid "_Report..." +msgstr "_Ataskaita..." + +msgid "_Print..." +msgstr "_Spausdinti..." + +msgid "_E-Mail..." +msgstr "Siųsti _el. paštu..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Prisegti pastabą prie įrašo" + +msgid "_Export Data..." +msgstr "_Eksportuoti duomenis..." + +msgid "_Import Data..." +msgstr "_Importuoti duomenis..." + +msgid "Copy _URL..." +msgstr "Kopijuoti URL..." + +msgid "_Close Tab" +msgstr "Uždaryti _kortelę" + +msgid "All fields" +msgstr "Galimi laukai" + +msgid "_Add" +msgstr "_Pridėti" + +msgid "_Remove" +msgstr "_Ištrinti" + +msgid "_Clear" +msgstr "Valyti" + +msgid "Fields selected" +msgstr "Pasirinkti laukai" + +msgid "CSV Parameters" +msgstr "CSV parametrai" + +msgid "Delimiter:" +msgstr "Skirtukas:" + +msgid "Quote char:" +msgstr "Citavimo simbolis:" + +msgid "Encoding:" +msgstr "Koduotė:" + +msgid "Use locale format" +msgstr "Naudoti lokalės formatą" + +msgid "Field name" +msgstr "Lauko pavadinimas" + +#, python-format +msgid "CSV Export: %s" +msgstr "CSV eksportavimas: %s" + +msgid "_Save Export" +msgstr "Išsaugoti eksporto šabloną" + +#, fuzzy +msgid "_URL Export" +msgstr "Išsaugoti eksporto šabloną" + +msgid "_Delete Export" +msgstr "Ištrinti eksporto šabloną" + +msgid "Predefined exports" +msgstr "Išsaugoti eksportavimo šablonai" + +msgid "Name" +msgstr "Pavadinimas" + +msgid "Open" +msgstr "Atvėrimas" + +msgid "Listed Records" +msgstr "Rodomi įrašai" + +msgid "Selected Records" +msgstr "Pasirinkti įrašai" + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Pridėti laukų pavadinimus" + +#, python-format +msgid "%s (string)" +msgstr "%s (eilutė)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (modelio pavadinimas)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (įrašo pavadinimas)" + +msgid "What is the name of this export?" +msgstr "Kokiu pavadinimu išsaugoti šį eksportavimo šabloną?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Perrašyti '%s' apibrėžimą?" + +#, python-format +msgid "%d record saved." +msgstr "%d įrašas išsaugotas." + +#, python-format +msgid "%d records saved." +msgstr "%d įrašai išsaugoti." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Veiksmas neįvykdytas.\n" +"Klaidos pranešimas:\n" +"%s" + +msgid "Error" +msgstr "Klaida" + +msgid "Link" +msgstr "Nuoroda" + +msgid "Delete" +msgstr "Naikinti" + +msgid "New" +msgstr "Naujas" + +msgid "Switch" +msgstr "Perjungti" + +msgid "Add" +msgstr "Pridėti" + +msgid "Remove " +msgstr "Šalinti " + +msgid "Create a new record " +msgstr "Sukurti naują įrašą " + +msgid "Delete selected record " +msgstr "Pašalinti pasirinktą įrašą " + +msgid "Undelete selected record " +msgstr "Atkurti pasirinktą įrašą " + +#, python-format +msgid "CSV Import: %s" +msgstr "CSV importas: %s" + +msgid "_Auto-Detect" +msgstr "Aptikti automatiškai" + +msgid "File to Import:" +msgstr "Importuoti iš failo:" + +msgid "Open..." +msgstr "Atverti..." + +msgid "Lines to Skip:" +msgstr "Praleisti eilutes:" + +msgid "You must select an import file first." +msgstr "Jūs pirmiausiai turite pasirinkti importo failą." + +msgid "Error opening CSV file" +msgstr "Klaida atveriant CSV failą" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Klaida apdorojant failą ties lauku %s." + +#, python-format +msgid "%d record imported." +msgstr "%d įrašas importuotas." + +#, python-format +msgid "%d records imported." +msgstr "%d įrašai importuoti." + +msgid "Search" +msgstr "Ieškoti" + +#, python-format +msgid "Search %s" +msgstr "Ieškoti %s" + +msgid "Wizard" +msgstr "Vedlys" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Negalima ištrinti %s vedlio" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Sukūręs naudotojas" + +msgid "Created at" +msgstr "Sukūrimo data" + +msgid "Edited by" +msgstr "Taisęs naudotojas" + +msgid "Edited at" +msgstr "Taisymo data" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Nepavyko gauti hierarchinio rodinio būsenos %s" + +msgid "Unable to set view tree state" +msgstr "Nepavyko nustatyti hierarchinio sąrašo rodinio" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" negalioja šioje srityje" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "Būtina nurodyti \"%s\"" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "\"%s\" vertės nėra leistinos" + +msgid "Pre-validation" +msgstr "Išankstinis patikrinimas" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Paveikslėlio dydis" + +msgid "Width:" +msgstr "Plotis:" + +msgid "Height:" +msgstr "Aukštis:" + +msgid "PNG image (*.png)" +msgstr "PNG paveikslėlis (*.png)" + +msgid "Save As" +msgstr "Įrašyti kaip" + +msgid "Image size too large." +msgstr "Paveikslėlis yra per didelis." + +msgid "Copy" +msgstr "Kopijuoti" + +msgid "Paste" +msgstr "Įdėti" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Atverti filtrus" + +msgid "Show bookmarks of filters" +msgstr "Rodyti filtrų žymeles" + +msgid "Remove this bookmark" +msgstr "Šalinti šią žymelę" + +msgid "Bookmark this filter" +msgstr "Sukurti šio filtro žymelę" + +msgid "Show active records" +msgstr "Rodyti aktyvius įrašus" + +msgid "Show inactive records" +msgstr "Rodyti neaktyvius įrašus" + +msgid "Bookmark Name:" +msgstr "Žymelės pavadinimas:" + +msgid "Find" +msgstr "Rasti" + +msgid "Today" +msgstr "Šiandien" + +msgid "go back" +msgstr "atgal" + +msgid "go forward" +msgstr "pirmyn" + +msgid "previous year" +msgstr "praeiti metai" + +msgid "next year" +msgstr "kiti metai" + +msgid "Day View" +msgstr "Dienos rodinys" + +msgid "Week View" +msgstr "Savaitės rodinys" + +msgid "Month View" +msgstr "Mėnesio rodinys" + +msgid "Week" +msgstr "Savaitė" + +msgid "Select..." +msgstr "Pasirinkti..." + +msgid "Clear" +msgstr "Valyti" + +msgid "All files" +msgstr "Visi failai" + +msgid "Show plain text" +msgstr "Rodyti paprastą tekstą" + +msgid "Add value" +msgstr "Pridėti reikšmę" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Šalinti \"%s\"" + +msgid "Images" +msgstr "Paveikslėliai" + +msgid "Add existing record" +msgstr "Pridėti turimą įrašą" + +msgid "Remove selected record" +msgstr "Pašalinti pasirinktą įrašą" + +msgid "Open the record " +msgstr "Atverti įrašą " + +msgid "Clear the field " +msgstr "Išvalyti lauką " + +msgid "Search a record " +msgstr "Ieškoti įrašo " + +#, fuzzy +msgid "Edit selected record" +msgstr "Keisti pasirinktą įrašą " + +#, fuzzy +msgid "Delete selected record" +msgstr "Pašalinti pasirinktą įrašą" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Pasirinkti kalbą" + +msgid "Translation" +msgstr "Vertimas" + +msgid "Edit" +msgstr "Keisti" + +msgid "Fuzzy" +msgstr "Netikslus" + +msgid "You need to save the record before adding translations." +msgstr "Išsaugokite įrašą prieš pridedant vertimą." + +msgid "No other language available." +msgstr "Nėra kitų kalbų." + +msgid "Translate view" +msgstr "Išversti rodinį" diff --git a/tryton/tryton/data/locale/nl/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/nl/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..95d8353a8c --- /dev/null +++ b/tryton/tryton/data/locale/nl/LC_MESSAGES/tryton.po @@ -0,0 +1,1173 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "specificeer een vervangend configuratiebestand" + +msgid "development mode" +msgstr "Ontwikkelingsmodus" + +msgid "logging everything at INFO level" +msgstr "logboek bijhouden op INFO niveau" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "specificeer het log niveau: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "specificeer de inlog gebruiker" + +msgid "specify the server hostname:port" +msgstr "servernaam : poort" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Kan configuratiebestand %s niet opslaan." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Kan omgeving %s niet instellen" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Selecteer je handeling" + +msgid "No action defined." +msgstr "Geen handeling gedefinieerd." + +msgid "By: " +msgstr "Door: " + +msgid "Selection" +msgstr "Selectie" + +msgid "Cancel" +msgstr "Annuleren" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Uw selectie:" + +msgid "Select" +msgstr "Selecteer" + +msgid "Save" +msgstr "Opslaan" + +msgid "Save As..." +msgstr "Opslaan als..." + +msgid "Do you want to proceed?" +msgstr "Doorgaan?" + +msgid "Always ignore this warning." +msgstr "Deze waarschuwing altijd negeren." + +msgid "No" +msgstr "Nee" + +msgid "Yes" +msgstr "Ja" + +msgid "Concurrency Exception" +msgstr "Simultaan gebruik (error)" + +msgid "This record has been modified while you were editing it." +msgstr "Dit veld werd gewijzigd terwijl u het aan het bewerken was." + +msgid "Cancel saving" +msgstr "Opslaan afbreken" + +msgid "Compare" +msgstr "Vergelijken" + +msgid "See the modified version" +msgstr "Toon de gewijzigde versie" + +msgid "Write Anyway" +msgstr "Overschrijven" + +msgid "Save your current version" +msgstr "Huidige versie opslaan" + +#, python-format +msgid "Compare: %s" +msgstr "Vergelijk: %s" + +msgid "Close" +msgstr "Sluiten" + +msgid "Application Error" +msgstr "Applicatiefout" + +msgid "Report Bug" +msgstr "Programmafout melden" + +#, python-format +msgid "Check URL: %s" +msgstr "controle van de URL: %s" + +msgid "Unable to check for new version." +msgstr "Kon niet controleren op nieuwe versie." + +msgid "A new version is available!" +msgstr "Een nieuwe versie is beschikbaar!" + +msgid "Download" +msgstr "Download" + +msgid "Too many requests. Try again later." +msgstr "Te veel verzoeken. Probeer het later nog eens." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Zoek..." + +msgid "Create..." +msgstr "Maak..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Kon niet zoeken naar voltooiing van %s" + +msgid "Value" +msgstr "Waarde" + +msgid "Displayed value" +msgstr "Weergegeven waarde" + +msgid "Format" +msgstr "Formaat" + +msgid "Display format" +msgstr "Weergaveformaat" + +msgid "Open the calendar" +msgstr "Open de kalender" + +msgid "Date Format" +msgstr "Datumnotatie" + +msgid "Displayed date format" +msgstr "Weergegeven datumformaat" + +msgid "y" +msgstr "J" + +msgid "True" +msgstr "Waar" + +msgid "t" +msgstr "w (t)" + +msgid "False" +msgstr "Niet waar" + +msgid "Digits" +msgstr "cijfers" + +msgid "The number of decimal" +msgstr "Aantal decimalen" + +msgid "Template" +msgstr "Sjabloon" + +msgid "Edit..." +msgstr "Bewerk..." + +msgid "Attachments..." +msgstr "Bijlagen..." + +msgid "Notes..." +msgstr "Notities..." + +msgid "Actions..." +msgstr "Acties ..." + +msgid "Relate..." +msgstr "Relatie..." + +msgid "Report..." +msgstr "Verslag..." + +msgid "Print..." +msgstr "Afdrukken..." + +msgid "E-Mail..." +msgstr "E-mail..." + +msgid "Bold" +msgstr "Vet" + +msgid "Italic" +msgstr "Cursief" + +msgid "Underline" +msgstr "Onderlijnen" + +msgid "Align Left" +msgstr "Links uitlijnen" + +msgid "Align Center" +msgstr "Centreren" + +msgid "Align Right" +msgstr "Rechts uitlijnen" + +msgid "Justify" +msgstr "Rechtvaardigen" + +msgid "Foreground Color" +msgstr "Voorgrondkleur" + +msgid "Select a color" +msgstr "Selecteer een kleur" + +msgid "Y" +msgstr "J" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "w" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Voorkeuren..." + +msgid "Toolbar" +msgstr "Werkbalk" + +msgid "Default" +msgstr "Standaard" + +msgid "Text and Icons" +msgstr "Tekst en pictogrammen" + +msgid "Text" +msgstr "Tekst" + +msgid "Icons" +msgstr "Pictogrammen" + +msgid "Form" +msgstr "Formulier" + +msgid "Save Column Width" +msgstr "Kolombreedte opslaan" + +msgid "Save Tree State" +msgstr "Boomstatus opslaan" + +msgid "Spell Checking" +msgstr "Spellingscontrole uitvoeren" + +msgid "PDA Mode" +msgstr "PDA-modus" + +msgid "Search Limit..." +msgstr "Zoek limiet..." + +msgid "Check Version" +msgstr "Controleer de versie" + +msgid "Options" +msgstr "Opties" + +msgid "Keyboard Shortcuts..." +msgstr "Sneltoetsen..." + +msgid "About..." +msgstr "Betreft..." + +msgid "Help" +msgstr "Help" + +msgid "No result found." +msgstr "Geen resultaat gevonden." + +msgid "Favorites" +msgstr "Favorieten" + +msgid "Manage..." +msgstr "Beheren..." + +msgid "Action" +msgstr "Actie" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"De volgende actie vereist het sluiten van alle tabbladen.\n" +"Verder gaan?" + +msgid "Application Shortcuts" +msgstr "Applicatiesnelkoppelingen" + +msgid "Global" +msgstr "Globaal" + +msgid "Preferences" +msgstr "Voorkeuren" + +msgid "Search menu" +msgstr "Zoekmenu" + +msgid "Toggle menu" +msgstr "Menu wisselen" + +msgid "Previous tab" +msgstr "Vorig tabblad" + +msgid "Next tab" +msgstr "Volgende tabblad" + +msgid "Shortcuts" +msgstr "shortcuts" + +msgid "Quit" +msgstr "Stop" + +msgid "Edition Shortcuts" +msgstr "Bewerk snelkoppelingen" + +msgid "Text Entries" +msgstr "Tekstinvoer" + +msgid "Cut selected text" +msgstr "Knip geselecteerde tekst" + +msgid "Copy selected text" +msgstr "Kopieer geselecteerde tekst" + +msgid "Paste copied text" +msgstr "Plak geselecteerde tekst" + +msgid "Next entry" +msgstr "Volgend veld" + +msgid "Previous entry" +msgstr "Vorig veld" + +msgid "Relation Entries" +msgstr "Relatie-invoer" + +msgid "Create new relation" +msgstr "Nieuwe relatie aanmaken" + +msgid "Open/Search relation" +msgstr "Zoek / open relatie" + +msgid "List Entries" +msgstr "Lijst invoer" + +msgid "Switch view" +msgstr "Wissel van weergave" + +msgid "Create/Select new line" +msgstr "Maak/Selecteer nieuwe lijn" + +msgid "Open relation" +msgstr "Open relatie" + +msgid "Mark line for deletion/removal" +msgstr "Lijn markeren om wissen / verwijderen (delete/removal)" + +msgid "Mark line for removal" +msgstr "Lijn voor verwijdering markeren" + +msgid "Unmark line for deletion" +msgstr "Annuleer markering voor verwijdering" + +msgid "List/Tree Shortcuts" +msgstr "Lijst- / boomsnelkoppelingen" + +msgid "Move Cursor" +msgstr "Beweeg cursor" + +msgid "Move right" +msgstr "Naar rechts verplaatsen" + +msgid "Move left" +msgstr "Naar links verplaatsen" + +msgid "Move up" +msgstr "Naar boven verplaatsen" + +msgid "Move down" +msgstr "Naar beneden verplaatsen" + +msgid "Move up of one page" +msgstr "Verplaats één pagina omhoog" + +msgid "Move down of one page" +msgstr "Verplaats één pagina omlaag" + +msgid "Move to top" +msgstr "Verplaatsen naar begin" + +msgid "Move to bottom" +msgstr "Verplaatsen naar einde" + +msgid "Move to parent" +msgstr "Verplaats naar bovenliggend niveau" + +msgid "Select all" +msgstr "Alles selecteren" + +msgid "Unselect all" +msgstr "Alles deselecteren" + +msgid "Select parent" +msgstr "Selecteer bovenliggend niveau (parent)" + +msgid "Select/Activate current row" +msgstr "Selecteer/Activeer huidige rij" + +msgid "Toggle selection" +msgstr "Selectie wisselen" + +msgid "Expand/Collapse" +msgstr "uitvouwen/ opvouwen" + +msgid "Expand row" +msgstr "Rij uitvouwen" + +msgid "Collapse row" +msgstr "rijen samenvouwen" + +msgid "Toggle row" +msgstr "Wissel rij" + +msgid "Collapse all rows" +msgstr "Alle rijen samenvouwen" + +msgid "Expand all rows" +msgstr "Vouw alle rijen uit" + +msgid "Close Tab" +msgstr "Tabblad sluiten" + +msgid "modularity, scalability and security" +msgstr "modulariteit, schaalbaarheid en veiligheid" + +msgid "translator-credits" +msgstr "Bert Defoor" + +#, python-format +msgid "Attachments (%s)" +msgstr "Bijlagen (%s)" + +msgid "Profile Editor" +msgstr "Profiel-editor" + +msgid "Profile" +msgstr "Profiel" + +msgid "Add new profile" +msgstr "Nieuw profiel toevoegen" + +msgid "Remove selected profile" +msgstr "Verwijder het geselecteerde profiel" + +msgid "Host:" +msgstr "Host:" + +msgid "Database:" +msgstr "Database:" + +msgid "Fetching databases list" +msgstr "Lijst met databases ophalen" + +msgid "Username:" +msgstr "Gebruikersnaam:" + +msgid "Incompatible version of the server." +msgstr "Deze versie is onverenigbaar met de server." + +msgid "Could not connect to the server." +msgstr "Geen verbinding met de server." + +msgid "Login" +msgstr "Inlognaam" + +msgid "_Cancel" +msgstr "_Annuleren" + +msgid "Cancel connection to the Tryton server" +msgstr "Verbreek verbinding met Tryton server" + +msgid "C_onnect" +msgstr "_Verbinden" + +msgid "Connect the Tryton server" +msgstr "Verbind met Tryton server" + +msgid "Profile:" +msgstr "Profiel:" + +msgid "Host / Database information" +msgstr "Host- / database-informatie" + +msgid "User name:" +msgstr "Gebruikersnaam:" + +msgid "Unable to complete email entry" +msgstr "E-mailinvoer kan niet worden voltooid" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "Aan:" + +msgid "Cc:" +msgstr "Cc:" + +msgid "Bcc:" +msgstr "Bcc:" + +msgid "Subject:" +msgstr "Onderwerp:" + +msgid "Body" +msgstr "Bericht" + +msgid "Reports" +msgstr "Rapporten" + +msgid "Attachments" +msgstr "Bijlagen" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "Sturen" + +#, fuzzy +msgid "Select File" +msgstr "Alles selecteren" + +#, fuzzy +msgid "Remove File" +msgstr "Verwijder " + +msgid "Add..." +msgstr "Toevoegen..." + +#, fuzzy +msgid "Preview" +msgstr "Vorige" + +msgid "Previous" +msgstr "Vorige" + +msgid "Next" +msgstr "Volgende" + +#, python-format +msgid "Attachment (%s)" +msgstr "Bijlage (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Opmerking (%d/%d)" + +msgid "You have to select one record." +msgstr "U moet één record selecteren." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Aangemaakt door:" + +msgid "Created at:" +msgstr "Aanmaakdatum:" + +msgid "Edited by:" +msgstr "Aangepast door:" + +msgid "Edited at:" +msgstr "Bewerkt op:" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Weet je zeker dat je dit record wilt verwijderen?" + +msgid "Are you sure to remove those records?" +msgstr "Weet je zeker dat je deze records wilt verwijderen?" + +msgid "Records not removed." +msgstr "Records niet verwijderd." + +msgid "Records removed." +msgstr "Records verwijderd." + +msgid "Working now on the duplicated record(s)." +msgstr "Nu aan het werk op het/de gecopieërde record(s)." + +msgid "Record saved." +msgstr "Record opgeslagen." + +msgid " of " +msgstr " van " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Dit veld is aangepast.\n" +"Wil je de wijzigingen opslaan?" + +msgid "Launch action" +msgstr "Start actie" + +msgid "Relate" +msgstr "Relatie" + +msgid "Open related records" +msgstr "Open gerelateerde records" + +msgid "Report" +msgstr "Verslag" + +msgid "Open report" +msgstr "Open rapport" + +msgid "Print" +msgstr "Afdrukken" + +msgid "Print report" +msgstr "Rapport afdrukken" + +msgid "_Copy URL" +msgstr "_Kopieer URL" + +msgid "Copy URL into clipboard" +msgstr "Kopieer de URL naar het klembord" + +msgid "Unknown" +msgstr "Onbekend" + +msgid "Limit" +msgstr "Limiet" + +msgid "Search Limit Settings" +msgstr "Zoek limiet instellen" + +msgid "Limit:" +msgstr "Limiet:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notities (%s)" + +msgid "Edit User Preferences" +msgstr "Voorkeuren gebruiker aanpassen" + +msgid "Preference" +msgstr "Voorkeur" + +msgid "Revision" +msgstr "Herziening" + +msgid "Select a revision" +msgstr "Selecteer een revisie" + +msgid "Revision:" +msgstr "Revisie :" + +msgid "_Switch View" +msgstr "Wijzig beeld" + +msgid "Switch View" +msgstr "Wissel van weergave" + +msgid "_Previous" +msgstr "V_orige" + +msgid "Previous Record" +msgstr "Vorig record" + +msgid "_Next" +msgstr "_Volgende" + +msgid "Next Record" +msgstr "Volgende record" + +msgid "_Search" +msgstr "_Zoeken" + +msgid "_New" +msgstr "_Nieuw" + +msgid "Create a new record" +msgstr "Maak een nieuw record" + +msgid "_Save" +msgstr "Op_slaan" + +msgid "Save this record" +msgstr "Bewaar deze record" + +msgid "_Reload/Undo" +msgstr "_Ongedaan maken" + +msgid "Reload/Undo" +msgstr "Herladen/Annuleren" + +msgid "_Duplicate" +msgstr "_Dupliceren" + +msgid "_Delete..." +msgstr "_Verwijderen..." + +msgid "View _Logs..." +msgstr "Bekijk logregister..." + +msgid "Show revisions..." +msgstr "Herzieningen tonen ..." + +msgid "A_ttachments..." +msgstr "_Bijlage..." + +msgid "Add an attachment to the record" +msgstr "Voeg een bijlage toe aan het record" + +msgid "_Notes..." +msgstr "_Notas ..." + +msgid "Add a note to the record" +msgstr "Voeg een nota toe aan het veld" + +msgid "_Actions..." +msgstr "_Acties.." + +msgid "_Relate..." +msgstr "_Relatie...." + +msgid "_Report..." +msgstr "_Verslag..." + +msgid "_Print..." +msgstr "_Afdrukken..." + +msgid "_E-Mail..." +msgstr "_E-mail..." + +msgid "Send an e-mail using the record" +msgstr "Stuur een e-mail met behulp van het record" + +msgid "_Export Data..." +msgstr "_Exporteer gegevens..." + +msgid "_Import Data..." +msgstr "_Importeer gegevens..." + +msgid "Copy _URL..." +msgstr "Kopieer _URL..." + +msgid "_Close Tab" +msgstr "Tabblad sluiten" + +msgid "All fields" +msgstr "Alle velden" + +msgid "_Add" +msgstr "_Toevoegen" + +msgid "_Remove" +msgstr "_Verwijderen" + +msgid "_Clear" +msgstr "_Wissen" + +msgid "Fields selected" +msgstr "Geselecteerde velden" + +msgid "CSV Parameters" +msgstr "CSV parameters" + +msgid "Delimiter:" +msgstr "scheidingsteken:" + +msgid "Quote char:" +msgstr "aanhalingstekens :" + +msgid "Encoding:" +msgstr "Codering:" + +msgid "Use locale format" +msgstr "Gebruik lokaal formaat" + +msgid "Field name" +msgstr "Veldnaam" + +#, python-format +msgid "CSV Export: %s" +msgstr "Exporteer CSV: %s" + +msgid "_Save Export" +msgstr "_Exportgegevens opslaan" + +msgid "_URL Export" +msgstr "_URL exporteren" + +msgid "_Delete Export" +msgstr "_Exportgegevens verwijderen" + +msgid "Predefined exports" +msgstr "Voorgedefinieerde exportinstellingen" + +msgid "Name" +msgstr "Naam" + +msgid "Open" +msgstr "Openen" + +msgid "Listed Records" +msgstr "Vermelde records" + +msgid "Selected Records" +msgstr "Geselecteerde records" + +msgid "Ignore search limit" +msgstr "Zoeklimiet negeren" + +msgid "Add field names" +msgstr "Voeg veldnamen toe" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (modelnaam)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (recordnaam)" + +msgid "What is the name of this export?" +msgstr "Wat is de naam voor deze export?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Vervang '%s' definitie?" + +#, python-format +msgid "%d record saved." +msgstr "%d item opgeslagen." + +#, python-format +msgid "%d records saved." +msgstr "%d items opgeslagen." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Handeling mislukt.\n" +"Foutmelding:\n" +"%s" + +msgid "Error" +msgstr "Fout" + +msgid "Link" +msgstr "Koppeling" + +msgid "Delete" +msgstr "Verwijderen" + +msgid "New" +msgstr "Nieuw" + +msgid "Switch" +msgstr "Omschakelen" + +msgid "Add" +msgstr "Toevoegen" + +msgid "Remove " +msgstr "Verwijder " + +msgid "Create a new record " +msgstr "Maak een nieuw record " + +msgid "Delete selected record " +msgstr "Geselecteerde record verwijderen " + +msgid "Undelete selected record " +msgstr "Verwijderen van geselecteerde record ongedaan maken" + +#, python-format +msgid "CSV Import: %s" +msgstr "CSV-Import: %s" + +msgid "_Auto-Detect" +msgstr "_Automatische detectie" + +msgid "File to Import:" +msgstr "Bestand om te importeren:" + +msgid "Open..." +msgstr "Openen..." + +msgid "Lines to Skip:" +msgstr "Regels overslaan:" + +msgid "You must select an import file first." +msgstr "U moet eerst een importbestand selecteren." + +msgid "Error opening CSV file" +msgstr "Fout bij openen CSV-bestand" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Fout bij verwerken van het bestand op veld %s." + +#, python-format +msgid "%d record imported." +msgstr "%d record geïmporteerd." + +#, python-format +msgid "%d records imported." +msgstr "%d record geïmporteerd." + +msgid "Search" +msgstr "Zoeken" + +#, python-format +msgid "Search %s" +msgstr "Zoek %s" + +msgid "Wizard" +msgstr "Assistent" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Kon de wizard %s niet verwijderen" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Gemaakt door" + +msgid "Created at" +msgstr "Aanmaakdatum" + +msgid "Edited by" +msgstr "Bewerk door" + +msgid "Edited at" +msgstr "Bewerkt op" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Kan view tree-status voor %s niet krijgen" + +msgid "Unable to set view tree state" +msgstr "Kan weergave boomstructuur niet instellen" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" is niet geldig volgens zijn domein." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" is vereist." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "De waarden van \"%s\" zijn niet geldig." + +msgid "Pre-validation" +msgstr "Pre-validatie" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Afbeeldingsgrootte" + +msgid "Width:" +msgstr "Breedte:" + +msgid "Height:" +msgstr "Hoogte:" + +msgid "PNG image (*.png)" +msgstr "PNG afbeelding (*.png)" + +msgid "Save As" +msgstr "Opslaan als" + +msgid "Image size too large." +msgstr "Afbeelding te groot." + +msgid "Copy" +msgstr "Kopiëren" + +msgid "Paste" +msgstr "Plakken" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Open filters" + +msgid "Show bookmarks of filters" +msgstr "Bladwijzers van filters weergeven" + +msgid "Remove this bookmark" +msgstr "Verwijder deze bladwijzer" + +msgid "Bookmark this filter" +msgstr "Maak een bladwijzer van deze filter" + +msgid "Show active records" +msgstr "Toon actieve records" + +msgid "Show inactive records" +msgstr "Toon niet actieve records" + +msgid "Bookmark Name:" +msgstr "Bladwijzernaam:" + +msgid "Find" +msgstr "Zoeken" + +msgid "Today" +msgstr "Vandaag" + +msgid "go back" +msgstr "ga terug" + +msgid "go forward" +msgstr "ga vooruit" + +msgid "previous year" +msgstr "vorig jaar" + +msgid "next year" +msgstr "volgend jaar" + +msgid "Day View" +msgstr "Dagweergave" + +msgid "Week View" +msgstr "Weekoverzicht" + +msgid "Month View" +msgstr "Maandweergave" + +msgid "Week" +msgstr "Week" + +msgid "Select..." +msgstr "Selecteer ..." + +msgid "Clear" +msgstr "Wissen" + +msgid "All files" +msgstr "Alle bestanden" + +msgid "Show plain text" +msgstr "Laat platte tekst zien" + +msgid "Add value" +msgstr "Waarde toevoegen" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Verwijder \"%s\"" + +msgid "Images" +msgstr "Afbeeldingen" + +msgid "Add existing record" +msgstr "Bestaand record toevoegen" + +msgid "Remove selected record" +msgstr "Geselecteerd item verwijderen" + +msgid "Open the record " +msgstr "Open het record " + +msgid "Clear the field " +msgstr "Wis het veld " + +msgid "Search a record " +msgstr "Zoek een record " + +msgid "Edit selected record" +msgstr "Bewerk geselecteerde record" + +msgid "Delete selected record" +msgstr "Geselecteerde record verwijderen" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Kies een taal" + +msgid "Translation" +msgstr "Vertaling" + +msgid "Edit" +msgstr "Bewerk" + +msgid "Fuzzy" +msgstr "Flauwe" + +msgid "You need to save the record before adding translations." +msgstr "U moet de record opslaan voordat u vertalingen toevoegt." + +msgid "No other language available." +msgstr "Geen andere taal beschikbaar." + +msgid "Translate view" +msgstr "Vertaal aanzicht" diff --git a/tryton/tryton/data/locale/pl/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/pl/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..42812b0745 --- /dev/null +++ b/tryton/tryton/data/locale/pl/LC_MESSAGES/tryton.po @@ -0,0 +1,1173 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "podaj alternatywny plik konfiguracji" + +msgid "development mode" +msgstr "tryb dewelopera" + +msgid "logging everything at INFO level" +msgstr "rejestrowanie wszystkiego na poziomie INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "podaj poziom rejestrowania: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "podaj login użytkownika" + +msgid "specify the server hostname:port" +msgstr "podaj nazwę:port serwera" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Nie można zapisać pliku konfiguracyjnego %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Nie można ustawić lokalizacji %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Wybierz akcję" + +msgid "No action defined." +msgstr "Brak zdefiniowanej akcji." + +msgid "By: " +msgstr "Przez: " + +msgid "Selection" +msgstr "Zaznaczenie" + +msgid "Cancel" +msgstr "Anuluj" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Twoje zaznaczenie:" + +msgid "Select" +msgstr "Wybierz" + +msgid "Save" +msgstr "Zapisz" + +msgid "Save As..." +msgstr "Zapisz jako..." + +msgid "Do you want to proceed?" +msgstr "Czy chcesz kontynuować?" + +msgid "Always ignore this warning." +msgstr "Zawsze ignoruj takie ostrzeżenie." + +msgid "No" +msgstr "Nie" + +msgid "Yes" +msgstr "Tak" + +msgid "Concurrency Exception" +msgstr "Wyjątek równoczesnego zapisu danych" + +msgid "This record has been modified while you were editing it." +msgstr "Ten rekord został zmodyfikowany w trakcie twojej edycji." + +msgid "Cancel saving" +msgstr "Anuluj zapis" + +msgid "Compare" +msgstr "Porównaj" + +msgid "See the modified version" +msgstr "Zobacz zmienioną wersję" + +msgid "Write Anyway" +msgstr "Zapisz" + +msgid "Save your current version" +msgstr "Zapisz swoją bieżącą wersję" + +#, python-format +msgid "Compare: %s" +msgstr "Porównaj: %s" + +msgid "Close" +msgstr "Zamknij" + +msgid "Application Error" +msgstr "Błąd aplikacji" + +msgid "Report Bug" +msgstr "Zgłoś błąd" + +#, python-format +msgid "Check URL: %s" +msgstr "Sprawdź URL: %s" + +msgid "Unable to check for new version." +msgstr "Nie można sprawdzić dostępności nowej wersji." + +msgid "A new version is available!" +msgstr "Nowa wersja jest dostępna!" + +msgid "Download" +msgstr "Pobierz" + +msgid "Too many requests. Try again later." +msgstr "Za dużo połączeń. Spróbuj później." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Szukaj..." + +msgid "Create..." +msgstr "Utwórz..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Brak możliwości wyszukania uzupełnienia dla %s" + +msgid "Value" +msgstr "Wartość" + +msgid "Displayed value" +msgstr "Wyświetlana wartość" + +msgid "Format" +msgstr "Format" + +msgid "Display format" +msgstr "Wyświetl format" + +msgid "Open the calendar" +msgstr "Otwórz kalendarz" + +msgid "Date Format" +msgstr "Format daty" + +msgid "Displayed date format" +msgstr "Wyświetlany format daty" + +msgid "y" +msgstr "t" + +msgid "True" +msgstr "Tak" + +msgid "t" +msgstr "p" + +msgid "False" +msgstr "Nie" + +msgid "Digits" +msgstr "Cyfry" + +msgid "The number of decimal" +msgstr "Liczba miejsc dziesiętnych" + +msgid "Template" +msgstr "Szablon" + +msgid "Edit..." +msgstr "Edycja..." + +msgid "Attachments..." +msgstr "Załączniki..." + +msgid "Notes..." +msgstr "Notatki..." + +msgid "Actions..." +msgstr "Akcje..." + +msgid "Relate..." +msgstr "Otwórz..." + +msgid "Report..." +msgstr "Utwórz raport..." + +msgid "Print..." +msgstr "Wydrukuj..." + +msgid "E-Mail..." +msgstr "E-mail..." + +msgid "Bold" +msgstr "Pogrubiony" + +msgid "Italic" +msgstr "Kursywa" + +msgid "Underline" +msgstr "Podkreślenie" + +msgid "Align Left" +msgstr "Wyrównanie do lewej" + +msgid "Align Center" +msgstr "Wyrównanie do środka" + +msgid "Align Right" +msgstr "Wyrównanie do prawej" + +msgid "Justify" +msgstr "Wyjustowanie" + +msgid "Foreground Color" +msgstr "Kolor czcionki" + +msgid "Select a color" +msgstr "Wybierz kolor" + +msgid "Y" +msgstr "R" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "t" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "g" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferencje..." + +msgid "Toolbar" +msgstr "Pasek narzędzi" + +msgid "Default" +msgstr "Domyślnie" + +msgid "Text and Icons" +msgstr "Tekst i ikony" + +msgid "Text" +msgstr "Tekst" + +msgid "Icons" +msgstr "Ikony" + +msgid "Form" +msgstr "Formularz" + +msgid "Save Column Width" +msgstr "Zapisz szerokość kolumny" + +msgid "Save Tree State" +msgstr "Zapamiętaj stan drzewa" + +msgid "Spell Checking" +msgstr "Sprawdzanie pisowni" + +msgid "PDA Mode" +msgstr "Tryb PDA" + +msgid "Search Limit..." +msgstr "Ograniczenie wyszukiwania..." + +msgid "Check Version" +msgstr "Sprawdź wersję" + +msgid "Options" +msgstr "Opcje" + +msgid "Keyboard Shortcuts..." +msgstr "Skróty klawiszowe..." + +msgid "About..." +msgstr "O programie..." + +msgid "Help" +msgstr "Pomoc" + +msgid "No result found." +msgstr "Nie znaleziono wyniku." + +msgid "Favorites" +msgstr "Ulubione" + +msgid "Manage..." +msgstr "Zarządzaj..." + +msgid "Action" +msgstr "Akcja" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Wykonanie polecenia wymaga zamknięcia wszystkich kart.\n" +"Czy chcesz kontynuować?" + +msgid "Application Shortcuts" +msgstr "Skróty aplikacji" + +msgid "Global" +msgstr "Globalne" + +msgid "Preferences" +msgstr "Preferencje" + +msgid "Search menu" +msgstr "Menu wyszukiwania" + +msgid "Toggle menu" +msgstr "Menu boczne" + +msgid "Previous tab" +msgstr "Poprzednia karta" + +msgid "Next tab" +msgstr "Następna karta" + +msgid "Shortcuts" +msgstr "Skróty" + +msgid "Quit" +msgstr "Zakończ" + +msgid "Edition Shortcuts" +msgstr "Skróty edycji" + +msgid "Text Entries" +msgstr "Wpisy tekstowe" + +msgid "Cut selected text" +msgstr "Wytnij zaznaczony tekst" + +msgid "Copy selected text" +msgstr "Skopiuj zaznaczony tekst" + +msgid "Paste copied text" +msgstr "Wklej zaznaczony tekst" + +msgid "Next entry" +msgstr "Następny wpis" + +msgid "Previous entry" +msgstr "Poprzedni wpis" + +msgid "Relation Entries" +msgstr "Powiązane wpisy" + +msgid "Create new relation" +msgstr "Utwórz nowe odniesienie" + +msgid "Open/Search relation" +msgstr "Otwórz/Szukaj odniesienie" + +msgid "List Entries" +msgstr "Lista wpisów" + +msgid "Switch view" +msgstr "Przełącz widok" + +msgid "Create/Select new line" +msgstr "Utwórz/Wybierz nowy wiersz" + +msgid "Open relation" +msgstr "Utwórz odnośnik" + +msgid "Mark line for deletion/removal" +msgstr "Zaznacz wiersz do usunięcia" + +msgid "Mark line for removal" +msgstr "Zaznacz wiersz do usunięcia" + +msgid "Unmark line for deletion" +msgstr "Odznacz wiersz do usunięcia" + +msgid "List/Tree Shortcuts" +msgstr "Skróty listy/drzewa" + +msgid "Move Cursor" +msgstr "Przesuń kursor" + +msgid "Move right" +msgstr "Przesuń w prawo" + +msgid "Move left" +msgstr "Przesuń w lewo" + +msgid "Move up" +msgstr "Przesuń w górę" + +msgid "Move down" +msgstr "Przesuń w dół" + +msgid "Move up of one page" +msgstr "Przesuń w górę o jedną stronę" + +msgid "Move down of one page" +msgstr "Przesuń w dół o jedną stronę" + +msgid "Move to top" +msgstr "Przesuń do góry" + +msgid "Move to bottom" +msgstr "Przesuń do dołu" + +msgid "Move to parent" +msgstr "Przesuń do elementu nadrzędnego" + +msgid "Select all" +msgstr "Zaznacz wszystko" + +msgid "Unselect all" +msgstr "Odznacz wszystko" + +msgid "Select parent" +msgstr "Zaznacz element nadrzędny" + +msgid "Select/Activate current row" +msgstr "Zaznacz/Aktywuj bieżący wiersz" + +msgid "Toggle selection" +msgstr "Przełącz zaznaczenie" + +msgid "Expand/Collapse" +msgstr "Rozwiń/Zwiń" + +msgid "Expand row" +msgstr "Rozwiń wiersz" + +msgid "Collapse row" +msgstr "Zwiń wiersz" + +msgid "Toggle row" +msgstr "Przełącz wiersz" + +msgid "Collapse all rows" +msgstr "Zwiń wszystkie wiersze" + +msgid "Expand all rows" +msgstr "Rozwiń wszystkie wiersze" + +msgid "Close Tab" +msgstr "Zamknij kartę" + +msgid "modularity, scalability and security" +msgstr "modułowy, skalowalny i bezpieczny" + +msgid "translator-credits" +msgstr "Wojciech Warczakowski" + +#, python-format +msgid "Attachments (%s)" +msgstr "Załączniki (%s)" + +msgid "Profile Editor" +msgstr "Edytor profili" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "Dodaj nowy profil" + +msgid "Remove selected profile" +msgstr "Usuń wybrany profil" + +msgid "Host:" +msgstr "Host:" + +msgid "Database:" +msgstr "Baza danych:" + +msgid "Fetching databases list" +msgstr "Pobieranie listy baz danych" + +msgid "Username:" +msgstr "Użytkownik:" + +msgid "Incompatible version of the server." +msgstr "Niewłaściwa wersja serwera." + +msgid "Could not connect to the server." +msgstr "Nie można połączyć się z serwerem." + +msgid "Login" +msgstr "Login" + +msgid "_Cancel" +msgstr "Anuluj" + +msgid "Cancel connection to the Tryton server" +msgstr "Anuluj połączenie z serwerem Tryton" + +msgid "C_onnect" +msgstr "Połącz" + +msgid "Connect the Tryton server" +msgstr "Połącz z serwerem Tryton" + +msgid "Profile:" +msgstr "Profil:" + +msgid "Host / Database information" +msgstr "Informacje o hoście / bazie danych" + +msgid "User name:" +msgstr "Użytkownik:" + +msgid "Unable to complete email entry" +msgstr "Brak możliwości ukończenia edycji e-maila" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "Do:" + +msgid "Cc:" +msgstr "DW:" + +msgid "Bcc:" +msgstr "UDW:" + +msgid "Subject:" +msgstr "Temat:" + +msgid "Body" +msgstr "Treść" + +msgid "Reports" +msgstr "Raporty" + +msgid "Attachments" +msgstr "Załączniki" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "Wyślij" + +#, fuzzy +msgid "Select File" +msgstr "Zaznacz wszystko" + +#, fuzzy +msgid "Remove File" +msgstr "Usuń " + +msgid "Add..." +msgstr "Dodaj..." + +#, fuzzy +msgid "Preview" +msgstr "Poprzedni" + +msgid "Previous" +msgstr "Poprzedni" + +msgid "Next" +msgstr "Następny" + +#, python-format +msgid "Attachment (%s)" +msgstr "Załącznik (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Notatka (%d/%d)" + +msgid "You have to select one record." +msgstr "Musisz wybrać jeden rekord." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Utworzył:" + +msgid "Created at:" +msgstr "Data utworzenia:" + +msgid "Edited by:" +msgstr "Zmodyfikował:" + +msgid "Edited at:" +msgstr "Data modyfikacji:" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Czy na pewno usunąć wybrany rekord?" + +msgid "Are you sure to remove those records?" +msgstr "Czy na pewno usunąć wybrane rekordy?" + +msgid "Records not removed." +msgstr "Rekordy nie zostały usunięte." + +msgid "Records removed." +msgstr "Rekordy zostały usunięte." + +msgid "Working now on the duplicated record(s)." +msgstr "Pracujesz na zduplikowanym(ch) rekordzie(ch)." + +msgid "Record saved." +msgstr "Rekord został zapisany." + +msgid " of " +msgstr " z " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Rekord został zmodyfikowany.\n" +"Czy chcesz go zapisać?" + +msgid "Launch action" +msgstr "Uruchom akcję" + +msgid "Relate" +msgstr "Odnośnik" + +msgid "Open related records" +msgstr "Otwórz powiązane rekordy" + +msgid "Report" +msgstr "Raport" + +msgid "Open report" +msgstr "Otwórz raport" + +msgid "Print" +msgstr "Wydruk" + +msgid "Print report" +msgstr "Drukuj raport" + +msgid "_Copy URL" +msgstr "Skopiuj adres URL" + +msgid "Copy URL into clipboard" +msgstr "Skopiuj adres URL do schowka" + +msgid "Unknown" +msgstr "Nieznany" + +msgid "Limit" +msgstr "Limit" + +msgid "Search Limit Settings" +msgstr "Ustawienia limitu wyszukiwania" + +msgid "Limit:" +msgstr "Limit:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notatki (%s)" + +msgid "Edit User Preferences" +msgstr "Edytuj preferencje użytkownika" + +msgid "Preference" +msgstr "Preferencje" + +msgid "Revision" +msgstr "Rewizja" + +msgid "Select a revision" +msgstr "Wybierz rewizję" + +msgid "Revision:" +msgstr "Rewizja:" + +msgid "_Switch View" +msgstr "Przełącz widok" + +msgid "Switch View" +msgstr "Przełącz widok" + +msgid "_Previous" +msgstr "Poprzedni" + +msgid "Previous Record" +msgstr "Poprzedni rekord" + +msgid "_Next" +msgstr "Następny" + +msgid "Next Record" +msgstr "Następny rekord" + +msgid "_Search" +msgstr "Szukaj" + +msgid "_New" +msgstr "Nowy" + +msgid "Create a new record" +msgstr "Utwórz nowy rekord" + +msgid "_Save" +msgstr "Zapisz" + +msgid "Save this record" +msgstr "Zapisz rekord" + +msgid "_Reload/Undo" +msgstr "Odśwież/Przywróć" + +msgid "Reload/Undo" +msgstr "Odśwież/Przywróć" + +msgid "_Duplicate" +msgstr "Duplikuj" + +msgid "_Delete..." +msgstr "Usuń..." + +msgid "View _Logs..." +msgstr "Przeglądaj dziennik..." + +msgid "Show revisions..." +msgstr "Pokaż rewizje..." + +msgid "A_ttachments..." +msgstr "Załączniki..." + +msgid "Add an attachment to the record" +msgstr "Dodaj załącznik do rekordu" + +msgid "_Notes..." +msgstr "Notatki..." + +msgid "Add a note to the record" +msgstr "Dodaj notatkę do rekordu" + +msgid "_Actions..." +msgstr "Akcje..." + +msgid "_Relate..." +msgstr "Odnośnik..." + +msgid "_Report..." +msgstr "Raport..." + +msgid "_Print..." +msgstr "Wydruk..." + +msgid "_E-Mail..." +msgstr "E-mail..." + +msgid "Send an e-mail using the record" +msgstr "Wyślij e-mail używając rekordu" + +msgid "_Export Data..." +msgstr "Eksportuj dane..." + +msgid "_Import Data..." +msgstr "Importuj dane..." + +msgid "Copy _URL..." +msgstr "Skopiuj adres URL..." + +msgid "_Close Tab" +msgstr "Zamknij kartę" + +msgid "All fields" +msgstr "Wszystkie pola" + +msgid "_Add" +msgstr "Dodaj" + +msgid "_Remove" +msgstr "Usuń" + +msgid "_Clear" +msgstr "Wyczyść" + +msgid "Fields selected" +msgstr "Pola wybrane" + +msgid "CSV Parameters" +msgstr "Parametry CSV" + +msgid "Delimiter:" +msgstr "Separator:" + +msgid "Quote char:" +msgstr "Znak cudzysłowu:" + +msgid "Encoding:" +msgstr "Kodowanie:" + +msgid "Use locale format" +msgstr "Użyj formatu lokalnego" + +msgid "Field name" +msgstr "Nazwa pola" + +#, python-format +msgid "CSV Export: %s" +msgstr "Eksport do CSV: %s" + +msgid "_Save Export" +msgstr "Zapisz eksport" + +msgid "_URL Export" +msgstr "Eksport URL" + +msgid "_Delete Export" +msgstr "Usuń eksport" + +msgid "Predefined exports" +msgstr "Predefiniowane eksporty" + +msgid "Name" +msgstr "Nazwa" + +msgid "Open" +msgstr "Otwórz" + +msgid "Listed Records" +msgstr "Wymienione rekordy" + +msgid "Selected Records" +msgstr "Wybrane rekordy" + +msgid "Ignore search limit" +msgstr "Ignoruj limit wyszukiwań" + +msgid "Add field names" +msgstr "Dodaj nazwy pól" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (model name)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (record name)" + +msgid "What is the name of this export?" +msgstr "Jaka jest nazwa tego eksportu?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Czy nadpisać definicję '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "%d rekord został zapisany." + +#, python-format +msgid "%d records saved." +msgstr "%d rekordy zostały zapisane." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operacja nie powiodła się.\n" +"Komunikat błędu:\n" +"%s" + +msgid "Error" +msgstr "Błąd" + +msgid "Link" +msgstr "Link" + +msgid "Delete" +msgstr "Usuń" + +msgid "New" +msgstr "Nowy" + +msgid "Switch" +msgstr "Przełącz" + +msgid "Add" +msgstr "Dodaj" + +msgid "Remove " +msgstr "Usuń " + +msgid "Create a new record " +msgstr "Utwórz nowy rekord " + +msgid "Delete selected record " +msgstr "Usuń zaznaczony rekord " + +msgid "Undelete selected record " +msgstr "Przywróć zaznaczony rekord " + +#, python-format +msgid "CSV Import: %s" +msgstr "Import z CSV: %s" + +msgid "_Auto-Detect" +msgstr "Rozpoznaj" + +msgid "File to Import:" +msgstr "Plik do zaimportowania:" + +msgid "Open..." +msgstr "Otwórz..." + +msgid "Lines to Skip:" +msgstr "Wiersze do ominięcia:" + +msgid "You must select an import file first." +msgstr "Wybierz plik do zaimportowania." + +msgid "Error opening CSV file" +msgstr "Błąd otwarcia pliku CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Błąd przetwarzania pliku w polu %s." + +#, python-format +msgid "%d record imported." +msgstr "Rekord %d został zaimportowany." + +#, python-format +msgid "%d records imported." +msgstr "Rekordy %d zostały zaimportowane." + +msgid "Search" +msgstr "Szukaj" + +#, python-format +msgid "Search %s" +msgstr "Szukaj %s" + +msgid "Wizard" +msgstr "Kreator" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Brak możliwości usunięcia kreatora %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Utworzył" + +msgid "Created at" +msgstr "Data utworzenia" + +msgid "Edited by" +msgstr "Zmodyfikował" + +msgid "Edited at" +msgstr "Data modyfikacji" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Brak możliwości uzyskania stanu widoku drzewa dla %s" + +msgid "Unable to set view tree state" +msgstr "Brak możliwości ustawienia stanu widoku drzewa" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" jest nieprawidłowy pod względem swojej domeny." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" jest wymagany." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Wartości \"%s\" nie są prawidłowe." + +msgid "Pre-validation" +msgstr "Wstępna walidacja" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Rozmiar obrazka" + +msgid "Width:" +msgstr "Szerokość:" + +msgid "Height:" +msgstr "Wysokość:" + +msgid "PNG image (*.png)" +msgstr "Obrazek w formacie PNG (*.png)" + +msgid "Save As" +msgstr "Zapisz jako" + +msgid "Image size too large." +msgstr "Za duży rozmiar obrazka." + +msgid "Copy" +msgstr "Kopiuj" + +msgid "Paste" +msgstr "Wklej" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Otwórz filtry" + +msgid "Show bookmarks of filters" +msgstr "Pokaż zakładki filtrów" + +msgid "Remove this bookmark" +msgstr "Usuń zakładkę" + +msgid "Bookmark this filter" +msgstr "Dodaj filtr do zakładek" + +msgid "Show active records" +msgstr "Pokaż aktywne rekordy" + +msgid "Show inactive records" +msgstr "Pokaż nieaktywne rekordy" + +msgid "Bookmark Name:" +msgstr "Dodaj nazwę zakładek:" + +msgid "Find" +msgstr "Znajdź" + +msgid "Today" +msgstr "Dzisiaj" + +msgid "go back" +msgstr "wstecz" + +msgid "go forward" +msgstr "dalej" + +msgid "previous year" +msgstr "poprzedni rok" + +msgid "next year" +msgstr "następny rok" + +msgid "Day View" +msgstr "Widok dnia" + +msgid "Week View" +msgstr "Widok tygodnia" + +msgid "Month View" +msgstr "Widok miesiąca" + +msgid "Week" +msgstr "Tydzień" + +msgid "Select..." +msgstr "Wybierz..." + +msgid "Clear" +msgstr "Wyczyść" + +msgid "All files" +msgstr "Wszystkie pliki" + +msgid "Show plain text" +msgstr "Pokaż tekst" + +msgid "Add value" +msgstr "Dodaj wartość" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Usuń \"%s\"" + +msgid "Images" +msgstr "Obrazki" + +msgid "Add existing record" +msgstr "Dodaj istniejący rekord" + +msgid "Remove selected record" +msgstr "Usuń wybrany rekord" + +msgid "Open the record " +msgstr "Otwórz rekord " + +msgid "Clear the field " +msgstr "Wyczyść pole " + +msgid "Search a record " +msgstr "Szukaj rekord " + +msgid "Edit selected record" +msgstr "Edytuj wybrany rekord" + +msgid "Delete selected record" +msgstr "Usuń wybrany rekord" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Wybierz język" + +msgid "Translation" +msgstr "Tłumaczenie" + +msgid "Edit" +msgstr "Edycja" + +msgid "Fuzzy" +msgstr "Niejasne" + +msgid "You need to save the record before adding translations." +msgstr "Musisz zapisać rekord przed dodaniem tłumaczeń." + +msgid "No other language available." +msgstr "Brak innych języków." + +msgid "Translate view" +msgstr "Widok tłumaczenia" diff --git a/tryton/tryton/data/locale/pt/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/pt/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..c23f86d0e0 --- /dev/null +++ b/tryton/tryton/data/locale/pt/LC_MESSAGES/tryton.po @@ -0,0 +1,1200 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "especificar arquivo de configuração alternativo" + +msgid "development mode" +msgstr "modo de desenvolvimento" + +msgid "logging everything at INFO level" +msgstr "Registrar (log) tudo com nível INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" +"Especifique o nível de registro (log level): DEBUG, INFO, WARNING, ERROR, " +"CRITICAL" + +msgid "specify the login user" +msgstr "Especifique o nome do usuário" + +msgid "specify the server hostname:port" +msgstr "Especifique o servidor hostname:porta" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Não foi possível gravar o arquivo de configuração %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Não foi possível definir as configurações regionais %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Selecione sua ação" + +msgid "No action defined." +msgstr "Nenhuma ação definida." + +msgid "By: " +msgstr "Por: " + +msgid "Selection" +msgstr "Seleção" + +msgid "Cancel" +msgstr "Cancelar" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Sua seleção:" + +msgid "Select" +msgstr "Selecionar" + +msgid "Save" +msgstr "Gravar" + +msgid "Save As..." +msgstr "Salvar como..." + +msgid "Do you want to proceed?" +msgstr "Você deseja prosseguir?" + +msgid "Always ignore this warning." +msgstr "Sempre ignorar este aviso." + +msgid "No" +msgstr "Não" + +msgid "Yes" +msgstr "Sim" + +msgid "Concurrency Exception" +msgstr "Exceção de concorrência" + +msgid "This record has been modified while you were editing it." +msgstr "Este registro foi modificado enquanto você o editava." + +msgid "Cancel saving" +msgstr "Cancelar" + +msgid "Compare" +msgstr "Comparar" + +msgid "See the modified version" +msgstr "Ver versão modificada" + +msgid "Write Anyway" +msgstr "Gravar mesmo assim" + +msgid "Save your current version" +msgstr "Gravar sua versão atual" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Comparar: %s" + +msgid "Close" +msgstr "Fechar" + +msgid "Application Error" +msgstr "Erro no programa" + +msgid "Report Bug" +msgstr "Reportar erro (bug)" + +#, python-format +msgid "Check URL: %s" +msgstr "Verificar URL: %s" + +#, fuzzy +msgid "Unable to check for new version." +msgstr "Não foi possível verificar por nova versão" + +msgid "A new version is available!" +msgstr "Nova versão disponível!" + +msgid "Download" +msgstr "Baixar" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Buscar..." + +msgid "Create..." +msgstr "Criar..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Não foi possível encontrar complementos de %s" + +msgid "Value" +msgstr "Valor" + +msgid "Displayed value" +msgstr "Valor exibido" + +msgid "Format" +msgstr "Formato" + +msgid "Display format" +msgstr "Formato de exibição" + +msgid "Open the calendar" +msgstr "Abrir o calendário" + +msgid "Date Format" +msgstr "Formato de Data" + +msgid "Displayed date format" +msgstr "Formato de data exibido" + +msgid "y" +msgstr "s" + +msgid "True" +msgstr "Verdadeiro" + +msgid "t" +msgstr "v" + +msgid "False" +msgstr "Falso" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "Relacionar" + +msgid "Edit..." +msgstr "Editar..." + +msgid "Attachments..." +msgstr "Anexos..." + +msgid "Notes..." +msgstr "Notas..." + +msgid "Actions..." +msgstr "Ações..." + +msgid "Relate..." +msgstr "Relacionado..." + +msgid "Report..." +msgstr "Relatório..." + +msgid "Print..." +msgstr "Imprimir..." + +msgid "E-Mail..." +msgstr "E-mail..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "Selecione uma cor" + +msgid "Y" +msgstr "A" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "s" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferências..." + +msgid "Toolbar" +msgstr "Barra de Ferramentas" + +msgid "Default" +msgstr "Padrão" + +msgid "Text and Icons" +msgstr "Texto e Ícones" + +msgid "Text" +msgstr "Texto" + +msgid "Icons" +msgstr "Ícones" + +msgid "Form" +msgstr "Formulário" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "Gravar Estado da Árvore" + +msgid "Spell Checking" +msgstr "Verificar Ortografia" + +msgid "PDA Mode" +msgstr "Modo dispositivo portátil" + +msgid "Search Limit..." +msgstr "Limite de Buscas..." + +msgid "Check Version" +msgstr "Verificar Versão" + +msgid "Options" +msgstr "Opções" + +msgid "Keyboard Shortcuts..." +msgstr "Atalhos..." + +msgid "About..." +msgstr "Sobre..." + +msgid "Help" +msgstr "Ajuda" + +msgid "No result found." +msgstr "Nenhum resultado encontrado." + +msgid "Favorites" +msgstr "Favoritos" + +msgid "Manage..." +msgstr "Gerenciar..." + +msgid "Action" +msgstr "Ação" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"A ação seguinte requer que todas as abas sejam fechadas.\n" +"Você deseja prosseguir?" + +msgid "Application Shortcuts" +msgstr "Atalhos" + +msgid "Global" +msgstr "Globais" + +msgid "Preferences" +msgstr "Preferências" + +msgid "Search menu" +msgstr "Menu de busca" + +msgid "Toggle menu" +msgstr "Menu" + +msgid "Previous tab" +msgstr "Aba Anterior" + +msgid "Next tab" +msgstr "Próxima Aba" + +msgid "Shortcuts" +msgstr "Atalhos" + +msgid "Quit" +msgstr "Sair" + +msgid "Edition Shortcuts" +msgstr "Atalhos de Edição" + +msgid "Text Entries" +msgstr "Campos de texto" + +msgid "Cut selected text" +msgstr "Recortar o texto selecionado" + +msgid "Copy selected text" +msgstr "Copiar o texto selecionado" + +msgid "Paste copied text" +msgstr "Colar o texto copiado" + +msgid "Next entry" +msgstr "Próximo campo" + +msgid "Previous entry" +msgstr "Campo anterior" + +msgid "Relation Entries" +msgstr "Campos de relação" + +msgid "Create new relation" +msgstr "Criar nova relação" + +msgid "Open/Search relation" +msgstr "Abrir/Buscar relação" + +msgid "List Entries" +msgstr "Campos em lista" + +msgid "Switch view" +msgstr "Alternar exibição" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "Abrir relação" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "Desmarcar linha para apagar" + +msgid "List/Tree Shortcuts" +msgstr "Atalhos da tela de Lista/Árvore" + +msgid "Move Cursor" +msgstr "Mover cursor" + +msgid "Move right" +msgstr "Mover para a direita" + +msgid "Move left" +msgstr "Mover para a esquerda" + +msgid "Move up" +msgstr "Mover para cima" + +msgid "Move down" +msgstr "Mover para baixo" + +msgid "Move up of one page" +msgstr "Mover uma página para cima" + +msgid "Move down of one page" +msgstr "Mover uma página para baixo" + +msgid "Move to top" +msgstr "Mover para o topo" + +msgid "Move to bottom" +msgstr "Mover para o final" + +msgid "Move to parent" +msgstr "Mover para o pai" + +msgid "Select all" +msgstr "Selecionar todos" + +msgid "Unselect all" +msgstr "Desmarcar Todos" + +msgid "Select parent" +msgstr "Selecionar pai" + +msgid "Select/Activate current row" +msgstr "Selecionar/Ativar linha atual" + +msgid "Toggle selection" +msgstr "Alternar seleção" + +msgid "Expand/Collapse" +msgstr "Expandir/Recolher" + +msgid "Expand row" +msgstr "Expandir a linha" + +msgid "Collapse row" +msgstr "Recolher a linha" + +msgid "Toggle row" +msgstr "Alternar linha" + +msgid "Collapse all rows" +msgstr "Recolher todas as linhas" + +msgid "Expand all rows" +msgstr "Expandir todas as linhas" + +msgid "Close Tab" +msgstr "Fechar Aba" + +msgid "modularity, scalability and security" +msgstr "modularidade, escalabilidade e segurança" + +msgid "translator-credits" +msgstr "" +"Felipe Morato\n" +"Pedro" + +#, python-format +msgid "Attachments (%s)" +msgstr "Anexos (%s)" + +msgid "Profile Editor" +msgstr "Editor de Perfil" + +msgid "Profile" +msgstr "Perfil" + +msgid "Add new profile" +msgstr "Adicionar novo perfil" + +msgid "Remove selected profile" +msgstr "Remover perfil selecionado" + +msgid "Host:" +msgstr "Servidor:" + +msgid "Database:" +msgstr "Banco de Dados:" + +msgid "Fetching databases list" +msgstr "Obtendo a lista de bancos de dados" + +msgid "Username:" +msgstr "Usuário:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Versão do servidor incompatível" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Não foi possível se conectar ao servidor" + +msgid "Login" +msgstr "Entrar" + +msgid "_Cancel" +msgstr "_Cancelar" + +msgid "Cancel connection to the Tryton server" +msgstr "Cancelar a conexão com o servidor do Tryton" + +msgid "C_onnect" +msgstr "C_onectar" + +msgid "Connect the Tryton server" +msgstr "Conectar ao servidor do Tryton" + +msgid "Profile:" +msgstr "Perfil:" + +msgid "Host / Database information" +msgstr "Informações do Servidor / Banco de Dados" + +msgid "User name:" +msgstr "Nome de Usuário:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Não é possível apagar o assistente %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Email %s" + +msgid "To:" +msgstr "Para:" + +#, fuzzy +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Assunto:" + +#, fuzzy +msgid "Body" +msgstr "Conteúdo:" + +#, fuzzy +msgid "Reports" +msgstr "Relatório" + +#, fuzzy +msgid "Attachments" +msgstr "Anexo:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Selecionar todos" + +#, fuzzy +msgid "Remove File" +msgstr "Remover " + +msgid "Add..." +msgstr "Adicionar..." + +#, fuzzy +msgid "Preview" +msgstr "Anterior" + +msgid "Previous" +msgstr "Anterior" + +msgid "Next" +msgstr "Próximo" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Anexos (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "Você precisa selecionar um registo." + +msgid "ID:" +msgstr "ID:" + +#, fuzzy +msgid "Created by:" +msgstr "Data de criação" + +#, fuzzy +msgid "Created at:" +msgstr "Data de criação" + +#, fuzzy +msgid "Edited by:" +msgstr "Editar" + +#, fuzzy +msgid "Edited at:" +msgstr "Editar" + +msgid "Model:" +msgstr "Modelo:" + +msgid "Are you sure to remove this record?" +msgstr "Você tem certeza que deseja apagar este registro?" + +msgid "Are you sure to remove those records?" +msgstr "Você tem certeza que deseja apagar estes registros?" + +msgid "Records not removed." +msgstr "Registros não apagados." + +msgid "Records removed." +msgstr "Registos apagados." + +msgid "Working now on the duplicated record(s)." +msgstr "Trabalhando agora no(s) registo(s) duplicado(s)" + +msgid "Record saved." +msgstr "Registo gravado." + +msgid " of " +msgstr " de " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Este registo foi modificado\n" +"você deseja gravá-lo?" + +msgid "Launch action" +msgstr "Executar ação" + +msgid "Relate" +msgstr "Relacionar" + +msgid "Open related records" +msgstr "Abrir registos relacionados" + +msgid "Report" +msgstr "Relatório" + +msgid "Open report" +msgstr "Abrir relatório" + +msgid "Print" +msgstr "Imprimir" + +msgid "Print report" +msgstr "Imprimir relatório" + +msgid "_Copy URL" +msgstr "_Copiar URL" + +msgid "Copy URL into clipboard" +msgstr "Copiar URL para a área de transferência" + +msgid "Unknown" +msgstr "Desconhecido" + +msgid "Limit" +msgstr "Limite" + +msgid "Search Limit Settings" +msgstr "Preferências de limite de buscas" + +msgid "Limit:" +msgstr "Limite:" + +#, python-format +msgid "Notes (%s)" +msgstr "Notas (%s)" + +msgid "Edit User Preferences" +msgstr "Editar preferências do usuário" + +msgid "Preference" +msgstr "Preferência" + +msgid "Revision" +msgstr "Revisão" + +msgid "Select a revision" +msgstr "Selecione uma revisão" + +msgid "Revision:" +msgstr "Revisão:" + +msgid "_Switch View" +msgstr "_Alternar Exibição" + +msgid "Switch View" +msgstr "Alternar" + +msgid "_Previous" +msgstr "_Anterior" + +msgid "Previous Record" +msgstr "Registro Anterior" + +msgid "_Next" +msgstr "_Próximo" + +msgid "Next Record" +msgstr "Próximo Registro" + +msgid "_Search" +msgstr "_Buscar" + +msgid "_New" +msgstr "_Novo" + +msgid "Create a new record" +msgstr "Criar novo registro" + +msgid "_Save" +msgstr "_Salvar" + +msgid "Save this record" +msgstr "Gravar este registro" + +msgid "_Reload/Undo" +msgstr "_Recarregar/Desfazer" + +msgid "Reload/Undo" +msgstr "Recarregar/Desfazer" + +msgid "_Duplicate" +msgstr "_Duplicado" + +msgid "_Delete..." +msgstr "_Apagar..." + +msgid "View _Logs..." +msgstr "Ver _Logs..." + +msgid "Show revisions..." +msgstr "Mostrar revisões..." + +msgid "A_ttachments..." +msgstr "A_nexos..." + +msgid "Add an attachment to the record" +msgstr "Adicionar um anexo ao registro" + +msgid "_Notes..." +msgstr "_Notas..." + +msgid "Add a note to the record" +msgstr "Adicionar uma nota ao registro" + +msgid "_Actions..." +msgstr "_Ações..." + +msgid "_Relate..." +msgstr "_Relacionar" + +msgid "_Report..." +msgstr "_Relatório..." + +msgid "_Print..." +msgstr "_Imprimir..." + +msgid "_E-Mail..." +msgstr "_E-Mail..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Adicionar uma nota ao registro" + +msgid "_Export Data..." +msgstr "_Exportar Dados..." + +msgid "_Import Data..." +msgstr "_Importar Dados..." + +msgid "Copy _URL..." +msgstr "Copiar _URL..." + +msgid "_Close Tab" +msgstr "_Fechar aba" + +msgid "All fields" +msgstr "Todos os campos" + +msgid "_Add" +msgstr "_Adicionar" + +msgid "_Remove" +msgstr "_Remover" + +msgid "_Clear" +msgstr "_Limpar" + +msgid "Fields selected" +msgstr "Campos selecionados" + +msgid "CSV Parameters" +msgstr "Parâmetros CSV" + +msgid "Delimiter:" +msgstr "Delimitador:" + +msgid "Quote char:" +msgstr "Caractere de citação:" + +msgid "Encoding:" +msgstr "Codificação:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "Nome do campo" + +#, python-format +msgid "CSV Export: %s" +msgstr "Exportar CSV: %s" + +msgid "_Save Export" +msgstr "_Gravar exportação" + +#, fuzzy +msgid "_URL Export" +msgstr "_Gravar exportação" + +msgid "_Delete Export" +msgstr "_Apagar exportação" + +msgid "Predefined exports" +msgstr "Exportações pré-definidas" + +msgid "Name" +msgstr "Nome" + +msgid "Open" +msgstr "Abrir" + +#, fuzzy +msgid "Listed Records" +msgstr "Editar o registro selecionado " + +#, fuzzy +msgid "Selected Records" +msgstr "Editar o registro selecionado " + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Adicionar _nomes de campos" + +#, python-format +msgid "%s (string)" +msgstr "%s (texto)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (nome do modelo)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (nome do registro)" + +msgid "What is the name of this export?" +msgstr "Qual é o nome dessa exportação?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Sobrescrever a definição '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "%d registro gravado." + +#, python-format +msgid "%d records saved." +msgstr "%d registros gravados." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operação Falhou.\n" +"Mensagem de erro:\n" +"%s" + +msgid "Error" +msgstr "Erro" + +msgid "Link" +msgstr "Vínculo" + +msgid "Delete" +msgstr "Apagar" + +msgid "New" +msgstr "Novo" + +msgid "Switch" +msgstr "Alternar" + +msgid "Add" +msgstr "Adicionar" + +msgid "Remove " +msgstr "Remover " + +msgid "Create a new record " +msgstr "Criar novo registro " + +msgid "Delete selected record " +msgstr "Apagar o registro selecionado " + +msgid "Undelete selected record " +msgstr "Recuperar o registro selecionado " + +#, python-format +msgid "CSV Import: %s" +msgstr "Importar CSV: %s" + +msgid "_Auto-Detect" +msgstr "_Auto-Detectar" + +msgid "File to Import:" +msgstr "Arquivo para importar:" + +msgid "Open..." +msgstr "Abrir..." + +msgid "Lines to Skip:" +msgstr "Linhas a ignorar:" + +msgid "You must select an import file first." +msgstr "Primeiro você deve selecionar um arquivo para importar." + +msgid "Error opening CSV file" +msgstr "Erro ao abrir arquivo CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Erro ao processar o arquivo no campo %s." + +#, python-format +msgid "%d record imported." +msgstr "%d registros importados." + +#, python-format +msgid "%d records imported." +msgstr "%d registros importados." + +msgid "Search" +msgstr "Busca" + +#, python-format +msgid "Search %s" +msgstr "Buscar %s" + +msgid "Wizard" +msgstr "Assistente" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Não é possível apagar o assistente %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Data de criação" + +#, fuzzy +msgid "Edited by" +msgstr "Editar" + +#, fuzzy +msgid "Edited at" +msgstr "Editar" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Não foi possível obter o estado da visualização em árvore para %s" + +msgid "Unable to set view tree state" +msgstr "Não foi possível definir o estado da visão em árvore" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" não é válido de acordo com seu domínio" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" é obrigatório" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Os valores de \"%s\" não são válidos" + +msgid "Pre-validation" +msgstr "Pré-validação" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Tamanho da imagem" + +msgid "Width:" +msgstr "Largura:" + +msgid "Height:" +msgstr "Altura:" + +msgid "PNG image (*.png)" +msgstr "Imagem PNG (*.png)" + +msgid "Save As" +msgstr "Salvar como" + +msgid "Image size too large." +msgstr "Imagem muito grande." + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Abrir filtros" + +msgid "Show bookmarks of filters" +msgstr "Mostrar filtros favoritos" + +msgid "Remove this bookmark" +msgstr "Remover este favorito" + +msgid "Bookmark this filter" +msgstr "Marcar este filtro como favorito" + +msgid "Show active records" +msgstr "Mostrar registros ativos" + +msgid "Show inactive records" +msgstr "Mostrar registros inativos" + +msgid "Bookmark Name:" +msgstr "Nome do favorito:" + +msgid "Find" +msgstr "Localizar" + +msgid "Today" +msgstr "Hoje" + +msgid "go back" +msgstr "voltar" + +msgid "go forward" +msgstr "avançar" + +msgid "previous year" +msgstr "ano anterior" + +msgid "next year" +msgstr "próximo ano" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "Visão Semanal" + +msgid "Month View" +msgstr "Visão Mensal" + +msgid "Week" +msgstr "Semana" + +msgid "Select..." +msgstr "Selecionar..." + +msgid "Clear" +msgstr "Limpar" + +msgid "All files" +msgstr "Todos os arquivos" + +msgid "Show plain text" +msgstr "Mostrar em formato texto" + +msgid "Add value" +msgstr "Adicionar valor" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Remover \"%s\"" + +msgid "Images" +msgstr "Imagens" + +msgid "Add existing record" +msgstr "Adicionar um registro existente" + +msgid "Remove selected record" +msgstr "Remover o registro selecionado" + +msgid "Open the record " +msgstr "Abrir o registro " + +msgid "Clear the field " +msgstr "Apagar o registro " + +msgid "Search a record " +msgstr "Buscar um registro " + +#, fuzzy +msgid "Edit selected record" +msgstr "Editar o registro selecionado " + +#, fuzzy +msgid "Delete selected record" +msgstr "Remover o registro selecionado" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "Tradução" + +msgid "Edit" +msgstr "Editar" + +msgid "Fuzzy" +msgstr "Vago" + +msgid "You need to save the record before adding translations." +msgstr "Você precisa salvar o registro antes de adicionar traduções." + +msgid "No other language available." +msgstr "Não há outros idiomas disponíveis." + +msgid "Translate view" +msgstr "Traduzir visão" diff --git a/tryton/tryton/data/locale/ro/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/ro/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..c62f8494ac --- /dev/null +++ b/tryton/tryton/data/locale/ro/LC_MESSAGES/tryton.po @@ -0,0 +1,1167 @@ +# +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "specificare alt fisier de configurare" + +msgid "development mode" +msgstr "mod de dezvoltare" + +msgid "logging everything at INFO level" +msgstr "logare totul la nivel INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "specificați nivelul jurnalului: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "specificati utilizatorul" + +msgid "specify the server hostname:port" +msgstr "specificați numele gazdă server: port" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Imposibil de scris fișierul de configurare %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Imposibil de setat regiunea locală %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Selectați acțiunea" + +msgid "No action defined." +msgstr "Nicio acțiune definită." + +msgid "By: " +msgstr "De: " + +msgid "Selection" +msgstr "Selecţie" + +msgid "Cancel" +msgstr "Anulare" + +msgid "OK" +msgstr "OK" + +msgid "Your selection:" +msgstr "Selecția dvs.:" + +msgid "Select" +msgstr "Selectați" + +msgid "Save" +msgstr "Salvare" + +msgid "Save As..." +msgstr "Salvare că..." + +msgid "Do you want to proceed?" +msgstr "Doriți să continuați?" + +msgid "Always ignore this warning." +msgstr "Ignorați întotdeauna acest avertisment." + +msgid "No" +msgstr "Nu" + +msgid "Yes" +msgstr "Da" + +msgid "Concurrency Exception" +msgstr "Excepție de concurgență" + +msgid "This record has been modified while you were editing it." +msgstr "Această înregistrare a fost modificată în timp ce o editați." + +msgid "Cancel saving" +msgstr "Anulați salvarea" + +msgid "Compare" +msgstr "Comparaţie" + +msgid "See the modified version" +msgstr "Visualizare a versiunii modificate" + +msgid "Write Anyway" +msgstr "Scrie oricum" + +msgid "Save your current version" +msgstr "Salvați versiunea curentă" + +#, python-format +msgid "Compare: %s" +msgstr "Comparați: %s" + +msgid "Close" +msgstr "Închidere" + +msgid "Application Error" +msgstr "Eroare de aplicație" + +msgid "Report Bug" +msgstr "Raporteati eroare" + +#, python-format +msgid "Check URL: %s" +msgstr "Verificați adresa URL: %s" + +msgid "Unable to check for new version." +msgstr "Nu se poate verifica dacă există o versiune nouă." + +msgid "A new version is available!" +msgstr "O nouă versiune este disponibilă!" + +msgid "Download" +msgstr "Descarca" + +msgid "Too many requests. Try again later." +msgstr "Prea multe cereri. Încercați mai târziu." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Căutare..." + +msgid "Create..." +msgstr "Creare..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "Imposibil de căutat pentru finalizarea %s" + +msgid "Value" +msgstr "Valoare" + +msgid "Displayed value" +msgstr "Valoarea afișată" + +msgid "Format" +msgstr "Format" + +msgid "Display format" +msgstr "Formatul afisare" + +msgid "Open the calendar" +msgstr "Deschidere calendar" + +msgid "Date Format" +msgstr "Formatul datei" + +msgid "Displayed date format" +msgstr "Formatul datei afișat" + +msgid "y" +msgstr "y" + +msgid "True" +msgstr "Adevărat" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "Fals" + +msgid "Digits" +msgstr "Cifre" + +msgid "The number of decimal" +msgstr "Zecimale" + +msgid "Template" +msgstr "Șablon" + +msgid "Edit..." +msgstr "Editare..." + +msgid "Attachments..." +msgstr "Atașamente..." + +msgid "Notes..." +msgstr "Note..." + +msgid "Actions..." +msgstr "Acțiuni..." + +msgid "Relate..." +msgstr "Relata..." + +msgid "Report..." +msgstr "Raportare..." + +msgid "Print..." +msgstr "Imprimare..." + +msgid "E-Mail..." +msgstr "E-Mail..." + +msgid "Bold" +msgstr "Text ingroșat" + +msgid "Italic" +msgstr "Cursiv" + +msgid "Underline" +msgstr "Subliniere" + +msgid "Align Left" +msgstr "Aliniere stânga" + +msgid "Align Center" +msgstr "Aliniere centru" + +msgid "Align Right" +msgstr "Aliniere dreapta" + +msgid "Justify" +msgstr "Margini" + +msgid "Foreground Color" +msgstr "Culoare prim plan" + +msgid "Select a color" +msgstr "Selectare culoare" + +msgid "Y" +msgstr "Y" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "w" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "h" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +msgid "Preferences..." +msgstr "Preferințe ..." + +msgid "Toolbar" +msgstr "Bara de instrumente" + +msgid "Default" +msgstr "Implicit" + +msgid "Text and Icons" +msgstr "Text și icoane" + +msgid "Text" +msgstr "Text" + +msgid "Icons" +msgstr "Pictograme" + +msgid "Form" +msgstr "Formular" + +msgid "Save Column Width" +msgstr "Salvați lățimea coloanei" + +msgid "Save Tree State" +msgstr "Salvare stare arbore" + +msgid "Spell Checking" +msgstr "Verificare ortografia" + +msgid "PDA Mode" +msgstr "Modul PDA" + +msgid "Search Limit..." +msgstr "Limita de căutare ..." + +msgid "Check Version" +msgstr "Verificare versiune" + +msgid "Options" +msgstr "Opțiuni" + +msgid "Keyboard Shortcuts..." +msgstr "Comenzi rapide..." + +msgid "About..." +msgstr "Despre..." + +msgid "Help" +msgstr "Ajutor" + +msgid "No result found." +msgstr "Niciun rezultat găsit." + +msgid "Favorites" +msgstr "Favorite" + +msgid "Manage..." +msgstr "Gestionare..." + +msgid "Action" +msgstr "Acțiune" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Următoarea acțiune necesită închiderea tuturor filelor.\n" +"Doriți să continuați?" + +msgid "Application Shortcuts" +msgstr "Comenzi rapide pentru aplicații" + +msgid "Global" +msgstr "Global" + +msgid "Preferences" +msgstr "Preferințe" + +msgid "Search menu" +msgstr "Meniu Căutare" + +msgid "Toggle menu" +msgstr "Meniu Comutare" + +msgid "Previous tab" +msgstr "Fila anterioară" + +msgid "Next tab" +msgstr "Fila următoare" + +msgid "Shortcuts" +msgstr "Scurtaturi" + +msgid "Quit" +msgstr "Inchidere" + +msgid "Edition Shortcuts" +msgstr "Comenzi rapide de ediție" + +msgid "Text Entries" +msgstr "Inregistrari text" + +msgid "Cut selected text" +msgstr "Tăiere text selectat" + +msgid "Copy selected text" +msgstr "Copiere text selectat" + +msgid "Paste copied text" +msgstr "Inserare text copiat" + +msgid "Next entry" +msgstr "Următoarea inregistrare" + +msgid "Previous entry" +msgstr "Inregistrare anterioară" + +msgid "Relation Entries" +msgstr "Inregistrare relații" + +msgid "Create new relation" +msgstr "Creare relație noua" + +msgid "Open/Search relation" +msgstr "Deschidere/Căutare relație" + +msgid "List Entries" +msgstr "Lista inregistrari" + +msgid "Switch view" +msgstr "Comutare vedere" + +msgid "Create/Select new line" +msgstr "Creare/Selectie rând nou" + +msgid "Open relation" +msgstr "Deschidere relație" + +msgid "Mark line for deletion/removal" +msgstr "Selectare rând pentru ștergere/eliminare" + +msgid "Mark line for removal" +msgstr "Selectare linia pentru eliminare" + +msgid "Unmark line for deletion" +msgstr "Deselectare rând pentru ștergere" + +msgid "List/Tree Shortcuts" +msgstr "Scurtaturi Listă/Arbore" + +msgid "Move Cursor" +msgstr "Mutare cursor" + +msgid "Move right" +msgstr "Mutare dreapta" + +msgid "Move left" +msgstr "Mutre la stânga" + +msgid "Move up" +msgstr "Mutare în sus" + +msgid "Move down" +msgstr "Mutre în jos" + +msgid "Move up of one page" +msgstr "Mutare sus o pagina" + +msgid "Move down of one page" +msgstr "Mutare jos o pagina" + +msgid "Move to top" +msgstr "Mută în varf" + +msgid "Move to bottom" +msgstr "Mutați-vă cel mai jos" + +msgid "Move to parent" +msgstr "Mutare la parinte" + +msgid "Select all" +msgstr "Selectare tot" + +msgid "Unselect all" +msgstr "Deselectare tot" + +msgid "Select parent" +msgstr "Selectare parinte" + +msgid "Select/Activate current row" +msgstr "Selectare/Activare rândul curent" + +msgid "Toggle selection" +msgstr "Comutare selecția" + +msgid "Expand/Collapse" +msgstr "Expansiune/Reducere" + +msgid "Expand row" +msgstr "Extindere rându" + +msgid "Collapse row" +msgstr "Reducere rând" + +msgid "Toggle row" +msgstr "Comutare rând" + +msgid "Collapse all rows" +msgstr "Reducere toate rândurile" + +msgid "Expand all rows" +msgstr "Extindeți toate rândurile" + +msgid "Close Tab" +msgstr "Închidere fila" + +msgid "modularity, scalability and security" +msgstr "modularitate, scalabilitate și securitate" + +msgid "translator-credits" +msgstr "Dimitrios Moustos" + +#, python-format +msgid "Attachments (%s)" +msgstr "Atașamente (%s)" + +msgid "Profile Editor" +msgstr "Editor de profil" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "Adăugare profil nou" + +msgid "Remove selected profile" +msgstr "Ștergere profil selectat" + +msgid "Host:" +msgstr "Gazdă:" + +msgid "Database:" +msgstr "Bază de date:" + +msgid "Fetching databases list" +msgstr "Obținerea listei de baze de date" + +msgid "Username:" +msgstr "Utilizator:" + +msgid "Incompatible version of the server." +msgstr "Versiunea incompatibilă a serverului." + +msgid "Could not connect to the server." +msgstr "Nu s-a putut conecta la server." + +msgid "Login" +msgstr "Autentificare" + +msgid "_Cancel" +msgstr "_Anulare" + +msgid "Cancel connection to the Tryton server" +msgstr "Intrerupere al conexiunii la serverul Tryton" + +msgid "C_onnect" +msgstr "C_onectare" + +msgid "Connect the Tryton server" +msgstr "Conectați serverul Tryton" + +msgid "Profile:" +msgstr "Profil:" + +msgid "Host / Database information" +msgstr "Informații despre gazdă / bază de date" + +msgid "User name:" +msgstr "Utilizator:" + +msgid "Unable to complete email entry" +msgstr "Nu se poate finaliza introducerea e-mailului" + +#, python-format +msgid "E-mail %s" +msgstr "E-mail %s" + +msgid "To:" +msgstr "Destinatar:" + +msgid "Cc:" +msgstr "CC:" + +msgid "Bcc:" +msgstr "Bcc:" + +msgid "Subject:" +msgstr "Subiect:" + +msgid "Body" +msgstr "Corp" + +msgid "Reports" +msgstr "Raport" + +msgid "Attachments" +msgstr "Atașamente" + +msgid "Files" +msgstr "Fişiere" + +msgid "Send" +msgstr "Trimite" + +msgid "Select File" +msgstr "Selectare Fişier" + +msgid "Remove File" +msgstr "Eliminare Fişier" + +msgid "Add..." +msgstr "Adăuga..." + +msgid "Preview" +msgstr "Previzualizare" + +msgid "Previous" +msgstr "Anterior" + +msgid "Next" +msgstr "Următor" + +#, python-format +msgid "Attachment (%s)" +msgstr "Atachment (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "Notă (%d/%d)" + +msgid "You have to select one record." +msgstr "Trebuie selectaa o singura înregistrare." + +msgid "ID:" +msgstr "ID:" + +msgid "Created by:" +msgstr "Creat de:" + +msgid "Created at:" +msgstr "Creat la:" + +msgid "Edited by:" +msgstr "Editat de:" + +msgid "Edited at:" +msgstr "Editat la:" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Sigur eliminați această înregistrare?" + +msgid "Are you sure to remove those records?" +msgstr "Sigur eliminați această înregistrare?" + +msgid "Records not removed." +msgstr "Înregistrările nu au fost eliminate." + +msgid "Records removed." +msgstr "Înregistrările au fost eliminate." + +msgid "Working now on the duplicated record(s)." +msgstr "Lucrează acum la înregistrările duplicate." + +msgid "Record saved." +msgstr "Înregistrare salvată." + +msgid " of " +msgstr " de " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Această înregistrare a fost modificată\n" +"vrei sa o salvezi?" + +msgid "Launch action" +msgstr "Lansare acțiune" + +msgid "Relate" +msgstr "Relata" + +msgid "Open related records" +msgstr "Deschidere înregistrări conexe" + +msgid "Report" +msgstr "Raport" + +msgid "Open report" +msgstr "Deschidere Raport" + +msgid "Print" +msgstr "Tiparire" + +msgid "Print report" +msgstr "Tipărire raport" + +msgid "_Copy URL" +msgstr "_Copiere URL" + +msgid "Copy URL into clipboard" +msgstr "Copiați URL-ul în clipboard" + +msgid "Unknown" +msgstr "Necunoscut" + +msgid "Limit" +msgstr "Limită" + +msgid "Search Limit Settings" +msgstr "Setări limită de căutare" + +msgid "Limit:" +msgstr "Limită:" + +#, python-format +msgid "Notes (%s)" +msgstr "Note (%s)" + +msgid "Edit User Preferences" +msgstr "Editare Preferințe utilizator" + +msgid "Preference" +msgstr "Preferințe" + +msgid "Revision" +msgstr "Revizuire" + +msgid "Select a revision" +msgstr "Selectare o revizie" + +msgid "Revision:" +msgstr "Revizuire:" + +msgid "_Switch View" +msgstr "_Comutare vedere" + +msgid "Switch View" +msgstr "Comutare vedere" + +msgid "_Previous" +msgstr "_Anterior" + +msgid "Previous Record" +msgstr "Înregistrare anterioară" + +msgid "_Next" +msgstr "Următor" + +msgid "Next Record" +msgstr "Următoarea înregistrare" + +msgid "_Search" +msgstr "_Căutare" + +msgid "_New" +msgstr "_Nou" + +msgid "Create a new record" +msgstr "Creați o înregistrare nouă" + +msgid "_Save" +msgstr "_Salvare" + +msgid "Save this record" +msgstr "Salvare înregistrare" + +msgid "_Reload/Undo" +msgstr "_Reincarcare/Desfacere" + +msgid "Reload/Undo" +msgstr "Reincarcare/Desfacere" + +msgid "_Duplicate" +msgstr "_Duplicat" + +msgid "_Delete..." +msgstr "_Ștergere..." + +msgid "View _Logs..." +msgstr "Vizualizare_jurnalele ..." + +msgid "Show revisions..." +msgstr "Afișați revizii..." + +msgid "A_ttachments..." +msgstr "A_tasamente..." + +msgid "Add an attachment to the record" +msgstr "Adăugați un atașament la înregistrare" + +msgid "_Notes..." +msgstr "Note..." + +msgid "Add a note to the record" +msgstr "Adăugați o notă la înregistrare" + +msgid "_Actions..." +msgstr "_Acțiuni..." + +msgid "_Relate..." +msgstr "_Relata..." + +msgid "_Report..." +msgstr "_Raportare..." + +msgid "_Print..." +msgstr "_Imprimare..." + +msgid "_E-Mail..." +msgstr "_E-mail..." + +msgid "Send an e-mail using the record" +msgstr "Trimiteți un e-mail folosind înregistrarea" + +msgid "_Export Data..." +msgstr "_Export de date..." + +msgid "_Import Data..." +msgstr "_Import de date..." + +msgid "Copy _URL..." +msgstr "Copiază _URL ..." + +msgid "_Close Tab" +msgstr "_Închideți fila" + +msgid "All fields" +msgstr "Toate câmpurile" + +msgid "_Add" +msgstr "_Adăuga" + +msgid "_Remove" +msgstr "_Elimina" + +msgid "_Clear" +msgstr "_Clar" + +msgid "Fields selected" +msgstr "Câmpurile selectate" + +msgid "CSV Parameters" +msgstr "Parametri CSV" + +msgid "Delimiter:" +msgstr "Delimitator:" + +msgid "Quote char:" +msgstr "Caracter de cotatie:" + +msgid "Encoding:" +msgstr "Codifiere:" + +msgid "Use locale format" +msgstr "Utilizați formatul local" + +msgid "Field name" +msgstr "Denumire Câmp" + +#, python-format +msgid "CSV Export: %s" +msgstr "Export CSV: %s" + +msgid "_Save Export" +msgstr "_Salvare Export" + +msgid "_URL Export" +msgstr "_URL Export" + +msgid "_Delete Export" +msgstr "_Ștergere Export" + +msgid "Predefined exports" +msgstr "Exporturi predefinite" + +msgid "Name" +msgstr "Denumire" + +msgid "Open" +msgstr "Deschidere" + +msgid "Listed Records" +msgstr "Înregistrări enumerate" + +msgid "Selected Records" +msgstr "Înregistrări selectate" + +msgid "Ignore search limit" +msgstr "Ignorați limita de căutare" + +msgid "Add field names" +msgstr "Adăugare denumire de câmp" + +#, python-format +msgid "%s (string)" +msgstr "%s (string)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (model name)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (denmire inregistrare)" + +msgid "What is the name of this export?" +msgstr "Care este numele acestui export?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Trecere peste definiția '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "inregistrare %d salvata." + +#, python-format +msgid "%d records saved." +msgstr "inregistrari %d salvate." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operația a eșuat.\n" +"Mesaj de eroare:\n" +"%s" + +msgid "Error" +msgstr "Eroare" + +msgid "Link" +msgstr "Legătură" + +msgid "Delete" +msgstr "Ștergere" + +msgid "New" +msgstr "Nou" + +msgid "Switch" +msgstr "Schimba" + +msgid "Add" +msgstr "Adăuga" + +msgid "Remove " +msgstr "Eliminare " + +msgid "Create a new record " +msgstr "Creați o nouă înregistrare " + +msgid "Delete selected record " +msgstr "Ștergeți înregistrarea selectată " + +msgid "Undelete selected record " +msgstr "Anulare stergere pentru înregistrarea selectată " + +#, python-format +msgid "CSV Import: %s" +msgstr "Import CSV: %s" + +msgid "_Auto-Detect" +msgstr "_Auto detectare" + +msgid "File to Import:" +msgstr "Fișier de importat:" + +msgid "Open..." +msgstr "Deschidere..." + +msgid "Lines to Skip:" +msgstr "Rânduri de sarit:" + +msgid "You must select an import file first." +msgstr "Mai întâi trebuie să selectați un fișier de import." + +msgid "Error opening CSV file" +msgstr "Eroare la deschiderea fișierului CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Eroare la procesarea fișierului în câmpul %s." + +#, python-format +msgid "%d record imported." +msgstr "înregistrare %d importată." + +#, python-format +msgid "%d records imported." +msgstr "înregistrări %d importate." + +msgid "Search" +msgstr "Căutare" + +#, python-format +msgid "Search %s" +msgstr "Căutare %s" + +msgid "Wizard" +msgstr "Asistent" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "Imposibil de șters asistent de configurare %s" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "Creat de" + +msgid "Created at" +msgstr "Creat la" + +msgid "Edited by" +msgstr "Editat de" + +msgid "Edited at" +msgstr "Editat la" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "Nu se poate obține starea arborelui de vizualizare pentru %s" + +msgid "Unable to set view tree state" +msgstr "Imposibil de setat starea arborelui de vizualizare" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "\"%s\" nu este valid conform domeniului său." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" este obligatoriu." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Valorile lui \"%s\" nu sunt valide." + +msgid "Pre-validation" +msgstr "Pre-validare" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Marimea imaginii" + +msgid "Width:" +msgstr "Lăţime:" + +msgid "Height:" +msgstr "Înălţime:" + +msgid "PNG image (*.png)" +msgstr "Imagine PNG (* .png)" + +msgid "Save As" +msgstr "Salvează ca" + +msgid "Image size too large." +msgstr "Dimensiunea imaginii este prea mare." + +msgid "Copy" +msgstr "Copiere" + +msgid "Paste" +msgstr "Lipire" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Deschide filtre" + +msgid "Show bookmarks of filters" +msgstr "Afișați marcajele filtrelor" + +msgid "Remove this bookmark" +msgstr "Eliminare marcaj" + +msgid "Bookmark this filter" +msgstr "Marcare filtru" + +msgid "Show active records" +msgstr "Afișează înregistrări active" + +msgid "Show inactive records" +msgstr "Afișează înregistrări active" + +msgid "Bookmark Name:" +msgstr "Denumire marcaj:" + +msgid "Find" +msgstr "Căutare" + +msgid "Today" +msgstr "Astăzi" + +msgid "go back" +msgstr "întoarcre" + +msgid "go forward" +msgstr "mergi inainte" + +msgid "previous year" +msgstr "anul precedent" + +msgid "next year" +msgstr "anul urmator" + +msgid "Day View" +msgstr "Visualizare zi" + +msgid "Week View" +msgstr "Visualizare saptamana" + +msgid "Month View" +msgstr "Vizualizare lunară" + +msgid "Week" +msgstr "Săptămână" + +msgid "Select..." +msgstr "Selectare..." + +msgid "Clear" +msgstr "Golire" + +msgid "All files" +msgstr "Toate fișierele" + +msgid "Show plain text" +msgstr "Afișare text simplu" + +msgid "Add value" +msgstr "Adaugă valoare" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Eliminare \"%s\"" + +msgid "Images" +msgstr "Imagini" + +msgid "Add existing record" +msgstr "Adăugare înregistrare existentă" + +msgid "Remove selected record" +msgstr "Eliminare înregistrare selectată" + +msgid "Open the record " +msgstr "Deschidere înregistrare " + +msgid "Clear the field " +msgstr "Golire a câmpului " + +msgid "Search a record " +msgstr "Căutați o înregistrare " + +msgid "Edit selected record" +msgstr "Editeare înregistrare selectată" + +msgid "Delete selected record" +msgstr "Ștergere înregistrare selectată" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "Alegere limbă" + +msgid "Translation" +msgstr "Traducere" + +msgid "Edit" +msgstr "Editare" + +msgid "Fuzzy" +msgstr "Cautare aproximativa" + +msgid "You need to save the record before adding translations." +msgstr "Trebuie să salvați înregistrarea înainte de a adăuga traduceri." + +msgid "No other language available." +msgstr "Nu există altă limbă disponibilă." + +msgid "Translate view" +msgstr "Traducere vedere" diff --git a/tryton/tryton/data/locale/ru/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/ru/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..cd1b3f7b01 --- /dev/null +++ b/tryton/tryton/data/locale/ru/LC_MESSAGES/tryton.po @@ -0,0 +1,1246 @@ +# Russian (Russia) translations for tryton. +# Copyright (C) 2009 B2CK +# This file is distributed under the same license as the tryton project. +# Dmitry Klimanov , 2010. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "указать альтернативный конфигурационный файл" + +msgid "development mode" +msgstr "режим разработки" + +msgid "logging everything at INFO level" +msgstr "все сообщения на уровне INFO" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" +"указать уровень вывода сообщений: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "указать имя пользователя" + +#, fuzzy +msgid "specify the server hostname:port" +msgstr "укажите имя сервера" + +#, fuzzy, python-format +msgid "Unable to write config file %s." +msgstr "Невозможно записать файл конфигурации %s!" + +#, python-format +msgid "Unable to set locale %s" +msgstr "Не удается установить локализацию %s" + +#, fuzzy +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "Выберите действие" + +#, fuzzy +msgid "No action defined." +msgstr "Не определены действия!" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "Выбор" + +#, fuzzy +msgid "Cancel" +msgstr "Отмена" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "Ваш выбор:" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "Сохранить" + +msgid "Save As..." +msgstr "Сохранить как..." + +msgid "Do you want to proceed?" +msgstr "Вы хотите продолжить?" + +msgid "Always ignore this warning." +msgstr "Всегда игнорировать это предупреждение." + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "Прерывание конкурентных записей" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "Сравнить" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "Записать все равно" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Сравнить: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "Ошибка приложения!" + +msgid "Report Bug" +msgstr "Сообщить об ошибке" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +#, fuzzy +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Поиск..." + +msgid "Create..." +msgstr "Создать..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +#, fuzzy +msgid "Value" +msgstr "Добавить значение" + +#, fuzzy +msgid "Displayed value" +msgstr "Добавить значение" + +#, fuzzy +msgid "Format" +msgstr "Нормальный" + +msgid "Display format" +msgstr "" + +#, fuzzy +msgid "Open the calendar" +msgstr "Открыть календарь " + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "Истинный" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "Ложный" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "Связанные" + +#, fuzzy +msgid "Edit..." +msgstr "_Выход..." + +msgid "Attachments..." +msgstr "Вложения..." + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "Действия..." + +msgid "Relate..." +msgstr "Связанные..." + +msgid "Report..." +msgstr "Отчет..." + +msgid "Print..." +msgstr "Печать..." + +msgid "E-Mail..." +msgstr "Эл.почта..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "Г" + +msgid "M" +msgstr "М" + +msgid "w" +msgstr "н" + +msgid "d" +msgstr "д" + +msgid "h" +msgstr "ч" + +msgid "m" +msgstr "м" + +msgid "s" +msgstr "" + +#, fuzzy +msgid "Preferences..." +msgstr "Настройки..." + +#, fuzzy +msgid "Toolbar" +msgstr "Панель инструментов" + +#, fuzzy +msgid "Default" +msgstr "По умолчанию" + +#, fuzzy +msgid "Text and Icons" +msgstr "Текст и Значки" + +#, fuzzy +msgid "Text" +msgstr "Текст" + +#, fuzzy +msgid "Icons" +msgstr "Значки" + +#, fuzzy +msgid "Form" +msgstr "Форма" + +msgid "Save Column Width" +msgstr "" + +#, fuzzy +msgid "Save Tree State" +msgstr "Запомнить состояние дерева" + +msgid "Spell Checking" +msgstr "Проверка орфографии" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "Максимальное количество строк..." + +msgid "Check Version" +msgstr "" + +#, fuzzy +msgid "Options" +msgstr "_Параметры" + +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "_Быстрые клавиши" + +#, fuzzy +msgid "About..." +msgstr "О программе" + +#, fuzzy +msgid "Help" +msgstr "Помощь" + +msgid "No result found." +msgstr "Ничего не найдено." + +#, fuzzy +msgid "Favorites" +msgstr "Избранное" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "Действие" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Данное действие приведет к закрытию всех вкладок.\n" +"Продолжить?" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "Настройки" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "Предыдущая вкладка" + +#, fuzzy +msgid "Next tab" +msgstr "Следующая вкладка" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Вырезать выделенный текст" + +msgid "Copy selected text" +msgstr "Скопировать выделенный текст" + +msgid "Paste copied text" +msgstr "Вставить скопированный текст" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "Создать новую связь" + +msgid "Open/Search relation" +msgstr "Открыть / Поиск связей" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "Переключить вид" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "Открыть связь" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "Убрать отметку на удаление" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "Закрыть вкладку" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "Вложений (%s)" + +msgid "Profile Editor" +msgstr "Редактор профилей" + +msgid "Profile" +msgstr "Профиль" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "Сервер:порт" + +msgid "Database:" +msgstr "База данных:" + +msgid "Fetching databases list" +msgstr "Получение списка баз данных" + +msgid "Username:" +msgstr "Имя пользователя:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Несовместимая версия сервера" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Не удается подключиться к серверу!" + +msgid "Login" +msgstr "Логин" + +msgid "_Cancel" +msgstr "Отмена" + +msgid "Cancel connection to the Tryton server" +msgstr "Отмена подключения к серверу" + +msgid "C_onnect" +msgstr "Соединение" + +msgid "Connect the Tryton server" +msgstr "Соединение с сервером" + +msgid "Profile:" +msgstr "Профиль:" + +msgid "Host / Database information" +msgstr "Сервер / база данных" + +msgid "User name:" +msgstr "Имя пользователя:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Не удается установить локализацию %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Эл.почта %s" + +msgid "To:" +msgstr "Кому:" + +#, fuzzy +msgid "Cc:" +msgstr "Скрытая копия:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Тема:" + +#, fuzzy +msgid "Body" +msgstr "Сообщение:" + +#, fuzzy +msgid "Reports" +msgstr "Отчет" + +#, fuzzy +msgid "Attachments" +msgstr "Вложение:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Выбор" + +#, fuzzy +msgid "Remove File" +msgstr "Удалить " + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "Предыдущий" + +msgid "Previous" +msgstr "Предыдущий" + +msgid "Next" +msgstr "Следующий" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Вложений (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +#, fuzzy +msgid "You have to select one record." +msgstr "Вы должны выбрать одну запись!" + +msgid "ID:" +msgstr "Номер строки" + +#, fuzzy +msgid "Created by:" +msgstr "Дата создания" + +#, fuzzy +msgid "Created at:" +msgstr "Дата создания" + +#, fuzzy +msgid "Edited by:" +msgstr "Редактировать" + +#, fuzzy +msgid "Edited at:" +msgstr "Редактировать" + +msgid "Model:" +msgstr "Модель:" + +msgid "Are you sure to remove this record?" +msgstr "Вы уверены что хотите удалить эту запись?" + +msgid "Are you sure to remove those records?" +msgstr "Вы уверены что хотите удалить эти записи?" + +#, fuzzy +msgid "Records not removed." +msgstr "Записи не удалены!" + +#, fuzzy +msgid "Records removed." +msgstr "Записи удалены!" + +#, fuzzy +msgid "Working now on the duplicated record(s)." +msgstr "Сейчас работает на дублирующихся записях!" + +#, fuzzy +msgid "Record saved." +msgstr "Запись сохранена!" + +msgid " of " +msgstr " из " + +#, fuzzy +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Эта запись была изменена\n" +"Вы хотите её сохранить?" + +msgid "Launch action" +msgstr "Выполнить действие" + +msgid "Relate" +msgstr "Связанные" + +msgid "Open related records" +msgstr "Открыть связанные записи" + +msgid "Report" +msgstr "Отчет" + +msgid "Open report" +msgstr "Открыть отчет" + +msgid "Print" +msgstr "Печать" + +msgid "Print report" +msgstr "Печать отчета" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "Неизвестно" + +msgid "Limit" +msgstr "Ограничение" + +msgid "Search Limit Settings" +msgstr "Настройки ограничения поиска" + +msgid "Limit:" +msgstr "Кол-во записей" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "Изменить настройки пользователя" + +msgid "Preference" +msgstr "Настройка" + +#, fuzzy +msgid "Revision" +msgstr "Предыдущий" + +#, fuzzy +msgid "Select a revision" +msgstr "Выбор" + +#, fuzzy +msgid "Revision:" +msgstr "Предыдущий" + +msgid "_Switch View" +msgstr "Сменить просмотр" + +#, fuzzy +msgid "Switch View" +msgstr "Сменить просмотр" + +msgid "_Previous" +msgstr "Предыдущая" + +msgid "Previous Record" +msgstr "Предыдущая запись" + +msgid "_Next" +msgstr "Следующая" + +msgid "Next Record" +msgstr "Следующая запись" + +msgid "_Search" +msgstr "Поиск" + +msgid "_New" +msgstr "Новый" + +msgid "Create a new record" +msgstr "Создать новую запись" + +msgid "_Save" +msgstr "Сохранить" + +msgid "Save this record" +msgstr "Сохранить эту запись" + +msgid "_Reload/Undo" +msgstr "Обновить/Отменить" + +#, fuzzy +msgid "Reload/Undo" +msgstr "Обновить/Отменить" + +msgid "_Duplicate" +msgstr "Копировать" + +msgid "_Delete..." +msgstr "Удалить..." + +msgid "View _Logs..." +msgstr "Просмотр изменений" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "Вложения..." + +msgid "Add an attachment to the record" +msgstr "Добавить вложение для этой записи" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "Действия" + +msgid "_Relate..." +msgstr "Связанные..." + +msgid "_Report..." +msgstr "Отчет..." + +msgid "_Print..." +msgstr "печать" + +msgid "_E-Mail..." +msgstr "Эл.почта..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "Добавить существующую запись" + +msgid "_Export Data..." +msgstr "Экспорт данных" + +msgid "_Import Data..." +msgstr "Импорт данных" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "Закрыть вкладку" + +msgid "All fields" +msgstr "Все поля" + +msgid "_Add" +msgstr "_Добавить" + +msgid "_Remove" +msgstr "_Удалить" + +#, fuzzy +msgid "_Clear" +msgstr "Очистить" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "Параметры CVS" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "Кодировка:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "Наименование поля" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +#, fuzzy +msgid "_Save Export" +msgstr "Сохранить настройку экспорта" + +#, fuzzy +msgid "_URL Export" +msgstr "Сохранить настройку экспорта" + +#, fuzzy +msgid "_Delete Export" +msgstr "Удалить настройку экспорта" + +msgid "Predefined exports" +msgstr "Предопределенные экспорты" + +msgid "Name" +msgstr "Имя" + +msgid "Open" +msgstr "Открыть" + +#, fuzzy +msgid "Listed Records" +msgstr "Изменить выбранную запись " + +#, fuzzy +msgid "Selected Records" +msgstr "Изменить выбранную запись " + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Добавить имя поля" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "Укажите имя для экспорта." + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, fuzzy, python-format +msgid "%d record saved." +msgstr "%d запись сохранена!" + +#, fuzzy, python-format +msgid "%d records saved." +msgstr "%d записей сохранено!" + +#, fuzzy, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Не удалось выполнить операцию!\n" +"Сообщение об ошибке:\n" +"%s" + +msgid "Error" +msgstr "Ошибка" + +msgid "Link" +msgstr "Ссылка" + +msgid "Delete" +msgstr "Удалить" + +msgid "New" +msgstr "Новый" + +msgid "Switch" +msgstr "Переключить" + +msgid "Add" +msgstr "Добавить" + +msgid "Remove " +msgstr "Удалить " + +msgid "Create a new record " +msgstr "Создать новую запись " + +msgid "Delete selected record " +msgstr "Удалить выбраную запись " + +msgid "Undelete selected record " +msgstr "Восстановить выбраную запись " + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +#, fuzzy +msgid "_Auto-Detect" +msgstr "Авто-обнаружение" + +msgid "File to Import:" +msgstr "Файл для импорта:" + +msgid "Open..." +msgstr "Открыть..." + +msgid "Lines to Skip:" +msgstr "Строки для пропуска:" + +#, fuzzy +msgid "You must select an import file first." +msgstr "Вы должны сначала выбрать файл для импорта!" + +msgid "Error opening CSV file" +msgstr "Ошибка открытия файла CSV" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Ошибка при обработке файла на поле %s." + +#, fuzzy, python-format +msgid "%d record imported." +msgstr "%d запись импортирована!" + +#, fuzzy, python-format +msgid "%d records imported." +msgstr "%d записей импортировано!" + +msgid "Search" +msgstr "Поиск" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "Мастер" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Дата создания" + +#, fuzzy +msgid "Edited by" +msgstr "Редактировать" + +#, fuzzy +msgid "Edited at" +msgstr "Редактировать" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "Не удается установить локализацию %s" + +#, fuzzy +msgid "Unable to set view tree state" +msgstr "Не удается установить локализацию %s" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Размер изображения" + +msgid "Width:" +msgstr "Ширина:" + +msgid "Height:" +msgstr "Ввысота:" + +msgid "PNG image (*.png)" +msgstr "PNG изображение (*.png)" + +msgid "Save As" +msgstr "Сохранить как" + +#, fuzzy +msgid "Image size too large." +msgstr "Размер изображения слишком большой!" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "Показать сохраненные фильтры" + +msgid "Remove this bookmark" +msgstr "Удалить эту закладку" + +msgid "Bookmark this filter" +msgstr "Запомнить этот фильтр" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "Имя закладки:" + +msgid "Find" +msgstr "Найти" + +#, fuzzy +msgid "Today" +msgstr "Сообщение:" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +#, fuzzy +msgid "Week View" +msgstr "Переключить вид" + +#, fuzzy +msgid "Month View" +msgstr "Переключить вид" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "Очистить" + +msgid "All files" +msgstr "Все фалы" + +msgid "Show plain text" +msgstr "Показать в виде текста" + +msgid "Add value" +msgstr "Добавить значение" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Удалить \"%s\"" + +msgid "Images" +msgstr "Изображения" + +msgid "Add existing record" +msgstr "Добавить существующую запись" + +msgid "Remove selected record" +msgstr "Удалить выбраную запись" + +#, fuzzy +msgid "Open the record " +msgstr "Открыть запись " + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "Поиск записи " + +#, fuzzy +msgid "Edit selected record" +msgstr "Изменить выбранную запись " + +#, fuzzy +msgid "Delete selected record" +msgstr "Удалить выбраную запись" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "Перевод" + +msgid "Edit" +msgstr "Редактировать" + +msgid "Fuzzy" +msgstr "Неточный" + +#, fuzzy +msgid "You need to save the record before adding translations." +msgstr "Необходимо сохранить запись перед добавлением перевода!" + +#, fuzzy +msgid "No other language available." +msgstr "Отсутствуют другие доступные языки!" + +msgid "Translate view" +msgstr "Перевод текущего окна" diff --git a/tryton/tryton/data/locale/sl/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/sl/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..0dcc93c89a --- /dev/null +++ b/tryton/tryton/data/locale/sl/LC_MESSAGES/tryton.po @@ -0,0 +1,1215 @@ +# Slovenian (Slovenia) translations for tryton. +# Copyright (C) 2010 B2CK +# This file is distributed under the same license as the tryton project. +# Venceslav Vezjak , 2010. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "določi alterntivno konfiguracijsko datoteko" + +msgid "development mode" +msgstr "Razvijalski način" + +msgid "logging everything at INFO level" +msgstr "beleženje vsega na INFO ravni" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "določi nivo beleženja: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "določi uporabniško ime" + +msgid "specify the server hostname:port" +msgstr "določi ime in vrata strežniškega gostitelja" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Ni možno zapisati konfiguracijske datoteke %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "Ni možno nastaviti krajevnih nastavitev %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +# ? +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "Izberi ukrep" + +msgid "No action defined." +msgstr "Nobenega ukrepa ni določenega." + +msgid "By: " +msgstr "Od: " + +msgid "Selection" +msgstr "Izbira" + +#, fuzzy +msgid "Cancel" +msgstr "_Prekliči" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "Vaša izbira:" + +msgid "Select" +msgstr "Izberi" + +msgid "Save" +msgstr "Shrani" + +msgid "Save As..." +msgstr "Shrani kot..." + +msgid "Do you want to proceed?" +msgstr "Ali želite nadaljevati?" + +msgid "Always ignore this warning." +msgstr "Vedno prezri to opozorilo." + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "Da" + +msgid "Concurrency Exception" +msgstr "Izjema sočasnega izvajanja" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "Primerjaj" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "Vseeno zapiši" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Primerjaj: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "Napaka programa." + +msgid "Report Bug" +msgstr "Prijava programske napake" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "Išči..." + +msgid "Create..." +msgstr "Ustvari..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "Vrednost" + +msgid "Displayed value" +msgstr "Prikazana vrednost" + +msgid "Format" +msgstr "Oblika" + +msgid "Display format" +msgstr "Oblika prikaza" + +msgid "Open the calendar" +msgstr "Odpri koledar" + +msgid "Date Format" +msgstr "Oblika datuma" + +msgid "Displayed date format" +msgstr "Prikazana oblika datuma" + +msgid "y" +msgstr "d" + +msgid "True" +msgstr "Da" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "Ne" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +#, fuzzy +msgid "Template" +msgstr "Veza" + +msgid "Edit..." +msgstr "Uredi ..." + +msgid "Attachments..." +msgstr "Priponke..." + +msgid "Notes..." +msgstr "Zabeležke ..." + +msgid "Actions..." +msgstr "_Ukrepi..." + +msgid "Relate..." +msgstr "_Veze..." + +msgid "Report..." +msgstr "Poročila..." + +msgid "Print..." +msgstr "_Izpis..." + +msgid "E-Mail..." +msgstr "_Elektronska pošta..." + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "Izberi barvo" + +msgid "Y" +msgstr "l" + +msgid "M" +msgstr "M" + +msgid "w" +msgstr "t" + +msgid "d" +msgstr "d" + +msgid "h" +msgstr "u" + +msgid "m" +msgstr "m" + +msgid "s" +msgstr "s" + +#, fuzzy +msgid "Preferences..." +msgstr "_Nastavitve..." + +#, fuzzy +msgid "Toolbar" +msgstr "_Orodna vrstica" + +#, fuzzy +msgid "Default" +msgstr "_Privzeto" + +#, fuzzy +msgid "Text and Icons" +msgstr "Bese_dilo in ikone" + +#, fuzzy +msgid "Text" +msgstr "_Besedilo" + +#, fuzzy +msgid "Icons" +msgstr "_Ikone" + +#, fuzzy +msgid "Form" +msgstr "O_brazec" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "Shrani stanje drevesa" + +msgid "Spell Checking" +msgstr "Črkovanje" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "Omejitev iskanja..." + +msgid "Check Version" +msgstr "" + +#, fuzzy +msgid "Options" +msgstr "_Možnosti" + +#, fuzzy +msgid "Keyboard Shortcuts..." +msgstr "_Bližnjice na tipkovnici..." + +#, fuzzy +msgid "About..." +msgstr "_Vizitka..." + +#, fuzzy +msgid "Help" +msgstr "_Pomoč" + +msgid "No result found." +msgstr "Brez rezultatov." + +#, fuzzy +msgid "Favorites" +msgstr "P_riljubljeno" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "Ukrep" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"Za naslednji ukrep morajo biti vsi zavihki zaprti.\n" +"Ali želite nadaljevati?" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "Nastavitve" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +#, fuzzy +msgid "Previous tab" +msgstr "P_rejšnji zavihek" + +#, fuzzy +msgid "Next tab" +msgstr "_Naslednji zavihek" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "Izreži vsebino" + +msgid "Copy selected text" +msgstr "Kopiraj vsebino" + +msgid "Paste copied text" +msgstr "Prilepi vsebino" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "Dodajanje nove veze" + +msgid "Open/Search relation" +msgstr "Urejanje/iskanje veze" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "Preklopi pogled" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "Urejenje postavke" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "Postavka neoznačena za brisanje" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "Premiki" + +#, fuzzy +msgid "Move right" +msgstr "Premik desno" + +#, fuzzy +msgid "Move left" +msgstr "Premik levo" + +msgid "Move up" +msgstr "Premik gor" + +msgid "Move down" +msgstr "Premik dol" + +msgid "Move up of one page" +msgstr "Premik za eno stran gor" + +msgid "Move down of one page" +msgstr "Premik za eno stran dol" + +msgid "Move to top" +msgstr "Premik na začetek" + +msgid "Move to bottom" +msgstr "Premik na konec" + +msgid "Move to parent" +msgstr "Premik na nadrejeno vejo" + +msgid "Select all" +msgstr "Izberi vse" + +msgid "Unselect all" +msgstr "Odznači vse" + +msgid "Select parent" +msgstr "Izberi nadrejeno vejo" + +msgid "Select/Activate current row" +msgstr "Izberi/Aktiviraj trenutno vrstico" + +msgid "Toggle selection" +msgstr "Vklopi/Izklopi izbor" + +msgid "Expand/Collapse" +msgstr "Razširi/Skrči" + +msgid "Expand row" +msgstr "Razširi vrstico" + +msgid "Collapse row" +msgstr "Skrči vrstico" + +msgid "Toggle row" +msgstr "Razširi/Skrči vrstico" + +msgid "Collapse all rows" +msgstr "Skrči vse vrstice" + +msgid "Expand all rows" +msgstr "Razširi vse vrstice" + +msgid "Close Tab" +msgstr "Zapri zavihek" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "Priponke (%s)" + +msgid "Profile Editor" +msgstr "Urejevalnik profilov" + +msgid "Profile" +msgstr "Profil" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "Strežnik:" + +msgid "Database:" +msgstr "Podatkovna baza:" + +msgid "Fetching databases list" +msgstr "Pridobivanje seznama podatkovnih baz" + +msgid "Username:" +msgstr "Uporabniško ime:" + +#, fuzzy +msgid "Incompatible version of the server." +msgstr "Nezdružljiva inačica strežnika" + +#, fuzzy +msgid "Could not connect to the server." +msgstr "Ni se možno povezati s strežnikom" + +msgid "Login" +msgstr "Prijava" + +msgid "_Cancel" +msgstr "_Prekliči" + +msgid "Cancel connection to the Tryton server" +msgstr "Prekliči povezavo do Tryton strežnika" + +msgid "C_onnect" +msgstr "P_oveži" + +msgid "Connect the Tryton server" +msgstr "Poveži se s Tryton strežnikom" + +msgid "Profile:" +msgstr "Profil:" + +msgid "Host / Database information" +msgstr "Strežnik in podatkovna baza" + +msgid "User name:" +msgstr "Uporabnik:" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Ni možno nastaviti krajevnih nastavitev %s" + +#, fuzzy, python-format +msgid "E-mail %s" +msgstr "Epošta %s" + +msgid "To:" +msgstr "Za:" + +#, fuzzy +msgid "Cc:" +msgstr "Kp:" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "Zadeva:" + +#, fuzzy +msgid "Body" +msgstr "Vsebina:" + +#, fuzzy +msgid "Reports" +msgstr "Poročila" + +#, fuzzy +msgid "Attachments" +msgstr "Priponka:" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Izberi vse" + +#, fuzzy +msgid "Remove File" +msgstr "Odstrani " + +msgid "Add..." +msgstr "" + +#, fuzzy +msgid "Preview" +msgstr "Prejšnji" + +msgid "Previous" +msgstr "Prejšnji" + +msgid "Next" +msgstr "Naslednji" + +#, fuzzy, python-format +msgid "Attachment (%s)" +msgstr "Priponke (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "En zapis mora biti izbran." + +msgid "ID:" +msgstr "ID:" + +#, fuzzy +msgid "Created by:" +msgstr "Ustvarjeno" + +#, fuzzy +msgid "Created at:" +msgstr "Ustvarjeno" + +#, fuzzy +msgid "Edited by:" +msgstr "Uredi" + +#, fuzzy +msgid "Edited at:" +msgstr "Uredi" + +msgid "Model:" +msgstr "Model:" + +msgid "Are you sure to remove this record?" +msgstr "Ali res želiš izbrisati ta zapis?" + +msgid "Are you sure to remove those records?" +msgstr "Ali res želiš izbrisati te zapise?" + +msgid "Records not removed." +msgstr "Zapisi niso izbrisani." + +msgid "Records removed." +msgstr "Zapisi izbrisani." + +msgid "Working now on the duplicated record(s)." +msgstr "Trenutno se dela na podvojenih zapisih." + +msgid "Record saved." +msgstr "Zapis shranjen." + +msgid " of " +msgstr " od " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"Ta zapis je bil spremenjen,\n" +"ga želiš shraniti?" + +msgid "Launch action" +msgstr "Zaženi ukrep" + +msgid "Relate" +msgstr "Veza" + +msgid "Open related records" +msgstr "Odpri vezo" + +msgid "Report" +msgstr "Poročila" + +msgid "Open report" +msgstr "Odpri poročilo" + +msgid "Print" +msgstr "Tisk" + +msgid "Print report" +msgstr "Izpis poročila" + +msgid "_Copy URL" +msgstr "_Kopiraj URL" + +msgid "Copy URL into clipboard" +msgstr "Kopiraj URL na odložišče" + +msgid "Unknown" +msgstr "Neznano" + +msgid "Limit" +msgstr "Omejitev" + +msgid "Search Limit Settings" +msgstr "Nastavitev omejitev iskanja" + +msgid "Limit:" +msgstr "Omejitev:" + +#, python-format +msgid "Notes (%s)" +msgstr "Zabeležke (%s)" + +msgid "Edit User Preferences" +msgstr "Uredi uporabniške nastavitve" + +msgid "Preference" +msgstr "Nastavitev" + +msgid "Revision" +msgstr "Različica" + +msgid "Select a revision" +msgstr "Izberi različico" + +msgid "Revision:" +msgstr "Različica:" + +msgid "_Switch View" +msgstr "Preklopi po_gled" + +msgid "Switch View" +msgstr "Preklopi pogled" + +msgid "_Previous" +msgstr "P_rejšnji" + +msgid "Previous Record" +msgstr "Prejšnji zapis" + +msgid "_Next" +msgstr "Nas_lednji" + +msgid "Next Record" +msgstr "Naslednji zapis" + +msgid "_Search" +msgstr "_Išči" + +msgid "_New" +msgstr "_Nov" + +msgid "Create a new record" +msgstr "Ustvari nov zapis" + +msgid "_Save" +msgstr "_Shrani" + +msgid "Save this record" +msgstr "Shrani ta zapis" + +msgid "_Reload/Undo" +msgstr "Ponovno naloži/Razvel_javi" + +msgid "Reload/Undo" +msgstr "Ponovno naloži/Razveljavi" + +msgid "_Duplicate" +msgstr "_Podvoji" + +msgid "_Delete..." +msgstr "Z_briši..." + +msgid "View _Logs..." +msgstr "Poglej dnevni_k..." + +msgid "Show revisions..." +msgstr "Prikaži različice ..." + +msgid "A_ttachments..." +msgstr "P_riponke..." + +msgid "Add an attachment to the record" +msgstr "Dodaj priponko zapisu" + +msgid "_Notes..." +msgstr "_Zabeležke ..." + +msgid "Add a note to the record" +msgstr "K zapisu dodaj zabeležko" + +msgid "_Actions..." +msgstr "_Ukrepi..." + +msgid "_Relate..." +msgstr "_Veze..." + +msgid "_Report..." +msgstr "P_oročila..." + +msgid "_Print..." +msgstr "_Tiskaj..." + +msgid "_E-Mail..." +msgstr "_Elektronska pošta..." + +#, fuzzy +msgid "Send an e-mail using the record" +msgstr "K zapisu dodaj zabeležko" + +msgid "_Export Data..." +msgstr "I_zvozi podatke..." + +msgid "_Import Data..." +msgstr "Uv_ozi podatke..." + +msgid "Copy _URL..." +msgstr "_Kopiraj URL ..." + +msgid "_Close Tab" +msgstr "Zapri zavi_hek" + +msgid "All fields" +msgstr "Vsa polja" + +msgid "_Add" +msgstr "_Dodaj" + +msgid "_Remove" +msgstr "_Odstrani" + +msgid "_Clear" +msgstr "_Počisti" + +msgid "Fields selected" +msgstr "Izbrana polja" + +msgid "CSV Parameters" +msgstr "CSV parametri" + +msgid "Delimiter:" +msgstr "Ločilo:" + +msgid "Quote char:" +msgstr "Narekovaj:" + +msgid "Encoding:" +msgstr "Kodiranje:" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "Naziv polja" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "_Shrani izvoz" + +#, fuzzy +msgid "_URL Export" +msgstr "_Shrani izvoz" + +msgid "_Delete Export" +msgstr "_Izbriši izvoz" + +msgid "Predefined exports" +msgstr "Predefinirani izvozi" + +msgid "Name" +msgstr "Naziv" + +msgid "Open" +msgstr "Odpri" + +#, fuzzy +msgid "Listed Records" +msgstr "Uredi izbran zapis " + +#, fuzzy +msgid "Selected Records" +msgstr "Uredi izbran zapis " + +msgid "Ignore search limit" +msgstr "" + +#, fuzzy +msgid "Add field names" +msgstr "Dodaj imena _polj" + +#, python-format +msgid "%s (string)" +msgstr "%s (niz)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (ime modela)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (ime zapisa)" + +msgid "What is the name of this export?" +msgstr "Kako se imenuje ta izvoz?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "Prepišem definicijo izvoza '%s'?" + +#, python-format +msgid "%d record saved." +msgstr "%d zapis shranjen." + +#, python-format +msgid "%d records saved." +msgstr "%d zapisov shranjenih." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"Operacija odpovedala.\n" +"Sporočilo napake:\n" +"%s" + +msgid "Error" +msgstr "Napaka" + +msgid "Link" +msgstr "Povezava" + +msgid "Delete" +msgstr "Zbriši" + +msgid "New" +msgstr "Novo" + +msgid "Switch" +msgstr "Preklopi" + +msgid "Add" +msgstr "Dodaj" + +msgid "Remove " +msgstr "Odstrani " + +msgid "Create a new record " +msgstr "Ustvari nov zapis " + +msgid "Delete selected record " +msgstr "Zbriši izbran zapis " + +msgid "Undelete selected record " +msgstr "Povrni izbran zapis " + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "_Samozaznava" + +msgid "File to Import:" +msgstr "Datoteka za uvoz:" + +msgid "Open..." +msgstr "Odpri..." + +msgid "Lines to Skip:" +msgstr "Izpuščene vrstice:" + +msgid "You must select an import file first." +msgstr "Najprej morate izbrati datoteke za uvažanje." + +msgid "Error opening CSV file" +msgstr "Napaka pri odpiranju CSV datoteke" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "Napaka obdelave datoteke pri polju %s." + +#, python-format +msgid "%d record imported." +msgstr "%d zapis uvožen." + +#, python-format +msgid "%d records imported." +msgstr "%d zapisov uvoženih." + +msgid "Search" +msgstr "Išči" + +#, python-format +msgid "Search %s" +msgstr "Išči %s" + +msgid "Wizard" +msgstr "Čarovnik" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "ID" + +msgid "Create by" +msgstr "" + +#, fuzzy +msgid "Created at" +msgstr "Ustvarjeno" + +#, fuzzy +msgid "Edited by" +msgstr "Uredi" + +#, fuzzy +msgid "Edited at" +msgstr "Uredi" + +#, fuzzy, python-format +msgid "Unable to get view tree state for %s" +msgstr "Ni možno nastaviti stanja obrazca %s" + +msgid "Unable to set view tree state" +msgstr "Ni možno nastaviti stanja obrazca" + +#, fuzzy, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "Polje \"%s\" ni veljavno glede na svojo domeno" + +#, fuzzy, python-format +msgid "\"%s\" is required." +msgstr "Polje \"%s\" je obvezno" + +#, fuzzy, python-format +msgid "The values of \"%s\" are not valid." +msgstr "Vrednosti v polju \"%s\" niso veljavne" + +msgid "Pre-validation" +msgstr "Predpreverjanje" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "Velikost slike" + +msgid "Width:" +msgstr "Širina:" + +msgid "Height:" +msgstr "Velikost:" + +msgid "PNG image (*.png)" +msgstr "PNG slika (*.png)" + +msgid "Save As" +msgstr "Shrani kot" + +msgid "Image size too large." +msgstr "Slika je prevelika." + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "Odpri filtre" + +msgid "Show bookmarks of filters" +msgstr "Prikaži zaznamke filtrov" + +msgid "Remove this bookmark" +msgstr "Odstrani ta zaznamek" + +msgid "Bookmark this filter" +msgstr "Shrani ta filter" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "Ime zaznamka:" + +msgid "Find" +msgstr "Najdi" + +msgid "Today" +msgstr "Danes" + +msgid "go back" +msgstr "pojdi nazaj" + +msgid "go forward" +msgstr "pojdi naprej" + +msgid "previous year" +msgstr "Prejšnje leto" + +msgid "next year" +msgstr "naslednje leto" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "Tedensko" + +msgid "Month View" +msgstr "Mesečno" + +msgid "Week" +msgstr "Teden" + +msgid "Select..." +msgstr "Izberi ..." + +msgid "Clear" +msgstr "Počisti" + +msgid "All files" +msgstr "Vse datoteke" + +msgid "Show plain text" +msgstr "Prikaži navadno besedilo" + +msgid "Add value" +msgstr "Dodaj vrednost" + +#, python-format +msgid "Remove \"%s\"" +msgstr "Odstrani \"%s\"" + +msgid "Images" +msgstr "Slike" + +msgid "Add existing record" +msgstr "Dodaj obstoječi zapis" + +msgid "Remove selected record" +msgstr "Odstrani izbran zapis" + +msgid "Open the record " +msgstr "Odpri zapis " + +#, fuzzy +msgid "Clear the field " +msgstr "Počisti zapis " + +msgid "Search a record " +msgstr "Poišči zapis " + +#, fuzzy +msgid "Edit selected record" +msgstr "Uredi izbran zapis " + +#, fuzzy +msgid "Delete selected record" +msgstr "Odstrani izbran zapis" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "Prevod" + +msgid "Edit" +msgstr "Uredi" + +msgid "Fuzzy" +msgstr "Nejasno" + +msgid "You need to save the record before adding translations." +msgstr "Pred dodajanjem prevodov morate zapis shraniti." + +msgid "No other language available." +msgstr "Drugih jezikov ni na voljo." + +msgid "Translate view" +msgstr "Prevedi pogled" diff --git a/tryton/tryton/data/locale/tr/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/tr/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..a1a4e1e3fc --- /dev/null +++ b/tryton/tryton/data/locale/tr/LC_MESSAGES/tryton.po @@ -0,0 +1,1168 @@ +# Translations template for tryton. +# Copyright (C) 2017 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2017. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "Alternative config dosyasını belirle" + +msgid "development mode" +msgstr "Geliştirme Modu" + +msgid "logging everything at INFO level" +msgstr "Tamamına INFO seviyesinde giriş" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "Log seviyesini belirle: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "Giriş kullanıcısını belirle" + +msgid "specify the server hostname:port" +msgstr "Server hostname belirle: port" + +#, python-format +msgid "Unable to write config file %s." +msgstr "Config dosyasına %s yazılamadı" + +#, python-format +msgid "Unable to set locale %s" +msgstr "Lokal %s ayarlanamadı" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "İşleminizi seçin" + +msgid "No action defined." +msgstr "İşlem belirtilmedi." + +msgid "By: " +msgstr "Tarafından: " + +msgid "Selection" +msgstr "Seçim" + +msgid "Cancel" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "Seçiminiz:" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "" + +#, fuzzy +msgid "Save As..." +msgstr "Save As..." + +msgid "Do you want to proceed?" +msgstr "İlerlemek istiyor musunuz?" + +msgid "Always ignore this warning." +msgstr "Bu uyarıyı daima ihmal et." + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "Karşılaştır" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "Tüm Durumlarda Yaz" + +msgid "Save your current version" +msgstr "" + +#, fuzzy, python-format +msgid "Compare: %s" +msgstr "Karşılaştır: %s" + +msgid "Close" +msgstr "" + +#, fuzzy +msgid "Application Error" +msgstr "Uygulama Hatası." + +msgid "Report Bug" +msgstr "Hatayı Raporla" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Displayed value" +msgstr "" + +msgid "Format" +msgstr "" + +msgid "Display format" +msgstr "" + +msgid "Open the calendar" +msgstr "" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "" + +msgid "Edit..." +msgstr "" + +msgid "Attachments..." +msgstr "" + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "" + +msgid "Relate..." +msgstr "" + +msgid "Report..." +msgstr "" + +msgid "Print..." +msgstr "" + +msgid "E-Mail..." +msgstr "" + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "M" +msgstr "" + +msgid "w" +msgstr "" + +msgid "d" +msgstr "" + +msgid "h" +msgstr "" + +msgid "m" +msgstr "" + +msgid "s" +msgstr "" + +msgid "Preferences..." +msgstr "" + +msgid "Toolbar" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Text and Icons" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Icons" +msgstr "" + +msgid "Form" +msgstr "" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "" + +msgid "Options" +msgstr "" + +msgid "Keyboard Shortcuts..." +msgstr "" + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "" + +msgid "Copy selected text" +msgstr "" + +msgid "Paste copied text" +msgstr "" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "" + +msgid "Database:" +msgstr "" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "" + +msgid "Incompatible version of the server." +msgstr "" + +msgid "Could not connect to the server." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "_Cancel" +msgstr "" + +msgid "Cancel connection to the Tryton server" +msgstr "" + +msgid "C_onnect" +msgstr "" + +msgid "Connect the Tryton server" +msgstr "" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "" + +#, fuzzy +msgid "Unable to complete email entry" +msgstr "Lokal %s ayarlanamadı" + +#, python-format +msgid "E-mail %s" +msgstr "" + +msgid "To:" +msgstr "" + +msgid "Cc:" +msgstr "" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "" + +msgid "Body" +msgstr "" + +#, fuzzy +msgid "Reports" +msgstr "Hatayı Raporla" + +msgid "Attachments" +msgstr "" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +#, fuzzy +msgid "Select File" +msgstr "Seçim" + +msgid "Remove File" +msgstr "" + +msgid "Add..." +msgstr "" + +msgid "Preview" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" + +#, python-format +msgid "Attachment (%s)" +msgstr "" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "" + +msgid "ID:" +msgstr "" + +msgid "Created by:" +msgstr "" + +msgid "Created at:" +msgstr "" + +msgid "Edited by:" +msgstr "" + +msgid "Edited at:" +msgstr "" + +msgid "Model:" +msgstr "" + +msgid "Are you sure to remove this record?" +msgstr "" + +msgid "Are you sure to remove those records?" +msgstr "" + +msgid "Records not removed." +msgstr "" + +msgid "Records removed." +msgstr "" + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "" + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" + +msgid "Launch action" +msgstr "" + +msgid "Relate" +msgstr "" + +msgid "Open related records" +msgstr "" + +msgid "Report" +msgstr "" + +msgid "Open report" +msgstr "" + +msgid "Print" +msgstr "" + +msgid "Print report" +msgstr "" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Search Limit Settings" +msgstr "" + +msgid "Limit:" +msgstr "" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "" + +msgid "Preference" +msgstr "" + +msgid "Revision" +msgstr "" + +msgid "Select a revision" +msgstr "" + +msgid "Revision:" +msgstr "" + +msgid "_Switch View" +msgstr "" + +msgid "Switch View" +msgstr "" + +msgid "_Previous" +msgstr "" + +msgid "Previous Record" +msgstr "" + +msgid "_Next" +msgstr "" + +msgid "Next Record" +msgstr "" + +msgid "_Search" +msgstr "" + +msgid "_New" +msgstr "" + +msgid "Create a new record" +msgstr "" + +msgid "_Save" +msgstr "" + +msgid "Save this record" +msgstr "" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "" + +msgid "_Delete..." +msgstr "" + +msgid "View _Logs..." +msgstr "" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "" + +msgid "Add an attachment to the record" +msgstr "" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "" + +msgid "_Relate..." +msgstr "" + +msgid "_Report..." +msgstr "" + +msgid "_Print..." +msgstr "" + +msgid "_E-Mail..." +msgstr "" + +msgid "Send an e-mail using the record" +msgstr "" + +msgid "_Export Data..." +msgstr "" + +msgid "_Import Data..." +msgstr "" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "" + +msgid "All fields" +msgstr "" + +msgid "_Add" +msgstr "" + +msgid "_Remove" +msgstr "" + +msgid "_Clear" +msgstr "" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "" + +msgid "_URL Export" +msgstr "" + +msgid "_Delete Export" +msgstr "" + +msgid "Predefined exports" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Listed Records" +msgstr "" + +msgid "Selected Records" +msgstr "" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "" + +#, python-format +msgid "%d records saved." +msgstr "" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "Hata" + +msgid "Link" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "New" +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Remove " +msgstr "" + +msgid "Create a new record " +msgstr "" + +msgid "Delete selected record " +msgstr "" + +msgid "Undelete selected record " +msgstr "" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "" + +msgid "File to Import:" +msgstr "" + +msgid "Open..." +msgstr "" + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "" + +msgid "Error opening CSV file" +msgstr "" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "" + +#, python-format +msgid "%d record imported." +msgstr "" + +#, python-format +msgid "%d records imported." +msgstr "" + +msgid "Search" +msgstr "" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Create by" +msgstr "" + +msgid "Created at" +msgstr "" + +msgid "Edited by" +msgstr "" + +msgid "Edited at" +msgstr "" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "" + +msgid "Image size too large." +msgstr "" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "" + +msgid "Month View" +msgstr "" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "" + +msgid "All files" +msgstr "" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Add existing record" +msgstr "" + +msgid "Remove selected record" +msgstr "" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +msgid "Edit selected record" +msgstr "" + +msgid "Delete selected record" +msgstr "" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Fuzzy" +msgstr "" + +msgid "You need to save the record before adding translations." +msgstr "" + +msgid "No other language available." +msgstr "" + +msgid "Translate view" +msgstr "" diff --git a/tryton/tryton/data/locale/tryton.pot b/tryton/tryton/data/locale/tryton.pot new file mode 100644 index 0000000000..926d91cba4 --- /dev/null +++ b/tryton/tryton/data/locale/tryton.pot @@ -0,0 +1,1157 @@ +msgid "specify alternate config file" +msgstr "" + +msgid "development mode" +msgstr "" + +msgid "logging everything at INFO level" +msgstr "" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "" + +msgid "specify the login user" +msgstr "" + +msgid "specify the server hostname:port" +msgstr "" + +#, python-format +msgid "Unable to write config file %s." +msgstr "" + +#, python-format +msgid "Unable to set locale %s" +msgstr "" + +msgid ", " +msgstr "" + +msgid ",..." +msgstr "" + +#, python-format +msgid "%s (%s)" +msgstr "" + +msgid "Select your action" +msgstr "" + +msgid "No action defined." +msgstr "" + +msgid "By: " +msgstr "" + +msgid "Selection" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Your selection:" +msgstr "" + +msgid "Select" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save As..." +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + +msgid "Always ignore this warning." +msgstr "" + +msgid "No" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "Concurrency Exception" +msgstr "" + +msgid "This record has been modified while you were editing it." +msgstr "" + +msgid "Cancel saving" +msgstr "" + +msgid "Compare" +msgstr "" + +msgid "See the modified version" +msgstr "" + +msgid "Write Anyway" +msgstr "" + +msgid "Save your current version" +msgstr "" + +#, python-format +msgid "Compare: %s" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Application Error" +msgstr "" + +msgid "Report Bug" +msgstr "" + +#, python-format +msgid "Check URL: %s" +msgstr "" + +msgid "Unable to check for new version." +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Too many requests. Try again later." +msgstr "" + +msgid "..." +msgstr "" + +msgid "Search..." +msgstr "" + +msgid "Create..." +msgstr "" + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Displayed value" +msgstr "" + +msgid "Format" +msgstr "" + +msgid "Display format" +msgstr "" + +msgid "Open the calendar" +msgstr "" + +msgid "Date Format" +msgstr "" + +msgid "Displayed date format" +msgstr "" + +msgid "y" +msgstr "" + +msgid "True" +msgstr "" + +msgid "t" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Digits" +msgstr "" + +msgid "The number of decimal" +msgstr "" + +msgid "Template" +msgstr "" + +msgid "Edit..." +msgstr "" + +msgid "Attachments..." +msgstr "" + +msgid "Notes..." +msgstr "" + +msgid "Actions..." +msgstr "" + +msgid "Relate..." +msgstr "" + +msgid "Report..." +msgstr "" + +msgid "Print..." +msgstr "" + +msgid "E-Mail..." +msgstr "" + +msgid "Bold" +msgstr "" + +msgid "Italic" +msgstr "" + +msgid "Underline" +msgstr "" + +msgid "Align Left" +msgstr "" + +msgid "Align Center" +msgstr "" + +msgid "Align Right" +msgstr "" + +msgid "Justify" +msgstr "" + +msgid "Foreground Color" +msgstr "" + +msgid "Select a color" +msgstr "" + +msgid "Y" +msgstr "" + +msgid "M" +msgstr "" + +msgid "w" +msgstr "" + +msgid "d" +msgstr "" + +msgid "h" +msgstr "" + +msgid "m" +msgstr "" + +msgid "s" +msgstr "" + +msgid "Preferences..." +msgstr "" + +msgid "Toolbar" +msgstr "" + +msgid "Default" +msgstr "" + +msgid "Text and Icons" +msgstr "" + +msgid "Text" +msgstr "" + +msgid "Icons" +msgstr "" + +msgid "Form" +msgstr "" + +msgid "Save Column Width" +msgstr "" + +msgid "Save Tree State" +msgstr "" + +msgid "Spell Checking" +msgstr "" + +msgid "PDA Mode" +msgstr "" + +msgid "Search Limit..." +msgstr "" + +msgid "Check Version" +msgstr "" + +msgid "Options" +msgstr "" + +msgid "Keyboard Shortcuts..." +msgstr "" + +msgid "About..." +msgstr "" + +msgid "Help" +msgstr "" + +msgid "No result found." +msgstr "" + +msgid "Favorites" +msgstr "" + +msgid "Manage..." +msgstr "" + +msgid "Action" +msgstr "" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" + +msgid "Application Shortcuts" +msgstr "" + +msgid "Global" +msgstr "" + +msgid "Preferences" +msgstr "" + +msgid "Search menu" +msgstr "" + +msgid "Toggle menu" +msgstr "" + +msgid "Previous tab" +msgstr "" + +msgid "Next tab" +msgstr "" + +msgid "Shortcuts" +msgstr "" + +msgid "Quit" +msgstr "" + +msgid "Edition Shortcuts" +msgstr "" + +msgid "Text Entries" +msgstr "" + +msgid "Cut selected text" +msgstr "" + +msgid "Copy selected text" +msgstr "" + +msgid "Paste copied text" +msgstr "" + +msgid "Next entry" +msgstr "" + +msgid "Previous entry" +msgstr "" + +msgid "Relation Entries" +msgstr "" + +msgid "Create new relation" +msgstr "" + +msgid "Open/Search relation" +msgstr "" + +msgid "List Entries" +msgstr "" + +msgid "Switch view" +msgstr "" + +msgid "Create/Select new line" +msgstr "" + +msgid "Open relation" +msgstr "" + +msgid "Mark line for deletion/removal" +msgstr "" + +msgid "Mark line for removal" +msgstr "" + +msgid "Unmark line for deletion" +msgstr "" + +msgid "List/Tree Shortcuts" +msgstr "" + +msgid "Move Cursor" +msgstr "" + +msgid "Move right" +msgstr "" + +msgid "Move left" +msgstr "" + +msgid "Move up" +msgstr "" + +msgid "Move down" +msgstr "" + +msgid "Move up of one page" +msgstr "" + +msgid "Move down of one page" +msgstr "" + +msgid "Move to top" +msgstr "" + +msgid "Move to bottom" +msgstr "" + +msgid "Move to parent" +msgstr "" + +msgid "Select all" +msgstr "" + +msgid "Unselect all" +msgstr "" + +msgid "Select parent" +msgstr "" + +msgid "Select/Activate current row" +msgstr "" + +msgid "Toggle selection" +msgstr "" + +msgid "Expand/Collapse" +msgstr "" + +msgid "Expand row" +msgstr "" + +msgid "Collapse row" +msgstr "" + +msgid "Toggle row" +msgstr "" + +msgid "Collapse all rows" +msgstr "" + +msgid "Expand all rows" +msgstr "" + +msgid "Close Tab" +msgstr "" + +msgid "modularity, scalability and security" +msgstr "" + +msgid "translator-credits" +msgstr "" + +#, python-format +msgid "Attachments (%s)" +msgstr "" + +msgid "Profile Editor" +msgstr "" + +msgid "Profile" +msgstr "" + +msgid "Add new profile" +msgstr "" + +msgid "Remove selected profile" +msgstr "" + +msgid "Host:" +msgstr "" + +msgid "Database:" +msgstr "" + +msgid "Fetching databases list" +msgstr "" + +msgid "Username:" +msgstr "" + +msgid "Incompatible version of the server." +msgstr "" + +msgid "Could not connect to the server." +msgstr "" + +msgid "Login" +msgstr "" + +msgid "_Cancel" +msgstr "" + +msgid "Cancel connection to the Tryton server" +msgstr "" + +msgid "C_onnect" +msgstr "" + +msgid "Connect the Tryton server" +msgstr "" + +msgid "Profile:" +msgstr "" + +msgid "Host / Database information" +msgstr "" + +msgid "User name:" +msgstr "" + +msgid "Unable to complete email entry" +msgstr "" + +#, python-format +msgid "E-mail %s" +msgstr "" + +msgid "To:" +msgstr "" + +msgid "Cc:" +msgstr "" + +msgid "Bcc:" +msgstr "" + +msgid "Subject:" +msgstr "" + +msgid "Body" +msgstr "" + +msgid "Reports" +msgstr "" + +msgid "Attachments" +msgstr "" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "" + +msgid "Select File" +msgstr "" + +msgid "Remove File" +msgstr "" + +msgid "Add..." +msgstr "" + +msgid "Preview" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" + +#, python-format +msgid "Attachment (%s)" +msgstr "" + +#, python-format +msgid "Note (%d/%d)" +msgstr "" + +msgid "You have to select one record." +msgstr "" + +msgid "ID:" +msgstr "" + +msgid "Created by:" +msgstr "" + +msgid "Created at:" +msgstr "" + +msgid "Edited by:" +msgstr "" + +msgid "Edited at:" +msgstr "" + +msgid "Model:" +msgstr "" + +msgid "Are you sure to remove this record?" +msgstr "" + +msgid "Are you sure to remove those records?" +msgstr "" + +msgid "Records not removed." +msgstr "" + +msgid "Records removed." +msgstr "" + +msgid "Working now on the duplicated record(s)." +msgstr "" + +msgid "Record saved." +msgstr "" + +msgid " of " +msgstr "" + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" + +msgid "Launch action" +msgstr "" + +msgid "Relate" +msgstr "" + +msgid "Open related records" +msgstr "" + +msgid "Report" +msgstr "" + +msgid "Open report" +msgstr "" + +msgid "Print" +msgstr "" + +msgid "Print report" +msgstr "" + +msgid "_Copy URL" +msgstr "" + +msgid "Copy URL into clipboard" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Limit" +msgstr "" + +msgid "Search Limit Settings" +msgstr "" + +msgid "Limit:" +msgstr "" + +#, python-format +msgid "Notes (%s)" +msgstr "" + +msgid "Edit User Preferences" +msgstr "" + +msgid "Preference" +msgstr "" + +msgid "Revision" +msgstr "" + +msgid "Select a revision" +msgstr "" + +msgid "Revision:" +msgstr "" + +msgid "_Switch View" +msgstr "" + +msgid "Switch View" +msgstr "" + +msgid "_Previous" +msgstr "" + +msgid "Previous Record" +msgstr "" + +msgid "_Next" +msgstr "" + +msgid "Next Record" +msgstr "" + +msgid "_Search" +msgstr "" + +msgid "_New" +msgstr "" + +msgid "Create a new record" +msgstr "" + +msgid "_Save" +msgstr "" + +msgid "Save this record" +msgstr "" + +msgid "_Reload/Undo" +msgstr "" + +msgid "Reload/Undo" +msgstr "" + +msgid "_Duplicate" +msgstr "" + +msgid "_Delete..." +msgstr "" + +msgid "View _Logs..." +msgstr "" + +msgid "Show revisions..." +msgstr "" + +msgid "A_ttachments..." +msgstr "" + +msgid "Add an attachment to the record" +msgstr "" + +msgid "_Notes..." +msgstr "" + +msgid "Add a note to the record" +msgstr "" + +msgid "_Actions..." +msgstr "" + +msgid "_Relate..." +msgstr "" + +msgid "_Report..." +msgstr "" + +msgid "_Print..." +msgstr "" + +msgid "_E-Mail..." +msgstr "" + +msgid "Send an e-mail using the record" +msgstr "" + +msgid "_Export Data..." +msgstr "" + +msgid "_Import Data..." +msgstr "" + +msgid "Copy _URL..." +msgstr "" + +msgid "_Close Tab" +msgstr "" + +msgid "All fields" +msgstr "" + +msgid "_Add" +msgstr "" + +msgid "_Remove" +msgstr "" + +msgid "_Clear" +msgstr "" + +msgid "Fields selected" +msgstr "" + +msgid "CSV Parameters" +msgstr "" + +msgid "Delimiter:" +msgstr "" + +msgid "Quote char:" +msgstr "" + +msgid "Encoding:" +msgstr "" + +msgid "Use locale format" +msgstr "" + +msgid "Field name" +msgstr "" + +#, python-format +msgid "CSV Export: %s" +msgstr "" + +msgid "_Save Export" +msgstr "" + +msgid "_URL Export" +msgstr "" + +msgid "_Delete Export" +msgstr "" + +msgid "Predefined exports" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Open" +msgstr "" + +msgid "Listed Records" +msgstr "" + +msgid "Selected Records" +msgstr "" + +msgid "Ignore search limit" +msgstr "" + +msgid "Add field names" +msgstr "" + +#, python-format +msgid "%s (string)" +msgstr "" + +#, python-format +msgid "%s (model name)" +msgstr "" + +#, python-format +msgid "%s (record name)" +msgstr "" + +msgid "What is the name of this export?" +msgstr "" + +#, python-format +msgid "Override '%s' definition?" +msgstr "" + +#, python-format +msgid "%d record saved." +msgstr "" + +#, python-format +msgid "%d records saved." +msgstr "" + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Link" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "New" +msgstr "" + +msgid "Switch" +msgstr "" + +msgid "Add" +msgstr "" + +msgid "Remove " +msgstr "" + +msgid "Create a new record " +msgstr "" + +msgid "Delete selected record " +msgstr "" + +msgid "Undelete selected record " +msgstr "" + +#, python-format +msgid "CSV Import: %s" +msgstr "" + +msgid "_Auto-Detect" +msgstr "" + +msgid "File to Import:" +msgstr "" + +msgid "Open..." +msgstr "" + +msgid "Lines to Skip:" +msgstr "" + +msgid "You must select an import file first." +msgstr "" + +msgid "Error opening CSV file" +msgstr "" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "" + +#, python-format +msgid "%d record imported." +msgstr "" + +#, python-format +msgid "%d records imported." +msgstr "" + +msgid "Search" +msgstr "" + +#, python-format +msgid "Search %s" +msgstr "" + +msgid "Wizard" +msgstr "" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "Create by" +msgstr "" + +msgid "Created at" +msgstr "" + +msgid "Edited by" +msgstr "" + +msgid "Edited at" +msgstr "" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "" + +msgid "Unable to set view tree state" +msgstr "" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "" + +#, python-format +msgid "\"%s\" is required." +msgstr "" + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "" + +msgid "Pre-validation" +msgstr "" + +msgid ":" +msgstr "" + +msgid "Image Size" +msgstr "" + +msgid "Width:" +msgstr "" + +msgid "Height:" +msgstr "" + +msgid "PNG image (*.png)" +msgstr "" + +msgid "Save As" +msgstr "" + +msgid "Image size too large." +msgstr "" + +msgid "Copy" +msgstr "" + +msgid "Paste" +msgstr "" + +msgid ".." +msgstr "" + +msgid "Open filters" +msgstr "" + +msgid "Show bookmarks of filters" +msgstr "" + +msgid "Remove this bookmark" +msgstr "" + +msgid "Bookmark this filter" +msgstr "" + +msgid "Show active records" +msgstr "" + +msgid "Show inactive records" +msgstr "" + +msgid "Bookmark Name:" +msgstr "" + +msgid "Find" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "go back" +msgstr "" + +msgid "go forward" +msgstr "" + +msgid "previous year" +msgstr "" + +msgid "next year" +msgstr "" + +msgid "Day View" +msgstr "" + +msgid "Week View" +msgstr "" + +msgid "Month View" +msgstr "" + +msgid "Week" +msgstr "" + +msgid "Select..." +msgstr "" + +msgid "Clear" +msgstr "" + +msgid "All files" +msgstr "" + +msgid "Show plain text" +msgstr "" + +msgid "Add value" +msgstr "" + +#, python-format +msgid "Remove \"%s\"" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Add existing record" +msgstr "" + +msgid "Remove selected record" +msgstr "" + +msgid "Open the record " +msgstr "" + +msgid "Clear the field " +msgstr "" + +msgid "Search a record " +msgstr "" + +msgid "Edit selected record" +msgstr "" + +msgid "Delete selected record" +msgstr "" + +#, python-format +msgid "%s%%" +msgstr "" + +msgid "Choose a language" +msgstr "" + +msgid "Translation" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Fuzzy" +msgstr "" + +msgid "You need to save the record before adding translations." +msgstr "" + +msgid "No other language available." +msgstr "" + +msgid "Translate view" +msgstr "" + diff --git a/tryton/tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po b/tryton/tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po new file mode 100644 index 0000000000..e7982f5088 --- /dev/null +++ b/tryton/tryton/data/locale/zh_CN/LC_MESSAGES/tryton.po @@ -0,0 +1,1173 @@ +# Translations template for tryton. +# Copyright (C) 2016 Tryton +# This file is distributed under the same license as the tryton project. +# FIRST AUTHOR , 2016. +msgid "" +msgstr "Content-Type: text/plain; charset=utf-8\n" + +msgid "specify alternate config file" +msgstr "指定备选配置文件" + +msgid "development mode" +msgstr "开发模式" + +msgid "logging everything at INFO level" +msgstr "INFO级别记录日志" + +msgid "specify the log level: DEBUG, INFO, WARNING, ERROR, CRITICAL" +msgstr "设置日志级别: DEBUG, INFO, WARNING, ERROR, CRITICAL" + +msgid "specify the login user" +msgstr "设置登录用户" + +msgid "specify the server hostname:port" +msgstr "设置服务器主机名:端口号" + +#, python-format +msgid "Unable to write config file %s." +msgstr "无法写入配置文件 %s." + +#, python-format +msgid "Unable to set locale %s" +msgstr "无法设置 locale %s" + +msgid ", " +msgstr ", " + +msgid ",..." +msgstr ",..." + +#, python-format +msgid "%s (%s)" +msgstr "%s (%s)" + +msgid "Select your action" +msgstr "选择一项操作" + +msgid "No action defined." +msgstr "没有定义任何操作 ." + +msgid "By: " +msgstr "By: " + +msgid "Selection" +msgstr "选择" + +msgid "Cancel" +msgstr "取消" + +msgid "OK" +msgstr "确定" + +msgid "Your selection:" +msgstr "已选择:" + +msgid "Select" +msgstr "选择" + +msgid "Save" +msgstr "保存" + +msgid "Save As..." +msgstr "另存为..." + +msgid "Do you want to proceed?" +msgstr "继续此项操作 ?" + +msgid "Always ignore this warning." +msgstr "下次不再提示." + +msgid "No" +msgstr "否" + +msgid "Yes" +msgstr "是" + +msgid "Concurrency Exception" +msgstr "并行操作异常" + +msgid "This record has been modified while you were editing it." +msgstr "在你编辑的时候,这条记录已经被更改." + +msgid "Cancel saving" +msgstr "取消保存" + +msgid "Compare" +msgstr "比较" + +msgid "See the modified version" +msgstr "查看修改后的版本" + +msgid "Write Anyway" +msgstr "覆写" + +msgid "Save your current version" +msgstr "保存当前版本" + +#, python-format +msgid "Compare: %s" +msgstr "比较: %s" + +msgid "Close" +msgstr "关闭" + +msgid "Application Error" +msgstr "程序错误" + +msgid "Report Bug" +msgstr "报告Bug" + +#, python-format +msgid "Check URL: %s" +msgstr "检查URL: %s" + +msgid "Unable to check for new version." +msgstr "无法检查新版本." + +msgid "A new version is available!" +msgstr "有新版本可用!" + +msgid "Download" +msgstr "下载" + +msgid "Too many requests. Try again later." +msgstr "请求太多,请稍后重试." + +msgid "..." +msgstr "..." + +msgid "Search..." +msgstr "查找..." + +msgid "Create..." +msgstr "创建..." + +#, python-format +msgid "Unable to search for completion of %s" +msgstr "无法找到 \" %s \" 的匹配项" + +msgid "Value" +msgstr "值" + +msgid "Displayed value" +msgstr "显示值" + +msgid "Format" +msgstr "格式" + +msgid "Display format" +msgstr "显示格式" + +msgid "Open the calendar" +msgstr "打开日历" + +msgid "Date Format" +msgstr "日期格式" + +msgid "Displayed date format" +msgstr "日期显示格式" + +msgid "y" +msgstr "y" + +msgid "True" +msgstr "True" + +msgid "t" +msgstr "t" + +msgid "False" +msgstr "False" + +msgid "Digits" +msgstr "有效数字位数" + +msgid "The number of decimal" +msgstr "小数位数" + +msgid "Template" +msgstr "模板" + +msgid "Edit..." +msgstr "编辑..." + +msgid "Attachments..." +msgstr "附件..." + +msgid "Notes..." +msgstr "注释..." + +msgid "Actions..." +msgstr "操作..." + +msgid "Relate..." +msgstr "关联..." + +msgid "Report..." +msgstr "报告..." + +msgid "Print..." +msgstr "打印..." + +msgid "E-Mail..." +msgstr "用电子邮件发送..." + +msgid "Bold" +msgstr "加粗" + +msgid "Italic" +msgstr "斜体" + +msgid "Underline" +msgstr "下划线" + +msgid "Align Left" +msgstr "左对齐" + +msgid "Align Center" +msgstr "居中" + +msgid "Align Right" +msgstr "右对齐" + +msgid "Justify" +msgstr "两头对齐" + +msgid "Foreground Color" +msgstr "字体颜色" + +msgid "Select a color" +msgstr "选择颜色" + +msgid "Y" +msgstr "年" + +msgid "M" +msgstr "月" + +msgid "w" +msgstr "星期" + +msgid "d" +msgstr "日" + +msgid "h" +msgstr "时" + +msgid "m" +msgstr "分" + +msgid "s" +msgstr "秒" + +msgid "Preferences..." +msgstr "首选项..." + +msgid "Toolbar" +msgstr "工具栏" + +msgid "Default" +msgstr "默认设置" + +msgid "Text and Icons" +msgstr "文字和图标" + +msgid "Text" +msgstr "显示文字(_T)" + +msgid "Icons" +msgstr "显示图标(_I)" + +msgid "Form" +msgstr "表单视图(_F)" + +msgid "Save Column Width" +msgstr "保存列宽度" + +msgid "Save Tree State" +msgstr "保存导航状态" + +msgid "Spell Checking" +msgstr "开启拼写检查" + +msgid "PDA Mode" +msgstr "PDA 模式" + +msgid "Search Limit..." +msgstr "设置查询限制 ..." + +msgid "Check Version" +msgstr "检查版本" + +msgid "Options" +msgstr "选项(_O)" + +msgid "Keyboard Shortcuts..." +msgstr "快捷键(K)..." + +msgid "About..." +msgstr "关于(_A)..." + +msgid "Help" +msgstr "帮助(_H)" + +msgid "No result found." +msgstr "无符合条件的数据." + +msgid "Favorites" +msgstr "收藏(_V)" + +msgid "Manage..." +msgstr "管理..." + +msgid "Action" +msgstr "操作" + +msgid "" +"The following action requires to close all tabs.\n" +"Do you want to continue?" +msgstr "" +"该操作需要关闭所有标签.\n" +"是否继续?" + +msgid "Application Shortcuts" +msgstr "应用程序快捷方式" + +msgid "Global" +msgstr "全局" + +msgid "Preferences" +msgstr "偏好" + +msgid "Search menu" +msgstr "搜索菜单" + +msgid "Toggle menu" +msgstr "切换菜单" + +msgid "Previous tab" +msgstr "上一标签(_P)" + +msgid "Next tab" +msgstr "下一标签(_N)" + +msgid "Shortcuts" +msgstr "快捷方式" + +msgid "Quit" +msgstr "退出" + +msgid "Edition Shortcuts" +msgstr "编辑快捷方式" + +msgid "Text Entries" +msgstr "文字输入" + +msgid "Cut selected text" +msgstr "剪切" + +msgid "Copy selected text" +msgstr "复制" + +msgid "Paste copied text" +msgstr "粘贴" + +msgid "Next entry" +msgstr "下一个条目" + +msgid "Previous entry" +msgstr "上一个条目" + +msgid "Relation Entries" +msgstr "关系条目" + +msgid "Create new relation" +msgstr "新建关联" + +msgid "Open/Search relation" +msgstr "打开或查找关系" + +msgid "List Entries" +msgstr "列表条目" + +msgid "Switch view" +msgstr "切换视图" + +msgid "Create/Select new line" +msgstr "创建/选择新行" + +msgid "Open relation" +msgstr "打开关联项" + +msgid "Mark line for deletion/removal" +msgstr "标记要删除/删除的行" + +msgid "Mark line for removal" +msgstr "标记要删除的行" + +msgid "Unmark line for deletion" +msgstr "取消删除标记" + +msgid "List/Tree Shortcuts" +msgstr "列表/树快捷方式" + +msgid "Move Cursor" +msgstr "光标移动" + +msgid "Move right" +msgstr "右移" + +msgid "Move left" +msgstr "左移" + +msgid "Move up" +msgstr "上移" + +msgid "Move down" +msgstr "下移" + +msgid "Move up of one page" +msgstr "上移一页" + +msgid "Move down of one page" +msgstr "下移一页" + +msgid "Move to top" +msgstr "移到顶端" + +msgid "Move to bottom" +msgstr "移到底部" + +msgid "Move to parent" +msgstr "上一级" + +msgid "Select all" +msgstr "全选" + +msgid "Unselect all" +msgstr "取消全选" + +msgid "Select parent" +msgstr "选择上一级" + +msgid "Select/Activate current row" +msgstr "选择/激活当前行" + +msgid "Toggle selection" +msgstr "切换选择状态" + +msgid "Expand/Collapse" +msgstr "展开/收起" + +msgid "Expand row" +msgstr "展开行" + +msgid "Collapse row" +msgstr "收起行" + +msgid "Toggle row" +msgstr "切换行" + +msgid "Collapse all rows" +msgstr "收起所有" + +msgid "Expand all rows" +msgstr "展开所有" + +msgid "Close Tab" +msgstr "关闭标签" + +msgid "modularity, scalability and security" +msgstr "模块化,可扩展性和安全性" + +msgid "translator-credits" +msgstr "翻译者 - 贡献" + +#, python-format +msgid "Attachments (%s)" +msgstr "附件 (%s)" + +msgid "Profile Editor" +msgstr "配置编辑器" + +msgid "Profile" +msgstr "配置" + +msgid "Add new profile" +msgstr "添加新配置" + +msgid "Remove selected profile" +msgstr "删除选择的配置" + +msgid "Host:" +msgstr "主机:" + +msgid "Database:" +msgstr "数据库:" + +msgid "Fetching databases list" +msgstr "正在获取数据库列表" + +msgid "Username:" +msgstr "用户名:" + +msgid "Incompatible version of the server." +msgstr "服务器版本与客户端不兼容." + +msgid "Could not connect to the server." +msgstr "无法连接服务器." + +msgid "Login" +msgstr "登录" + +msgid "_Cancel" +msgstr "取消(_C)" + +msgid "Cancel connection to the Tryton server" +msgstr "取消与 Tryton 服务器的连接" + +msgid "C_onnect" +msgstr "连机(_O)" + +msgid "Connect the Tryton server" +msgstr "连接 Tryton 服务器" + +msgid "Profile:" +msgstr "配置:" + +msgid "Host / Database information" +msgstr "主机/数据库信息" + +msgid "User name:" +msgstr "用户名:" + +msgid "Unable to complete email entry" +msgstr "无法完成电子邮件输入" + +#, python-format +msgid "E-mail %s" +msgstr "电子邮件 %s" + +msgid "To:" +msgstr "收件人:" + +msgid "Cc:" +msgstr "抄送:" + +msgid "Bcc:" +msgstr "私下抄送:" + +msgid "Subject:" +msgstr "主题:" + +msgid "Body" +msgstr "正文" + +msgid "Reports" +msgstr "报告(_R)" + +msgid "Attachments" +msgstr "附件" + +msgid "Files" +msgstr "" + +msgid "Send" +msgstr "发送" + +#, fuzzy +msgid "Select File" +msgstr "全选" + +#, fuzzy +msgid "Remove File" +msgstr "删除 " + +msgid "Add..." +msgstr "添加..." + +#, fuzzy +msgid "Preview" +msgstr "向前" + +msgid "Previous" +msgstr "向前" + +msgid "Next" +msgstr "向后" + +#, python-format +msgid "Attachment (%s)" +msgstr "附件 (%s)" + +#, python-format +msgid "Note (%d/%d)" +msgstr "批注 (%d/%d)" + +msgid "You have to select one record." +msgstr "需要选择一条记录." + +msgid "ID:" +msgstr "标识:" + +msgid "Created by:" +msgstr "创建者:" + +msgid "Created at:" +msgstr "创建日期:" + +msgid "Edited by:" +msgstr "编辑者:" + +msgid "Edited at:" +msgstr "编辑于:" + +msgid "Model:" +msgstr "模型:" + +msgid "Are you sure to remove this record?" +msgstr "确定要删除这条记录吗?" + +msgid "Are you sure to remove those records?" +msgstr "确定要删除这些记录?" + +msgid "Records not removed." +msgstr "所选记录未删除成功." + +msgid "Records removed." +msgstr "记录已成功删除." + +msgid "Working now on the duplicated record(s)." +msgstr "正在处理重复记录." + +msgid "Record saved." +msgstr "记录保存成功." + +msgid " of " +msgstr " of " + +msgid "" +"This record has been modified\n" +"do you want to save it?" +msgstr "" +"记录有改动\n" +"是否保存?" + +msgid "Launch action" +msgstr "执行操作" + +msgid "Relate" +msgstr "关联" + +msgid "Open related records" +msgstr "打开关联记录" + +msgid "Report" +msgstr "报告(_R)" + +msgid "Open report" +msgstr "打开报告" + +msgid "Print" +msgstr "打印" + +msgid "Print report" +msgstr "打印报告" + +msgid "_Copy URL" +msgstr "复制(_C)URL" + +msgid "Copy URL into clipboard" +msgstr "将URL复制到粘贴板" + +msgid "Unknown" +msgstr "未知" + +msgid "Limit" +msgstr "限制" + +msgid "Search Limit Settings" +msgstr "设置查询限制" + +msgid "Limit:" +msgstr "限制:" + +#, python-format +msgid "Notes (%s)" +msgstr "注释(%s)" + +msgid "Edit User Preferences" +msgstr "编辑用户偏好" + +msgid "Preference" +msgstr "偏好" + +msgid "Revision" +msgstr "修订版本" + +msgid "Select a revision" +msgstr "选择修订版本" + +msgid "Revision:" +msgstr "修订版本:" + +msgid "_Switch View" +msgstr "切换视图(_S)" + +msgid "Switch View" +msgstr "切换视图" + +msgid "_Previous" +msgstr "向前(_P)" + +msgid "Previous Record" +msgstr "前一条记录" + +msgid "_Next" +msgstr "向后(_N)" + +msgid "Next Record" +msgstr "后一条记录" + +msgid "_Search" +msgstr "查找(_S)" + +msgid "_New" +msgstr "新建(_N)" + +msgid "Create a new record" +msgstr "新建记录" + +msgid "_Save" +msgstr "保存(_S)" + +msgid "Save this record" +msgstr "保存当前记录" + +msgid "_Reload/Undo" +msgstr "刷新/撤销(_R)" + +msgid "Reload/Undo" +msgstr "刷新/撤销" + +msgid "_Duplicate" +msgstr "复制(_D)" + +msgid "_Delete..." +msgstr "删除(_D)..." + +msgid "View _Logs..." +msgstr "日志(_V)..." + +msgid "Show revisions..." +msgstr "显示修订版本..." + +msgid "A_ttachments..." +msgstr "附件(_T)..." + +msgid "Add an attachment to the record" +msgstr "为当前记录添加附件" + +msgid "_Notes..." +msgstr "注释(_N)..." + +msgid "Add a note to the record" +msgstr "为当前记录添加注释" + +msgid "_Actions..." +msgstr "操作(_A)..." + +msgid "_Relate..." +msgstr "关联(_L)..." + +msgid "_Report..." +msgstr "报告(_R)..." + +msgid "_Print..." +msgstr "打印(_P)..." + +msgid "_E-Mail..." +msgstr "电子邮件发送(_E)..." + +msgid "Send an e-mail using the record" +msgstr "使用当前记录发送电子邮件" + +msgid "_Export Data..." +msgstr "导出数据(_X)..." + +msgid "_Import Data..." +msgstr "导入数据(_I)..." + +msgid "Copy _URL..." +msgstr "复制URL(_U)..." + +msgid "_Close Tab" +msgstr "关闭标签页(_C)" + +msgid "All fields" +msgstr "所有字段" + +msgid "_Add" +msgstr "添加(_A)" + +msgid "_Remove" +msgstr "删除(_R)" + +msgid "_Clear" +msgstr "清除(_C)" + +msgid "Fields selected" +msgstr "选中字段" + +msgid "CSV Parameters" +msgstr "CSV 参数" + +msgid "Delimiter:" +msgstr "分隔符:" + +msgid "Quote char:" +msgstr "引用符:" + +msgid "Encoding:" +msgstr "编码:" + +msgid "Use locale format" +msgstr "使用 locale 格式" + +msgid "Field name" +msgstr "字段名" + +#, python-format +msgid "CSV Export: %s" +msgstr "CSV导出:%s" + +msgid "_Save Export" +msgstr "保存导出设置(_S)" + +msgid "_URL Export" +msgstr "URL 导出(_S)" + +msgid "_Delete Export" +msgstr "删除导出设置(_D)" + +msgid "Predefined exports" +msgstr "预定义导出设置" + +msgid "Name" +msgstr "名称" + +msgid "Open" +msgstr "打开" + +msgid "Listed Records" +msgstr "已列出记录" + +msgid "Selected Records" +msgstr "已选记录" + +msgid "Ignore search limit" +msgstr "忽略搜索限制" + +msgid "Add field names" +msgstr "添加字段名称(_F)" + +#, python-format +msgid "%s (string)" +msgstr "%s (字符串)" + +#, python-format +msgid "%s (model name)" +msgstr "%s (模型名称)" + +#, python-format +msgid "%s (record name)" +msgstr "%s (记录名称)" + +msgid "What is the name of this export?" +msgstr "导出设置的名称是?" + +#, python-format +msgid "Override '%s' definition?" +msgstr "替换设置 '%s' ?" + +#, python-format +msgid "%d record saved." +msgstr "%d 记录保存成功." + +#, python-format +msgid "%d records saved." +msgstr "%d 记录保存成功." + +#, python-format +msgid "" +"Operation failed.\n" +"Error message:\n" +"%s" +msgstr "" +"操作失败!\n" +"相关错误信息:\n" +"%s" + +msgid "Error" +msgstr "错误" + +msgid "Link" +msgstr "链接" + +msgid "Delete" +msgstr "删除" + +msgid "New" +msgstr "新建" + +msgid "Switch" +msgstr "切换" + +msgid "Add" +msgstr "添加" + +msgid "Remove " +msgstr "删除 " + +msgid "Create a new record " +msgstr "新建记录 " + +msgid "Delete selected record " +msgstr "删除已选记录 " + +msgid "Undelete selected record " +msgstr "已选记录取消删除 " + +#, python-format +msgid "CSV Import: %s" +msgstr "CSV导入: %s" + +msgid "_Auto-Detect" +msgstr "自动设置(_A)" + +msgid "File to Import:" +msgstr "导入文件:" + +msgid "Open..." +msgstr "打开..." + +msgid "Lines to Skip:" +msgstr "跳过行:" + +msgid "You must select an import file first." +msgstr "必须先选择要导入的文件." + +msgid "Error opening CSV file" +msgstr "打开CSV文件出错" + +#, python-format +msgid "Error processing the file at field %s." +msgstr "处理字段 %s 时出错." + +#, python-format +msgid "%d record imported." +msgstr "%d 条记录导入成功." + +#, python-format +msgid "%d records imported." +msgstr "%d 条记录导入成功." + +msgid "Search" +msgstr "查找" + +#, python-format +msgid "Search %s" +msgstr "查找 %s" + +msgid "Wizard" +msgstr "向导" + +#, python-format +msgid "Unable to delete wizard %s" +msgstr "无法删除向导 %s" + +msgid "ID" +msgstr "标识" + +msgid "Create by" +msgstr "创建者" + +msgid "Created at" +msgstr "创建日期" + +msgid "Edited by" +msgstr "编辑人" + +msgid "Edited at" +msgstr "编辑时间" + +#, python-format +msgid "Unable to get view tree state for %s" +msgstr "无法为 %s 获取视图树状态" + +msgid "Unable to set view tree state" +msgstr "无法设置视图树状态" + +#, python-format +msgid "\"%s\" is not valid according to its domain." +msgstr "根据域设置,\"%s\"不可用." + +#, python-format +msgid "\"%s\" is required." +msgstr "\"%s\" 是必填项." + +#, python-format +msgid "The values of \"%s\" are not valid." +msgstr "\"%s\" 的取值无效." + +msgid "Pre-validation" +msgstr "预校验" + +msgid ":" +msgstr ":" + +msgid "Image Size" +msgstr "图片尺寸" + +msgid "Width:" +msgstr "宽度:" + +msgid "Height:" +msgstr "高度:" + +msgid "PNG image (*.png)" +msgstr "PNG 图片格式 (*.png)" + +msgid "Save As" +msgstr "另存为" + +msgid "Image size too large." +msgstr "图片尺寸过大." + +msgid "Copy" +msgstr "复制" + +msgid "Paste" +msgstr "粘贴" + +msgid ".." +msgstr ".." + +msgid "Open filters" +msgstr "打开筛选器" + +msgid "Show bookmarks of filters" +msgstr "显示筛选器书签" + +msgid "Remove this bookmark" +msgstr "移除书签" + +msgid "Bookmark this filter" +msgstr "收藏当前筛选器" + +msgid "Show active records" +msgstr "显示已启用记录" + +msgid "Show inactive records" +msgstr "显示未启用记录" + +msgid "Bookmark Name:" +msgstr "书签名称:" + +msgid "Find" +msgstr "查找" + +msgid "Today" +msgstr "今天" + +msgid "go back" +msgstr "回退" + +msgid "go forward" +msgstr "前进" + +msgid "previous year" +msgstr "上一年度" + +msgid "next year" +msgstr "下一年度" + +msgid "Day View" +msgstr "日视图" + +msgid "Week View" +msgstr "周视图" + +msgid "Month View" +msgstr "月视图" + +msgid "Week" +msgstr "周" + +msgid "Select..." +msgstr "选择..." + +msgid "Clear" +msgstr "清除" + +msgid "All files" +msgstr "所有文件" + +msgid "Show plain text" +msgstr "显示纯文本" + +msgid "Add value" +msgstr "添加值" + +#, python-format +msgid "Remove \"%s\"" +msgstr "删除\"%s\"" + +msgid "Images" +msgstr "图片" + +msgid "Add existing record" +msgstr "添加已存在记录" + +msgid "Remove selected record" +msgstr "删除所选记录" + +msgid "Open the record " +msgstr "打开记录 " + +msgid "Clear the field " +msgstr "清除字段 " + +msgid "Search a record " +msgstr "查找记录 " + +msgid "Edit selected record" +msgstr "编辑所选记录" + +msgid "Delete selected record" +msgstr "删除所选记录" + +#, python-format +msgid "%s%%" +msgstr "%s%%" + +msgid "Choose a language" +msgstr "选择一种语言" + +msgid "Translation" +msgstr "翻译" + +msgid "Edit" +msgstr "编辑" + +msgid "Fuzzy" +msgstr "模糊" + +msgid "You need to save the record before adding translations." +msgstr "请先保存已翻译条目,然后再增加新翻译 ." + +msgid "No other language available." +msgstr "没有其他可翻译语言." + +msgid "Translate view" +msgstr "翻译视图" diff --git a/tryton/tryton/data/pixmaps/tryton/LICENSE b/tryton/tryton/data/pixmaps/tryton/LICENSE new file mode 100644 index 0000000000..7a4a3ea242 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-add.svg b/tryton/tryton/data/pixmaps/tryton/tryton-add.svg new file mode 100644 index 0000000000..08cc2308c8 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-archive.svg b/tryton/tryton/data/pixmaps/tryton/tryton-archive.svg new file mode 100644 index 0000000000..a5d45991b7 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-archive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-arrow-down.svg b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-down.svg new file mode 100644 index 0000000000..c1204da1cd --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-down.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-arrow-left.svg b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-left.svg new file mode 100644 index 0000000000..e084870266 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-left.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-arrow-right.svg b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-right.svg new file mode 100644 index 0000000000..0f56374381 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-right.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-arrow-up.svg b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-up.svg new file mode 100644 index 0000000000..b3d21d3434 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-arrow-up.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-attach.svg b/tryton/tryton/data/pixmaps/tryton/tryton-attach.svg new file mode 100644 index 0000000000..23979032c2 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-attach.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-back.svg b/tryton/tryton/data/pixmaps/tryton/tryton-back.svg new file mode 100644 index 0000000000..7ff9f9c11a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-back.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-bookmark-border.svg b/tryton/tryton/data/pixmaps/tryton/tryton-bookmark-border.svg new file mode 100644 index 0000000000..abcd01628a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-bookmark-border.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-bookmark.svg b/tryton/tryton/data/pixmaps/tryton/tryton-bookmark.svg new file mode 100644 index 0000000000..8b5a0a5539 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-bookmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-bookmarks.svg b/tryton/tryton/data/pixmaps/tryton/tryton-bookmarks.svg new file mode 100644 index 0000000000..79839be669 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-bookmarks.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-cancel.svg b/tryton/tryton/data/pixmaps/tryton/tryton-cancel.svg new file mode 100644 index 0000000000..b2dac49d45 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-cancel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-clear.svg b/tryton/tryton/data/pixmaps/tryton/tryton-clear.svg new file mode 100644 index 0000000000..289d406e75 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-clear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-close.svg b/tryton/tryton/data/pixmaps/tryton/tryton-close.svg new file mode 100644 index 0000000000..289d406e75 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-copy.svg b/tryton/tryton/data/pixmaps/tryton/tryton-copy.svg new file mode 100644 index 0000000000..dbd8b0cfc2 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-create.svg b/tryton/tryton/data/pixmaps/tryton/tryton-create.svg new file mode 100644 index 0000000000..f1b885f1c2 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-create.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-date.svg b/tryton/tryton/data/pixmaps/tryton/tryton-date.svg new file mode 100644 index 0000000000..1aab65c4e6 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-date.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-delete.svg b/tryton/tryton/data/pixmaps/tryton/tryton-delete.svg new file mode 100644 index 0000000000..9308f4df56 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-drag.svg b/tryton/tryton/data/pixmaps/tryton/tryton-drag.svg new file mode 100644 index 0000000000..b8d36b16f0 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-drag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-email.svg b/tryton/tryton/data/pixmaps/tryton/tryton-email.svg new file mode 100644 index 0000000000..e1560c6374 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-email.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-error.svg b/tryton/tryton/data/pixmaps/tryton/tryton-error.svg new file mode 100644 index 0000000000..19c6c83163 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-exit.svg b/tryton/tryton/data/pixmaps/tryton/tryton-exit.svg new file mode 100644 index 0000000000..dbd95bfd9c --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-exit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-export.svg b/tryton/tryton/data/pixmaps/tryton/tryton-export.svg new file mode 100644 index 0000000000..41628798b2 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-export.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-filter.svg b/tryton/tryton/data/pixmaps/tryton/tryton-filter.svg new file mode 100644 index 0000000000..f0a2e4f31f --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-filter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-align-center.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-center.svg new file mode 100644 index 0000000000..75873e9185 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-center.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-align-justify.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-justify.svg new file mode 100644 index 0000000000..288727edfc --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-justify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-align-left.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-left.svg new file mode 100644 index 0000000000..97645a3295 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-align-right.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-right.svg new file mode 100644 index 0000000000..c073397046 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-align-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-bold.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-bold.svg new file mode 100644 index 0000000000..ca6b4b63ce --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-color-text.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-color-text.svg new file mode 100644 index 0000000000..4bda2cb107 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-color-text.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-italic.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-italic.svg new file mode 100644 index 0000000000..fa65b97d56 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-italic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-format-underline.svg b/tryton/tryton/data/pixmaps/tryton/tryton-format-underline.svg new file mode 100644 index 0000000000..8615187f44 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-format-underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-forward.svg b/tryton/tryton/data/pixmaps/tryton/tryton-forward.svg new file mode 100644 index 0000000000..2ca445703a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-history.svg b/tryton/tryton/data/pixmaps/tryton/tryton-history.svg new file mode 100644 index 0000000000..05f201a445 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-history.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-icon.png b/tryton/tryton/data/pixmaps/tryton/tryton-icon.png new file mode 100644 index 0000000000..f26821a927 Binary files /dev/null and b/tryton/tryton/data/pixmaps/tryton/tryton-icon.png differ diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-import.svg b/tryton/tryton/data/pixmaps/tryton/tryton-import.svg new file mode 100644 index 0000000000..f093479f99 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-import.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-info.svg b/tryton/tryton/data/pixmaps/tryton/tryton-info.svg new file mode 100644 index 0000000000..5b31235b88 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-launch.svg b/tryton/tryton/data/pixmaps/tryton/tryton-launch.svg new file mode 100644 index 0000000000..7479e92628 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-launch.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-link.svg b/tryton/tryton/data/pixmaps/tryton/tryton-link.svg new file mode 100644 index 0000000000..08d13d80da --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-log.svg b/tryton/tryton/data/pixmaps/tryton/tryton-log.svg new file mode 100644 index 0000000000..3bcd81e84c --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-log.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-menu.svg b/tryton/tryton/data/pixmaps/tryton/tryton-menu.svg new file mode 100644 index 0000000000..37e9cfb552 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-menu.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-note.svg b/tryton/tryton/data/pixmaps/tryton/tryton-note.svg new file mode 100644 index 0000000000..a10719ede4 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-note.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-ok.svg b/tryton/tryton/data/pixmaps/tryton/tryton-ok.svg new file mode 100644 index 0000000000..535ff0390a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-ok.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-open.svg b/tryton/tryton/data/pixmaps/tryton/tryton-open.svg new file mode 100644 index 0000000000..f33176ea28 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-print.svg b/tryton/tryton/data/pixmaps/tryton/tryton-print.svg new file mode 100644 index 0000000000..7ffc5851fd --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-print.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-public.svg b/tryton/tryton/data/pixmaps/tryton/tryton-public.svg new file mode 100644 index 0000000000..24d6e503ed --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-public.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-question.svg b/tryton/tryton/data/pixmaps/tryton/tryton-question.svg new file mode 100644 index 0000000000..ac7a46002d --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-question.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-refresh.svg b/tryton/tryton/data/pixmaps/tryton/tryton-refresh.svg new file mode 100644 index 0000000000..e9618ab4f9 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-remove.svg b/tryton/tryton/data/pixmaps/tryton/tryton-remove.svg new file mode 100644 index 0000000000..ab7f7ba90e --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-save.svg b/tryton/tryton/data/pixmaps/tryton/tryton-save.svg new file mode 100644 index 0000000000..3754a0cf0d --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-save.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-search.svg b/tryton/tryton/data/pixmaps/tryton/tryton-search.svg new file mode 100644 index 0000000000..6859898f79 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-send.svg b/tryton/tryton/data/pixmaps/tryton/tryton-send.svg new file mode 100644 index 0000000000..033ab7a11a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-star-border.svg b/tryton/tryton/data/pixmaps/tryton/tryton-star-border.svg new file mode 100644 index 0000000000..21862f2cd7 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-star-border.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-star.svg b/tryton/tryton/data/pixmaps/tryton/tryton-star.svg new file mode 100644 index 0000000000..2b5e942ef8 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-switch.svg b/tryton/tryton/data/pixmaps/tryton/tryton-switch.svg new file mode 100644 index 0000000000..cc607585b9 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-switch.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-translate.svg b/tryton/tryton/data/pixmaps/tryton/tryton-translate.svg new file mode 100644 index 0000000000..e0ed1220a3 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-translate.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-unarchive.svg b/tryton/tryton/data/pixmaps/tryton/tryton-unarchive.svg new file mode 100644 index 0000000000..765b534c0a --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-unarchive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-undo.svg b/tryton/tryton/data/pixmaps/tryton/tryton-undo.svg new file mode 100644 index 0000000000..6763ef40d0 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton-warning.svg b/tryton/tryton/data/pixmaps/tryton/tryton-warning.svg new file mode 100644 index 0000000000..4a0fa05996 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton-warning.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/data/pixmaps/tryton/tryton.icns b/tryton/tryton/data/pixmaps/tryton/tryton.icns new file mode 100644 index 0000000000..b5016663ed Binary files /dev/null and b/tryton/tryton/data/pixmaps/tryton/tryton.icns differ diff --git a/tryton/tryton/data/pixmaps/tryton/tryton.ico b/tryton/tryton/data/pixmaps/tryton/tryton.ico new file mode 100644 index 0000000000..96e06f85dc Binary files /dev/null and b/tryton/tryton/data/pixmaps/tryton/tryton.ico differ diff --git a/tryton/tryton/data/pixmaps/tryton/tryton.svg b/tryton/tryton/data/pixmaps/tryton/tryton.svg new file mode 100644 index 0000000000..bcc0a828b4 --- /dev/null +++ b/tryton/tryton/data/pixmaps/tryton/tryton.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tryton/tryton/device_cookie.py b/tryton/tryton/device_cookie.py new file mode 100644 index 0000000000..b57b572f4f --- /dev/null +++ b/tryton/tryton/device_cookie.py @@ -0,0 +1,76 @@ +# 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 os +import json +import logging +from threading import Lock + +from tryton.config import get_config_dir, CONFIG + +logger = logging.getLogger(__name__) +COOKIES_PATH = os.path.join(get_config_dir(), 'device_cookies') + + +_lock = Lock() + + +def renew(): + from tryton.common import common + + def set_cookie(new_cookie): + try: + new_cookie = new_cookie() + except Exception: + logger.error("Cannot renew device cookie", exc_info=True) + else: + _set(new_cookie) + + current_cookie = get() + common.RPCExecute( + 'model', 'res.user.device', 'renew', current_cookie, + process_exception=False, callback=set_cookie) + + +def get(): + cookies = _load() + return cookies.get(_key()) + + +def _key(): + from tryton import common + + host = CONFIG['login.host'] + hostname = common.get_hostname(host) + port = common.get_port(host) + database = CONFIG['login.db'] + username = CONFIG['login.login'] + + return '%(username)s@%(hostname)s:%(port)s/%(database)s' % { + 'username': username, + 'hostname': hostname, + 'port': port, + 'database': database, + } + + +def _set(cookie): + cookies = _load() + cookies[_key()] = cookie + try: + with _lock: + with open(COOKIES_PATH, 'w') as cookies_file: + json.dump(cookies, cookies_file) + except Exception: + logger.error('Unable to save cookies file') + + +def _load(): + if not os.path.isfile(COOKIES_PATH): + return {} + try: + with open(COOKIES_PATH) as cookies: + cookies = json.load(cookies) + except Exception: + logger.error("Unable to load device cookies file", exc_info=True) + cookies = {} + return cookies diff --git a/tryton/tryton/exceptions.py b/tryton/tryton/exceptions.py new file mode 100644 index 0000000000..3adcce7f1b --- /dev/null +++ b/tryton/tryton/exceptions.py @@ -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 .jsonrpc import Fault + +TrytonServerError = Fault + + +class TrytonServerUnavailable(Exception): + pass + + +class TrytonError(Exception): + + def __init__(self, faultCode): + self.faultCode = faultCode diff --git a/tryton/tryton/fingerprints.py b/tryton/tryton/fingerprints.py new file mode 100644 index 0000000000..3abc142258 --- /dev/null +++ b/tryton/tryton/fingerprints.py @@ -0,0 +1,54 @@ +# 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 os +from threading import Lock + +from tryton.config import get_config_dir + +KNOWN_HOSTS_PATH = os.path.join(get_config_dir(), 'known_hosts') + +_lock = Lock() + + +def _load(): + if not os.path.isfile(KNOWN_HOSTS_PATH): + return {} + fingerprints = {} + with open(KNOWN_HOSTS_PATH) as known_hosts: + for line in known_hosts: + line = line.strip() + try: + host, sha1 = line.split(' ') + except ValueError: + host, sha1 = line, '' + fingerprints[host] = sha1 + return fingerprints + + +def exists(host): + return host in _load() + + +def get(host): + return _load().get(host) + + +def set(host, fingerprint): + fingerprints = _load() + assert isinstance(host, str) + if fingerprint: + assert len(fingerprint) == 59 # len of formated sha1 + else: + fingerprint = '' + changed = fingerprint != fingerprints.get(host) + fingerprints[host] = fingerprint + if changed: + _save(fingerprints) + + +def _save(fingerprints): + with _lock: + with open(KNOWN_HOSTS_PATH, 'w') as known_hosts: + known_hosts.writelines( + '%s %s' % (host, sha1) + os.linesep + for host, sha1 in fingerprints.items()) diff --git a/tryton/tryton/gui/__init__.py b/tryton/tryton/gui/__init__.py new file mode 100644 index 0000000000..27d571f1e9 --- /dev/null +++ b/tryton/tryton/gui/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from .main import Main + +__all__ = [Main] diff --git a/tryton/tryton/gui/main.py b/tryton/tryton/gui/main.py new file mode 100644 index 0000000000..a7914584f6 --- /dev/null +++ b/tryton/tryton/gui/main.py @@ -0,0 +1,1076 @@ +# 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 gettext +import json +import logging +import os +import sys +import threading +import traceback +import webbrowser +from urllib.parse import urlparse, parse_qsl, unquote + +from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk + +import tryton.common as common +import tryton.plugins +import tryton.rpc as rpc +import tryton.translate as translate +from tryton.action import Action +from tryton.common import RPCExecute, RPCException, RPCContextReload +from tryton.common.cellrendererclickablepixbuf import \ + CellRendererClickablePixbuf +from tryton.config import CONFIG, TRYTON_ICON, get_config_dir +from tryton.exceptions import TrytonError, TrytonServerUnavailable +from tryton.gui.window import Window +from tryton.jsonrpc import object_hook +from tryton.pyson import PYSONDecoder + +_ = gettext.gettext +logger = logging.getLogger(__name__) +_PRIORITIES = [getattr(Gio.NotificationPriority, p) + for p in ('LOW', 'NORMAL', 'HIGH', 'URGENT')] + + +class Main(Gtk.Application): + window = None + _instance = None + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super().__new__(cls, *args, **kwargs) + return cls._instance + + def do_startup(self): + Gtk.Application.do_startup(self) + + action = Gio.SimpleAction.new('preferences', None) + action.connect('activate', lambda *a: self.preferences()) + self.add_action(action) + self.set_accels_for_action('app.preferences', ['comma']) + + action = Gio.SimpleAction.new('menu-search', None) + action.connect( + 'activate', lambda *a: self.global_search_entry.grab_focus()) + self.add_action(action) + self.set_accels_for_action('app.menu-search', ['k']) + + action = Gio.SimpleAction.new('menu-toggle', None) + action.connect('activate', lambda *a: self.menu_toggle()) + self.add_action(action) + self.set_accels_for_action('app.menu-toggle', ['m']) + + toolbar_variant = GLib.Variant.new_string( + CONFIG['client.toolbar'] or 'both') + action = Gio.SimpleAction.new_stateful( + 'toolbar', toolbar_variant.get_type(), toolbar_variant) + action.connect('change-state', self.on_change_toolbar) + self.add_action(action) + + def on_change_action_boolean(action, value, key): + action.set_state(value) + CONFIG[key] = value.get_boolean() + if key == 'client.check_version' and CONFIG[key]: + common.check_version(self.info) + + for name, key in [ + ('mode-pda', 'client.modepda'), + ('save-tree-width', 'client.save_tree_width'), + ('save-tree-state', 'client.save_tree_state'), + ('spell-checking', 'client.spellcheck'), + ('check-version', 'client.check_version'), + ]: + variant = GLib.Variant.new_boolean(CONFIG[key]) + action = Gio.SimpleAction.new_stateful(name, None, variant) + action.connect('change-state', on_change_action_boolean, key) + self.add_action(action) + + action = Gio.SimpleAction.new('tab-previous', None) + action.connect('activate', lambda *a: self.win_prev()) + self.add_action(action) + self.set_accels_for_action('app.tab-previous', ['Tab']) + + action = Gio.SimpleAction.new('tab-next', None) + action.connect('activate', lambda *a: self.win_next()) + self.add_action(action) + self.set_accels_for_action('app.tab-next', ['Tab']) + + action = Gio.SimpleAction.new('search-limit', None) + action.connect('activate', lambda *a: self.edit_limit()) + self.add_action(action) + + self._shortcuts = None + action = Gio.SimpleAction.new('shortcuts', None) + action.connect('activate', lambda *a: self.shortcuts()) + self.add_action(action) + self.set_accels_for_action('app.shortcuts', ['F1']) + + action = Gio.SimpleAction.new('about', None) + action.connect('activate', lambda *a: self.about()) + self.add_action(action) + + action = Gio.SimpleAction.new('quit', None) + action.connect('activate', self.on_quit) + self.add_action(action) + self.set_accels_for_action('app.quit', ['q']) + + def do_activate(self): + if self.window: + self.window.present() + return + + self.window = Gtk.ApplicationWindow(application=self, title="Tryton") + self.window.set_default_size(960, 720) + self.window.maximize() + self.window.set_position(Gtk.WindowPosition.CENTER) + self.window.set_resizable(True) + self.window.set_icon(TRYTON_ICON) + self.window.connect("destroy", self.on_quit) + self.window.connect("delete_event", self.on_quit) + common.setup_window(self.window) + + self.header = Gtk.HeaderBar.new() + self.header.set_show_close_button(True) + self.window.set_titlebar(self.header) + self.set_title() + + self.primary_menu = Gtk.MenuButton.new() + self.primary_menu.set_menu_model(self._get_primary_menu()) + self.header.pack_end(self.primary_menu) + + menu = Gtk.Button.new() + menu.set_relief(Gtk.ReliefStyle.NONE) + menu.set_image( + common.IconFactory.get_image('tryton-menu', Gtk.IconSize.BUTTON)) + menu.connect('clicked', self.menu_toggle) + self.header.pack_start(menu) + + favorite = Gtk.MenuButton.new() + favorite.set_relief(Gtk.ReliefStyle.NONE) + favorite.set_image(common.IconFactory.get_image( + 'tryton-bookmarks', Gtk.IconSize.BUTTON)) + self.menu_favorite = Gtk.Menu.new() + favorite.set_popup(self.menu_favorite) + favorite.connect('button-press-event', self.favorite_set) + self.header.pack_start(favorite) + + self.set_global_search() + self.header.pack_start(self.global_search_entry) + + self.accel_group = Gtk.AccelGroup() + self.window.add_accel_group(self.accel_group) + + Gtk.AccelMap.add_entry( + '/Form/New', Gdk.KEY_N, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Save', Gdk.KEY_S, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Duplicate', Gdk.KEY_D, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + Gtk.AccelMap.add_entry( + '/Form/Delete', Gdk.KEY_D, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Next', Gdk.KEY_Page_Down, 0) + Gtk.AccelMap.add_entry( + '/Form/Previous', Gdk.KEY_Page_Up, 0) + Gtk.AccelMap.add_entry( + '/Form/Switch View', Gdk.KEY_L, + Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Close', Gdk.KEY_W, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Reload', Gdk.KEY_R, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Attachments', Gdk.KEY_T, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + Gtk.AccelMap.add_entry( + '/Form/Notes', Gdk.KEY_O, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + Gtk.AccelMap.add_entry( + '/Form/Relate', Gdk.KEY_R, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + Gtk.AccelMap.add_entry( + '/Form/Actions', Gdk.KEY_E, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Report', Gdk.KEY_P, Gdk.ModifierType.CONTROL_MASK) + Gtk.AccelMap.add_entry( + '/Form/Email', Gdk.KEY_E, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + Gtk.AccelMap.add_entry( + '/Form/Search', Gdk.KEY_F, + Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK) + + Gtk.AccelMap.load(os.path.join(get_config_dir(), 'accel.map')) + + self.tooltips = common.Tooltips() + + self.vbox = Gtk.VBox() + self.window.add(self.vbox) + + self.buttons = {} + + self.info = Gtk.VBox() + self.vbox.pack_start(self.info, expand=False, fill=True, padding=0) + if CONFIG['client.check_version']: + common.check_version(self.info) + GLib.timeout_add_seconds( + int(CONFIG['download.frequency']), common.check_version, + self.info) + + self.pane = Gtk.HPaned() + self.vbox.pack_start(self.pane, expand=True, fill=True, padding=0) + self.pane.set_position(int(CONFIG['menu.pane'])) + + self.menu_screen = None + self.menu = Gtk.VBox() + self.menu.set_vexpand(True) + self.pane.add1(self.menu) + + self.notebook = Gtk.Notebook() + self.notebook.popup_enable() + self.notebook.set_scrollable(True) + self.notebook.connect_after('switch-page', self._sig_page_changt) + self.pane.add2(self.notebook) + + self.window.show_all() + + self.pages = [] + self.previous_pages = {} + self.current_page = 0 + self.last_page = 0 + self.dialogs = [] + + # Register plugins + tryton.plugins.register() + + self.set_title() # Adds username/profile while password is asked + try: + common.Login() + except Exception as exception: + if (not isinstance(exception, TrytonError) + or exception.faultCode != 'QueryCanceled'): + common.error(exception, traceback.format_exc()) + return self.quit() + self.get_preferences() + + def do_command_line(self, cmd): + self.do_activate() + arguments = cmd.get_arguments() + if len(arguments) > 1: + url = arguments[1] + self.open_url(url) + return True + + def do_shutdown(self): + Gtk.Application.do_shutdown(self) + common.Logout() + CONFIG.save() + Gtk.AccelMap.save(os.path.join(get_config_dir(), 'accel.map')) + + def on_quit(self, *args): + try: + if not self.close_pages(): + return True + except TrytonServerUnavailable: + pass + rpc.logout() + self.quit() + + def add_window(self, window): + super().add_window(window) + common.setup_window(window) + + def _get_primary_menu(self): + menu = Gio.Menu.new() + menu.append(_("Preferences..."), 'app.preferences') + + section = Gio.Menu.new() + toolbar = Gio.Menu.new() + section.append_submenu(_("Toolbar"), toolbar) + toolbar.append(_("Default"), 'app.toolbar::default') + toolbar.append(_("Text and Icons"), 'app.toolbar::both') + toolbar.append(_("Text"), 'app.toolbar::text') + toolbar.append(_("Icons"), 'app.toolbar::icons') + + form = Gio.Menu.new() + section.append_submenu(_("Form"), form) + form.append(_("Save Column Width"), 'app.save-tree-width') + form.append(_("Save Tree State"), 'app.save-tree-state') + form.append(_("Spell Checking"), 'app.spell-checking') + + section.append(_("PDA Mode"), 'app.mode-pda') + section.append(_("Search Limit..."), 'app.search-limit') + section.append(_("Check Version"), 'app.check-version') + + menu.append_section(_("Options"), section) + + section = Gio.Menu.new() + section.append(_("Keyboard Shortcuts..."), 'app.shortcuts') + section.append(_("About..."), 'app.about') + menu.append_section(_("Help"), section) + return menu + + def set_global_search(self): + self.global_search_entry = Gtk.Entry.new() + self.global_search_entry.set_width_chars(20) + global_search_completion = Gtk.EntryCompletion() + global_search_completion.set_match_func(lambda *a: True) + global_search_completion.set_model( + Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, int, str)) + pixbuf_cell = Gtk.CellRendererPixbuf() + global_search_completion.pack_start(pixbuf_cell, expand=True) + global_search_completion.add_attribute(pixbuf_cell, 'pixbuf', 0) + text_cell = Gtk.CellRendererText() + global_search_completion.pack_start(text_cell, expand=True) + global_search_completion.add_attribute(text_cell, "markup", 1) + global_search_completion.props.popup_set_width = True + self.global_search_entry.set_completion(global_search_completion) + + def match_selected(completion, model, iter_): + model, record_id, model_name = model.get(iter_, 2, 3, 4) + if model == self.menu_screen.model_name: + # ids is not defined to prevent to add suffix + Action.exec_keyword('tree_open', { + 'model': model, + 'id': record_id, + }) + else: + Window.create(model, + res_id=record_id, + mode=['form', 'tree'], + name=model_name) + self.global_search_entry.set_text('') + return True + + global_search_completion.connect('match-selected', match_selected) + + def update(widget, search_text, callback=None): + def end(): + if callback: + callback() + return False + if search_text != widget.get_text(): + return end() + gmodel = global_search_completion.get_model() + if not search_text or not gmodel: + gmodel.clear() + gmodel.search_text = search_text + return end() + if getattr(gmodel, 'search_text', None) == search_text: + return end() + + def set_result(result): + try: + result = result() + except RPCException: + result = [] + if search_text != widget.get_text(): + if callback: + callback() + return False + gmodel.clear() + for r in result: + _, model, model_name, record_id, record_name, icon = r + if icon: + text = common.to_xml(record_name) + pixbuf = common.IconFactory.get_pixbuf( + icon, Gtk.IconSize.BUTTON) + else: + text = '%s:\n %s' % ( + common.to_xml(model_name), + common.to_xml(record_name)) + pixbuf = None + gmodel.append([pixbuf, text, model, record_id, model_name]) + gmodel.search_text = search_text + # Force display of popup + widget.emit('changed') + end() + + RPCExecute('model', 'ir.model', 'global_search', search_text, + CONFIG['client.limit'], self.menu_screen.model_name, + context=self.menu_screen.context, callback=set_result) + return False + + def changed(widget): + search_text = widget.get_text() + GLib.timeout_add(300, update, widget, search_text) + + def activate(widget): + def message(): + gmodel = global_search_completion.get_model() + if not len(gmodel): + common.message(_('No result found.')) + else: + widget.emit('changed') + search_text = widget.get_text() + update(widget, search_text, message) + + self.global_search_entry.connect('changed', changed) + self.global_search_entry.connect('activate', activate) + + def set_title(self, value=''): + if CONFIG['login.profile']: + login_info = CONFIG['login.profile'] + else: + login_info = '%s@%s/%s' % ( + CONFIG['login.login'], + CONFIG['login.host'], + CONFIG['login.db']) + titles = [] + if value: + titles.append(value) + titles.append(CONFIG['client.title']) + self.header.set_title(' - '.join(titles)) + self.header.set_subtitle(login_info) + try: + style_context = self.header.get_style_context() + except AttributeError: + pass + else: + for name in style_context.list_classes(): + if name.startswith('profile-'): + style_context.remove_class(name) + if CONFIG['login.profile']: + style_context.add_class( + 'profile-%s' % CONFIG['login.profile']) + + def favorite_set(self, *args): + if self.menu_favorite.get_children(): + return + + def _action_favorite(widget, id_): + event = Gtk.get_current_event() + allow_similar = (event.state & Gdk.ModifierType.MOD1_MASK + or event.state & Gdk.ModifierType.SHIFT_MASK) + with Window(allow_similar=allow_similar): + # ids is not defined to prevent to add suffix + Action.exec_keyword('tree_open', { + 'model': self.menu_screen.model_name, + 'id': id_, + }) + + def _manage_favorites(widget): + Window.create(self.menu_screen.model_name + '.favorite', + mode=['tree', 'form'], + name=_("Favorites")) + try: + favorites = RPCExecute('model', + self.menu_screen.model_name + '.favorite', 'get', + process_exception=False) + except Exception: + return False + for id_, name, icon in favorites: + menuitem = Gtk.MenuItem(label=name) + menuitem.connect('activate', _action_favorite, id_) + self.menu_favorite.add(menuitem) + self.menu_favorite.add(Gtk.SeparatorMenuItem()) + manage_favorites = Gtk.MenuItem(label=_("Manage..."), + use_underline=True) + manage_favorites.connect('activate', _manage_favorites) + self.menu_favorite.add(manage_favorites) + self.menu_favorite.show_all() + + def favorite_unset(self): + for child in self.menu_favorite.get_children(): + self.menu_favorite.remove(child) + + def on_change_toolbar(self, action, value): + action.set_state(value) + option = value.get_string() + CONFIG['client.toolbar'] = option + if option == 'default': + barstyle = False + elif option == 'both': + barstyle = Gtk.ToolbarStyle.BOTH + elif option == 'text': + barstyle = Gtk.ToolbarStyle.TEXT + elif option == 'icons': + barstyle = Gtk.ToolbarStyle.ICONS + for page_idx in range(self.notebook.get_n_pages()): + page = self.get_page(page_idx) + if page.toolbar: + page.toolbar.set_style(barstyle) + + def edit_limit(self): + from tryton.gui.window.limit import Limit + Limit().run() + + def win_next(self): + page = self.notebook.get_current_page() + if page == len(self.pages) - 1: + page = -1 + self.notebook.set_current_page(page + 1) + + def win_prev(self): + page = self.notebook.get_current_page() + self.notebook.set_current_page(page - 1) + + def get_preferences(self): + RPCContextReload() + try: + prefs = RPCExecute('model', 'res.user', 'get_preferences', False) + except RPCException: + prefs = {} + + threads = [] + for target in ( + common.IconFactory.load_icons, + common.MODELACCESS.load_models, + common.MODELHISTORY.load_history, + common.VIEW_SEARCH.load_searches, + ): + t = threading.Thread(target=target) + threads.append(t) + t.start() + for t in threads: + t.join() + if prefs and 'language_direction' in prefs: + translate.set_language_direction(prefs['language_direction']) + CONFIG['client.language_direction'] = \ + prefs['language_direction'] + self.sig_win_menu(prefs=prefs) + for action_id in prefs.get('actions', []): + Action.execute(action_id, {}) + self.set_title(prefs.get('status_bar', '')) + if prefs and 'language' in prefs: + translate.setlang(prefs['language'], prefs.get('locale')) + if CONFIG['client.lang'] != prefs['language']: + self.favorite_unset() + self.primary_menu.set_menu_model(self._get_primary_menu()) + CONFIG['client.lang'] = prefs['language'] + common.MODELNAME.clear() + # Set placeholder after language is set to get correct translation + self.global_search_entry.set_placeholder_text(_("Action")) + CONFIG.save() + + def preferences(self): + from tryton.gui.window.preference import Preference + if not self.close_pages(): + return False + Preference(rpc._USER, self.get_preferences) + + def sig_win_close(self, widget=None): + self._sig_remove_book(widget, + self.notebook.get_nth_page(self.notebook.get_current_page())) + + def close_pages(self): + if self.notebook.get_n_pages(): + if not common.sur( + _('The following action requires to close all tabs.\n' + 'Do you want to continue?')): + return False + res = True + while res: + wid = self.get_page() + if wid: + if not wid.sig_close(): + return False + res = self._win_del() + else: + res = False + if self.menu_screen: + self.menu_screen.save_tree_state() + if self.menu.get_visible(): + CONFIG['menu.pane'] = self.pane.get_position() + return True + + def about(self): + from tryton.gui.window.about import About + About() + + def shortcuts(self): + if self._shortcuts: + self._shortcuts.destroy() + self._shortcuts = window = Gtk.ShortcutsWindow() + window.set_transient_for(common.get_toplevel_window()) + window.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + window.set_destroy_with_parent(True) + self.add_window(window) + + section = Gtk.ShortcutsSection() + section.props.section_name = 'app' + section.props.title = _("Application Shortcuts") + section.props.visible = True + + group = Gtk.ShortcutsGroup() + group.props.title = _("Global") + section.add(group) + + for action, title in [ + ('app.preferences', _("Preferences")), + ('app.menu-search', _("Search menu")), + ('app.menu-toggle', _("Toggle menu")), + ('app.tab-previous', _("Previous tab"), ), + ('app.tab-next', _("Next tab")), + ('app.shortcuts', _("Shortcuts")), + ('app.quit', _("Quit")), + ]: + shortcut = Gtk.ShortcutsShortcut() + shortcut.props.title = title + accels = self.get_accels_for_action(action) + if accels: + shortcut.props.accelerator = ' '.join(accels) + group.add(shortcut) + + window.add(section) + + for name, title, groups in [ + ('entry', _("Edition Shortcuts"), [ + (_("Text Entries"), [ + ('x', _("Cut selected text")), + ('c', _("Copy selected text")), + ('v', _("Paste copied text")), + ('Tab', _("Next entry")), + ('Tab', _("Previous entry")), + ]), + (_("Relation Entries"), [ + ('F3', _("Create new relation")), + ('F2', _("Open/Search relation")), + ]), + (_("List Entries"), [ + ('F4', _("Switch view")), + ('F3', _("Create/Select new line")), + ('F2', _("Open relation")), + ('Delete', + _("Mark line for deletion/removal")), + ('Delete', _("Mark line for removal")), + ('Insert', _("Unmark line for deletion")), + ]), + ]), + ('tree', _("List/Tree Shortcuts"), [ + (_("Move Cursor"), [ + ('Right', _("Move right")), + ('Left', _("Move left")), + ('Up', _("Move up")), + ('Down', _("Move down")), + ('Page_Up', _("Move up of one page")), + ('Page_Down', _("Move down of one page")), + ('Home', _("Move to top")), + ('End', _("Move to bottom")), + ('BackSpace', _("Move to parent")), + ]), + (_("Selection"), [ + ('a slash', _("Select all")), + ('a slash', + _("Unselect all")), + ('BackSpace', _("Select parent")), + ('space', _("Select/Activate current row")), + ('space Return', + _("Select/Activate current row")), + ('space', _("Toggle selection")), + ]), + (_("Expand/Collapse"), [ + ('plus', _("Expand row")), + ('minus', _("Collapse row")), + ('space', _("Toggle row")), + ('Left', _("Collapse all rows")), + ('Right', _("Expand all rows")), + ]), + ]), + ]: + section = Gtk.ShortcutsSection() + section.props.section_name = name + section.props.title = title + section.props.visible = True + + for title, shortcuts in groups: + group = Gtk.ShortcutsGroup() + group.props.title = title + section.add(group) + + for accelerator, title in shortcuts: + shortcut = Gtk.ShortcutsShortcut() + shortcut.props.title = title + shortcut.props.accelerator = accelerator + group.add(shortcut) + + window.add(section) + + window.show_all() + + def menu_toggle(self, *args): + if self.menu.get_visible(): + CONFIG['menu.pane'] = self.pane.get_position() + self.pane.set_position(0) + self.notebook.grab_focus() + self.menu.set_visible(False) + else: + self.pane.set_position(int(CONFIG['menu.pane'])) + self.menu.set_visible(True) + if self.menu_screen: + self.menu_screen.set_cursor() + + def menu_row_activate(self): + screen = self.menu_screen + record_id = (screen.current_record.id + if screen.current_record else None) + # ids is not defined to prevent to add suffix + return Action.exec_keyword('tree_open', { + 'model': screen.model_name, + 'id': record_id, + }, warning=False) + + def sig_win_menu(self, prefs=None): + from tryton.gui.window.view_form.screen import Screen + + if not prefs: + try: + prefs = RPCExecute('model', 'res.user', 'get_preferences', + False) + except RPCException: + return False + + if self.menu_screen: + self.menu_screen.save_tree_state() + for child in self.menu.get_children(): + self.menu.remove(child) + + action = PYSONDecoder().decode(prefs['pyson_menu']) + view_ids = [] + if action.get('views', []): + view_ids = [x[0] for x in action['views']] + elif action.get('view_id', False): + view_ids = [action['view_id'][0]] + ctx = rpc.CONTEXT.copy() + decoder = PYSONDecoder(ctx) + action_ctx = decoder.decode(action.get('pyson_context') or '{}') + domain = decoder.decode(action['pyson_domain']) + screen = Screen(action['res_model'], mode=['tree'], view_ids=view_ids, + domain=domain, context=action_ctx, readonly=True, limit=None, + row_activate=self.menu_row_activate) + # Use alternate view to not show search box + screen.screen_container.alternate_view = True + screen.switch_view(view_type=screen.current_view.view_type) + + self.menu.pack_start( + screen.screen_container.alternate_viewport, + expand=True, fill=True, padding=0) + treeview = screen.current_view.treeview + treeview.set_headers_visible(False) + + # Favorite column + column = Gtk.TreeViewColumn() + column.name = None + column._type = None + favorite_renderer = CellRendererClickablePixbuf() + column.pack_start(favorite_renderer, expand=False) + + def favorite_setter(column, cell, store, iter_, user_data=None): + menu = store.get_value(iter_, 0) + favorite = menu.value.get('favorite') + if favorite: + icon = 'tryton-star' + elif favorite is False: + icon = 'tryton-star-border' + else: + icon = None + if icon: + pixbuf = common.IconFactory.get_pixbuf( + icon, Gtk.IconSize.MENU) + else: + pixbuf = None + cell.set_property('pixbuf', pixbuf) + column.set_cell_data_func(favorite_renderer, favorite_setter) + + def toggle_favorite(renderer, path, treeview): + if treeview.props.window: + self.toggle_favorite(renderer, path, treeview) + favorite_renderer.connect('clicked', + lambda *a: GLib.idle_add(toggle_favorite, *a), treeview) + # Unset fixed height mode to add column + treeview.set_fixed_height_mode(False) + treeview.set_property( + 'enable-grid-lines', Gtk.TreeViewGridLines.NONE) + treeview.append_column(column) + + screen.search_filter() + screen.display(set_cursor=True) + self.menu_screen = screen + + def toggle_favorite(self, renderer, path, treeview): + store = treeview.get_model() + iter_ = store.get_iter(path) + menu = store.get_value(iter_, 0) + favorite = menu.value.get('favorite') + if favorite: + value = False + method = 'unset' + elif favorite is False: + value = True + method = 'set' + else: + return + try: + RPCExecute('model', self.menu_screen.model_name + '.favorite', + method, menu.id) + except RPCException: + return + menu.value['favorite'] = value + store.row_changed(path, iter_) + self.favorite_unset() + + def win_set(self, page): + current_page = self.notebook.get_current_page() + page_num = self.notebook.page_num(page.widget) + page.widget.props.visible = True + self.notebook.set_current_page(page_num) + # In order to focus the page + if current_page == page_num: + self._sig_page_changt(self.notebook, None, page_num) + + def win_add(self, page, hide_current=False): + previous_page_id = self.notebook.get_current_page() + previous_widget = self.notebook.get_nth_page(previous_page_id) + if previous_widget and hide_current: + page_id = previous_page_id + 1 + else: + page_id = -1 + self.previous_pages[page] = previous_widget + self.pages.append(page) + hbox = Gtk.HBox(spacing=3) + if page.icon: + hbox.pack_start( + common.IconFactory.get_image( + page.icon, Gtk.IconSize.SMALL_TOOLBAR), + expand=False, fill=False, padding=0) + name = page.name + label = Gtk.Label( + label=common.ellipsize(name, 20), + halign=Gtk.Align.START) + self.tooltips.set_tip(label, page.name) + self.tooltips.enable() + hbox.pack_start(label, expand=True, fill=True, padding=0) + + button = Gtk.Button() + button.set_relief(Gtk.ReliefStyle.NONE) + button.set_can_focus(False) + button.add(common.IconFactory.get_image( + 'tryton-close', Gtk.IconSize.MENU)) + self.tooltips.set_tip(button, _('Close Tab')) + button.connect('clicked', self._sig_remove_book, page.widget) + hbox.pack_start(button, expand=False, fill=False, padding=0) + size = Gtk.IconSize.lookup(Gtk.IconSize.MENU) + button.set_size_request(size.width, size.height) + + hbox.show_all() + label_menu = Gtk.Label( + label=page.name, halign=Gtk.Align.START) + page.widget.props.margin = 3 + self.notebook.insert_page_menu(page.widget, hbox, label_menu, page_id) + self.notebook.set_tab_reorderable(page.widget, True) + self.notebook.set_current_page(page_id) + + def _sig_remove_book(self, widget, page_widget): + for page in self.pages: + if page.widget == page_widget: + if not page.widget.props.sensitive: + return + page_num = self.notebook.page_num(page.widget) + self.notebook.set_current_page(page_num) + res = page.sig_close() + if not res: + return + self._win_del(page_widget) + + def _win_del(self, page_widget=None): + if page_widget: + page_id = self.notebook.page_num(page_widget) + else: + page_id = int(self.notebook.get_current_page()) + page_widget = self.notebook.get_nth_page(page_id) + if page_id != -1: + page = None + for i in range(len(self.pages)): + if self.pages[i].widget == page_widget: + page = self.pages.pop(i) + page.signal_unconnect(self) + break + self.notebook.remove_page(page_id) + + next_page_id = -1 + to_pop = [] + for i in self.previous_pages: + if self.previous_pages[i] == page_widget: + to_pop.append(i) + if i.widget == page_widget: + if self.previous_pages[i]: + next_page_id = self.notebook.page_num( + self.previous_pages[i]) + to_pop.append(i) + to_pop.reverse() + for i in to_pop: + self.previous_pages.pop(i) + + if hasattr(page, 'destroy'): + page.destroy() + del page + + current_widget = self.notebook.get_nth_page(next_page_id) + if current_widget: + current_widget.props.visible = True + self.notebook.set_current_page(next_page_id) + if not self.pages and self.menu_screen: + self.menu_screen.set_cursor() + return self.notebook.get_current_page() != -1 + + def get_page(self, page_id=None): + if page_id is None: + page_id = self.notebook.get_current_page() + if page_id == -1: + return None + page_widget = self.notebook.get_nth_page(page_id) + for page in self.pages: + if page.widget == page_widget: + return page + return None + + def _sig_page_changt(self, notebook, page, page_num): + self.last_page = self.current_page + last_form = self.get_page(self.current_page) + if last_form: + for dialog in last_form.dialogs: + dialog.hide() + + self.current_page = self.notebook.get_current_page() + current_form = self.get_page(self.current_page) + + def set_cursor(): + if self.current_page == self.notebook.get_current_page(): + current_form.set_cursor() + # Using idle_add because the Gtk.TreeView grabs the focus at the + # end of the event + GLib.idle_add(set_cursor) + for dialog in current_form.dialogs: + dialog.show() + + def _open_url(self, url): + urlp = urlparse(url) + if not urlp.scheme == 'tryton': + return + urlp = urlparse('http' + url[6:]) + database, path = list(map(unquote, + (urlp.path[1:].split('/', 1) + [''])[:2])) + if not path: + return + type_, path = (path.split('/', 1) + [''])[:2] + params = {} + if urlp.params: + try: + params.update(dict(parse_qsl(urlp.params, + strict_parsing=True))) + except ValueError: + return + + def open_model(path): + model, path = (path.split('/', 1) + [''])[:2] + if not model: + return + res_id = None + mode = None + try: + view_ids = json.loads(params.get('views', '[]')) + limit = json.loads(params.get('limit', 'null')) + name = json.loads(params.get('name', '""')) + search_value = json.loads(params.get('search_value', '[]'), + object_hook=object_hook) + domain = json.loads(params.get('domain', '[]'), + object_hook=object_hook) + context = json.loads(params.get('context', '{}'), + object_hook=object_hook) + context_model = params.get('context_model') + tab_domain = json.loads(params.get('tab_domain', '[]'), + object_hook=object_hook) + except ValueError: + return + if path: + try: + res_id = int(path) + except ValueError: + return + mode = ['form', 'tree'] + try: + Window.create(model, + view_ids=view_ids, + res_id=res_id, + domain=domain, + context=context, + context_model=context_model, + mode=mode, + name=name, + limit=limit, + search_value=search_value, + tab_domain=tab_domain) + except Exception: + # Prevent crashing the client + return + + def open_wizard(wizard): + if not wizard: + return + try: + data = json.loads(params.get('data', '{}'), + object_hook=object_hook) + direct_print = json.loads(params.get('direct_print', 'false')) + name = json.loads(params.get('name', '""')) + window = json.loads(params.get('window', 'false')) + context = json.loads(params.get('context', '{}'), + object_hook=object_hook) + except ValueError: + return + try: + Window.create_wizard( + wizard, data, direct_print=direct_print, name=name, + context=context, window=window) + except Exception: + # Prevent crashing the client + return + + def open_report(report): + if not report: + return + try: + data = json.loads(params.get('data'), object_hook=object_hook) + direct_print = json.loads(params.get('direct_print', 'false')) + context = json.loads(params.get('context', '{}'), + object_hook=object_hook) + except ValueError: + return + try: + Action.exec_report( + report, data, direct_print=direct_print, context=context) + except Exception: + # Prevent crashing the client + return + + def open_url(): + try: + url = json.loads(params.get('url', 'false')) + except ValueError: + return + if url: + webbrowser.open(url, new=2) + + if type_ == 'model': + open_model(path) + elif type_ == 'wizard': + open_wizard(path) + elif type_ == 'report': + open_report(path) + elif type_ == 'url': + open_url() + + self.window.present() + + def open_url(self, url): + def idle_open_url(): + self._open_url(url) + return False + GLib.idle_add(idle_open_url) + + def show_notification(self, title, msg, priority=1): + notification = Gio.Notification.new(title) + notification.set_body(msg) + notification.set_priority(_PRIORITIES[priority]) + if sys.platform != 'win32' or GLib.glib_version >= (2, 57, 0): + self.send_notification(None, notification) diff --git a/tryton/tryton/gui/window/__init__.py b/tryton/tryton/gui/window/__init__.py new file mode 100644 index 0000000000..f10bd31f9c --- /dev/null +++ b/tryton/tryton/gui/window/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from .window import Window + +__all__ = [Window] diff --git a/tryton/tryton/gui/window/about.py b/tryton/tryton/gui/window/about.py new file mode 100644 index 0000000000..e70444d96b --- /dev/null +++ b/tryton/tryton/gui/window/about.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# 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 os +import gettext +from gi.repository import Gtk, GdkPixbuf + +from tryton.config import PIXMAPS_DIR, CONFIG +from tryton.common import get_toplevel_window +from tryton import __version__ + +COPYRIGHT = '''\ +Copyright (C) 2004-2022 Tryton. +''' +AUTHORS = [ + 'Bertrand Chenal ', + 'Cédric Krier ', + 'Franz Wiesinger', + 'Hartmut Goebel', + 'Korbinian Preisler ', + 'Mathias Behrle ', + 'Maxime Richez ', + 'Nicolas Évrard ', + 'Sednacom ', + 'Udo Spallek ', + ] +_ = gettext.gettext + + +class About(object): + + def __init__(self): + parent = get_toplevel_window() + self.win = Gtk.AboutDialog() + self.win.set_transient_for(parent) + self.win.set_name(CONFIG['client.title']) + self.win.set_version(__version__) + self.win.set_comments(_("modularity, scalability and security")) + self.win.set_copyright(COPYRIGHT) + self.win.set_license_type(Gtk.License.GPL_3_0) + self.win.set_website('http://www.tryton.org/') + self.win.set_website_label("Tryton") + self.win.set_authors(AUTHORS) + self.win.set_translator_credits(_('translator-credits')) + self.win.set_logo(GdkPixbuf.Pixbuf.new_from_file( + os.path.join(PIXMAPS_DIR, 'tryton.svg'))) + self.win.run() + parent.present() + self.win.destroy() diff --git a/tryton/tryton/gui/window/attachment.py b/tryton/tryton/gui/window/attachment.py new file mode 100644 index 0000000000..1c619c3d27 --- /dev/null +++ b/tryton/tryton/gui/window/attachment.py @@ -0,0 +1,101 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Attachment" +import os +from urllib.request import urlopen +from urllib.parse import urlparse, unquote +import gettext +import webbrowser +from functools import partial + +from tryton.common import RPCExecute, RPCException, file_write, file_open +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_form import WinForm + +_ = gettext.gettext + + +class Attachment(WinForm): + "Attachment window" + + def __init__(self, record, callback=None): + self.resource = '%s,%s' % (record.model_name, record.id) + self.attachment_callback = callback + title = _('Attachments (%s)') % (record.rec_name()) + screen = Screen('ir.attachment', domain=[ + ('resource', '=', self.resource), + ], mode=['tree', 'form']) + super(Attachment, self).__init__(screen, self.callback, + view_type='tree', title=title) + screen.search_filter() + + def destroy(self): + self.prev_view.save_width() + super(Attachment, self).destroy() + + def callback(self, result): + if result: + self.screen.save_current() + if self.attachment_callback: + self.attachment_callback() + + def add_uri(self, uri): + data_field = self.screen.group.fields['data'] + link_field = self.screen.group.fields['link'] + type_field = self.screen.group.fields['type'] + name_field = self.screen.group.fields[data_field.attrs['filename']] + description_field = self.screen.group.fields['description'] + new_record = self.screen.new() + parse = urlparse(uri) + if not parse.scheme: + description_field.set_client(new_record, uri) + else: + file_name = os.path.basename(unquote(parse.path)) + name_field.set_client(new_record, file_name) + if parse.scheme == 'file': + data_field.set_client(new_record, urlopen(uri).read()) + type_field.set_client(new_record, 'data') + else: + link_field.set_client(new_record, uri) + type_field.set_client(new_record, 'link') + self.screen.display() + + def add_file(self, filename): + self.add_uri('file:///' + filename) + + @staticmethod + def get_attachments(record): + attachments = [] + context = {} + if record and record.id >= 0: + context = record.get_context() + try: + attachments = RPCExecute('model', 'ir.attachment', + 'search_read', [ + ('resource', '=', '%s,%s' % ( + record.model_name, record.id)), + ], 0, 20, None, ['rec_name', 'name', 'type', 'link'], + context=context) + except RPCException: + pass + for attachment in attachments: + name = attachment['rec_name'] + callback = getattr( + Attachment, 'open_' + attachment['type'], Attachment.open_data) + yield name, partial( + callback, attachment=attachment, context=context) + + @staticmethod + def open_link(attachment, context): + if attachment['link']: + webbrowser.open(attachment['link'], new=2) + + @staticmethod + def open_data(attachment, context): + try: + value, = RPCExecute('model', 'ir.attachment', 'read', + [attachment['id']], ['data'], context=context) + except RPCException: + return + filepath = file_write(attachment['name'], value['data']) + file_open(filepath) diff --git a/tryton/tryton/gui/window/board.py b/tryton/tryton/gui/window/board.py new file mode 100644 index 0000000000..4968414b12 --- /dev/null +++ b/tryton/tryton/gui/window/board.py @@ -0,0 +1,74 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Board" +import gettext +import xml.dom.minidom + +from tryton.signal_event import SignalEvent +from tryton.gui import Main +from tryton.gui.window.view_board import ViewBoard +from tryton.common import RPCExecute, RPCException, MODELNAME + +from .tabcontent import TabContent + +_ = gettext.gettext + + +class Board(SignalEvent, TabContent): + 'Board' + + def __init__(self, model, name='', **attributes): + super(Board, self).__init__(**attributes) + + context = attributes.get('context') + self.view_ids = attributes.get('view_ids') + + try: + view, = RPCExecute('model', 'ir.ui.view', 'read', + self.view_ids, ['arch'], context=context) + except RPCException: + raise + + xml_dom = xml.dom.minidom.parseString(view['arch']) + root, = xml_dom.childNodes + self.board = ViewBoard(root, context=context) + self.model = model + self.dialogs = [] + if not name: + name = MODELNAME.get(model) + self.name = name + + self.create_tabcontent() + self.board.reload() + + def get_toolbars(self): + return {} + + def widget_get(self): + return self.board.widget_get() + + def sig_reload(self, test_modified=True): + self.board.reload() + return True + + def sig_close(self): + return True + + def compare(self, model, attributes): + if not attributes: + return False + return (self.model == model + and self.attributes.get('view_ids') == attributes.get('view_ids') + and self.attributes.get('context') == attributes.get('context')) + + def __hash__(self): + return id(self) + + def sig_win_close(self, widget): + Main().sig_win_close(widget) + + def set_cursor(self): + if not self.board.actions: + return + first_action = self.board.actions[0] + first_action.screen.set_cursor() diff --git a/tryton/tryton/gui/window/dblogin.py b/tryton/tryton/gui/window/dblogin.py new file mode 100644 index 0000000000..5340e12bd7 --- /dev/null +++ b/tryton/tryton/gui/window/dblogin.py @@ -0,0 +1,646 @@ +# 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 configparser +import os +import gettext +import threading +import logging + +from gi.repository import GLib, GObject, Gtk + +from tryton import __version__ +import tryton.common as common +from tryton.config import CONFIG, TRYTON_ICON, PIXMAPS_DIR, get_config_dir +import tryton.rpc as rpc +from tryton.common.underline import set_underline + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +class DBListEditor(object): + + def __init__(self, parent, profile_store, profiles, callback): + self.profiles = profiles + self.current_database = None + self.old_profile, self.current_profile = None, None + self.db_cache = None + self.updating_db = False + + # GTK Stuffs + self.parent = parent + self.dialog = Gtk.Dialog( + title=_('Profile Editor'), transient_for=parent, + modal=True, destroy_with_parent=True) + self.ok_button = self.dialog.add_button( + set_underline(_("Close")), Gtk.ResponseType.CLOSE) + self.dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.dialog.set_icon(TRYTON_ICON) + common.setup_window(self.dialog) + + tooltips = common.Tooltips() + + hpaned = Gtk.HPaned() + vbox_profiles = Gtk.VBox(homogeneous=False, spacing=6) + self.cell = Gtk.CellRendererText() + self.cell.set_property('editable', True) + self.cell.connect('editing-started', self.edit_started) + self.profile_tree = Gtk.TreeView() + self.profile_tree.set_model(profile_store) + self.profile_tree.insert_column_with_attributes(-1, _('Profile'), + self.cell, text=0) + self.profile_tree.connect('cursor-changed', self.profile_selected) + scroll = Gtk.ScrolledWindow() + scroll.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.add(self.profile_tree) + self.add_button = Gtk.Button() + self.add_button.set_image(common.IconFactory.get_image( + 'tryton-add', Gtk.IconSize.BUTTON)) + tooltips.set_tip(self.add_button, _("Add new profile")) + self.add_button.connect('clicked', self.profile_create) + self.remove_button = Gtk.Button() + self.remove_button.set_image(common.IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.BUTTON)) + tooltips.set_tip(self.remove_button, _("Remove selected profile")) + self.remove_button.get_style_context().add_class( + Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION) + self.remove_button.connect('clicked', self.profile_delete) + bbox = Gtk.ButtonBox() + bbox.pack_start(self.remove_button, expand=True, fill=True, padding=0) + bbox.pack_start(self.add_button, expand=True, fill=True, padding=0) + vbox_profiles.pack_start(scroll, expand=True, fill=True, padding=0) + vbox_profiles.pack_start(bbox, expand=False, fill=True, padding=0) + hpaned.add1(vbox_profiles) + + grid = Gtk.Grid(column_spacing=3, row_spacing=3) + host = Gtk.Label( + label=set_underline(_('Host:')), + use_underline=True, halign=Gtk.Align.END) + self.host_entry = Gtk.Entry(hexpand=True) + self.host_entry.connect('focus-out-event', self.display_dbwidget) + self.host_entry.connect('changed', self.update_profiles, 'host') + self.host_entry.set_activates_default(True) + host.set_mnemonic_widget(self.host_entry) + grid.attach(host, 0, 1, 1, 1) + grid.attach(self.host_entry, 1, 1, 1, 1) + database = Gtk.Label( + label=set_underline(_('Database:')), + use_underline=True, halign=Gtk.Align.END) + self.database_entry = Gtk.Entry() + self.database_entry.connect('changed', self.dbentry_changed) + self.database_entry.connect('changed', self.update_profiles, + 'database') + self.database_entry.set_activates_default(True) + self.database_label = Gtk.Label(valign=Gtk.Align.START) + self.database_label.set_use_markup(True) + self.database_combo = Gtk.ComboBoxText() + self.database_combo.connect('changed', self.dbcombo_changed) + self.database_progressbar = Gtk.ProgressBar() + self.database_progressbar.set_text(_('Fetching databases list')) + db_box = Gtk.VBox(homogeneous=True, hexpand=True) + db_box.pack_start( + self.database_entry, expand=True, fill=True, padding=0) + db_box.pack_start( + self.database_combo, expand=True, fill=True, padding=0) + db_box.pack_start( + self.database_label, expand=True, fill=True, padding=0) + db_box.pack_start( + self.database_progressbar, expand=True, fill=True, padding=0) + # Compute size_request of box in order to prevent "form jumping" + width, height = 0, 0 + for child in db_box.get_children(): + request = child.get_preferred_size()[0] + width = max(width, request.width) + height = max(height, request.height) + db_box.set_size_request(width, height) + grid.attach(database, 0, 2, 1, 1) + grid.attach(db_box, 1, 2, 1, 1) + username = Gtk.Label( + label=set_underline(_('Username:')), + use_underline=True, halign=Gtk.Align.END) + self.username_entry = Gtk.Entry(hexpand=True) + self.username_entry.connect('changed', self.update_profiles, + 'username') + username.set_mnemonic_widget(self.username_entry) + self.username_entry.set_activates_default(True) + grid.attach(username, 0, 3, 1, 1) + grid.attach(self.username_entry, 1, 3, 1, 1) + hpaned.add2(grid) + hpaned.set_position(250) + + self.dialog.vbox.pack_start(hpaned, expand=True, fill=True, padding=0) + self.dialog.set_default_size(640, 350) + self.dialog.set_default_response(Gtk.ResponseType.CLOSE) + + self.dialog.connect('close', lambda *a: False) + self.dialog.connect('response', self.response) + self.callback = callback + + def response(self, widget, response): + if self.callback: + self.callback(self.current_profile['name']) + self.parent.present() + self.dialog.destroy() + + def run(self, profile_name): + self.clear_entries() # must be done before show_all for windows + self.dialog.show_all() + model = self.profile_tree.get_model() + if model: + for i, row in enumerate(model): + if row[0] == profile_name: + break + else: + i = 0 + self.profile_tree.get_selection().select_path((i,)) + self.profile_selected(self.profile_tree) + + def _current_profile(self): + model, selection = self.profile_tree.get_selection().get_selected() + if not selection: + return {'name': None, 'iter': None} + return {'name': model[selection][0], 'iter': selection} + + def clear_entries(self): + for entryname in ('host', 'database', 'username'): + entry = getattr(self, '%s_entry' % entryname) + entry.handler_block_by_func(self.update_profiles) + entry.set_text('') + entry.handler_unblock_by_func(self.update_profiles) + self.current_database = None + self.database_combo.set_active(-1) + self.database_combo.get_model().clear() + self.hide_database_info() + + def hide_database_info(self): + self.database_entry.hide() + self.database_combo.hide() + self.database_label.hide() + self.database_progressbar.hide() + + def profile_create(self, button): + self.clear_entries() + model = self.profile_tree.get_model() + model.append(['', False]) + column = self.profile_tree.get_column(0) + self.profile_tree.set_cursor(len(model) - 1, column, + start_editing=True) + self.db_cache = None + + def profile_delete(self, button): + self.clear_entries() + model, selection = self.profile_tree.get_selection().get_selected() + if not selection: + return + profile_name = model[selection][0] + self.profiles.remove_section(profile_name) + del model[selection] + + def profile_selected(self, treeview): + self.old_profile = self.current_profile + self.current_profile = self._current_profile() + if not self.current_profile['name']: + return + if self.updating_db: + self.current_profile = self.old_profile + selection = treeview.get_selection() + selection.select_iter(self.old_profile['iter']) + return + fields = ('host', 'database', 'username') + for field in fields: + entry = getattr(self, '%s_entry' % field) + try: + entry_value = self.profiles.get(self.current_profile['name'], + field) + except configparser.NoOptionError: + entry_value = '' + entry.set_text(entry_value) + if field == 'database': + self.current_database = entry_value + + self.display_dbwidget(None, None) + + def edit_started(self, renderer, editable, path): + if isinstance(editable, Gtk.Entry): + editable.connect('focus-out-event', self.edit_profilename, + renderer, path) + + def edit_profilename(self, editable, event, renderer, path): + newtext = editable.get_text() + model = self.profile_tree.get_model() + try: + oldname = model[path][0] + except IndexError: + return + if oldname == newtext == '': + del model[path] + return + elif oldname == newtext or newtext == '': + return + elif newtext in self.profiles.sections(): + if not oldname: + del model[path] + return + elif oldname in self.profiles.sections(): + self.profiles.add_section(newtext) + for itemname, value in self.profiles.items(oldname): + self.profiles.set(newtext, itemname, value) + self.profiles.remove_section(oldname) + model[path][0] = newtext + else: + model[path][0] = newtext + self.profiles.add_section(newtext) + self.current_profile = self._current_profile() + self.host_entry.grab_focus() + + def update_profiles(self, editable, entryname): + new_value = editable.get_text() + if not new_value: + return + section = self._current_profile()['name'] + self.profiles.set(section, entryname, new_value) + self.validate_profile(section) + + def validate_profile(self, profile_name): + model, selection = self.profile_tree.get_selection().get_selected() + if not selection: + return + active = all(self.profiles.has_option(profile_name, option) + for option in ('host', 'database')) + model[selection][1] = active + + @classmethod + def test_server_version(cls, host, port): + ''' + Tests if the server version is compatible with the client version + It returns None if no information on server version is available. + ''' + version = rpc.server_version(host, port) + if not version: + return None + return version.split('.')[:2] == __version__.split('.')[:2] + + def refresh_databases(self, host, port): + self.dbs_updated = threading.Event() + threading.Thread(target=self.refresh_databases_start, + args=(host, port)).start() + GLib.timeout_add(100, self.refresh_databases_end, host, port) + + def refresh_databases_start(self, host, port): + dbs = None + try: + dbs = rpc.db_list(host, port) + except Exception: + pass + finally: + self.dbs = dbs + self.dbs_updated.set() + + def refresh_databases_end(self, host, port): + if not self.dbs_updated.isSet(): + self.database_progressbar.show() + self.database_progressbar.pulse() + return True + self.database_progressbar.hide() + dbs = self.dbs + + label = None + if self.test_server_version(host, port) is False: + label = _('Incompatible version of the server.') + elif dbs is None: + label = _('Could not connect to the server.') + if label: + self.database_label.set_label( + '%s' % GLib.markup_escape_text(label)) + self.database_label.show() + else: + self.database_combo.remove_all() + index = -1 + for db_num, db_name in enumerate(dbs): + self.database_combo.append_text(db_name) + if self.current_database and db_name == self.current_database: + index = db_num + if index == -1: + index = 0 + self.database_combo.set_active(index) + self.database_entry.set_text(self.current_database + if self.current_database else '') + if dbs: + self.database_combo.show() + else: + self.database_entry.show() + self.db_cache = (host, port, self.current_profile['name']) + + self.add_button.set_sensitive(True) + self.remove_button.set_sensitive(True) + self.ok_button.set_sensitive(True) + self.cell.set_property('editable', True) + self.host_entry.set_sensitive(True) + self.updating_db = False + return False + + def display_dbwidget(self, entry, event): + netloc = self.host_entry.get_text() + host = common.get_hostname(netloc) + if not host: + return + port = common.get_port(netloc) + if (host, port, self.current_profile['name']) == self.db_cache: + return + if self.updating_db: + return + + self.hide_database_info() + self.add_button.set_sensitive(False) + self.remove_button.set_sensitive(False) + self.ok_button.set_sensitive(False) + self.cell.set_property('editable', False) + self.host_entry.set_sensitive(False) + self.updating_db = True + self.refresh_databases(host, port) + + def dbcombo_changed(self, combobox): + dbname = combobox.get_active_text() + if dbname: + self.current_database = dbname + self.profiles.set(self.current_profile['name'], 'database', dbname) + self.validate_profile(self.current_profile['name']) + + def dbentry_changed(self, entry): + dbname = entry.get_text() + if dbname: + self.current_database = dbname + self.profiles.set(self.current_profile['name'], 'database', dbname) + self.validate_profile(self.current_profile['name']) + + def insert_text_port(self, entry, new_text, new_text_length, position): + value = entry.get_text() + position = entry.get_position() + new_value = value[:position] + new_text + value[position:] + try: + int(new_value) + except ValueError: + entry.stop_emission_by_name('insert-text') + + +class DBLogin(object): + def __init__(self): + # Fake windows to avoid warning about Dialog without transient + self._window = Gtk.Window() + self.dialog = Gtk.Dialog(title="Tryton - " + _('Login'), modal=True) + self.dialog.set_transient_for(self._window) + self.dialog.set_icon(TRYTON_ICON) + self.dialog.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + self.dialog.set_resizable(False) + common.setup_window(self.dialog) + + tooltips = common.Tooltips() + button_cancel = Gtk.Button(label=_('_Cancel'), use_underline=True) + tooltips.set_tip(button_cancel, + _('Cancel connection to the Tryton server')) + self.dialog.add_action_widget(button_cancel, Gtk.ResponseType.CANCEL) + self.button_connect = Gtk.Button( + label=_('C_onnect'), use_underline=True) + self.button_connect.get_style_context().add_class( + Gtk.STYLE_CLASS_SUGGESTED_ACTION) + self.button_connect.set_can_default(True) + tooltips.set_tip(self.button_connect, _('Connect the Tryton server')) + self.dialog.add_action_widget(self.button_connect, Gtk.ResponseType.OK) + self.dialog.set_default_response(Gtk.ResponseType.OK) + grid = Gtk.Grid( + column_spacing=3, row_spacing=3, valign=Gtk.Align.START) + self.dialog.vbox.pack_start(grid, expand=True, fill=True, padding=0) + + image = Gtk.Image() + image.set_from_file(os.path.join(PIXMAPS_DIR, 'tryton.svg')) + image.set_valign(Gtk.Align.START) + overlay = Gtk.Overlay() + overlay.add(image) + label = Gtk.Label( + label='%s' % __version__, + use_markup=True) + label.props.halign = Gtk.Align.END + label.props.valign = Gtk.Align.START + label.props.margin_right = 10 + label.props.margin_top = 5 + overlay.add_overlay(label) + grid.attach(overlay, 0, 0, 3, 1) + + self.profile_store = Gtk.ListStore( + GObject.TYPE_STRING, GObject.TYPE_BOOLEAN) + self.combo_profile = Gtk.ComboBox(hexpand=True) + cell = Gtk.CellRendererText() + self.combo_profile.pack_start(cell, expand=True) + self.combo_profile.add_attribute(cell, 'text', 0) + self.combo_profile.add_attribute(cell, 'sensitive', 1) + self.combo_profile.set_model(self.profile_store) + self.combo_profile.connect('changed', self.profile_changed) + self.profile_label = Gtk.Label( + label=set_underline(_('Profile:')), + use_underline=True, halign=Gtk.Align.END) + self.profile_label.set_mnemonic_widget(self.combo_profile) + self.profile_button = Gtk.Button( + label=set_underline(_('Manage...')), use_underline=True) + self.profile_button.connect('clicked', self.profile_manage) + grid.attach(self.profile_label, 0, 1, 1, 1) + grid.attach(self.combo_profile, 1, 1, 1, 1) + grid.attach(self.profile_button, 2, 1, 1, 1) + self.expander = Gtk.Expander() + self.expander.set_label(_('Host / Database information')) + self.expander.connect('notify::expanded', self.expand_hostspec) + grid.attach(self.expander, 0, 2, 3, 1) + self.label_host = Gtk.Label( + label=set_underline(_('Host:')), + use_underline=True, halign=Gtk.Align.END) + self.entry_host = Gtk.Entry(hexpand=True) + self.entry_host.connect_after('focus-out-event', + self.clear_profile_combo) + self.entry_host.set_activates_default(True) + self.label_host.set_mnemonic_widget(self.entry_host) + grid.attach(self.label_host, 0, 3, 1, 1) + grid.attach(self.entry_host, 1, 3, 2, 1) + self.label_database = Gtk.Label( + label=set_underline(_('Database:')), + use_underline=True, halign=Gtk.Align.END) + self.entry_database = Gtk.Entry(hexpand=True) + self.entry_database.connect_after('focus-out-event', + self.clear_profile_combo) + self.entry_database.set_activates_default(True) + self.label_database.set_mnemonic_widget(self.entry_database) + grid.attach(self.label_database, 0, 4, 1, 1) + grid.attach(self.entry_database, 1, 4, 2, 1) + self.entry_login = Gtk.Entry(hexpand=True) + self.entry_login.set_activates_default(True) + grid.attach(self.entry_login, 1, 5, 2, 1) + label_username = Gtk.Label( + label=set_underline(_("User name:")), + use_underline=True, halign=Gtk.Align.END, margin=3) + label_username.set_mnemonic_widget(self.entry_login) + grid.attach(label_username, 0, 5, 1, 1) + + # Profile information + self.profile_cfg = os.path.join(get_config_dir(), 'profiles.cfg') + self.profiles = configparser.ConfigParser() + if not os.path.exists(self.profile_cfg): + short_version = '.'.join(__version__.split('.', 2)[:2]) + name = 'demo%s.tryton.org' % short_version + self.profiles.add_section(name) + self.profiles.set(name, 'host', name) + self.profiles.set(name, 'database', 'demo%s' % short_version) + self.profiles.set(name, 'username', 'demo') + else: + try: + self.profiles.read(self.profile_cfg) + except configparser.ParsingError: + logger.error("Fail to parse profiles.cfg", exc_info=True) + for section in self.profiles.sections(): + active = all(self.profiles.has_option(section, option) + for option in ('host', 'database')) + self.profile_store.append([section, active]) + + def profile_manage(self, widget): + def callback(profile_name): + with open(self.profile_cfg, 'w') as configfile: + self.profiles.write(configfile) + + for idx, row in enumerate(self.profile_store): + if row[0] == profile_name and row[1]: + self.combo_profile.set_active(idx) + self.profile_changed(self.combo_profile) + break + + dia = DBListEditor(self.dialog, self.profile_store, self.profiles, + callback) + active_profile = self.combo_profile.get_active() + profile_name = None + if active_profile != -1: + profile_name = self.profile_store[active_profile][0] + dia.run(profile_name) + + def profile_changed(self, combobox): + position = combobox.get_active() + if position == -1: + return + profile = self.profile_store[position][0] + try: + username = self.profiles.get(profile, 'username') + except configparser.NoOptionError: + username = '' + host = self.profiles.get(profile, 'host') + self.entry_host.set_text('%s' % host) + self.entry_database.set_text(self.profiles.get(profile, 'database')) + if username: + self.entry_login.set_text(username) + else: + self.entry_login.set_text('') + + def clear_profile_combo(self, *args): + netloc = self.entry_host.get_text() + host = common.get_hostname(netloc) + port = common.get_port(netloc) + database = self.entry_database.get_text().strip() + login = self.entry_login.get_text() + for idx, profile_info in enumerate(self.profile_store): + if not profile_info[1]: + continue + profile = profile_info[0] + try: + profile_host = self.profiles.get(profile, 'host') + profile_db = self.profiles.get(profile, 'database') + profile_login = self.profiles.get(profile, 'username') + except configparser.NoOptionError: + continue + if (host == common.get_hostname(profile_host) + and port == common.get_port(profile_host) + and database == profile_db + and (not login or login == profile_login)): + break + else: + idx = -1 + self.combo_profile.set_active(idx) + return False + + def expand_hostspec(self, expander, *args): + visibility = expander.props.expanded + self.entry_host.props.visible = visibility + self.label_host.props.visible = visibility + self.entry_database.props.visible = visibility + self.label_database.props.visible = visibility + + def run(self): + profile_name = CONFIG['login.profile'] + can_use_profile = self.profiles.has_section(profile_name) + if can_use_profile: + for (configname, option) in [ + ('login.host', 'host'), + ('login.db', 'database'), + ]: + try: + value = self.profiles.get(profile_name, option) + except configparser.NoOptionError: + value = None + if value != CONFIG[configname]: + can_use_profile = False + break + + if can_use_profile: + for idx, row in enumerate(self.profile_store): + if row[0] == profile_name: + self.combo_profile.set_active(idx) + break + else: + self.combo_profile.set_active(-1) + host = CONFIG['login.host'] if CONFIG['login.host'] else '' + self.entry_host.set_text(host) + db = CONFIG['login.db'] if CONFIG['login.db'] else '' + self.entry_database.set_text(db) + self.entry_login.set_text(CONFIG['login.login']) + self.clear_profile_combo() + self.dialog.show_all() + + self.entry_login.grab_focus() + + self.expander.set_expanded(CONFIG['login.expanded']) + # The previous action did not called expand_hostspec + self.expand_hostspec(self.expander) + + response, result = None, ('', '', '', '') + while not all(result): + response = self.dialog.run() + if response != Gtk.ResponseType.OK: + break + self.clear_profile_combo() + active_profile = self.combo_profile.get_active() + if active_profile != -1: + profile = self.profile_store[active_profile][0] + else: + profile = '' + host = self.entry_host.get_text() + hostname = common.get_hostname(host) + port = common.get_port(host) + test = DBListEditor.test_server_version(hostname, port) + if not test: + if test is False: + common.warning('', + _('Incompatible version of the server.'), + parent=self.dialog) + else: + common.warning('', + _('Could not connect to the server.'), + parent=self.dialog) + continue + database = self.entry_database.get_text() + login = self.entry_login.get_text() + CONFIG['login.profile'] = profile + CONFIG['login.host'] = host + CONFIG['login.db'] = database + CONFIG['login.expanded'] = self.expander.props.expanded + CONFIG['login.login'] = login + result = ( + hostname, port, database, self.entry_login.get_text()) + + self.dialog.destroy() + self._window.destroy() + return response == Gtk.ResponseType.OK diff --git a/tryton/tryton/gui/window/email_.py b/tryton/tryton/gui/window/email_.py new file mode 100644 index 0000000000..9b9a38a607 --- /dev/null +++ b/tryton/tryton/gui/window/email_.py @@ -0,0 +1,357 @@ +# 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 gettext +import logging +import os + +from gi.repository import Gtk, GLib, GObject +try: + from gi.repository import GtkSpell +except ImportError: + GtkSpell = None + +from tryton.common import IconFactory, Tooltips, RPCExecute, RPCException +from tryton.common.richtext import ( + register_format, add_toolbar, get_content, set_content) +from tryton.common.treeviewcontrol import TreeViewControl +from tryton.common.underline import set_underline +from tryton.common.widget_style import widget_class +from tryton.config import TRYTON_ICON, CONFIG +from tryton.exceptions import TrytonServerError, TrytonError +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +class EmailEntry(Gtk.Entry): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._completion = Gtk.EntryCompletion() + self._completion.set_model(Gtk.ListStore(str, str)) + self._completion.set_text_column(0) + self._completion.set_match_func(lambda *a: True) + self._completion.connect('match-selected', self._match_selected) + self.set_completion(self._completion) + self.connect('changed', EmailEntry._changed) + + def _match_selected(self, completion, model, iter): + self.set_text(model.get_value(iter, 1)) + self.set_position(-1) + return True + + def _update_completion(self, text): + if not self.props.window: + return False + if text != self.get_text(): + return False + model = self._completion.get_model() + if not text: + model.clear() + model.text = text + return False + if getattr(model, 'text', None) == text: + return False + + def callback(results): + try: + results = results() + except (TrytonError, TrytonServerError): + logger.warning( + _("Unable to complete email entry"), exc_info=True) + results = [] + if text != self.get_text(): + return False + model.clear() + for ratio, address, addresses in results: + model.append([address, addresses]) + model.text = text + # Force display of popup + self.emit('changed') + + try: + RPCExecute( + 'model', 'ir.email', 'complete', text, CONFIG['client.limit'], + process_exception=False, callback=callback) + except Exception: + logger.warning( + _("Unable to complete email entry"), exc_info=True) + return False + + def _changed(self): + def keypress(): + if not self.props.window: + return + self._update_completion() + text = self.get_text() + if self.get_position() >= len(text) - 1: + GLib.timeout_add(300, self._update_completion, text) + else: + model = self._completion.get_model() + model.clear() + model.text = None + + +class Email(NoModal): + + def __init__(self, name, record, prints, template=None): + super().__init__() + self.record = record + self.dialog = Gtk.Dialog( + transient_for=self.parent, destroy_with_parent=True) + Main().add_window(self.dialog) + self.dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.dialog.set_icon(TRYTON_ICON) + self.dialog.set_default_size(*self.default_size()) + self.dialog.connect('response', self.response) + + self.dialog.set_title(_('E-mail %s') % name) + + grid = Gtk.Grid( + column_spacing=3, row_spacing=3, + border_width=3) + self.dialog.vbox.pack_start(grid, expand=True, fill=True, padding=0) + + label = Gtk.Label( + set_underline(_("To:")), use_underline=True, halign=Gtk.Align.END) + grid.attach(label, 0, 0, 1, 1) + self.to = EmailEntry(hexpand=True, activates_default=True) + widget_class(self.to, 'required', True) + label.set_mnemonic_widget(self.to) + grid.attach(self.to, 1, 0, 1, 1) + + label = Gtk.Label( + set_underline(_("Cc:")), use_underline=True, halign=Gtk.Align.END) + grid.attach(label, 0, 1, 1, 1) + self.cc = EmailEntry(hexpand=True, activates_default=True) + label.set_mnemonic_widget(self.cc) + grid.attach(self.cc, 1, 1, 1, 1) + + label = Gtk.Label( + set_underline(_("Bcc:")), use_underline=True, halign=Gtk.Align.END) + grid.attach(label, 0, 2, 1, 1) + self.bcc = EmailEntry(hexpand=True, activates_default=True) + label.set_mnemonic_widget(self.bcc) + grid.attach(self.bcc, 1, 2, 1, 1) + + label = Gtk.Label( + set_underline(_("Subject:")), + use_underline=True, halign=Gtk.Align.END) + grid.attach(label, 0, 3, 1, 1) + self.subject = Gtk.Entry(hexpand=True, activates_default=True) + label.set_mnemonic_widget(self.subject) + grid.attach(self.subject, 1, 3, 1, 1) + + self.body = Gtk.TextView() + body_frame = Gtk.Frame() + label = Gtk.Label( + set_underline(_("Body")), use_underline=True, halign=Gtk.Align.END) + label.set_mnemonic_widget(self.body) + body_frame.set_label_widget(label) + grid.attach(body_frame, 0, 4, 2, 1) + body_box = Gtk.VBox(hexpand=True, vexpand=True) + body_frame.add(body_box) + register_format(self.body) + body_toolbar = add_toolbar(self.body) + body_box.pack_start(body_toolbar, expand=False, fill=True, padding=0) + body_box.pack_start(self.body, expand=True, fill=True, padding=0) + + if GtkSpell and CONFIG['client.spellcheck']: + checker = GtkSpell.Checker() + checker.attach(self.body) + language = os.environ.get('LANGUAGE', 'en') + try: + checker.set_language(language) + except Exception: + logger.error( + 'Could not set spell checker for "%s"', language) + checker.detach() + + attachments_box = Gtk.HBox() + grid.attach(attachments_box, 0, 5, 2, 1) + + print_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE) + print_frame.set_label(_("Reports")) + attachments_box.pack_start( + print_frame, expand=True, fill=True, padding=0) + print_box = Gtk.VBox() + print_frame.add(print_box) + print_flowbox = Gtk.FlowBox(selection_mode=Gtk.SelectionMode.NONE) + print_box.pack_start( + print_flowbox, expand=False, fill=False, padding=0) + self.print_actions = {} + for print_ in prints: + print_check = Gtk.CheckButton.new_with_mnemonic( + set_underline(print_['name'])) + self.print_actions[print_['id']] = print_check + print_flowbox.add(print_check) + + attachment_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE) + attachment_frame.set_label(_("Attachments")) + attachments_box.pack_start( + attachment_frame, expand=True, fill=True, padding=0) + try: + attachments = RPCExecute('model', 'ir.attachment', + 'search_read', [ + ('resource', '=', '%s,%s' % ( + record.model_name, record.id)), + ['OR', + ('data', '!=', None), + ('file_id', '!=', None), + ], + ], 0, None, None, ['rec_name'], + context=record.get_context()) + except RPCException: + logger.error( + 'Could not fetch attachment for "%s"', record) + attachments = [] + scrolledwindow = Gtk.ScrolledWindow() + if len(attachments) > 2: + scrolledwindow.set_size_request(-1, 100) + attachment_frame.add(scrolledwindow) + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.attachments = TreeViewControl() + self.attachments.set_headers_visible(False) + scrolledwindow.add(self.attachments) + self.attachments.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + self.attachments.append_column( + Gtk.TreeViewColumn( + "Name", Gtk.CellRendererText(), text=0)) + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT) + for attachment in attachments: + model.append((attachment['rec_name'], attachment['id'])) + self.attachments.set_model(model) + self.attachments.set_search_column(0) + + file_frame = Gtk.Frame(shadow_type=Gtk.ShadowType.NONE) + file_frame.set_label(_("Files")) + attachments_box.pack_start( + file_frame, expand=True, fill=True, padding=0) + self.files = Gtk.VBox(spacing=6) + file_frame.add(self.files) + self._add_file_button() + + button_cancel = self.dialog.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + button_cancel.set_image(IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + + button_send = self.dialog.add_button( + set_underline(_("Send")), Gtk.ResponseType.OK) + button_send.set_image(IconFactory.get_image( + 'tryton-send', Gtk.IconSize.BUTTON)) + self.dialog.set_default_response(Gtk.ResponseType.OK) + + self._fill_with(template) + + self.dialog.show_all() + self.register() + + def _add_file_button(self): + tooltips = Tooltips() + box = Gtk.HBox(spacing=3) + self.files.pack_start(box, expand=False, fill=True, padding=0) + file_ = Gtk.FileChooserButton(title=_("Select File")) + box.pack_start(file_, expand=True, fill=True, padding=0) + button = Gtk.Button() + button.set_image(IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.BUTTON)) + tooltips.set_tip(button, _("Remove File")) + button.set_sensitive(False) + box.pack_start(button, expand=False, fill=True, padding=0) + + box.show_all() + + file_.connect('file-set', self._file_set, button) + button.connect('clicked', self._file_remove) + + def _file_set(self, file_, button): + button.set_sensitive(True) + self._add_file_button() + + def _file_remove(self, button): + self.files.remove(button.get_parent()) + + def get_files(self): + for box in self.files: + file_ = list(box)[0] + filename = file_.get_filename() + if not filename: + continue + with open(filename, 'rb') as fp: + data = fp.read() + name = os.path.basename(filename) + yield (name, data) + + def get_attachments(self): + model, paths = self.attachments.get_selection().get_selected_rows() + return [model[path][1] for path in paths] + + def _fill_with(self, template=None): + try: + if template: + values = RPCExecute( + 'model', 'ir.email.template', 'get', + template, self.record.id) + else: + values = RPCExecute( + 'model', 'ir.email.template', 'get_default', + self.record.model_name, self.record.id) + except RPCException: + return + self.to.set_text(', '.join(values.get('to', []))) + self.cc.set_text(', '.join(values.get('cc', []))) + self.bcc.set_text(', '.join(values.get('bcc', []))) + self.subject.set_text(values.get('subject', '')) + set_content(self.body, values.get('body', '')) + print_ids = values.get('reports', []) + for print_id, print_check in self.print_actions.items(): + print_check.set_active(print_id in print_ids) + + def validate(self): + valid = True + if not self.subject.get_text(): + valid = False + widget_class(self.subject, 'invalid', True) + self.subject.grab_focus() + else: + widget_class(self.subject, 'invalid', False) + if not self.to.get_text(): + valid = False + widget_class(self.to, 'invalid', True) + self.to.grab_focus() + else: + widget_class(self.to, 'invalid', False) + return valid + + def response(self, dialog, response): + if response == Gtk.ResponseType.OK: + if not self.validate(): + return + to = self.to.get_text() + cc = self.cc.get_text() + bcc = self.bcc.get_text() + subject = self.subject.get_text() + body = get_content(self.body) + files = list(self.get_files()) + reports = [ + id_ for id_, check in self.print_actions.items() + if check.get_active()] + attachments = self.get_attachments() + try: + RPCExecute( + 'model', 'ir.email', 'send', + to, cc, bcc, subject, body, + files, + [self.record.model_name, self.record.id], + reports, + attachments) + except RPCException: + return + self.destroy() + + def destroy(self): + super().destroy() + self.dialog.destroy() diff --git a/tryton/tryton/gui/window/form.py b/tryton/tryton/gui/window/form.py new file mode 100644 index 0000000000..b2c027a7e1 --- /dev/null +++ b/tryton/tryton/gui/window/form.py @@ -0,0 +1,916 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Form" +import csv +import datetime +import gettext +import locale +import os +import tempfile +from itertools import zip_longest + +from gi.repository import Gdk, GLib, Gtk + +from tryton.gui.window.view_form.screen import Screen +from tryton.action import Action +from tryton.gui import Main +from tryton.gui.window import Window +from tryton.gui.window.email_ import Email +from tryton.gui.window.win_export import WinExport +from tryton.gui.window.win_import import WinImport +from tryton.gui.window.attachment import Attachment +from tryton.gui.window.note import Note +from tryton.gui.window.revision import Revision +from tryton.signal_event import SignalEvent +from tryton.common import message, sur, sur_3b, timezoned_date +import tryton.common as common +from tryton.common import RPCExecute, RPCException +from tryton.common.common import selection as selection_ +from tryton.common.underline import set_underline +from tryton import plugins + +from .tabcontent import TabContent + +_ = gettext.gettext + + +class Form(SignalEvent, TabContent): + "Form" + + def __init__(self, model, res_id=None, name='', **attributes): + super(Form, self).__init__(**attributes) + + self.model = model + self.res_id = res_id + self.mode = attributes.get('mode') + self.view_ids = attributes.get('view_ids') + self.dialogs = [] + + if not name: + name = common.MODELNAME.get(model) + self.name = name + + self.screen = Screen(self.model, breadcrumb=[self.name], **attributes) + self.screen.widget.show() + + self.create_tabcontent() + + self.set_buttons_sensitive() + + self.screen.signal_connect(self, 'record-message', + self._record_message) + + self.screen.signal_connect(self, 'record-modified', + lambda *a: GLib.idle_add(self._record_modified, *a)) + self.screen.signal_connect(self, 'record-saved', self._record_saved) + self.screen.signal_connect( + self, 'resources', + lambda screen, resources: self.update_resources(resources)) + + self.attachment_screen = None + + if res_id not in (None, False): + if isinstance(res_id, int): + res_id = [res_id] + self.screen.load(res_id) + else: + if self.screen.current_view.view_type == 'form': + self.sig_new(None, autosave=False) + if self.screen.current_view.view_type \ + in ('tree', 'graph', 'calendar'): + self.screen.search_filter() + + self.update_revision() + self.activate_save() + + def get_toolbars(self): + try: + return RPCExecute('model', self.model, 'view_toolbar_get', + context=self.screen.context) + except RPCException: + return {} + + def create_tabcontent(self): + super().create_tabcontent() + + self.attachment_preview = Gtk.Viewport() + self.attachment_preview.set_shadow_type(Gtk.ShadowType.NONE) + self.attachment_preview.show() + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolledwindow.add(self.attachment_preview) + scrolledwindow.set_size_request(300, -1) + self.main.pack2(scrolledwindow, resize=False, shrink=True) + + def widget_get(self): + return self.screen.widget + + def compare(self, model, attributes): + if not attributes: + return False + return (self.model == model + and self.res_id == attributes.get('res_id') + and self.attributes.get('domain') == attributes.get('domain') + and self.attributes.get('view_ids') == attributes.get('view_ids') + and (attributes.get('view_ids') + or (self.attributes.get('mode') or ['tree', 'form']) == ( + attributes.get('mode') or ['tree', 'form'])) + and self.screen.local_context == attributes.get('context') + and self.attributes.get('search_value') == ( + attributes.get('search_value')) + and self.attributes.get('tab_domain') == ( + attributes.get('tab_domain'))) + + def __hash__(self): + return id(self) + + def destroy(self): + super(Form, self).destroy() + self.screen.signal_unconnect(self) + self.screen.destroy() + + def sig_attach(self, widget=None): + def window(widget): + return Attachment( + record, lambda: self.refresh_resources(reload=True)) + + def add_file(widget): + filenames = common.file_selection(_("Select"), multi=True) + if filenames: + attachment = window(widget) + for filename in filenames: + attachment.add_file(filename) + + def preview(widget): + children = self.attachment_preview.get_children() + for child in children: + self.attachment_preview.remove(child) + if widget.get_active(): + self.attachment_preview.add( + self._attachment_preview_widget()) + self.attachment_preview.get_parent().show() + self.refresh_attachment_preview() + else: + self.attachment_screen = None + self.attachment_preview.get_parent().hide() + + def activate(widget, callback): + callback() + + button = self.buttons['attach'] + if widget != button: + if button.props.sensitive: + button.props.active = True + return + record = self.screen.current_record + menu = button._menu = Gtk.Menu() + for name, callback in Attachment.get_attachments(record): + item = Gtk.MenuItem(label=name) + item.connect('activate', activate, callback) + menu.add(item) + menu.add(Gtk.SeparatorMenuItem()) + add_item = Gtk.MenuItem(label=_("Add...")) + add_item.connect('activate', add_file) + menu.add(add_item) + preview_item = Gtk.CheckMenuItem(label=_("Preview")) + preview_item.set_active(bool( + self.attachment_preview.get_children())) + preview_item.connect('toggled', preview) + menu.add(preview_item) + manage_item = Gtk.MenuItem(label=_("Manage...")) + manage_item.connect('activate', window) + menu.add(manage_item) + menu.show_all() + menu.connect('deactivate', self._popup_menu_hide, button) + self.action_popup(button) + + def _attachment_preview_widget(self): + vbox = Gtk.VBox(homogeneous=False, spacing=2) + vbox.set_margin_start(4) + hbox = Gtk.HBox(homogeneous=False, spacing=0) + hbox.set_halign(Gtk.Align.CENTER) + vbox.pack_start(hbox, expand=False, fill=True, padding=0) + hbox.set_border_width(2) + tooltips = common.Tooltips() + + but_prev = Gtk.Button() + tooltips.set_tip(but_prev, _("Previous")) + but_prev.add(common.IconFactory.get_image( + 'tryton-back', Gtk.IconSize.SMALL_TOOLBAR)) + but_prev.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_prev, expand=False, fill=False, padding=0) + + label = Gtk.Label(label='(0,0)') + hbox.pack_start(label, expand=False, fill=False, padding=0) + + but_next = Gtk.Button() + tooltips.set_tip(but_next, _("Next")) + but_next.add(common.IconFactory.get_image( + 'tryton-forward', Gtk.IconSize.SMALL_TOOLBAR)) + but_next.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_next, expand=False, fill=False, padding=0) + + vbox.show_all() + self.attachment_screen = screen = Screen( + 'ir.attachment', readonly=True, mode=['form'], + context={ + 'preview': True, + }) + screen.widget.show() + + but_prev.connect('clicked', lambda *a: screen.display_prev()) + but_next.connect('clicked', lambda *a: screen.display_next()) + + def update_label(screen, data): + position, length = data[:2] + label.set_text('(%s/%s)' % (position or '_', length)) + but_prev.set_sensitive(position and position > 1) + but_next.set_sensitive(position and position < length) + + screen.signal_connect(self, 'record-message', update_label) + vbox.pack_start(screen.widget, expand=True, fill=True, padding=0) + return vbox + + def refresh_attachment_preview(self, force=False): + if not self.attachment_screen: + return + record = self.screen.current_record + if not record: + return + resource = '%s,%s' % (record.model_name, record.id) + domain = [ + ('resource', '=', resource), + ('type', '=', 'data'), + ] + if self.attachment_screen.domain != domain or force: + self.attachment_screen.domain = domain + self.attachment_screen.search_filter() + + def sig_note(self, widget=None): + record = self.screen.current_record + if not record or record.id < 0: + return + Note(record, + lambda: self.refresh_resources(reload=True)) + + def refresh_resources(self, reload=False): + record = self.screen.current_record + self.update_resources( + record.get_resources(reload=reload) if record else None) + if reload: + self.refresh_attachment_preview(True) + + def update_resources(self, resources): + if not resources: + resources = {} + record = self.screen.current_record + sensitive = record.id >= 0 if record else False + + def update(name, label, icon, badge): + button = self.buttons[name] + button.set_label(label) + image = common.IconFactory.get_image( + icon, Gtk.IconSize.LARGE_TOOLBAR, badge=badge) + image.show() + button.set_icon_widget(image) + button.props.sensitive = sensitive + + attachment_count = resources.get('attachment_count', 0) + badge = 1 if attachment_count else None + label = _("Attachment (%s)") % attachment_count + update('attach', label, 'tryton-attach', badge) + + note_count = resources.get('note_count', 0) + note_unread = resources.get('note_unread', 0) + if note_unread: + badge = 2 + elif note_count: + badge = 1 + else: + badge = None + label = _("Note (%d/%d)") % (note_unread, note_count) + update('note', label, 'tryton-note', badge) + + def sig_switch(self, widget=None): + if not self.modified_save(): + return + self.screen.switch_view() + + def sig_logs(self, widget=None): + current_record = self.screen.current_record + if not current_record or current_record.id < 0: + self.message_info( + _('You have to select one record.'), Gtk.MessageType.INFO) + return False + + fields = [ + ('id', _('ID:')), + ('create_uid.rec_name', _('Created by:')), + ('create_date', _('Created at:')), + ('write_uid.rec_name', _('Edited by:')), + ('write_date', _('Edited at:')), + ] + + try: + data = RPCExecute('model', self.model, 'read', [current_record.id], + [x[0] for x in fields], context=self.screen.context)[0] + except RPCException: + return + date_format = self.screen.context.get('date_format', '%x') + datetime_format = date_format + ' %H:%M:%S.%f' + message_str = '' + for (key, label) in fields: + value = data + keys = key.split('.') + name = keys.pop(-1) + for key in keys: + value = value.get(key + '.', {}) + value = (value or {}).get(name, '/') + if isinstance(value, datetime.datetime): + value = timezoned_date(value).strftime(datetime_format) + message_str += '%s %s\n' % (label, value) + message_str += _('Model:') + ' ' + self.model + message(message_str) + return True + + def sig_revision(self, widget=None): + if not self.modified_save(): + return + current_id = (self.screen.current_record.id + if self.screen.current_record else None) + try: + revisions = RPCExecute('model', self.model, 'history_revisions', + [r.id for r in self.screen.selected_records]) + except RPCException: + return + revision = self.screen.context.get('_datetime') + format_ = self.screen.context.get('date_format', '%x') + format_ += ' %H:%M:%S.%f' + revision = Revision(revisions, revision, format_).run() + # Prevent too old revision in form view + if (self.screen.current_view.view_type == 'form' + and revision + and revision < revisions[-1][0]): + revision = revisions[-1][0] + if revision != self.screen.context.get('_datetime'): + self.screen.clear() + # Update root group context that will be propagated + self.screen.group._context['_datetime'] = revision + if self.screen.current_view.view_type != 'form': + self.screen.search_filter( + self.screen.screen_container.get_text()) + else: + # Test if record exist in revisions + self.screen.load([current_id]) + self.screen.display(set_cursor=True) + self.update_revision() + + def update_revision(self): + tooltips = common.Tooltips() + revision = self.screen.context.get('_datetime') + if revision: + format_ = self.screen.context.get('date_format', '%x') + format_ += ' %H:%M:%S.%f' + revision_label = ' @ %s' % revision.strftime(format_) + label = common.ellipsize( + self.name, 80 - len(revision_label)) + revision_label + tooltip = self.name + revision_label + else: + label = common.ellipsize(self.name, 80) + tooltip = self.name + self.title.set_text(label) + tooltips.set_tip(self.title, tooltip) + self.set_buttons_sensitive(revision) + + def set_buttons_sensitive(self, revision=None): + if not revision: + access = common.MODELACCESS[self.model] + for name, sensitive in [ + ('new', access['create']), + ('save', access['create'] or access['write']), + ('remove', access['delete']), + ('copy', access['create']), + ('import', access['create']), + ]: + if name in self.buttons: + self.buttons[name].props.sensitive = sensitive + if name in self.menu_buttons: + self.menu_buttons[name].props.sensitive = sensitive + else: + for name in ['new', 'save', 'remove', 'copy', 'import']: + if name in self.buttons: + self.buttons[name].props.sensitive = False + if name in self.menu_buttons: + self.menu_buttons[name].props.sensitive = False + + def sig_remove(self, widget=None): + if (not common.MODELACCESS[self.model]['delete'] + or not self.screen.deletable): + return + if self.screen.current_view.view_type == 'form': + msg = _('Are you sure to remove this record?') + else: + msg = _('Are you sure to remove those records?') + if sur(msg): + if not self.screen.remove(delete=True, force_remove=True): + self.message_info( + _('Records not removed.'), Gtk.MessageType.ERROR) + else: + self.message_info(_('Records removed.'), Gtk.MessageType.INFO) + self.screen.count_tab_domain(True) + + def sig_import(self, widget=None): + WinImport(self.title.get_text(), self.model, self.screen.context) + + def sig_export(self, widget=None): + if not self.modified_save(): + return + export = WinExport(self.title.get_text(), self.screen) + for name in self.screen.current_view.get_fields(): + type = self.screen.group.fields[name].attrs['type'] + if type == 'selection': + export.sel_field(name + '.translated') + elif type == 'reference': + export.sel_field(name + '.translated') + export.sel_field(name + '/rec_name') + else: + export.sel_field(name) + + def do_export(self, widget, export): + if not self.modified_save(): + return + if (self.screen.current_view + and self.screen.current_view.view_type == 'tree' + and self.screen.current_view.children_field): + ids = [r.id for r in self.screen.listed_records] + paths = self.screen.listed_paths + else: + ids = [r.id for r in self.screen.selected_records] + paths = self.screen.selected_paths + fields = [f['name'] for f in export['export_fields.']] + data = RPCExecute( + 'model', self.model, 'export_data', ids, fields, + context=self.screen.context) + delimiter = ',' + if os.name == 'nt' and ',' == locale.localeconv()['decimal_point']: + delimiter = ';' + fileno, fname = tempfile.mkstemp( + '.csv', common.slugify(export['name']) + '_') + with open(fname, 'w') as fp: + writer = csv.writer(fp, delimiter=delimiter) + writer.writerow(fields) + for row, path in zip_longest(data, paths or []): + indent = len(path) - 1 if path else 0 + if row: + writer.writerow(WinExport.format_row(row, indent=indent)) + os.close(fileno) + common.file_open(fname, 'csv') + + def sig_new(self, widget=None, autosave=True): + if not common.MODELACCESS[self.model]['create']: + return + if autosave: + if not self.modified_save(): + return + self.screen.new() + self.message_info() + self.activate_save() + + def sig_copy(self, widget=None): + if not common.MODELACCESS[self.model]['create']: + return + if not self.modified_save(): + return + if self.screen.copy(): + self.message_info(_('Working now on the duplicated record(s).'), + Gtk.MessageType.INFO) + self.screen.count_tab_domain(True) + + def sig_save(self, widget=None): + if widget: + # Called from button so we must save the tree state + self.screen.save_tree_state() + if not (common.MODELACCESS[self.model]['write'] + or common.MODELACCESS[self.model]['create'] + or self.screen.writable): + return + if self.screen.save_current(): + self.message_info(_('Record saved.'), Gtk.MessageType.INFO) + self.screen.count_tab_domain(True) + return True + else: + self.message_info( + self.screen.invalid_message(), Gtk.MessageType.ERROR) + return False + + def sig_previous(self, widget=None): + if not self.modified_save(): + return + self.screen.display_prev() + self.message_info() + self.activate_save() + + def sig_next(self, widget=None): + if not self.modified_save(): + return + self.screen.display_next() + self.message_info() + self.activate_save() + + def sig_reload(self, test_modified=True): + if test_modified: + if not self.modified_save(): + return False + else: + self.screen.save_tree_state(store=False) + self.screen.cancel_current() + set_cursor = False + record_id = (self.screen.current_record.id + if self.screen.current_record else None) + if self.screen.current_view.view_type != 'form': + self.screen.search_filter(self.screen.screen_container.get_text()) + for record in self.screen.group: + if record.id == record_id: + self.screen.current_record = record + set_cursor = True + break + self.screen.display(set_cursor=set_cursor) + self.message_info() + self.set_buttons_sensitive() + self.activate_save() + self.screen.count_tab_domain() + return True + + def sig_action(self, widget): + if self.buttons['action'].props.sensitive: + self.buttons['action'].props.active = True + + def sig_print(self, widget): + if self.buttons['print'].props.sensitive: + self.buttons['print'].props.active = True + + def sig_print_open(self, widget): + if self.buttons['open'].props.sensitive: + self.buttons['open'].props.active = True + + def sig_email(self, widget): + def is_report(action): + return action['type'] == 'ir.action.report' + + if self.buttons['email'].props.sensitive: + if not self.modified_save(): + return + record = self.screen.current_record + if not record or record.id < 0: + return + toolbars = self.get_toolbars() + title = self.title.get_text() + prints = filter(is_report, toolbars['print']) + emails = {e['name']: e['id'] for e in toolbars['emails']} + template = selection_(_("Template"), emails, alwaysask=True) + if template: + template = template[1] + Email( + '%s: %s' % (title, record.rec_name()), record, prints, + template=template) + + def sig_relate(self, widget): + if self.buttons['relate'].props.sensitive: + self.buttons['relate'].props.active = True + + def sig_copy_url(self, widget): + if self.buttons['copy_url'].props.sensitive: + self.buttons['copy_url'].props.active = True + + def sig_search(self, widget): + search_container = self.screen.screen_container + if hasattr(search_container, 'search_entry'): + search_container.search_entry.grab_focus() + + def action_popup(self, widget): + button, = widget.get_children() + button.grab_focus() + menu = widget._menu + if not widget.props.active: + menu.popdown() + return + + def menu_position(menu, x, y, user_data): + widget_allocation = widget.get_allocation() + x, y = widget.get_window().get_root_coords( + widget_allocation.x, widget_allocation.y) + return (x, y + widget_allocation.height, False) + menu.show_all() + if hasattr(menu, 'popup_at_widget'): + menu.popup_at_widget( + widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, + Gtk.get_current_event()) + else: + menu.popup( + None, None, menu_position, None, 0, + Gtk.get_current_event_time()) + + def _record_message(self, screen, signal_data): + name = '_' + if signal_data[0]: + name = str(signal_data[0]) + for button_id in ('print', 'relate', 'email', 'open', 'save', + 'attach'): + button = self.buttons[button_id] + can_be_sensitive = getattr(button, '_can_be_sensitive', True) + if button_id in {'print', 'relate', 'email', 'open'}: + action_type = button_id + if button_id == 'open': + action_type = 'print' + can_be_sensitive |= any( + b.attrs.get('keyword', 'action') == action_type + for b in screen.get_buttons()) + elif button_id == 'save': + can_be_sensitive &= not self.screen.readonly + button.props.sensitive = (bool(signal_data[0]) + and can_be_sensitive) + button_switch = self.buttons['switch'] + button_switch.props.sensitive = self.screen.number_of_views > 1 + + menu_delete = self.menu_buttons['remove'] + menu_delete.props.sensitive = self.screen.deletable + menu_save = self.menu_buttons['save'] + menu_save.props.sensitive = not self.screen.readonly + + msg = name + ' / ' + str(signal_data[1]) + if (signal_data[1] < signal_data[2] + and self.screen.limit is not None + and signal_data[2] > self.screen.limit): + msg += _(' of ') + str(signal_data[2]) + self.status_label.set_text(msg) + self.message_info() + self.activate_save() + self.refresh_attachment_preview() + + def _record_modified(self, screen, signal_data): + # As it is called via idle_add, the form could have been destroyed in + # the meantime. + if self.widget_get().props.window: + self.activate_save() + + def _record_saved(self, screen, signal_data): + self.activate_save() + self.refresh_resources() + + def modified_save(self): + self.screen.save_tree_state() + self.screen.current_view.set_value() + if self.screen.modified(): + value = sur_3b( + _('This record has been modified\n' + 'do you want to save it?')) + if value == 'ok': + return self.sig_save(None) + if value == 'ko': + record_id = self.screen.current_record.id + if self.sig_reload(test_modified=False): + if record_id < 0: + return None + elif self.screen.current_record: + return record_id == self.screen.current_record.id + return False + return True + + def sig_close(self, widget=None): + for dialog in reversed(self.dialogs[:]): + dialog.destroy() + modified_save = self.modified_save() + return True if modified_save is None else modified_save + + def _action(self, action, atype): + if not self.modified_save(): + return + action = action.copy() + record_id = (self.screen.current_record.id + if self.screen.current_record else None) + if action.get('records') == 'listed': + record_ids = [r.id for r in self.screen.listed_records] + record_paths = self.screen.listed_paths + else: + record_ids = [r.id for r in self.screen.selected_records] + record_paths = self.screen.selected_paths + data = { + 'model': self.screen.model_name, + 'model_context': ( + self.screen.context_screen.model_name + if self.screen.context_screen else None), + 'id': record_id, + 'ids': record_ids, + 'paths': record_paths, + } + Action.execute(action, data, context=self.screen.local_context) + + def activate_save(self): + self.buttons['save'].props.sensitive = self.screen.modified() + + def sig_win_close(self, widget): + Main().sig_win_close(widget) + + def create_toolbar(self, toolbars): + gtktoolbar = super(Form, self).create_toolbar(toolbars) + + attach_btn = self.buttons['attach'] + attach_btn.drag_dest_set( + Gtk.DestDefaults.ALL, [ + Gtk.TargetEntry.new('text/uri-list', 0, 0), + Gtk.TargetEntry.new('text/plain', 0, 0), + ], + Gdk.DragAction.MOVE | Gdk.DragAction.COPY) + attach_btn.connect('drag_data_received', + self.attach_drag_data_received) + + pos = gtktoolbar.get_item_index(self.buttons['email']) + iconstock = { + 'print': 'tryton-print', + 'action': 'tryton-launch', + 'relate': 'tryton-link', + 'open': 'tryton-open', + } + for action_type, special_action, action_name, tooltip in ( + ('action', 'action', _('Action'), _('Launch action')), + ('relate', 'relate', _('Relate'), _('Open related records')), + (None,) * 4, + ('print', 'open', _('Report'), _('Open report')), + ('print', 'print', _('Print'), _('Print report')), + ): + if action_type is not None: + tbutton = Gtk.ToggleToolButton() + tbutton.set_icon_widget(common.IconFactory.get_image( + iconstock.get(special_action), + Gtk.IconSize.LARGE_TOOLBAR)) + tbutton.set_label(action_name) + tbutton._menu = self._create_popup_menu(tbutton, + action_type, toolbars[action_type], special_action) + tbutton.connect('toggled', self.action_popup) + self.tooltips.set_tip(tbutton, tooltip) + self.buttons[special_action] = tbutton + if action_type != 'action': + tbutton._can_be_sensitive = bool( + tbutton._menu.get_children()) + else: + tbutton = Gtk.SeparatorToolItem() + gtktoolbar.insert(tbutton, pos) + pos += 1 + + exports = toolbars['exports'] + if exports: + tbutton = self.buttons['open'] + tbutton._can_be_sensitive = True + menu = tbutton._menu + if menu.get_children(): + menu.add(Gtk.SeparatorMenuItem()) + for export in exports: + menuitem = Gtk.MenuItem(set_underline(export['name'])) + menuitem.set_use_underline(True) + menuitem.connect('activate', self.do_export, export) + menu.add(menuitem) + + last_item = gtktoolbar.get_nth_item(gtktoolbar.get_n_items() - 1) + if not isinstance(last_item, Gtk.SeparatorToolItem): + gtktoolbar.insert(Gtk.SeparatorToolItem(), -1) + + url_button = Gtk.ToggleToolButton() + url_button.set_icon_widget( + common.IconFactory.get_image( + 'tryton-public', Gtk.IconSize.LARGE_TOOLBAR)) + url_button.set_label(_('_Copy URL')) + url_button.set_use_underline(True) + self.tooltips.set_tip( + url_button, _('Copy URL into clipboard')) + url_button._menu = url_menu = Gtk.Menu() + url_menuitem = Gtk.MenuItem() + url_menuitem.connect('activate', self.url_copy) + url_menu.add(url_menuitem) + url_menu.show_all() + url_menu.connect('deactivate', self._popup_menu_hide, url_button) + url_button.connect('toggled', self.url_set, url_menuitem) + url_button.connect('toggled', self.action_popup) + self.buttons['copy_url'] = url_button + gtktoolbar.insert(url_button, -1) + return gtktoolbar + + def _create_popup_menu(self, widget, keyword, actions, special_action): + menu = Gtk.Menu() + menu.connect('deactivate', self._popup_menu_hide, widget) + widget.connect('toggled', self._update_popup_menu, menu, keyword) + + for action in actions: + new_action = action.copy() + if special_action == 'print': + new_action['direct_print'] = True + menuitem = Gtk.MenuItem(label=set_underline(action['name'])) + menuitem.set_use_underline(True) + menuitem.connect('activate', self._popup_menu_selected, widget, + new_action, keyword) + menu.add(menuitem) + return menu + + def _popup_menu_selected(self, menuitem, togglebutton, action, keyword): + event = Gtk.get_current_event() + allow_similar = False + if (event.state & Gdk.ModifierType.CONTROL_MASK + or event.state & Gdk.ModifierType.MOD1_MASK): + allow_similar = True + with Window(hide_current=True, allow_similar=allow_similar): + self._action(action, keyword) + togglebutton.props.active = False + + def _popup_menu_hide(self, menuitem, togglebutton): + togglebutton.props.active = False + + def _update_popup_menu(self, tbutton, menu, keyword): + for item in menu.get_children(): + if getattr(item, '_update_action', False): + menu.remove(item) + + buttons = [b for b in self.screen.get_buttons() + if keyword == b.attrs.get('keyword', 'action')] + if buttons and menu.get_children(): + separator = Gtk.SeparatorMenuItem() + separator._update_action = True + menu.add(separator) + for button in buttons: + menuitem = Gtk.MenuItem( + label=set_underline(button.attrs.get('string', _('Unknown'))), + use_underline=True) + menuitem.connect('activate', + lambda m, attrs: self.screen.button(attrs), button.attrs) + menuitem._update_action = True + menu.add(menuitem) + + kw_plugins = [] + for plugin in plugins.MODULES: + for plugin_spec in plugin.get_plugins(self.model): + name, func = plugin_spec[:2] + try: + plugin_keyword = plugin_spec[2] + except IndexError: + plugin_keyword = 'action' + if keyword != plugin_keyword: + continue + kw_plugins.append((name, func)) + + if kw_plugins: + separator = Gtk.SeparatorMenuItem() + separator._update_action = True + menu.add(separator) + for name, func in kw_plugins: + menuitem = Gtk.MenuItem(label=set_underline(name)) + menuitem.set_use_underline(True) + menuitem.connect('activate', lambda m, func: func({ + 'model': self.screen.model_name, + 'model_context': ( + self.screen.context_screen.model_name + if self.screen.context_screen else None), + 'id': (self.screen.current_record.id + if self.screen.current_record else None), + 'ids': [r.id + for r in self.screen.selected_records], + 'paths': self.screen.selected_paths, + }), func) + menuitem._update_action = True + menu.add(menuitem) + + def url_copy(self, menuitem): + url = self.screen.get_url(self.name) + for selection in [ + Gdk.Atom.intern('PRIMARY', True), + Gdk.Atom.intern('CLIPBOARD', True), + ]: + clipboard = Gtk.Clipboard.get(selection) + clipboard.set_text(url, -1) + + def url_set(self, button, menuitem): + url = self.screen.get_url(self.name) + size = 80 + if len(url) > size: + url = url[:size // 2] + '...' + url[-size // 2:] + menuitem.set_label(url) + + def set_cursor(self): + if self.screen: + self.screen.set_cursor(reset_view=False) + + def attach_drag_data_received(self, widget, context, x, y, selection, info, + timestamp): + record = self.screen.current_record + if not record or record.id < 0: + return + win_attach = Attachment(record, + lambda: self.refresh_resources(reload=True)) + if info == 0: + if selection.get_uris(): + for uri in selection.get_uris(): + # Win32 cut&paste terminates the list with a NULL character + if not uri or uri == '\0': + continue + win_attach.add_uri(uri) + else: + win_attach.add_uri(selection.get_text()) diff --git a/tryton/tryton/gui/window/infobar.py b/tryton/tryton/gui/window/infobar.py new file mode 100644 index 0000000000..32b055d495 --- /dev/null +++ b/tryton/tryton/gui/window/infobar.py @@ -0,0 +1,35 @@ +# 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 gi.repository import Gtk + + +class InfoBar(object): + + toolbar = None + + def create_info_bar(self): + self.info_label = Gtk.Label() + + self.info_bar = Gtk.InfoBar() + self.info_bar.get_content_area().pack_start( + self.info_label, False, False, 0) + self.info_bar.set_show_close_button(True) + self.info_bar.connect('response', lambda i, r: i.hide()) + + def response(self, bar, response): + if response == Gtk.ResponseType.CLOSE: + bar.hide() + + def message_info(self, message=None, type_=Gtk.MessageType.ERROR): + if message: + # Work around https://bugzilla.gnome.org/show_bug.cgi?id=710888 + parent = self.info_bar.get_parent() + self.info_bar.unparent() + self.info_bar.set_parent(parent) + + self.info_label.set_label(message) + self.info_bar.set_message_type(type_) + self.info_bar.show_all() + else: + self.info_bar.hide() diff --git a/tryton/tryton/gui/window/limit.py b/tryton/tryton/gui/window/limit.py new file mode 100644 index 0000000000..a3a774f809 --- /dev/null +++ b/tryton/tryton/gui/window/limit.py @@ -0,0 +1,68 @@ +# 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 sys +import gettext + +from gi.repository import Gtk + +from tryton.common import get_toplevel_window, IconFactory +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON, CONFIG +from tryton.gui import Main + +_ = gettext.gettext + + +class Limit(object): + 'Set Search Limit' + + def __init__(self): + self.parent = get_toplevel_window() + self.win = Gtk.Dialog( + title=_('Limit'), transient_for=self.parent, modal=True, + destroy_with_parent=True) + Main().add_window(self.win) + cancel_button = self.win.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + cancel_button.set_image(IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + cancel_button.set_always_show_image(True) + ok_button = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + ok_button.set_image(IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + ok_button.set_always_show_image(True) + self.win.set_default_response(Gtk.ResponseType.OK) + self.win.set_icon(TRYTON_ICON) + self.win.vbox.set_spacing(3) + self.win.vbox.pack_start(Gtk.Label( + label=_('Search Limit Settings')), + expand=False, fill=True, padding=0) + self.win.vbox.pack_start( + Gtk.HSeparator(), expand=True, fill=True, padding=0) + hbox = Gtk.HBox(spacing=3) + label = Gtk.Label(label=_('Limit:')) + hbox.pack_start(label, expand=True, fill=True, padding=0) + adjustment = Gtk.Adjustment( + value=CONFIG['client.limit'], + lower=1, upper=sys.maxsize, + step_incr=10, page_incr=100) + self.spin_limit = Gtk.SpinButton() + self.spin_limit.configure(adjustment, climb_rate=1, digits=0) + self.spin_limit.set_numeric(False) + self.spin_limit.set_activates_default(True) + label.set_mnemonic_widget(self.spin_limit) + hbox.pack_start(self.spin_limit, expand=True, fill=True, padding=0) + self.win.vbox.pack_start(hbox, expand=True, fill=True, padding=0) + + self.win.show_all() + + def run(self): + 'Run the window' + res = self.win.run() + if res == Gtk.ResponseType.OK: + CONFIG['client.limit'] = self.spin_limit.get_value_as_int() + CONFIG.save() + self.parent.present() + self.win.destroy() + return res diff --git a/tryton/tryton/gui/window/nomodal.py b/tryton/tryton/gui/window/nomodal.py new file mode 100644 index 0000000000..cb28e8e9c7 --- /dev/null +++ b/tryton/tryton/gui/window/nomodal.py @@ -0,0 +1,43 @@ +# 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 tryton.common as common + + +class NoModal(object): + + def __init__(self): + self.parent = common.get_toplevel_window() + self.sensible_widget = common.get_sensible_widget(self.parent) + self.page = None + self.parent_focus = [] + focus = self.parent.get_focus() + while focus: + self.parent_focus.append(focus) + focus = focus.get_parent() + + def register(self): + from tryton.gui.main import Main + main = Main() + self.page = main.get_page() + if not self.page: + self.page = main + self.page.dialogs.append(self) + self.sensible_widget.props.sensitive = False + + def destroy(self): + if not self.page: + return + self.page.dialogs.remove(self) + self.parent.present() + self.sensible_widget.props.sensitive = True + for focus in self.parent_focus: + if focus and focus.is_ancestor(self.parent): + focus.grab_focus() + break + + def default_size(self): + from tryton.gui.main import Main + main = Main() + allocation = main.window.get_allocation() + width, height = allocation.width, allocation.height + return max(width - 150, 0), max(height - 150, 0) diff --git a/tryton/tryton/gui/window/note.py b/tryton/tryton/gui/window/note.py new file mode 100644 index 0000000000..73128a56e0 --- /dev/null +++ b/tryton/tryton/gui/window/note.py @@ -0,0 +1,38 @@ +# 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 gettext + +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_form import WinForm + +_ = gettext.gettext + + +class Note(WinForm): + "Note window" + + def __init__(self, record, callback=None): + self.resource = '%s,%s' % (record.model_name, record.id) + self.note_callback = callback + title = _('Notes (%s)') % (record.rec_name()) + screen = Screen('ir.note', domain=[ + ('resource', '=', self.resource), + ], mode=['tree', 'form']) + super(Note, self).__init__(screen, self.callback, view_type='tree', + title=title) + screen.search_filter() + + def destroy(self): + self.prev_view.save_width() + super(Note, self).destroy() + + def callback(self, result): + if result: + unread = self.screen.group.fields['unread'] + for record in self.screen.group: + if record.loaded or record.id < 0: + if 'unread' not in record.modified_fields: + unread.set_client(record, False) + self.screen.save_current() + if self.note_callback: + self.note_callback() diff --git a/tryton/tryton/gui/window/preference.py b/tryton/tryton/gui/window/preference.py new file mode 100644 index 0000000000..7161078419 --- /dev/null +++ b/tryton/tryton/gui/window/preference.py @@ -0,0 +1,112 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Preference" +import gettext +import copy + +from gi.repository import Gdk, Gtk + +import tryton.rpc as rpc +from tryton.common import RPCExecute, RPCException, IconFactory +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal +from tryton.gui.window.view_form.screen import Screen + +_ = gettext.gettext + + +class Preference(NoModal): + "Preference window" + + def __init__(self, user, callback): + NoModal.__init__(self) + self.callback = callback + self.win = Gtk.Dialog( + title=_('Preferences'), transient_for=self.parent, + destroy_with_parent=True) + Main().add_window(self.win) + self.win.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.win.set_icon(TRYTON_ICON) + + self.accel_group = Gtk.AccelGroup() + self.win.add_accel_group(self.accel_group) + + self.but_cancel = self.win.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + self.but_cancel.set_image( + IconFactory.get_image('tryton-cancel', Gtk.IconSize.BUTTON)) + self.but_cancel.set_always_show_image(True) + self.but_ok = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + self.but_ok.set_image( + IconFactory.get_image('tryton-ok', Gtk.IconSize.BUTTON)) + self.but_ok.set_always_show_image(True) + self.but_ok.add_accelerator( + 'clicked', self.accel_group, Gdk.KEY_Return, + Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) + + self.win.set_default_response(Gtk.ResponseType.OK) + self.win.connect('response', self.response) + + try: + view = RPCExecute('model', 'res.user', + 'get_preferences_fields_view') + except RPCException: + self.win.destroy() + self.win = None + return + + title = Gtk.Label(label=_('Edit User Preferences')) + title.show() + self.win.vbox.pack_start(title, expand=False, fill=True, padding=0) + self.screen = Screen('res.user', mode=[]) + # Reset readonly set automaticly by MODELACCESS + self.screen.readonly = False + self.screen.group.readonly = False + self.screen.group.skip_model_access = True + self.screen.add_view(view) + self.screen.switch_view() + self.screen.new(default=False) + + try: + preferences = RPCExecute('model', 'res.user', 'get_preferences', + False) + except RPCException: + self.win.destroy() + self.win = None + return + self.screen.current_record.cancel() + self.screen.current_record.set(preferences) + self.screen.current_record.id = rpc._USER + self.screen.current_record.validate(softvalidation=True) + self.screen.display(set_cursor=True) + + self.screen.widget.show() + self.win.vbox.pack_start( + self.screen.widget, expand=True, fill=True, padding=0) + self.win.set_title(_('Preference')) + + self.win.set_default_size(*self.default_size()) + + self.register() + self.win.show() + + def response(self, win, response_id): + if response_id == Gtk.ResponseType.OK: + if self.screen.current_record.validate(): + vals = copy.copy(self.screen.get()) + try: + RPCExecute('model', 'res.user', 'set_preferences', vals) + except RPCException: + return + rpc.context_reset() + self.parent.present() + self.destroy() + self.callback() + + def destroy(self): + self.screen.destroy() + self.win.destroy() + NoModal.destroy(self) diff --git a/tryton/tryton/gui/window/revision.py b/tryton/tryton/gui/window/revision.py new file mode 100644 index 0000000000..8b9fb16719 --- /dev/null +++ b/tryton/tryton/gui/window/revision.py @@ -0,0 +1,120 @@ +# 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 gettext + +from gi.repository import Gtk + +from tryton.common import (get_toplevel_window, IconFactory, + timezoned_date, untimezoned_date) +from tryton.common.datetime_ import date_parse +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON +from tryton.gui import Main + +_ = gettext.gettext + + +class Revision(object): + 'Ask revision' + + def __init__(self, revisions, revision=None, format_='%x %H:%M:%S.%f'): + self.parent = get_toplevel_window() + self.win = Gtk.Dialog( + title=_('Revision'), transient_for=self.parent, modal=True, + destroy_with_parent=True) + Main().add_window(self.win) + cancel_button = self.win.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + cancel_button.set_image(IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + cancel_button.set_always_show_image(True) + ok_button = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + ok_button.set_image(IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + ok_button.set_always_show_image(True) + self.win.set_default_response(Gtk.ResponseType.OK) + self.win.set_icon(TRYTON_ICON) + self.win.vbox.set_spacing(3) + self.win.vbox.pack_start(Gtk.Label( + label=_('Select a revision')), + expand=False, fill=True, padding=0) + self.win.vbox.pack_start( + Gtk.HSeparator(), expand=True, fill=True, padding=0) + hbox = Gtk.HBox(spacing=3) + label = Gtk.Label(label=_('Revision:')) + hbox.pack_start(label, expand=True, fill=True, padding=0) + list_store = Gtk.ListStore(str, str) + # Set model on instantiation to get the default cellrenderer as text + combobox = Gtk.ComboBox(model=list_store, has_entry=True) + self.entry = combobox.get_child() + self.entry.connect('focus-out-event', self.focus_out) + self.entry.connect('activate', self.activate) + label.set_mnemonic_widget(self.entry) + combobox.connect('changed', self.changed) + self.entry.set_property('activates_default', True) + self._format = format_ + if revision: + self.entry.set_text(revision.strftime(self._format)) + self._value = revision + active = -1 + else: + self._value = None + active = 0 + list_store.append(('', '')) + for i, (rev, id_, name) in enumerate(revisions, 1): + list_store.append( + (timezoned_date(rev).strftime(self._format), name)) + if rev == revision: + active = i + combobox.set_active(active) + cell = Gtk.CellRendererText() + combobox.pack_start(cell, expand=True) + combobox.add_attribute(cell, 'text', 1) + hbox.pack_start(combobox, expand=True, fill=True, padding=0) + combobox.set_entry_text_column(0) + self.win.vbox.pack_start(hbox, expand=True, fill=True, padding=0) + self.win.show_all() + + def focus_out(self, entry, event): + self.parse() + self.update() + return False + + def activate(self, entry): + self.parse() + self.update() + return False + + def changed(self, combobox): + # "changed" signal is also triggered by text editing + # so only parse when a row is active + if combobox.get_active_iter(): + self.parse() + self.update() + return False + + def parse(self): + text = self.entry.get_text() + value = None + if text: + try: + value = untimezoned_date(date_parse(text, self._format)) + except ValueError: + pass + self._value = value + + def update(self): + if not self._value: + self.entry.set_text('') + else: + self.entry.set_text(self._value.strftime(self._format)) + + def run(self): + response = self.win.run() + revision = None + if response == Gtk.ResponseType.OK: + revision = self._value + self.parent.present() + self.win.destroy() + return revision diff --git a/tryton/tryton/gui/window/tabcontent.py b/tryton/tryton/gui/window/tabcontent.py new file mode 100644 index 0000000000..cece3c7eda --- /dev/null +++ b/tryton/tryton/gui/window/tabcontent.py @@ -0,0 +1,320 @@ +# 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 gettext + +from gi.repository import Gtk, Pango + +import tryton.common as common +from tryton.config import CONFIG +from tryton.common.widget_style import widget_class +from tryton.gui import Main +from .infobar import InfoBar + +_ = gettext.gettext + + +class ToolbarItem(object): + def __init__(self, id, label, + tooltip=None, icon_name=None, accel_path=None, toggle=False): + self.id = id + self.label = label + self.tooltip = tooltip + self.icon_name = icon_name + self.accel_path = accel_path + self.toggle = toggle + + @property + def menu(self): + return True + + @property + def toolbar(self): + return bool(self.tooltip) + + +class TabContent(InfoBar): + + def __init__(self, **attributes): + super(TabContent, self).__init__() + self.attributes = attributes.copy() + + @property + def menu_def(self): + return [ + ToolbarItem( + id='switch', + label=_("_Switch View"), + tooltip=_("Switch View"), + icon_name='tryton-switch', + accel_path='/Form/Switch View'), + ToolbarItem( + id='previous', + label=_("_Previous"), + tooltip=_("Previous Record"), + icon_name='tryton-back', + accel_path='/Form/Previous'), + ToolbarItem( + id='next', + label=_("_Next"), + tooltip=_("Next Record"), + icon_name='tryton-forward', + accel_path='/Form/Next'), + ToolbarItem( + id='search', + label=_("_Search"), + icon_name='tryton-search', + accel_path='/Form/Search'), + None, + ToolbarItem( + id='new', + label=_("_New"), + tooltip=_("Create a new record"), + icon_name='tryton-create', + accel_path='/Form/New'), + ToolbarItem( + id='save', + label=_("_Save"), + tooltip=_("Save this record"), + icon_name='tryton-save', + accel_path='/Form/Save'), + ToolbarItem( + id='reload', + label=_("_Reload/Undo"), + tooltip=_("Reload/Undo"), + icon_name='tryton-refresh', + accel_path='/Form/Reload'), + ToolbarItem( + id='copy', + label=_("_Duplicate"), + icon_name='tryton-copy', + accel_path='/Form/Duplicate'), + ToolbarItem( + id='remove', + label=_("_Delete..."), + icon_name='tryton-delete', + accel_path='/Form/Delete'), + None, + ToolbarItem( + id='logs', + label=_("View _Logs..."), + icon_name='tryton-log'), + ToolbarItem( + id='revision' if self.model in common.MODELHISTORY else None, + label=_("Show revisions..."), + icon_name='tryton-history'), + None, + ToolbarItem( + id='attach', + label=_("A_ttachments..."), + tooltip=_("Add an attachment to the record"), + icon_name='tryton-attach', + accel_path='/Form/Attachments', + toggle=True), + ToolbarItem( + id='note', + label=_("_Notes..."), + tooltip=_("Add a note to the record"), + icon_name='tryton-note', + accel_path='/Form/Notes'), + ToolbarItem( + id='action', + label=_("_Actions..."), + icon_name='tryton-launch', + accel_path='/Form/Actions'), + ToolbarItem( + id='relate', + label=_("_Relate..."), + icon_name='tryton-link', + accel_path='/Form/Relate'), + None, + ToolbarItem( + id='print_open', + label=_("_Report..."), + icon_name='tryton-open', + accel_path='/Form/Report'), + ToolbarItem( + id='print', + label=_("_Print..."), + icon_name='tryton-print', + accel_path='/Form/Print'), + ToolbarItem( + id='email', + label=_("_E-Mail..."), + tooltip=_("Send an e-mail using the record"), + icon_name='tryton-email', + accel_path='/Form/Email'), + None, + ToolbarItem( + id='export', + label=_("_Export Data..."), + icon_name='tryton-export', + accel_path='/Form/Export Data'), + ToolbarItem( + id='import', + label=_("_Import Data..."), + icon_name='tryton-import', + accel_path='/Form/Import Data'), + ToolbarItem( + id='copy_url', + label=_("Copy _URL..."), + icon_name='tryton-public', + accel_path='/Form/Copy URL'), + None, + ToolbarItem( + id='win_close', + label=_("_Close Tab"), + icon_name='tryton-close', + accel_path='/Form/Close'), + ] + + def create_tabcontent(self): + self.buttons = {} + self.menu_buttons = {} + self.tooltips = common.Tooltips() + self.accel_group = Main().accel_group + + self.widget = Gtk.VBox(spacing=3) + self.widget.show() + + title_box = self.make_title_bar() + self.widget.pack_start(title_box, expand=False, fill=True, padding=3) + + self.toolbar = self.create_toolbar(self.get_toolbars()) + self.toolbar.show_all() + self.widget.pack_start( + self.toolbar, expand=False, fill=True, padding=0) + + self.main = Gtk.HPaned() + self.main.show() + self.widget.pack_start( + self.main, expand=True, fill=True, padding=0) + + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + viewport.add(self.widget_get()) + viewport.show() + self.scrolledwindow = Gtk.ScrolledWindow() + self.scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + self.scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.scrolledwindow.add(viewport) + self.scrolledwindow.show() + self.main.pack1(self.scrolledwindow, resize=True, shrink=False) + + self.create_info_bar() + self.widget.pack_start( + self.info_bar, expand=False, fill=True, padding=0) + + def make_title_bar(self): + tooltips = common.Tooltips() + self.title = title = Gtk.Label( + label=common.ellipsize(self.name, 80), + halign=Gtk.Align.START, margin=5, + ellipsize=Pango.EllipsizeMode.END) + tooltips.set_tip(title, self.name) + title.set_size_request(0, -1) # Allow overflow + title.show() + + menu = Gtk.MenuButton.new() + menu.set_relief(Gtk.ReliefStyle.NONE) + menu.set_popup(self.set_menu_form()) + menu.show() + + self.status_label = Gtk.Label( + margin=5, halign=Gtk.Align.END) + widget_class(self.status_label, 'status', True) + self.status_label.show() + + hbox = Gtk.HBox() + hbox.pack_start(title, expand=True, fill=True, padding=0) + hbox.pack_start(self.status_label, expand=False, fill=True, padding=0) + hbox.show() + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + widget_class(frame, 'window-title', True) + frame.add(hbox) + frame.show() + + frame_menu = Gtk.Frame() + frame_menu.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + frame_menu.add(menu) + frame_menu.show() + + title_box = Gtk.HBox() + title_box.pack_start(frame_menu, expand=False, fill=True, padding=0) + title_box.pack_start(frame, expand=True, fill=True, padding=0) + title_box.show() + return title_box + + def create_base_toolbar(self, toolbar): + previous = None + for item in self.menu_def: + if item and item.toolbar: + callback = getattr(self, 'sig_%s' % item.id, None) + if not callback: + continue + if item.toggle: + toolitem = Gtk.ToggleToolButton() + toolitem.connect('toggled', callback) + else: + toolitem = Gtk.ToolButton() + toolitem.connect('clicked', callback) + toolitem.set_icon_widget( + common.IconFactory.get_image( + item.icon_name, Gtk.IconSize.LARGE_TOOLBAR)) + toolitem.set_label(item.label) + toolitem.set_use_underline(True) + self.tooltips.set_tip(toolitem, item.tooltip) + self.buttons[item.id] = toolitem + elif not item and previous: + toolitem = Gtk.SeparatorToolItem() + else: + continue + previous = item + toolbar.insert(toolitem, -1) + + def set_menu_form(self): + menu_form = Gtk.Menu() + menu_form.set_accel_group(self.accel_group) + menu_form.set_accel_path('/Form') + previous = None + for item in self.menu_def: + if item and item.menu: + callback = getattr(self, 'sig_%s' % item.id, None) + if not callback: + continue + menuitem = Gtk.MenuItem( + label=item.label, + use_underline=True) + menuitem.connect('activate', callback) + if item.accel_path: + menuitem.set_accel_path(item.accel_path) + self.menu_buttons[item.id] = menuitem + elif not item and previous: + menuitem = Gtk.SeparatorMenuItem() + else: + continue + previous = item + menu_form.add(menuitem) + + menu_form.show_all() + return menu_form + + def create_toolbar(self, toolbars): + gtktoolbar = Gtk.Toolbar() + option = CONFIG['client.toolbar'] + if option == 'default': + gtktoolbar.set_style(False) + elif option == 'both': + gtktoolbar.set_style(Gtk.ToolbarStyle.BOTH) + elif option == 'text': + gtktoolbar.set_style(Gtk.ToolbarStyle.TEXT) + elif option == 'icons': + gtktoolbar.set_style(Gtk.ToolbarStyle.ICONS) + self.create_base_toolbar(gtktoolbar) + return gtktoolbar + + def compare(self, model, attributes): + return False diff --git a/tryton/tryton/gui/window/view_board/__init__.py b/tryton/tryton/gui/window/view_board/__init__.py new file mode 100644 index 0000000000..f566de670d --- /dev/null +++ b/tryton/tryton/gui/window/view_board/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from .view_board import ViewBoard + +__all__ = [ViewBoard] diff --git a/tryton/tryton/gui/window/view_board/action.py b/tryton/tryton/gui/window/view_board/action.py new file mode 100644 index 0000000000..145dfc545d --- /dev/null +++ b/tryton/tryton/gui/window/view_board/action.py @@ -0,0 +1,164 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +'Action' +import gettext + +from gi.repository import GLib, Gtk + +from tryton.gui.window.view_form.screen import Screen +import tryton.rpc as rpc +import tryton.common as common +from tryton.config import CONFIG +from tryton.pyson import PYSONDecoder +from tryton.signal_event import SignalEvent +from tryton.gui.window.win_form import WinForm +from tryton.common import RPCExecute, RPCException +from tryton.action import Action as GenericAction +_ = gettext.gettext + + +class Action(SignalEvent): + + def __init__(self, attrs=None, context=None): + if context is None: + context = {} + super(Action, self).__init__() + self.name = attrs['name'] + + try: + self.action = RPCExecute('model', 'ir.action.act_window', 'get', + self.name) + except RPCException: + raise + + view_ids = [] + view_mode = None + if self.action.get('views', []): + view_ids = [x[0] for x in self.action['views']] + view_mode = [x[1] for x in self.action['views']] + elif self.action.get('view_id', False): + view_ids = [self.action['view_id'][0]] + + self.action.setdefault('pyson_domain', '[]') + ctx = {} + ctx.update(rpc.CONTEXT) + ctx['_user'] = rpc._USER + decoder = PYSONDecoder(ctx) + action_ctx = context.copy() + action_ctx.update( + decoder.decode(self.action.get('pyson_context') or '{}')) + ctx.update(action_ctx) + + ctx['context'] = ctx + decoder = PYSONDecoder(ctx) + self.domain = decoder.decode(self.action['pyson_domain']) + order = decoder.decode(self.action['pyson_order']) + search_value = decoder.decode( + self.action['pyson_search_value'] or '[]') + tab_domain = [(n, decoder.decode(d), c) + for n, d, c in self.action['domains']] + + limit = self.action.get('limit') + if limit is None: + limit = CONFIG['client.limit'] + + self.context = ctx + self.domain = [] + self.update_domain([]) + + self.widget = Gtk.Frame() + self.widget.set_border_width(0) + + vbox = Gtk.VBox(homogeneous=False, spacing=3) + self.widget.add(vbox) + + self.title = Gtk.Label() + self.widget.set_label_widget(self.title) + self.widget.set_label_align(0.0, 0.5) + self.widget.show_all() + + self.screen = Screen(self.action['res_model'], + view_ids=view_ids, + domain=self.domain, + context=action_ctx, + order=order, + mode=view_mode, + limit=limit, + search_value=search_value, + tab_domain=tab_domain, + context_model=self.action['context_model'], + context_domain=self.action['context_domain'], + row_activate=self.row_activate) + vbox.pack_start( + self.screen.widget, expand=True, fill=True, padding=0) + self.screen.signal_connect(self, 'record-message', + self._active_changed) + + if attrs.get('string'): + self.title.set_text(attrs['string']) + else: + self.title.set_text(self.action['name']) + + self.widget.set_size_request(int(attrs.get('width', -1)), + int(attrs.get('height', -1))) + + self.screen.search_filter() + + def row_activate(self): + if not self.screen.current_record: + return + + if (self.screen.current_view.view_type == 'tree' + and int(self.screen.current_view.attributes.get( + 'keyword_open', 0))): + GenericAction.exec_keyword('tree_open', { + 'model': self.screen.model_name, + 'id': (self.screen.current_record.id + if self.screen.current_record else None), + 'ids': [r.id for r in self.screen.selected_records], + }, context=self.screen.group._context.copy(), + warning=False) + else: + def callback(result): + if result: + self.screen.current_record.save() + else: + self.screen.current_record.cancel() + WinForm(self.screen, callback, title=self.title.get_text()) + + def set_value(self, mode, model_field): + self.screen.current_view.set_value() + return True + + def display(self): + self.screen.search_filter(self.screen.screen_container.get_text()) + + def _active_changed(self, *args): + self.signal('active-changed') + + def _get_active(self): + if self.screen and self.screen.current_record: + return common.EvalEnvironment(self.screen.current_record) + + active = property(_get_active) + + def update_domain(self, actions): + domain_ctx = self.context.copy() + domain_ctx['context'] = domain_ctx + domain_ctx['_user'] = rpc._USER + for action in actions: + if action.active: + domain_ctx[action.name] = action.active + new_domain = PYSONDecoder(domain_ctx).decode( + self.action['pyson_domain']) + if self.domain == new_domain: + return + del self.domain[:] + self.domain.extend(new_domain) + if hasattr(self, 'screen'): # Catch early update + # Using idle_add to prevent corruption of the event who triggered + # the update. + def display(): + if self.screen.widget.props.window: + self.display() + GLib.idle_add(display) diff --git a/tryton/tryton/gui/window/view_board/view_board.py b/tryton/tryton/gui/window/view_board/view_board.py new file mode 100644 index 0000000000..8b21e96ce9 --- /dev/null +++ b/tryton/tryton/gui/window/view_board/view_board.py @@ -0,0 +1,59 @@ +# 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 gettext + +from tryton.gui.window.view_form.view.form import Container, FormXMLViewParser +from tryton.common import node_attributes +from .action import Action + +_ = gettext.gettext + + +class BoardXMLViewParser(FormXMLViewParser): + + def _parse_board(self, node, attributes): + container_attributes = node_attributes(node) + container = Container.constructor( + int(container_attributes.get('col', 4)), + container_attributes.get('homogeneous', False)) + self.view.widget = container.container + self.parse_child(node, container) + assert not self._containers + + def _parse_action(self, node, attributes): + attributes.setdefault('yexpand', True) + attributes.setdefault('yfill', True) + action = Action(attributes, self.view.context) + action.signal_connect( + self.view, 'active-changed', self.view._active_changed) + self.view.actions.append(action) + self.container.add(action.widget, attributes) + + +class ViewBoard(object): + 'View board' + widget = None + xml_parser = BoardXMLViewParser + + def __init__(self, xml, context=None): + self.context = context + self.actions = [] + self.state_widgets = [] + self.xml_parser(self, None, {}).parse(xml) + self.widget.show_all() + self._active_changed(None) + + def widget_get(self): + return self.widget + + def reload(self): + for action in self.actions: + action.display() + for state_widget in self.state_widgets: + state_widget.state_set(None) + + def _active_changed(self, event_action, *args): + for action in self.actions: + if action == event_action: + continue + action.update_domain(self.actions) diff --git a/tryton/tryton/gui/window/view_form/__init__.py b/tryton/tryton/gui/window/view_form/__init__.py new file mode 100644 index 0000000000..360cfd1aa9 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from . import screen + +__all__ = [screen] diff --git a/tryton/tryton/gui/window/view_form/model/__init__.py b/tryton/tryton/gui/window/view_form/model/__init__.py new file mode 100644 index 0000000000..4effdfa4a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/model/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/tryton/tryton/gui/window/view_form/model/field.py b/tryton/tryton/gui/window/view_form/model/field.py new file mode 100644 index 0000000000..d4355af606 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/model/field.py @@ -0,0 +1,1169 @@ +# 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 os +from itertools import chain +import tempfile +import logging +import locale +from tryton.common import \ + domain_inversion, eval_domain, localize_domain, \ + merge, inverse_leaf, filter_leaf, prepare_reference_domain, \ + extract_reference_models, concat, simplify, unique_value, \ + EvalEnvironment +import tryton.common as common +import datetime +import decimal +from decimal import Decimal +import math +from tryton.common import RPCExecute, RPCException +from tryton.common.htmltextbuffer import guess_decode +from tryton.pyson import PYSONDecoder +from tryton.config import CONFIG + +logger = logging.getLogger(__name__) + + +class Field(object): + ''' + get: return the values to write to the server + get_client: return the value for the client widget (form_gtk) + set: save the value from the server + set_client: save the value from the widget + ''' + _default = None + + @staticmethod + def get_field(ctype): + return TYPES.get(ctype, CharField) + + def __init__(self, attrs): + self.attrs = attrs + self.name = attrs['name'] + self.views = set() + + def sig_changed(self, record): + record.on_change([self.name]) + record.on_change_with([self.name]) + record.autocomplete_with(self.name) + record.set_field_context() + + def domains_get(self, record, pre_validate=None): + screen_domain = domain_inversion( + [record.group.domain4inversion, pre_validate or []], + self.name, EvalEnvironment(record)) + if isinstance(screen_domain, bool) and not screen_domain: + screen_domain = [('id', '=', None)] + elif isinstance(screen_domain, bool) and screen_domain: + screen_domain = [] + attr_domain = record.expr_eval(self.attrs.get('domain', [])) + return screen_domain, attr_domain + + def domain_get(self, record): + screen_domain, attr_domain = self.domains_get(record) + return concat(localize_domain(screen_domain), attr_domain) + + def validation_domains(self, record, pre_validate=None): + return concat(*self.domains_get(record, pre_validate)) + + def get_context(self, record, record_context=None, local=False): + if record_context is not None: + context = record_context.copy() + else: + context = record.get_context(local=local) + context.update(record.expr_eval(self.attrs.get('context', {}))) + return context + + def get_search_context(self, record): + context = self.get_context(record) + context.update(record.expr_eval(self.attrs.get('search_context', {}))) + return context + + def get_search_order(self, record): + order = record.expr_eval(self.attrs.get('search_order', None)) + return order + + def _is_empty(self, record): + return not self.get_eval(record) + + def check_required(self, record): + state_attrs = self.get_state_attrs(record) + if bool(int(state_attrs.get('required') or 0)): + if (self._is_empty(record) + and not bool(int(state_attrs.get('readonly') or 0))): + return False + return True + + def validate(self, record, softvalidation=False, pre_validate=None): + if self.attrs.get('readonly'): + return True + invalid = False + self.get_state_attrs(record)['domain_readonly'] = False + domain = simplify(self.validation_domains(record, pre_validate)) + if not softvalidation: + if not self.check_required(record): + invalid = 'required' + if isinstance(domain, bool): + if not domain: + invalid = 'domain' + elif domain == [('id', '=', None)]: + invalid = 'domain' + else: + unique, leftpart, value = unique_value(domain) + if unique: + # If the inverted domain is so constraint that only one value + # is possible we should use it. But we must also pay attention + # to the fact that the original domain might be a 'OR' domain + # and thus not preventing the modification of fields. + if value is False: + # XXX to remove once server domains are fixed + value = None + setdefault = True + if record.group.domain: + original_domain = merge(record.group.domain) + else: + original_domain = merge(domain) + domain_readonly = original_domain[0] == 'AND' + if '.' in leftpart: + recordpart, localpart = leftpart.split('.', 1) + constraintfields = set() + if domain_readonly: + for leaf in localize_domain(original_domain[1:]): + constraintfields.add(leaf[0]) + if localpart != 'id' or recordpart not in constraintfields: + setdefault = False + if setdefault and not pre_validate: + self.set_client(record, value) + self.get_state_attrs(record)['domain_readonly'] = ( + domain_readonly) + if not eval_domain(domain, EvalEnvironment(record)): + invalid = domain + self.get_state_attrs(record)['invalid'] = invalid + return not invalid + + def set(self, record, value): + record.value[self.name] = value + + def get(self, record): + return record.value.get(self.name, self._default) + + def get_eval(self, record): + return self.get(record) + + def get_on_change_value(self, record): + return self.get_eval(record) + + def set_client(self, record, value, force_change=False): + previous_value = self.get(record) + self.set(record, value) + if previous_value != self.get(record): + record.modified_fields.setdefault(self.name) + record.signal('record-modified') + self.sig_changed(record) + record.validate(softvalidation=True) + record.signal('record-changed') + elif force_change: + self.sig_changed(record) + record.validate(softvalidation=True) + record.signal('record-changed') + + def get_client(self, record): + return self.get(record) + + def set_default(self, record, value): + self.set(record, value) + record.modified_fields.setdefault(self.name) + + def set_on_change(self, record, value): + record.modified_fields.setdefault(self.name) + self.set(record, value) + + def state_set(self, record, states=('readonly', 'required', 'invisible')): + state_changes = record.expr_eval(self.attrs.get('states', {})) + for key in states: + if key == 'readonly' and self.attrs.get(key, False): + continue + if key in state_changes: + self.get_state_attrs(record)[key] = state_changes[key] + elif key in self.attrs: + self.get_state_attrs(record)[key] = self.attrs[key] + if (record.group.readonly + or self.get_state_attrs(record).get('domain_readonly')): + self.get_state_attrs(record)['readonly'] = True + + def get_state_attrs(self, record): + if self.name not in record.state_attrs: + record.state_attrs[self.name] = self.attrs.copy() + if record.group.readonly or record.readonly: + record.state_attrs[self.name]['readonly'] = True + return record.state_attrs[self.name] + + def get_timestamp(self, record): + return {} + + +class CharField(Field): + _default = '' + + def get(self, record): + return super(CharField, self).get(record) or self._default + + def set_client(self, record, value, force_change=False): + if isinstance(value, bytes): + try: + value = guess_decode(value) + except UnicodeDecodeError: + logger.warning( + "The encoding can not be guessed for field '%(name)s'", + {'name': self.name}) + value = None + super().set_client(record, value, force_change) + + +class SelectionField(Field): + + _default = None + + +class MultiSelectionField(Field): + + _default = None + + def get(self, record): + value = super().get(record) + if not value: + value = self._default + else: + value.sort() + return value + + def get_eval(self, record): + value = super().get_eval(record) + if value is None: + value = [] + return value + + def set_client(self, record, value, force_change=False): + if value: + value = sorted(value) + super().set_client(record, value, force_change=force_change) + + +class DateTimeField(Field): + + _default = None + + def set_client(self, record, value, force_change=False): + if isinstance(value, datetime.time): + current_value = self.get_client(record) + if current_value: + value = datetime.datetime.combine( + current_value.date(), value) + else: + value = None + elif value and not isinstance(value, datetime.datetime): + current_value = self.get_client(record) + if current_value: + time = current_value.time() + else: + time = datetime.time() + value = datetime.datetime.combine(value, time) + if value: + value = common.untimezoned_date(value) + super(DateTimeField, self).set_client(record, value, + force_change=force_change) + + def get_client(self, record): + value = super(DateTimeField, self).get_client(record) + if value: + return common.timezoned_date(value) + + def date_format(self, record): + context = self.get_context(record) + return common.date_format(context.get('date_format')) + + def time_format(self, record): + return record.expr_eval(self.attrs['format']) + + +class DateField(Field): + + _default = None + + def set_client(self, record, value, force_change=False): + if isinstance(value, datetime.datetime): + assert(value.time() == datetime.time()) + value = value.date() + super(DateField, self).set_client(record, value, + force_change=force_change) + + def date_format(self, record): + context = self.get_context(record) + return common.date_format(context.get('date_format')) + + +class TimeField(Field): + + _default = None + + def _is_empty(self, record): + return self.get(record) is None + + def set_client(self, record, value, force_change=False): + if isinstance(value, datetime.datetime): + value = value.time() + super(TimeField, self).set_client(record, value, + force_change=force_change) + + def time_format(self, record): + return record.expr_eval(self.attrs['format']) + + +class TimeDeltaField(Field): + + _default = None + + def _is_empty(self, record): + return self.get(record) is None + + def converter(self, group): + return group.context.get(self.attrs.get('converter')) + + def set_client(self, record, value, force_change=False): + if isinstance(value, str): + value = common.timedelta.parse(value, self.converter(record.group)) + super(TimeDeltaField, self).set_client( + record, value, force_change=force_change) + + def get_client(self, record): + value = super(TimeDeltaField, self).get_client(record) + return common.timedelta.format(value, self.converter(record.group)) + + +class FloatField(Field): + _default = None + + def _is_empty(self, record): + return self.get(record) is None + + def get(self, record): + return record.value.get(self.name, self._default) + + def digits(self, record, factor=1): + digits = record.expr_eval(self.attrs.get('digits')) + if not digits or any(d is None for d in digits): + return + shift = int(round(math.log(abs(factor), 10))) + return (digits[0] + shift, digits[1] - shift) + + def get_symbol(self, record, symbol): + if record and symbol in record.group.fields: + value = self.get(record) or 0 + sign = 1 + if value < 0: + sign = -1 + elif value == 0: + sign = 0 + symbol_field = record.group.fields[symbol] + symbol_name = symbol_field.attrs.get('relation') + symbol_id = symbol_field.get(record) + if symbol_name and symbol_id is not None and symbol_id >= 0: + try: + return RPCExecute( + 'model', symbol_name, 'get_symbol', symbol_id, sign, + context=record.get_context()) + except RPCException: + logger.warn( + "Fail to fetch symbol for %s,%s", + symbol_name, symbol_id) + return '', 1 + + def convert(self, value): + try: + return locale.atof(value) + except ValueError: + return self._default + + def set_client(self, record, value, force_change=False, factor=1): + if isinstance(value, str): + value = self.convert(value) + if value is not None: + # Keep the same type + if isinstance(value, int): + value //= factor + else: + value /= factor + super(FloatField, self).set_client(record, value, + force_change=force_change) + + def get_client(self, record, factor=1): + value = record.value.get(self.name) + if value is not None: + digits = self.digits(record, factor=factor) + d = value * factor + if not isinstance(d, Decimal): + d = Decimal(repr(d)) + if digits: + p = int(digits[1]) + else: + p = -int(d.as_tuple().exponent) + return locale.localize('{0:.{1}f}'.format(d, p), True) + else: + return '' + + +class NumericField(FloatField): + + def convert(self, value): + try: + return Decimal(locale.delocalize(value)) + except decimal.InvalidOperation: + return self._default + + def set_client(self, record, value, force_change=False, factor=1): + return super(NumericField, self).set_client(record, value, + force_change=force_change, factor=Decimal(str(factor))) + + def get_client(self, record, factor=1): + return super(NumericField, self).get_client(record, + factor=Decimal(str(factor))) + + +class IntegerField(FloatField): + + def convert(self, value): + try: + return locale.atoi(value) + except ValueError: + return self._default + + def set_client(self, record, value, force_change=False, factor=1): + return super(IntegerField, self).set_client(record, value, + force_change=force_change, factor=int(factor)) + + def get_client(self, record, factor=1): + return super(IntegerField, self).get_client(record, factor=int(factor)) + + +class BooleanField(Field): + + _default = False + + def set_client(self, record, value, force_change=False): + value = bool(value) + super(BooleanField, self).set_client(record, value, + force_change=force_change) + + def get(self, record): + return bool(record.value.get(self.name)) + + def get_client(self, record): + return bool(record.value.get(self.name)) + + +class M2OField(Field): + ''' + internal = (id, name) + ''' + + _default = None + + def _is_empty(self, record): + return self.get(record) is None + + def get_client(self, record): + rec_name = record.value.get(self.name + '.', {}).get('rec_name') + if rec_name is None: + self.set(record, self.get(record)) + rec_name = record.value.get(self.name + '.', {}).get('rec_name') + return rec_name or '' + + def set_client(self, record, value, force_change=False): + if isinstance(value, (tuple, list)): + value, rec_name = value + else: + if value == self.get(record): + rec_name = record.value.get( + self.name + '.', {}).get('rec_name', '') + else: + rec_name = '' + record.value.setdefault(self.name + '.', {})['rec_name'] = rec_name + super(M2OField, self).set_client(record, value, + force_change=force_change) + + def set(self, record, value): + rec_name = record.value.get(self.name + '.', {}).get('rec_name') or '' + if not rec_name and value is not None and value >= 0: + try: + result, = RPCExecute('model', self.attrs['relation'], 'read', + [value], ['rec_name']) + except RPCException: + return + rec_name = result['rec_name'] or '' + record.value.setdefault(self.name + '.', {})['rec_name'] = rec_name + record.value[self.name] = value + + def get_context(self, record, record_context=None, local=False): + context = super(M2OField, self).get_context( + record, record_context=record_context, local=local) + if self.attrs.get('datetime_field'): + context['_datetime'] = record.get_eval( + )[self.attrs.get('datetime_field')] + return context + + def validation_domains(self, record, pre_validate=None): + screen_domain, attr_domain = self.domains_get(record, pre_validate) + return screen_domain + + def domain_get(self, record): + screen_domain, attr_domain = self.domains_get(record) + return concat(localize_domain(inverse_leaf(screen_domain), self.name), + attr_domain) + + def get_on_change_value(self, record): + if record.parent_name == self.name and record.parent: + return record.parent.get_on_change_value( + skip={record.group.child_name}) + return super(M2OField, self).get_on_change_value(record) + + +class O2OField(M2OField): + pass + + +class O2MField(Field): + ''' + internal = Group of the related objects + ''' + + _default = None + + def __init__(self, attrs): + super(O2MField, self).__init__(attrs) + + def _group_changed(self, group, record): + if not record.parent: + return + # Store parent as it could be removed by validation + parent = record.parent + parent.modified_fields.setdefault(self.name) + self.sig_changed(parent) + parent.validate(softvalidation=True) + parent.signal('record-changed') + + def _group_list_changed(self, group, signal): + if group.model_name == group.parent.model_name: + group.parent.group.signal('group-list-changed', signal) + + def _group_cleared(self, group, signal): + if group.model_name == group.parent.model_name: + group.parent.signal('group-cleared') + + def _record_modified(self, group, record): + if not record.parent: + return + record.parent.signal('record-modified') + + def _set_default_value(self, record, fields=None): + if record.value.get(self.name) is not None: + return + from .group import Group + parent_name = self.attrs.get('relation_field', '') + fields = fields or {} + context = record.expr_eval(self.attrs.get('context', {})) + group = Group(self.attrs['relation'], fields, + parent=record, + parent_name=parent_name, + child_name=self.name, + context=context, + parent_datetime_field=self.attrs.get('datetime_field')) + if not fields and record.model_name == self.attrs['relation']: + group.fields = record.group.fields + record.value[self.name] = group + self._connect_value(group) + + def _connect_value(self, group): + group.signal_connect(group, 'group-changed', self._group_changed) + group.signal_connect(group, 'group-list-changed', + self._group_list_changed) + group.signal_connect(group, 'group-cleared', self._group_cleared) + group.signal_connect(group, 'record-modified', self._record_modified) + + def get_client(self, record): + self._set_default_value(record) + return record.value.get(self.name) + + def get(self, record): + if record.value.get(self.name) is None: + return [] + record_removed = record.value[self.name].record_removed + record_deleted = record.value[self.name].record_deleted + result = [] + parent_name = self.attrs.get('relation_field', '') + to_add = [] + to_create = [] + to_write = [] + for record2 in record.value[self.name]: + if record2 in record_removed or record2 in record_deleted: + continue + if record2.id >= 0: + if record2.modified: + values = record2.get() + values.pop(parent_name, None) + if values: + to_write.extend(([record2.id], values)) + to_add.append(record2.id) + else: + values = record2.get() + values.pop(parent_name, None) + to_create.append(values) + if to_add: + result.append(('add', to_add)) + if to_create: + result.append(('create', to_create)) + if to_write: + result.append(('write',) + tuple(to_write)) + if record_removed: + result.append(('remove', [x.id for x in record_removed])) + if record_deleted: + result.append(('delete', [x.id for x in record_deleted])) + return result + + def get_timestamp(self, record): + if record.value.get(self.name) is None: + return {} + result = {} + record_modified = (r for r in record.value[self.name] if r.modified) + for record2 in chain(record_modified, + record.value[self.name].record_removed, + record.value[self.name].record_deleted): + result.update(record2.get_timestamp()) + return result + + def get_eval(self, record): + if record.value.get(self.name) is None: + return [] + record_removed = record.value[self.name].record_removed + record_deleted = record.value[self.name].record_deleted + return [x.id for x in record.value[self.name] + if x not in record_removed and x not in record_deleted] + + def get_on_change_value(self, record): + result = [] + if record.value.get(self.name) is None: + return [] + for record2 in record.value[self.name]: + if not (record2.deleted or record2.removed): + result.append( + record2.get_on_change_value( + skip={self.attrs.get('relation_field', '')})) + return result + + def _set_value(self, record, value, default=False, modified=False): + self._set_default_value(record) + group = record.value[self.name] + if value is None: + value = [] + if not value or isinstance(value[0], int): + mode = 'list ids' + else: + mode = 'list values' + + if mode == 'list values': + context = self.get_context(record) + field_names = set(f for v in value for f in v + if f not in group.fields and '.' not in f) + if field_names: + try: + fields = RPCExecute('model', self.attrs['relation'], + 'fields_get', list(field_names), context=context) + except RPCException: + return + group.load_fields(fields) + + if mode == 'list ids': + records_to_remove = [r for r in group if r.id not in value] + for record_to_remove in records_to_remove: + group.remove(record_to_remove, remove=True, signal=False) + group.load(value, modified=modified or default) + else: + for vals in value: + new_record = record.value[self.name].new(default=False) + if default: + # Don't validate as parent will validate + new_record.set_default(vals, signal=False, validate=False) + group.add(new_record, signal=False) + else: + new_record.set(vals, signal=False) + group.append(new_record) + # Trigger signal only once with the last record + new_record.signal('record-changed') + + def set(self, record, value, _default=False): + group = record.value.get(self.name) + fields = {} + if group is not None: + fields = group.fields.copy() + # Unconnect to prevent infinite loop + group.signal_unconnect(group) + group.destroy() + elif record.model_name == self.attrs['relation']: + fields = record.group.fields + if fields: + fields = dict((fname, field.attrs) + for fname, field in fields.items()) + + record.value[self.name] = None + self._set_default_value(record, fields=fields) + group = record.value[self.name] + + group.signal_unconnect(group) + self._set_value(record, value, default=_default) + self._connect_value(group) + + def set_client(self, record, value, force_change=False): + # domain inversion could try to set None as value + if value is None: + value = [] + # domain inversion could try to set id as value + if isinstance(value, int): + value = [value] + + previous_ids = self.get_eval(record) + # The order of the ids is not significant + modified = set(previous_ids) != set(value) + self._set_value(record, value, modified=modified) + if modified: + record.modified_fields.setdefault(self.name) + record.signal('record-modified') + self.sig_changed(record) + record.validate(softvalidation=True) + record.signal('record-changed') + elif force_change: + self.sig_changed(record) + record.validate(softvalidation=True) + record.signal('record-changed') + + def set_default(self, record, value): + self.set(record, value, _default=True) + record.modified_fields.setdefault(self.name) + + def set_on_change(self, record, value): + record.modified_fields.setdefault(self.name) + self._set_default_value(record) + if isinstance(value, (list, tuple)): + self._set_value(record, value, modified=True) + return + + if value and (value.get('add') or value.get('update')): + context = self.get_context(record) + fields = record.value[self.name].fields + values = chain(value.get('update', []), + (d for _, d in value.get('add', []))) + field_names = set(f for v in values + for f in v if f not in fields and f != 'id' and '.' not in f) + if field_names: + try: + new_fields = RPCExecute('model', self.attrs['relation'], + 'fields_get', list(field_names), context=context) + except RPCException: + return + else: + new_fields = {} + + group = record.value[self.name] + if value and value.get('delete'): + for record_id in value['delete']: + record2 = group.get(record_id) + if record2 is not None: + group.remove( + record2, remove=False, signal=False, + force_remove=False) + if value and value.get('remove'): + for record_id in value['remove']: + record2 = group.get(record_id) + if record2 is not None: + group.remove( + record2, remove=True, signal=False, + force_remove=False) + + if value and (value.get('add') or value.get('update', [])): + # First set already added fields to prevent triggering a + # second on_change call + for vals in value.get('update', []): + if 'id' not in vals: + continue + record2 = group.get(vals['id']) + if record2 is not None: + vals_to_set = { + k: v for k, v in vals.items() if k not in new_fields} + record2.set_on_change(vals_to_set) + + record.value[self.name].add_fields(new_fields) + for index, vals in value.get('add', []): + new_record = None + id_ = vals.pop('id', None) + if id_ is not None: + new_record = group.get(id_) + if not new_record: + new_record = group.new(obj_id=id_, default=False) + group.add(new_record, index, signal=False) + new_record.set_on_change(vals) + + for vals in value.get('update', []): + if 'id' not in vals: + continue + record2 = group.get(vals['id']) + if record2 is not None: + record2.set_on_change(vals) + + def validation_domains(self, record, pre_validate=None): + screen_domain, attr_domain = self.domains_get(record, pre_validate) + return screen_domain + + def validate(self, record, softvalidation=False, pre_validate=None): + invalid = False + ldomain = localize_domain(domain_inversion( + record.group.clean4inversion(pre_validate or []), self.name, + EvalEnvironment(record)), self.name) + if isinstance(ldomain, bool): + if ldomain: + ldomain = [] + else: + ldomain = [('id', '=', None)] + for record2 in record.value.get(self.name, []): + if not record2.loaded and record2.id >= 0 and not pre_validate: + continue + if not record2.validate(softvalidation=softvalidation, + pre_validate=ldomain): + invalid = 'children' + test = super(O2MField, self).validate(record, softvalidation, + pre_validate) + if test and invalid: + self.get_state_attrs(record)['invalid'] = invalid + return False + return test + + def state_set(self, record, states=('readonly', 'required', 'invisible')): + self._set_default_value(record) + super(O2MField, self).state_set(record, states=states) + + def get_removed_ids(self, record): + return [x.id for x in record.value[self.name].record_removed] + + def domain_get(self, record): + screen_domain, attr_domain = self.domains_get(record) + # Forget screen_domain because it only means at least one record + # and not all records + return attr_domain + + +class M2MField(O2MField): + + def get_on_change_value(self, record): + return self.get_eval(record) + + +class ReferenceField(Field): + + _default = None + + def _is_empty(self, record): + result = super(ReferenceField, self)._is_empty(record) + if not result and (record.value[self.name] is None + or record.value[self.name][1] < 0): + result = True + return result + + def get_client(self, record): + if record.value.get(self.name): + model, _ = record.value[self.name] + name = record.value.get(self.name + '.', {}).get('rec_name') or '' + return model, name + else: + return None + + def get(self, record): + if (record.value.get(self.name) + and record.value[self.name][0] + and record.value[self.name][1] is not None + and record.value[self.name][1] >= -1): + return ','.join(map(str, record.value[self.name])) + return None + + def set_client(self, record, value, force_change=False): + if value: + if isinstance(value, str): + value = value.split(',') + ref_model, ref_id = value + if isinstance(ref_id, (tuple, list)): + ref_id, rec_name = ref_id + else: + if ref_id: + try: + ref_id = int(ref_id) + except ValueError: + pass + if '%s,%s' % (ref_model, ref_id) == self.get(record): + rec_name = record.value.get( + self.name + '.', {}).get('rec_name') or '' + else: + rec_name = '' + record.value.setdefault(self.name + '.', {})['rec_name'] = rec_name + value = (ref_model, ref_id) + super(ReferenceField, self).set_client(record, value, + force_change=force_change) + + def set(self, record, value): + if not value: + record.value[self.name] = self._default + return + if isinstance(value, str): + ref_model, ref_id = value.split(',') + if not ref_id: + ref_id = None + else: + try: + ref_id = int(ref_id) + except ValueError: + pass + else: + ref_model, ref_id = value + rec_name = record.value.get(self.name + '.', {}).get('rec_name') or '' + if ref_model and ref_id is not None and ref_id >= 0: + if not rec_name and ref_id >= 0: + try: + result, = RPCExecute('model', ref_model, 'read', [ref_id], + ['rec_name']) + except RPCException: + return + rec_name = result['rec_name'] + elif ref_model: + rec_name = '' + else: + rec_name = str(ref_id) if ref_id is not None else '' + record.value[self.name] = ref_model, ref_id + record.value.setdefault(self.name + '.', {})['rec_name'] = rec_name + + def get_context(self, record, record_context=None, local=False): + context = super(ReferenceField, self).get_context( + record, record_context, local=local) + if self.attrs.get('datetime_field'): + context['_datetime'] = record.get_eval( + )[self.attrs.get('datetime_field')] + return context + + def get_on_change_value(self, record): + if record.parent_name == self.name and record.parent: + return record.parent.model_name, record.parent.get_on_change_value( + skip={record.group.child_name}) + return super(ReferenceField, self).get_on_change_value(record) + + def validation_domains(self, record, pre_validate=None): + screen_domain, attr_domain = self.domains_get(record, pre_validate) + return screen_domain + + def domain_get(self, record): + if record.value.get(self.name): + model = record.value[self.name][0] + else: + model = None + screen_domain, attr_domain = self.domains_get(record) + screen_domain = filter_leaf(screen_domain, self.name, model) + screen_domain = prepare_reference_domain(screen_domain, self.name) + return concat(localize_domain( + screen_domain, self.name, strip_target=True), attr_domain) + + def get_models(self, record): + screen_domain, attr_domain = self.domains_get(record) + screen_domain = prepare_reference_domain(screen_domain, self.name) + return extract_reference_models( + concat(screen_domain, attr_domain), self.name) + + +class _FileCache(object): + def __init__(self, path): + self.path = path + + def __del__(self): + try: + os.remove(self.path) + except IOError: + pass + + +class BinaryField(Field): + + _default = None + + def get(self, record): + result = record.value.get(self.name, self._default) + if isinstance(result, _FileCache): + try: + with open(result.path, 'rb') as fp: + result = fp.read() + except IOError: + result = self.get_data(record) + return result + + def get_client(self, record): + return self.get(record) + + def set_client(self, record, value, force_change=False): + _, filename = tempfile.mkstemp(prefix='tryton_') + data = value or b'' + if isinstance(data, str): + data = data.encode('utf-8') + with open(filename, 'wb') as fp: + fp.write(data) + self.set(record, _FileCache(filename)) + record.modified_fields.setdefault(self.name) + record.signal('record-modified') + self.sig_changed(record) + record.validate(softvalidation=True) + record.signal('record-changed') + + def get_size(self, record): + result = record.value.get(self.name) or 0 + if isinstance(result, _FileCache): + result = os.stat(result.path).st_size + elif isinstance(result, (str, bytes)): + result = len(result) + return result + + def get_data(self, record): + if not isinstance(record.value.get(self.name), + (str, bytes, _FileCache)): + if record.id < 0: + return b'' + context = record.get_context() + try: + values, = RPCExecute('model', record.model_name, 'read', + [record.id], [self.name], context=context) + except RPCException: + return b'' + _, filename = tempfile.mkstemp(prefix='tryton_') + data = values[self.name] or b'' + if isinstance(data, str): + data = data.encode('utf-8') + with open(filename, 'wb') as fp: + fp.write(data) + self.set(record, _FileCache(filename)) + return self.get(record) + + +class DictField(Field): + + _default = {} + + def __init__(self, attrs): + super(DictField, self).__init__(attrs) + self.keys = {} + + def get(self, record): + return super(DictField, self).get(record) or self._default + + def get_client(self, record): + return super(DictField, self).get_client(record) or self._default + + def validation_domains(self, record, pre_validate=None): + screen_domain, attr_domain = self.domains_get(record, pre_validate) + return screen_domain + + def domain_get(self, record): + screen_domain, attr_domain = self.domains_get(record) + return concat(localize_domain(screen_domain), attr_domain) + + def date_format(self, record): + context = self.get_context(record) + return common.date_format(context.get('date_format')) + + def time_format(self, record): + return '%X' + + def add_keys(self, keys, record): + schema_model = self.attrs['schema_model'] + context = self.get_context(record) + domain = self.domain_get(record) + batchlen = min(10, CONFIG['client.limit']) + for i in range(0, len(keys), batchlen): + sub_keys = keys[i:i + batchlen] + try: + key_ids = RPCExecute('model', schema_model, 'search', + [('name', 'in', sub_keys), domain], 0, + CONFIG['client.limit'], None, context=context) + except RPCException: + key_ids = [] + if not key_ids: + continue + try: + values = RPCExecute('model', schema_model, + 'get_keys', key_ids, context=context) + except RPCException: + values = [] + if not values: + continue + self.keys.update({k['name']: k for k in values}) + + def add_new_keys(self, key_ids, record): + schema_model = self.attrs['schema_model'] + context = self.get_context(record) + try: + new_fields = RPCExecute('model', schema_model, + 'get_keys', key_ids, context=context) + except RPCException: + new_fields = [] + + new_keys = [] + for new_field in new_fields: + name = new_field['name'] + new_keys.append(name) + self.keys[name] = new_field + + return new_keys + + def validate(self, record, softvalidation=False, pre_validate=None): + valid = super(DictField, self).validate( + record, softvalidation, pre_validate) + + if self.attrs.get('readonly'): + return valid + + decoder = PYSONDecoder() + field_value = self.get_eval(record) + domain = [] + for key in field_value: + if key not in self.keys: + continue + key_domain = self.keys[key].get('domain') + if key_domain: + domain.append(decoder.decode(key_domain)) + + valid_value = eval_domain(domain, field_value) + if not valid_value: + self.get_state_attrs(record)['invalid'] = 'domain' + + return valid and valid_value + + +TYPES = { + 'char': CharField, + 'integer': IntegerField, + 'biginteger': IntegerField, + 'float': FloatField, + 'numeric': NumericField, + 'many2one': M2OField, + 'many2many': M2MField, + 'one2many': O2MField, + 'reference': ReferenceField, + 'selection': SelectionField, + 'multiselection': MultiSelectionField, + 'boolean': BooleanField, + 'datetime': DateTimeField, + 'date': DateField, + 'time': TimeField, + 'timestamp': DateTimeField, + 'timedelta': TimeDeltaField, + 'one2one': O2OField, + 'binary': BinaryField, + 'dict': DictField, +} diff --git a/tryton/tryton/gui/window/view_form/model/group.py b/tryton/tryton/gui/window/view_form/model/group.py new file mode 100644 index 0000000000..40772ac82f --- /dev/null +++ b/tryton/tryton/gui/window/view_form/model/group.py @@ -0,0 +1,538 @@ +# 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 operator + +from .record import Record +from .field import Field, M2OField, ReferenceField +from tryton import rpc +from tryton.signal_event import SignalEvent +from tryton.common.domain_inversion import is_leaf +from tryton.common import RPCExecute, RPCException, MODELACCESS + + +class Group(SignalEvent, list): + + def __init__(self, model_name, fields, ids=None, parent=None, + parent_name='', child_name='', context=None, domain=None, + readonly=False, parent_datetime_field=None): + super(Group, self).__init__() + if domain is None: + domain = [] + self.__domain = domain + self.__domain4inversion = None + self.lock_signal = False + self.parent = parent + self.parent_name = parent_name or '' + self.children = [] + self.child_name = child_name + self.parent_datetime_field = parent_datetime_field + self._context = context or {} + self.model_name = model_name + self.fields = {} + self.load_fields(fields) + self.current_idx = None + self.load(ids) + self.record_deleted, self.record_removed = [], [] + self.on_write = set() + self.__readonly = readonly + self.__id2record = {} + self.__field_childs = None + self.exclude_field = None + self.skip_model_access = False + + if self.parent and self.parent.model_name == model_name: + self.parent.group.children.append(self) + + @property + def readonly(self): + # Must skip res.user for Preference windows + if (self.context.get('_datetime') + or (not (MODELACCESS[self.model_name]['write'] + or MODELACCESS[self.model_name]['create']) + and not self.skip_model_access)): + return True + return self.__readonly + + @readonly.setter + def readonly(self, value): + self.__readonly = value + + @property + def domain(self): + if self.parent and self.child_name: + field = self.parent.group.fields[self.child_name] + return [self.__domain, field.domain_get(self.parent)] + return self.__domain + + def clean4inversion(self, domain): + "This method will replace non relevant fields for domain inversion" + if domain in ([], ()): + return [] + head, tail = domain[0], domain[1:] + if head in ('AND', 'OR'): + pass + elif is_leaf(head): + field = head[0] + if (field in self.fields + and self.fields[field].attrs.get('readonly')): + head = [] + else: + head = self.clean4inversion(head) + return [head] + self.clean4inversion(tail) + + def __get_domain4inversion(self): + domain = self.domain + if (self.__domain4inversion is None + or self.__domain4inversion[0] != domain): + self.__domain4inversion = ( + domain, self.clean4inversion(domain)) + domain, domain4inversion = self.__domain4inversion + return domain4inversion + + domain4inversion = property(__get_domain4inversion) + + def insert(self, pos, record): + assert record.group is self + pos = min(pos, len(self)) + if pos >= 1: + self.__getitem__(pos - 1).next[id(self)] = record + if pos < self.__len__(): + record.next[id(self)] = self.__getitem__(pos) + else: + record.next[id(self)] = None + super(Group, self).insert(pos, record) + self.__id2record[record.id] = record + if not self.lock_signal: + self.signal('group-list-changed', ('record-added', record, pos)) + + def append(self, record): + assert record.group is self + if self.__len__() >= 1: + self.__getitem__(self.__len__() - 1).next[id(self)] = record + record.next[id(self)] = None + super(Group, self).append(record) + self.__id2record[record.id] = record + if not self.lock_signal: + self.signal('group-list-changed', ( + 'record-added', record, self.__len__() - 1)) + + def _remove(self, record): + idx = self.index(record) + if idx >= 1: + if idx + 1 < self.__len__(): + self.__getitem__(idx - 1).next[id(self)] = \ + self.__getitem__(idx + 1) + else: + self.__getitem__(idx - 1).next[id(self)] = None + self.signal('group-list-changed', ('record-removed', record, idx)) + super(Group, self).remove(record) + del self.__id2record[record.id] + + def clear(self): + # Use reversed order to minimize the cursor reposition as the cursor + # has more chances to be on top of the list. + length = self.__len__() + for record in reversed(self[:]): + # Destroy record before propagating the signal to recursively + # destroy also the underlying records + record.destroy() + self.signal( + 'group-list-changed', ('record-removed', record, length - 1)) + self.pop() + length -= 1 + self.__id2record = {} + self.record_removed, self.record_deleted = [], [] + + def move(self, record, pos): + if self.__len__() > pos >= 0: + idx = self.index(record) + self._remove(record) + if pos > idx: + pos -= 1 + self.insert(pos, record) + else: + self._remove(record) + self.append(record) + + def __setitem__(self, i, value): + super(Group, self).__setitem__(i, value) + if not self.lock_signal: + self.signal('group-list-changed', ('record-changed', i)) + + def __repr__(self): + return '' % (self.model_name, id(self)) + + def load_fields(self, fields): + for name, attr in fields.items(): + field = Field.get_field(attr['type']) + attr['name'] = name + self.fields[name] = field(attr) + + def save(self): + saved = [] + for record in self: + saved.append(record.save(force_reload=False)) + if self.record_deleted: + for record in self.record_deleted: + self._remove(record) + record.destroy() + self.delete(self.record_deleted) + del self.record_deleted[:] + return saved + + def delete(self, records): + if not records: + return + root_group = self.root_group + assert all(r.model_name == self.model_name for r in records) + assert all(r.group.root_group == root_group for r in records) + records = [r for r in records if r.id >= 0] + ctx = self.context + ctx['_timestamp'] = {} + for rec in records: + ctx['_timestamp'].update(rec.get_timestamp()) + record_ids = set(r.id for r in records) + reload_ids = set(root_group.on_write_ids(list(record_ids))) + reload_ids -= record_ids + reload_ids = list(reload_ids) + try: + RPCExecute('model', self.model_name, 'delete', list(record_ids), + context=ctx) + except RPCException: + return False + for rec in records: + rec.destroy() + if reload_ids: + root_group.reload(reload_ids) + return True + + @property + def root_group(self): + root = self + parent = self.parent + while parent: + root = parent.group + parent = parent.parent + return root + + def written(self, ids): + if isinstance(ids, int): + ids = [ids] + ids = [x for x in self.on_write_ids(ids) or [] if x not in ids] + if not ids: + return + self.root_group.reload(ids) + return ids + + def reload(self, ids): + for child in self.children: + child.reload(ids) + for id_ in ids: + record = self.get(id_) + if record and not record.modified: + record.cancel() + + def on_write_ids(self, ids): + if not self.on_write: + return [] + res = [] + for fnct in self.on_write: + try: + res += RPCExecute('model', self.model_name, fnct, ids, + context=self.context) + except RPCException: + return [] + return list({}.fromkeys(res)) + + def load(self, ids, modified=False, position=-1): + if not ids: + return True + + if len(ids) > 1: + self.lock_signal = True + + new_records = [] + for id in ids: + new_record = self.get(id) + if not new_record: + new_record = Record(self.model_name, id, group=self) + if position == -1: + self.append(new_record) + else: + self.insert(position, new_record) + position += 1 + new_record.signal_connect(self, 'record-changed', + self._record_changed) + new_record.signal_connect(self, 'record-modified', + self._record_modified) + new_records.append(new_record) + + # Remove previously removed or deleted records + for record in self.record_removed[:]: + if record.id in ids: + self.record_removed.remove(record) + for record in self.record_deleted[:]: + if record.id in ids: + self.record_deleted.remove(record) + + if self.lock_signal: + self.lock_signal = False + self.signal('group-cleared') + + if new_records and modified: + for record in new_records: + record.modified_fields.setdefault('id') + new_records[0].signal('record-modified') + new_records[0].signal('record-changed') + + self.current_idx = 0 + return True + + @property + def context(self): + return self._get_context(local=False) + + @property + def local_context(self): + return self._get_context(local=True) + + def _get_context(self, local=False): + if not local: + ctx = rpc.CONTEXT.copy() + else: + ctx = {} + if self.parent: + parent_context = self.parent.get_context(local=local) + ctx.update(parent_context) + if self.child_name in self.parent.group.fields: + field = self.parent.group.fields[self.child_name] + ctx.update(field.get_context( + self.parent, parent_context, local=local)) + ctx.update(self._context) + if self.parent_datetime_field: + ctx['_datetime'] = self.parent.get_eval( + )[self.parent_datetime_field] + return ctx + + @context.setter + def context(self, value): + self._context = value.copy() + + def add(self, record, position=-1, signal=True): + if record.group is not self: + record.signal_unconnect(record.group) + record.group = self + record.signal_connect(self, 'record-changed', self._record_changed) + record.signal_connect(self, 'record-modified', + self._record_modified) + if record not in self: + if position == -1: + self.append(record) + else: + self.insert(position, record) + for record_rm in self.record_removed: + if record_rm.id == record.id: + self.record_removed.remove(record_rm) + for record_del in self.record_deleted: + if record_del.id == record.id: + self.record_deleted.remove(record_del) + self.current_idx = position + record.modified_fields.setdefault('id') + record.signal('record-modified') + if signal: + self.signal('group-changed', record) + # Set parent field to trigger on_change + if self.parent and self.parent_name in self.fields: + field = self.fields[self.parent_name] + if isinstance(field, (M2OField, ReferenceField)): + value = self.parent.id, '' + if isinstance(field, ReferenceField): + value = self.parent.model_name, value + field.set_client(record, value) + return record + + def set_sequence(self, field='sequence', position=-1): + changed = False + prev = None + if position == 0: + cmp = operator.gt + else: + cmp = operator.lt + for record in self: + # Assume not loaded records are correctly ordered + # as far as we do not change any previous records. + if record.get_loaded([field]) or changed or record.id < 0: + if prev: + index = prev[field].get(prev) + else: + index = None + update = False + value = record[field].get(record) + if value is None: + if index: + update = True + elif prev: + if record.id >= 0: + update = cmp(record.id, prev.id) + elif position == 0: + update = True + elif value == index: + if prev: + if record.id >= 0: + update = cmp(record.id, prev.id) + elif position == 0: + update = True + elif value <= (index or 0): + update = True + if update: + if index is None: + index = 0 + index += 1 + record.signal_unconnect(self, 'record-changed') + try: + record[field].set_client(record, index) + finally: + record.signal_connect(self, 'record-changed', + self._record_changed) + changed = record + prev = record + if changed: + self.signal('group-changed', changed) + + def new(self, default=True, obj_id=None, rec_name=None): + record = Record(self.model_name, obj_id, group=self) + if default: + record.default_get(rec_name=rec_name) + record.signal_connect(self, 'record-changed', self._record_changed) + record.signal_connect(self, 'record-modified', self._record_modified) + return record + + def unremove(self, record, signal=True): + if record in self.record_removed: + self.record_removed.remove(record) + if record in self.record_deleted: + self.record_deleted.remove(record) + if signal: + record.signal('record-changed', record.parent) + + def remove(self, record, remove=False, modified=True, signal=True, + force_remove=False): + idx = self.index(record) + if record.id >= 0: + if remove: + if record in self.record_deleted: + self.record_deleted.remove(record) + if record not in self.record_removed: + self.record_removed.append(record) + else: + if record in self.record_removed: + self.record_removed.remove(record) + if record not in self.record_deleted: + self.record_deleted.append(record) + if record.parent: + record.parent.modified_fields.setdefault('id') + record.parent.signal('record-modified') + if modified: + record.modified_fields.setdefault('id') + record.signal('record-modified') + if record.id < 0 or force_remove: + self._remove(record) + + if len(self): + self.current_idx = min(idx, len(self) - 1) + else: + self.current_idx = None + + if signal: + record.signal('record-changed', record.parent) + + def _record_changed(self, record, signal_data): + self.signal('group-changed', record) + + def _record_modified(self, record, signal_data): + self.signal('record-modified', record) + + def prev(self): + if len(self) and self.current_idx is not None: + self.current_idx = (self.current_idx - 1) % len(self) + elif len(self): + self.current_idx = 0 + else: + return None + return self[self.current_idx] + + def __next__(self): + if len(self) and self.current_idx is not None: + self.current_idx = (self.current_idx + 1) % len(self) + elif len(self): + self.current_idx = 0 + else: + return None + return self[self.current_idx] + + def add_fields(self, fields): + to_add = {} + for name, attr in fields.items(): + if name not in self.fields: + to_add[name] = attr + else: + self.fields[name].attrs.update(attr) + self.load_fields(to_add) + + if not len(self): + return True + + new = [] + for record in self: + if record.id < 0: + new.append(record) + + if len(new) and len(to_add): + try: + values = RPCExecute('model', self.model_name, 'default_get', + list(to_add.keys()), context=self.context) + except RPCException: + return False + for record in new: + record.set_default(values, signal=False) + # Trigger signal only once with the last record + record.signal('record-changed') + + def get(self, id): + 'Return record with the id' + return self.__id2record.get(id) + + def id_changed(self, old_id): + 'Update index for old id' + record = self.__id2record[old_id] + self.__id2record[record.id] = record + del self.__id2record[old_id] + + def destroy(self): + if self.parent: + try: + self.parent.group.children.remove(self) + except ValueError: + pass + # One2Many connect the group to itself to send signals to the parent + # but as we are destroying the group, we do not need to notify the + # parent otherwise it will trigger unnecessary display. + self.signal_unconnect(self) + self.clear() + super(Group, self).destroy() + + def get_by_path(self, path): + 'return record by path' + group = self + record = None + for child_name, id_ in path: + record = group.get(id_) + if not record: + return None + if not child_name: + continue + record[child_name] + group = record.value.get(child_name) + if not isinstance(group, Group): + return None + return record diff --git a/tryton/tryton/gui/window/view_form/model/record.py b/tryton/tryton/gui/window/view_form/model/record.py new file mode 100644 index 0000000000..dbb6865a57 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/model/record.py @@ -0,0 +1,672 @@ +# 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 tryton.signal_event import SignalEvent +import tryton.common as common +from tryton.pyson import PYSONDecoder +from . import field as fields +from tryton.common import RPCExecute, RPCException +from tryton.config import CONFIG + + +class Record(SignalEvent): + + id = -1 + + def __init__(self, model_name, obj_id, group=None): + super(Record, self).__init__() + self.model_name = model_name + if obj_id is None: + self.id = Record.id + else: + self.id = obj_id + if self.id < 0: + Record.id -= 1 + self._loaded = set() + self.group = group + if group is not None: + assert model_name == group.model_name + self.state_attrs = {} + self.modified_fields = {} + self._timestamp = None + self._write = True + self._delete = True + self.resources = None + self.button_clicks = {} + self.links_counts = {} + self.next = {} # Used in Group list + self.value = {} + self.autocompletion = {} + self.exception = False + self.destroyed = False + + def __getitem__(self, name): + if not self.destroyed and self.id >= 0 and name not in self._loaded: + id2record = { + self.id: self, + } + if name == '*': + loading = 'eager' + views = set() + for field in self.group.fields.values(): + if field.attrs.get('loading', 'eager') == 'lazy': + loading = 'lazy' + views |= field.views + # Set a valid name for next loaded check + for fname, field in self.group.fields.items(): + if field.attrs.get('loading', 'eager') == loading: + name = fname + break + else: + loading = self.group.fields[name].attrs.get('loading', 'eager') + views = self.group.fields[name].views + + if loading == 'eager': + fields = ((fname, field) + for fname, field in self.group.fields.items() + if field.attrs.get('loading', 'eager') == 'eager') + else: + fields = self.group.fields.items() + + fnames = [fname for fname, field in fields + if fname not in self._loaded + and (not views or (views & field.views))] + fnames.extend(('%s.rec_name' % fname for fname in fnames[:] + if self.group.fields[fname].attrs['type'] + in ('many2one', 'one2one', 'reference'))) + if 'rec_name' not in fnames: + fnames.append('rec_name') + fnames.extend(['_timestamp', '_write', '_delete']) + + record_context = self.get_context() + if loading == 'eager': + limit = int(CONFIG['client.limit'] / len(fnames)) + + def filter_group(record): + return (not record.destroyed + and record.id >= 0 + and name not in record._loaded) + + def filter_parent_group(record): + return (filter_group(record) + and record.id not in id2record + and ((record.group == self.group) + # Don't compute context for same group + or (record.get_context() == record_context))) + + if self.parent and self.parent.model_name == self.model_name: + group = sum(self.parent.group.children, []) + filter_ = filter_parent_group + else: + group = self.group + filter_ = filter_group + if self in group: + idx = group.index(self) + length = len(group) + n = 1 + while len(id2record) < limit and (idx - n >= 0 + or idx + n < length) and n < 2 * limit: + if idx - n >= 0: + record = group[idx - n] + if filter_(record): + id2record[record.id] = record + if idx + n < length: + record = group[idx + n] + if filter_(record): + id2record[record.id] = record + n += 1 + + ctx = record_context.copy() + ctx.update(dict(('%s.%s' % (self.model_name, fname), 'size') + for fname, field in self.group.fields.items() + if field.attrs['type'] == 'binary' and fname in fnames)) + exception = False + try: + values = RPCExecute('model', self.model_name, 'read', + list(id2record.keys()), fnames, context=ctx) + except RPCException: + values = [{'id': x} for x in id2record] + default_values = dict((f, None) for f in fnames) + for value in values: + value.update(default_values) + self.exception = exception = True + id2value = dict((value['id'], value) for value in values) + for id, record in id2record.items(): + if not record.exception: + record.exception = exception + value = id2value.get(id) + if record and not record.destroyed and value: + for key in record.modified_fields: + value.pop(key, None) + record.set(value, signal=False) + if name != '*': + return self.group.fields[name] + + def __repr__(self): + return '' % (self.id, self.model_name, id(self)) + + @property + def modified(self): + return bool(self.modified_fields) + + @property + def parent(self): + return self.group.parent + + @property + def parent_name(self): + return self.group.parent_name + + @property + def root_parent(self): + parent = self + while parent.parent: + parent = parent.parent + return parent + + @property + def depth(self): + parent = self.parent + i = 0 + while parent: + i += 1 + parent = parent.parent + return i + + def set_modified(self, value): + if value: + self.signal('record-modified') + + def children_group(self, field_name): + if not field_name: + return [] + self._check_load([field_name]) + group = self.value.get(field_name) + if group is None: + return None + + if id(group.fields) != id(self.group.fields): + self.group.fields.update(group.fields) + group.fields = self.group.fields + group.on_write = self.group.on_write + group.readonly = self.group.readonly + group._context.update(self.group._context) + return group + + def get_path(self, group): + path = [] + i = self + child_name = '' + while i: + path.append((child_name, i.id)) + if i.group is group: + break + child_name = i.group.child_name + i = i.parent + path.reverse() + return tuple(path) + + def get_index_path(self, group=None): + path = [] + record = self + while record: + path.append(record.group.index(record)) + if record.group is group: + break + record = record.parent + path.reverse() + return tuple(path) + + def get_removed(self): + if self.group is not None: + return self in self.group.record_removed + return False + + removed = property(get_removed) + + def get_deleted(self): + if self.group is not None: + return self in self.group.record_deleted + return False + + deleted = property(get_deleted) + + def get_readonly(self): + return (self.deleted + or self.removed + or self.exception + or not self._write) + + readonly = property(get_readonly) + + @property + def deletable(self): + return self._delete + + def fields_get(self): + return self.group.fields + + def _check_load(self, fields=None): + if fields is not None: + if not self.get_loaded(fields): + self.reload(fields) + return True + return False + if not self.loaded: + self.reload() + return True + return False + + def get_loaded(self, fields=None): + if fields: + return set(fields) <= (self._loaded | set(self.modified_fields)) + return set(self.group.fields.keys()) == self._loaded + + loaded = property(get_loaded) + + def get(self): + value = {} + for name, field in self.group.fields.items(): + if (field.attrs.get('readonly') + and not (isinstance(field, fields.O2MField) + and not isinstance(field, fields.M2MField))): + continue + if field.name not in self.modified_fields and self.id >= 0: + continue + value[name] = field.get(self) + # Sending an empty x2MField breaks ModelFieldAccess.check + if isinstance(field, fields.O2MField) and not value[name]: + del value[name] + return value + + def get_eval(self): + value = {} + for name, field in self.group.fields.items(): + if name not in self._loaded and self.id >= 0: + continue + value[name] = field.get_eval(self) + value['id'] = self.id + return value + + def get_on_change_value(self, skip=None): + value = {} + for name, field in self.group.fields.items(): + if skip and name in skip: + continue + if (self.id >= 0 + and (name not in self._loaded + or name not in self.modified_fields)): + continue + value[name] = field.get_on_change_value(self) + value['id'] = self.id + return value + + def cancel(self): + self._loaded.clear() + self.modified_fields.clear() + self._timestamp = None + self.button_clicks.clear() + self.links_counts.clear() + + def get_timestamp(self): + result = {self.model_name + ',' + str(self.id): self._timestamp} + for name, field in self.group.fields.items(): + if name in self._loaded: + result.update(field.get_timestamp(self)) + return result + + def pre_validate(self): + if not self.modified_fields: + return True + values = self._get_on_change_args(['id'] + list(self.modified_fields)) + try: + RPCExecute('model', self.model_name, 'pre_validate', values, + context=self.get_context()) + except RPCException: + return False + return True + + def save(self, force_reload=True): + if self.id < 0 or self.modified: + value = self.get() + if self.id < 0: + try: + res, = RPCExecute('model', self.model_name, 'create', + [value], context=self.get_context()) + except RPCException: + return False + old_id = self.id + self.id = res + self.group.id_changed(old_id) + elif self.modified: + if value: + context = self.get_context() + context = context.copy() + context['_timestamp'] = self.get_timestamp() + try: + RPCExecute('model', self.model_name, 'write', + [self.id], value, context=context) + except RPCException: + return False + self.cancel() + if force_reload: + self.reload() + if self.group: + self.group.written(self.id) + if self.parent: + self.parent.modified_fields.pop(self.group.child_name, None) + self.parent.save(force_reload=force_reload) + return self.id + + def default_get(self, rec_name=None): + if len(self.group.fields): + context = self.get_context() + context.setdefault('default_rec_name', rec_name) + try: + vals = RPCExecute('model', self.model_name, 'default_get', + list(self.group.fields.keys()), context=context) + except RPCException: + return + if (self.parent + and self.parent_name in self.group.fields): + parent_field = self.group.fields[self.parent_name] + if isinstance(parent_field, fields.ReferenceField): + vals[self.parent_name] = ( + self.parent.model_name, self.parent.id) + elif (self.group.fields[self.parent_name].attrs['relation'] + == self.group.parent.model_name): + vals[self.parent_name] = self.parent.id + self.set_default(vals) + return vals + + def rec_name(self): + try: + return RPCExecute('model', self.model_name, 'read', [self.id], + ['rec_name'], context=self.get_context())[0]['rec_name'] + except RPCException: + return '' + + def validate(self, fields=None, softvalidation=False, pre_validate=None): + if isinstance(fields, list) and fields: + self._check_load(fields) + elif fields is None: + self._check_load() + res = True + for field_name, field in list(self.group.fields.items()): + if fields is not None and field_name not in fields: + continue + if field.attrs.get('readonly'): + continue + if field_name == self.group.exclude_field: + continue + if not field.validate(self, softvalidation, pre_validate): + res = False + return res + + def _get_invalid_fields(self): + fields = {} + for fname, field in self.group.fields.items(): + invalid = field.get_state_attrs(self).get('invalid') + if invalid: + fields[fname] = invalid + return fields + + invalid_fields = property(_get_invalid_fields) + + def get_context(self, local=False): + if not local: + return self.group.context + else: + return self.group.local_context + + def set_default(self, val, signal=True, validate=True): + fieldnames = [] + for fieldname, value in list(val.items()): + if fieldname not in self.group.fields: + continue + if fieldname == self.group.exclude_field: + continue + if isinstance(self.group.fields[fieldname], (fields.M2OField, + fields.ReferenceField)): + related = fieldname + '.' + self.value[related] = val.get(related) or {} + self.group.fields[fieldname].set_default(self, value) + self._loaded.add(fieldname) + fieldnames.append(fieldname) + self.on_change(fieldnames) + self.on_change_with(fieldnames) + if validate: + self.validate(softvalidation=True) + if signal: + self.signal('record-changed') + + def set(self, val, signal=True, validate=True): + later = {} + fieldnames = [] + for fieldname, value in val.items(): + if fieldname == '_timestamp': + # Always keep the older timestamp + if not self._timestamp: + self._timestamp = value + continue + if fieldname in {'_write', '_delete'}: + setattr(self, fieldname, value) + continue + if fieldname not in self.group.fields: + if fieldname == 'rec_name': + self.value['rec_name'] = value + continue + if isinstance(self.group.fields[fieldname], fields.O2MField): + later[fieldname] = value + continue + if isinstance(self.group.fields[fieldname], (fields.M2OField, + fields.ReferenceField)): + related = fieldname + '.' + self.value[related] = val.get(related) or {} + self.group.fields[fieldname].set(self, value) + self._loaded.add(fieldname) + fieldnames.append(fieldname) + for fieldname, value in later.items(): + self.group.fields[fieldname].set(self, value) + self._loaded.add(fieldname) + if validate: + self.validate(fieldnames, softvalidation=True) + if signal: + self.signal('record-changed') + + def set_on_change(self, values): + for fieldname, value in list(values.items()): + if fieldname not in self.group.fields: + continue + if isinstance(self.group.fields[fieldname], (fields.M2OField, + fields.ReferenceField)): + related = fieldname + '.' + self.value[related] = values.get(related) or {} + # Load fieldname before setting value + self[fieldname].set_on_change(self, value) + + def reload(self, fields=None): + if self.id < 0: + return + if not fields: + self['*'] + else: + for field in fields: + self[field] + self.validate(fields or []) + + def reset(self, value): + self.cancel() + self.set(value, signal=False) + + if self.parent: + self.parent.on_change([self.group.child_name]) + self.parent.on_change_with([self.group.child_name]) + + self.signal('record-changed') + + def expr_eval(self, expr): + if not isinstance(expr, str): + return expr + if not expr: + return + elif expr == '[]': + return [] + elif expr == '{}': + return {} + ctx = self.get_eval() + ctx['context'] = self.get_context() + ctx['active_model'] = self.model_name + ctx['active_id'] = self.id + if self.parent and self.parent_name: + ctx['_parent_' + self.parent_name] = \ + common.EvalEnvironment(self.parent) + val = PYSONDecoder(ctx).decode(expr) + return val + + def _get_on_change_args(self, args): + res = {} + values = common.EvalEnvironment(self, 'on_change') + for arg in args: + scope = values + for i in arg.split('.'): + if i not in scope: + break + scope = scope[i] + else: + res[arg] = scope + return res + + def on_change(self, fieldnames): + values = {} + for fieldname in fieldnames: + on_change = self.group.fields[fieldname].attrs.get('on_change') + if not on_change: + continue + values.update(self._get_on_change_args(on_change)) + + if values: + try: + if len(fieldnames) == 1: + fieldname, = fieldnames + changes = [] + changes.append(RPCExecute( + 'model', self.model_name, 'on_change_' + fieldname, + values, context=self.get_context())) + else: + changes = RPCExecute( + 'model', self.model_name, 'on_change', + values, fieldnames, context=self.get_context()) + except RPCException: + return + for change in changes: + self.set_on_change(change) + + def on_change_with(self, field_names): + field_names = set(field_names) + fieldnames = set() + values = {} + later = set() + for fieldname in self.group.fields: + on_change_with = self.group.fields[fieldname].attrs.get( + 'on_change_with') + if not on_change_with: + continue + if not field_names & set(on_change_with): + continue + if fieldnames & set(on_change_with): + later.add(fieldname) + continue + fieldnames.add(fieldname) + values.update(self._get_on_change_args(on_change_with)) + if isinstance(self.group.fields[fieldname], (fields.M2OField, + fields.ReferenceField)): + self.value.pop(fieldname + '.', None) + if fieldnames: + try: + if len(fieldnames) == 1: + fieldname, = fieldnames + result = {} + result[fieldname] = RPCExecute( + 'model', self.model_name, + 'on_change_with_' + fieldname, + values, context=self.get_context()) + else: + result = RPCExecute( + 'model', self.model_name, 'on_change_with', + values, list(fieldnames), context=self.get_context()) + except RPCException: + return + self.set_on_change(result) + for fieldname in later: + on_change_with = self.group.fields[fieldname].attrs.get( + 'on_change_with') + values = self._get_on_change_args(on_change_with) + try: + result = RPCExecute('model', self.model_name, + 'on_change_with_' + fieldname, values, + context=self.get_context()) + except RPCException: + return + # Load fieldname before setting value + self[fieldname].set_on_change(self, result) + + def autocomplete_with(self, field_name): + for fieldname, fieldinfo in self.group.fields.items(): + autocomplete = fieldinfo.attrs.get('autocomplete', []) + if field_name not in autocomplete: + continue + self.do_autocomplete(fieldname) + + def do_autocomplete(self, fieldname): + self.autocompletion[fieldname] = [] + autocomplete = self.group.fields[fieldname].attrs['autocomplete'] + args = self._get_on_change_args(autocomplete) + try: + res = RPCExecute( + 'model', self.model_name, + 'autocomplete_' + fieldname, args, context=self.get_context()) + except RPCException: + # ensure res is a list + res = [] + self.autocompletion[fieldname] = res + + def set_field_context(self): + from .group import Group + for name, field in self.group.fields.items(): + value = self.value.get(name) + if not isinstance(value, Group): + continue + context = field.attrs.get('context') + if context: + value.context = self.expr_eval(context) + + def get_resources(self, reload=False): + if self.id >= 0 and (not self.resources or reload): + try: + self.resources = RPCExecute( + 'model', self.model_name, 'resources', self.id, + context=self.get_context()) + except RPCException: + pass + return self.resources + + def get_button_clicks(self, name): + if self.id < 0: + return + clicks = self.button_clicks.get(name) + if clicks is not None: + return clicks + try: + clicks = RPCExecute('model', 'ir.model.button.click', + 'get_click', self.model_name, name, self.id) + self.button_clicks[name] = clicks + except RPCException: + return + return clicks + + def destroy(self): + for v in self.value.values(): + if hasattr(v, 'destroy'): + v.destroy() + super(Record, self).destroy() + self.destroyed = True diff --git a/tryton/tryton/gui/window/view_form/screen/__init__.py b/tryton/tryton/gui/window/view_form/screen/__init__.py new file mode 100644 index 0000000000..e6a2ce4a14 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/screen/__init__.py @@ -0,0 +1,5 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from .screen import Screen + +__all__ = [Screen] diff --git a/tryton/tryton/gui/window/view_form/screen/screen.py b/tryton/tryton/gui/window/view_form/screen/screen.py new file mode 100644 index 0000000000..e246ecac2e --- /dev/null +++ b/tryton/tryton/gui/window/view_form/screen/screen.py @@ -0,0 +1,1312 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Screen" +import copy +import functools +import datetime +import calendar +import json +import collections +import urllib.parse +import xml.dom.minidom +import gettext +import logging +from operator import itemgetter + +from gi.repository import GLib, Gtk + +from tryton.gui.window.view_form.model.group import Group +from tryton.gui.window.view_form.view.screen_container import ScreenContainer +from tryton.gui.window.view_form.view import View +from tryton.signal_event import SignalEvent +from tryton.config import CONFIG +from tryton.jsonrpc import JSONEncoder +from tryton.common.domain_parser import DomainParser +from tryton.common import RPCExecute, RPCException, MODELACCESS, \ + node_attributes, sur, RPCContextReload, warning +from tryton.action import Action +from tryton.pyson import PYSONDecoder +from tryton.rpc import clear_cache + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +class Screen(SignalEvent): + "Screen" + + # Width of tree columns per model + # It is shared with all connection but it is the price for speed. + tree_column_width = collections.defaultdict(lambda: {}) + + def __init__(self, model_name, **attributes): + context = attributes.get('context', {}) + self.limit = attributes.get('limit', CONFIG['client.limit']) + self.offset = 0 + super(Screen, self).__init__() + + self.readonly = attributes.get('readonly', False) + if not (MODELACCESS[model_name]['write'] + or MODELACCESS[model_name]['create']): + self.readonly = True + self.search_count = 0 + if not attributes.get('row_activate'): + self.row_activate = self.default_row_activate + else: + self.row_activate = attributes['row_activate'] + self.domain = attributes.get('domain', []) + self.context_domain = attributes.get('context_domain') + self.size_limit = None + self.views_preload = attributes.get('views_preload', {}) + self.model_name = model_name + self.views = [] + self.view_ids = attributes.get('view_ids', [])[:] + self.parent = None + self.parent_name = None + self.exclude_field = attributes.get('exclude_field') + self.filter_widget = None + self.tree_states = collections.defaultdict( + lambda: collections.defaultdict(lambda: None)) + self.tree_states_done = set() + self.__group = None + self.new_group(context or {}) + self.__current_record = None + self.current_record = None + self.screen_container = ScreenContainer(attributes.get('tab_domain')) + self.screen_container.alternate_view = attributes.get( + 'alternate_view', False) + self.widget = self.screen_container.widget_get() + self.breadcrumb = attributes.get('breadcrumb') or [] + + self.context_screen = None + if attributes.get('context_model'): + self.context_screen = Screen( + attributes['context_model'], mode=['form'], context=context) + self.context_screen.new() + context_widget = self.context_screen.widget + + def walk_descendants(widget): + yield widget + if not hasattr(widget, 'get_children'): + return + for child in widget.get_children(): + for widget in walk_descendants(child): + yield widget + + for widget in reversed(list(walk_descendants(context_widget))): + if isinstance(widget, Gtk.Entry): + widget.connect_after( + 'activate', self.screen_container.activate) + elif isinstance(widget, Gtk.CheckButton): + widget.connect_after( + 'toggled', self.screen_container.activate) + + def remove_bin(widget): + assert isinstance(widget, (Gtk.ScrolledWindow, Gtk.Viewport)) + parent = widget.get_parent() + parent.remove(widget) + child = widget.get_child() + while isinstance(child, (Gtk.ScrolledWindow, Gtk.Viewport)): + child = child.get_child() + child.get_parent().remove(child) + parent.add(child) + return child + + # Remove first level Viewport and ScrolledWindow to fill the Vbox + for widget in [ + self.context_screen.screen_container.viewport, + self.context_screen.current_view.widget.get_children()[0], + ]: + remove_bin(widget) + + self.screen_container.filter_vbox.pack_start( + context_widget, expand=False, fill=True, padding=0) + self.screen_container.filter_vbox.reorder_child( + context_widget, 0) + self.context_screen.widget.show() + + self.__current_view = 0 + self.search_value = attributes.get('search_value') + self.fields_view_tree = {} + self.order = self.default_order = attributes.get('order') + self.view_to_load = [] + self._domain_parser = {} + self.pre_validate = False + mode = attributes.get('mode') + if mode is None: + mode = ['tree', 'form'] + self.view_to_load = mode[:] + if self.view_ids or self.view_to_load: + self.switch_view() + self.count_tab_domain() + + def __repr__(self): + return '' % (self.model_name, id(self)) + + @property + def readonly(self): + return (self.__readonly + or any(r.readonly for r in self.selected_records)) + + @readonly.setter + def readonly(self, value): + self.__readonly = value + + @property + def deletable(self): + return all(r.deletable for r in self.selected_records) + + def search_active(self, active=True): + if active and not self.parent: + self.screen_container.set_screen(self) + self.screen_container.show_filter() + else: + self.screen_container.hide_filter() + + @property + def domain_parser(self): + view_id = self.current_view.view_id if self.current_view else None + + if view_id in self._domain_parser: + return self._domain_parser[view_id] + + if view_id not in self.fields_view_tree: + context = self.context + context['view_tree_width'] = CONFIG['client.save_tree_width'] + try: + self.fields_view_tree[view_id] = view_tree = RPCExecute( + 'model', self.model_name, 'fields_view_get', False, 'tree', + context=context) + except RPCException: + view_tree = { + 'fields': {}, + } + else: + view_tree = self.fields_view_tree[view_id] + + fields = copy.deepcopy(view_tree['fields']) + for name, props in fields.items(): + if props['type'] not in ('selection', 'reference'): + continue + if isinstance(props['selection'], (tuple, list)): + continue + props['selection'] = self.get_selection(props) + + if 'arch' in view_tree: + # Filter only fields in XML view + xml_dom = xml.dom.minidom.parseString(view_tree['arch']) + root_node, = xml_dom.childNodes + ofields = collections.OrderedDict() + for node in root_node.childNodes: + if node.nodeName != 'field': + continue + attributes = node_attributes(node) + name = attributes['name'] + # If a field is defined multiple times in the XML, + # take only the first definition + if name in ofields: + continue + ofields[name] = fields[name] + for attr in ['string', 'factor']: + if attributes.get(attr): + ofields[name][attr] = attributes[attr] + symbol = attributes.get('symbol') + if symbol and symbol not in ofields: + ofields[symbol] = fields[symbol] + fields = ofields + + if 'active' in view_tree['fields']: + self.screen_container.but_active.show() + else: + self.screen_container.but_active.hide() + + # Add common fields + for name, string, type_ in ( + ('id', _('ID'), 'integer'), + ('create_uid', _('Create by'), 'many2one'), + ('create_date', _('Created at'), 'datetime'), + ('write_uid', _('Edited by'), 'many2one'), + ('write_date', _('Edited at'), 'datetime'), + ): + if name not in fields: + fields[name] = { + 'string': string, + 'name': name, + 'type': type_, + } + if type_ == 'datetime': + fields[name]['format'] = '"%H:%M:%S"' + + domain_parser = DomainParser(fields, self.context) + self._domain_parser[view_id] = domain_parser + return domain_parser + + def get_selection(self, props): + try: + change_with = props.get('selection_change_with') + if change_with: + selection = RPCExecute('model', self.model_name, + props['selection'], dict((p, None) for p in change_with)) + else: + selection = RPCExecute('model', self.model_name, + props['selection']) + except RPCException: + selection = [] + selection.sort(key=itemgetter(1)) + return selection + + def search_prev(self, search_string): + if self.limit: + self.offset = max(self.offset - self.limit, 0) + self.search_filter(search_string=search_string) + + def search_next(self, search_string): + if self.limit: + self.offset += self.limit + self.search_filter(search_string=search_string) + + def search_complete(self, search_string): + return list(self.domain_parser.completion(search_string)) + + def search_filter(self, search_string=None, only_ids=False): + if self.context_screen and not only_ids: + context_record = self.context_screen.current_record + if not context_record.validate(): + self.clear() + self.context_screen.display(set_cursor=True) + return False + context = self.local_context + screen_context = self.context_screen.get_on_change_value() + screen_context.pop('id') + context.update(screen_context) + self.new_group(context) + + domain = self.search_domain(search_string, True) + + context = self.context + if self.screen_container.but_active.get_active(): + context['active_test'] = False + ids = [] + while True: + try: + ids = RPCExecute('model', self.model_name, 'search', domain, + self.offset, self.limit, self.order, context=context) + except RPCException: + break + if ids or self.offset <= 0: + break + self.offset = max(self.offset - self.limit, 0) + if not only_ids: + if self.limit is not None and len(ids) == self.limit: + try: + self.search_count = RPCExecute('model', self.model_name, + 'search_count', domain, context=context) + except RPCException: + self.search_count = 0 + else: + self.search_count = len(ids) + self.screen_container.but_prev.set_sensitive(bool(self.offset)) + if (self.limit is not None + and len(ids) == self.limit + and self.search_count > self.limit + self.offset): + self.screen_container.but_next.set_sensitive(True) + else: + self.screen_container.but_next.set_sensitive(False) + if only_ids: + return ids + self.clear() + self.load(ids) + self.count_tab_domain() + return bool(ids) + + def search_domain(self, search_string=None, set_text=False, with_tab=True): + domain = [] + # Test first parent to avoid calling unnecessary domain_parser + if not self.parent and self.domain_parser: + if search_string is not None: + domain = self.domain_parser.parse(search_string) + else: + domain = self.search_value + self.search_value = None + if set_text: + self.screen_container.set_text( + self.domain_parser.string(domain)) + else: + domain = [('id', 'in', [x.id for x in self.group])] + + if domain: + if self.domain: + domain = ['AND', domain, self.domain] + else: + domain = self.domain + + if self.screen_container.but_active.get_active(): + if domain: + domain = [domain, ('active', '=', False)] + else: + domain = [('active', '=', False)] + if self.current_view and self.current_view.view_type == 'calendar': + if domain: + domain = ['AND', domain, self.current_view.current_domain()] + else: + domain = self.current_view.current_domain() + if self.context_domain: + decoder = PYSONDecoder(self.context) + domain = ['AND', domain, decoder.decode(self.context_domain)] + if with_tab: + tab_domain = self.screen_container.get_tab_domain() + if tab_domain: + domain = ['AND', domain, tab_domain] + return domain + + def count_tab_domain(self, current=False): + def set_tab_counter(count, idx): + try: + count = count() + except RPCException: + count = None + self.screen_container.set_tab_counter(count, idx) + screen_domain = self.search_domain( + self.screen_container.get_text(), with_tab=False) + index = self.screen_container.get_tab_index() + for idx, (name, domain, count) in enumerate( + self.screen_container.tab_domain): + if not count or (current and idx != index): + continue + domain = ['AND', domain, screen_domain] + set_tab_counter(lambda: None, idx) + RPCExecute('model', self.model_name, + 'search_count', domain, context=self.context, + callback=functools.partial(set_tab_counter, idx=idx)) + + @property + def context(self): + context = self.group.context + if self.context_screen: + context['context_model'] = self.context_screen.model_name + return context + + @property + def local_context(self): + context = self.group.local_context + if self.context_screen: + context['context_model'] = self.context_screen.model_name + return context + + def __get_group(self): + return self.__group + + def __set_group(self, group): + fields = {} + fields_views = {} + if self.group is not None: + self.group.signal_unconnect(self) + for name, field in self.group.fields.items(): + fields[name] = field.attrs + fields_views[name] = field.views + self.tree_states_done.clear() + self.__group = group + self.parent = group.parent + self.parent_name = group.parent_name + if self.parent: + self.filter_widget = None + self.order = None + if len(group): + self.current_record = group[0] + else: + self.current_record = None + self.__group.signal_connect(self, 'group-cleared', self._group_cleared) + self.__group.signal_connect(self, 'group-list-changed', + self._group_list_changed) + self.__group.signal_connect(self, 'record-modified', + self._record_modified) + self.__group.signal_connect(self, 'group-changed', self._group_changed) + self.__group.add_fields(fields) + for name, views in fields_views.items(): + self.__group.fields[name].views.update(views) + self.__group.exclude_field = self.exclude_field + + group = property(__get_group, __set_group) + + def new_group(self, context=None): + context = context if context is not None else self.context + self.group = Group(self.model_name, {}, domain=self.domain, + context=context, readonly=self.__readonly) + + def _group_cleared(self, group, signal): + for view in self.views: + if view.view_type == 'tree': + view.display(force=True) + + def _group_list_changed(self, group, signal): + for view in self.views: + if hasattr(view, 'group_list_changed'): + view.group_list_changed(group, signal) + + def _record_modified(self, group, signal): + self.signal('record-modified', signal) + + def _group_changed(self, group, record): + if not self.parent: + self.display() + self.signal('group-changed', record) + + def __get_current_record(self): + if (self.__current_record is not None + and self.__current_record.group is None): + self.__current_record = None + return self.__current_record + + def __set_current_record(self, record): + self.__current_record = record + if record: + try: + pos = self.group.index(record) + self.offset + 1 + except ValueError: + # XXX offset? + pos = record.get_index_path() + else: + pos = 0 + self.signal('record-message', (pos, len(self.group) + self.offset, + self.search_count, record and record.id)) + self.signal('resources', record.resources if record else None) + # update resources after 1 second + GLib.timeout_add(1000, self._update_resources, record) + return True + + current_record = property(__get_current_record, __set_current_record) + + def _update_resources(self, record): + if (record + and record == self.current_record + and self.signal_connected('resources')): + self.signal('resources', record.get_resources()) + return False + + def destroy(self): + for view in self.views: + view.destroy() + del self.views[:] + super(Screen, self).destroy() + self.group.signal_unconnect(self) + self.group.destroy() + + def default_row_activate(self): + if (self.current_view.view_type == 'tree' + and int(self.current_view.attributes.get('keyword_open', 0))): + return Action.exec_keyword('tree_open', { + 'model': self.model_name, + 'id': self.current_record.id if self.current_record else None, + 'ids': [r.id for r in self.selected_records], + }, context=self.local_context, warning=False) + else: + if not self.modified(): + self.switch_view(view_type='form') + return True + + @property + def number_of_views(self): + return len(self.views) + len(self.view_to_load) + + def switch_view(self, view_type=None, view_id=None, display=True): + if view_id is not None: + view_id = int(view_id) + if self.current_view: + self.current_view.set_value() + if (self.current_record + and self.current_record not in self.current_record.group): + self.current_record = None + fields = self.current_view.get_fields() + if (self.current_record and self.current_view.editable + and not self.current_record.validate(fields)): + self.screen_container.set(self.current_view.widget) + self.set_cursor() + self.current_view.display() + return + + def found(): + if not self.current_view: + return False + elif not view_type and view_id is None: + return False + elif view_id is not None: + return self.current_view.view_id == view_id + else: + return self.current_view.view_type == view_type + while not found(): + if len(self.view_to_load): + self.load_view_to_load() + self.__current_view = len(self.views) - 1 + elif (view_id is not None + and view_id not in {v.view_id for v in self.views}): + self.add_view_id(view_id, view_type) + self.__current_view = len(self.views) - 1 + break + else: + self.__current_view = ((self.__current_view + 1) + % len(self.views)) + if not view_type and view_id is None: + break + if view_type and not view_id and not len(self.view_to_load): + break + self.screen_container.set(self.current_view.widget) + if display: + self.display() + # Postpone set of the cursor to ensure widgets are allocated + GLib.idle_add(self.set_cursor) + + def load_view_to_load(self): + if len(self.view_to_load): + if self.view_ids: + view_id = self.view_ids.pop(0) + else: + view_id = None + view_type = self.view_to_load.pop(0) + self.add_view_id(view_id, view_type) + + def add_view_id(self, view_id, view_type): + if view_id and str(view_id) in self.views_preload: + view = self.views_preload[str(view_id)] + elif not view_id and view_type in self.views_preload: + view = self.views_preload[view_type] + else: + context = self.context + context['view_tree_width'] = CONFIG['client.save_tree_width'] + try: + view = RPCExecute( + 'model', self.model_name, 'fields_view_get', view_id, + view_type, context=context) + except RPCException: + return + return self.add_view(view) + + def add_view(self, view): + arch = view['arch'] + fields = view['fields'] + view_id = view['view_id'] + + xml_dom = xml.dom.minidom.parseString(arch) + root, = xml_dom.childNodes + if root.tagName == 'tree': + self.fields_view_tree[view_id] = view + + # Ensure that loading is always lazy for fields on form view + # and always eager for fields on tree or graph view + if root.tagName == 'form': + loading = 'lazy' + else: + loading = 'eager' + for field in fields: + if field not in self.group.fields or loading == 'eager': + fields[field]['loading'] = loading + else: + fields[field]['loading'] = \ + self.group.fields[field].attrs['loading'] + self.group.add_fields(fields) + for field in fields: + self.group.fields[field].views.add(view_id) + view = View.parse( + self, view_id, view['type'], xml_dom, view.get('field_childs')) + self.views.append(view) + + return view + + def new(self, default=True, rec_name=None): + previous_view = self.current_view + if self.current_view.view_type == 'calendar': + selected_date = self.current_view.get_selected_date() + if self.current_view and not self.current_view.editable: + self.switch_view('form', display=False) + if self.current_view.view_type != 'form': + return None + if self.current_record: + group = self.current_record.group + else: + group = self.group + record = group.new(default, rec_name=rec_name) + group.add(record, self.new_position) + if previous_view.view_type == 'calendar': + previous_view.set_default_date(record, selected_date) + self.current_record = record + self.display() + # Postpone set of the cursor to ensure widgets are allocated + GLib.idle_add(self.set_cursor, True) + return self.current_record + + @property + def new_position(self): + if self.order is not None: + order = self.order + else: + order = self.default_order + if order: + for oexpr, otype in order: + if oexpr == 'id' and otype: + if otype.startswith('DESC'): + return 0 + elif otype.startswith('ASC'): + return -1 + if self.parent: + return -1 + else: + return 0 + + def set_on_write(self, func_name): + if func_name: + self.group.on_write.add(func_name) + + def cancel_current(self, initial_value=None): + if self.current_record: + self.current_record.cancel() + if self.current_record.id < 0: + if initial_value is not None: + self.current_record.reset(initial_value) + else: + self.remove(records=[self.current_record]) + + def save_current(self): + if not self.current_record: + if self.current_view.view_type == 'tree' and len(self.group): + self.current_record = self.group[0] + else: + return True + self.current_view.set_value() + saved = False + record_id = None + fields = self.current_view.get_fields() + path = self.current_record.get_path(self.group) + if self.current_view.view_type == 'tree': + # False value must be not saved + saved = all(( + x is not False and x >= 0 + for x in self.group.save())) + record_id = self.current_record.id if self.current_record else None + elif self.current_record.validate(fields): + record_id = self.current_record.save(force_reload=True) + # False value must be not saved + saved = record_id is not False and record_id >= 0 + else: + self.set_cursor() + self.current_view.display() + return False + if path and record_id: + path = path[:-1] + ((path[-1][0], record_id),) + self.current_record = self.group.get_by_path(path) + self.display() + self.signal('record-saved') + return saved + + def __get_current_view(self): + if not len(self.views): + return None + return self.views[self.__current_view] + + current_view = property(__get_current_view) + + def set_cursor(self, new=False, reset_view=True): + current_view = self.current_view + if not current_view: + return + elif current_view.view_type in ('tree', 'form', 'list-form'): + current_view.set_cursor(new=new, reset_view=reset_view) + + def get(self): + if not self.current_record: + return None + self.current_view.set_value() + return self.current_record.get() + + def get_on_change_value(self): + if not self.current_record: + return None + self.current_view.set_value() + return self.current_record.get_on_change_value() + + def modified(self): + if self.current_view.view_type != 'tree': + if self.current_record: + if self.current_record.modified or self.current_record.id < 0: + return True + else: + for record in self.group: + if record.modified or record.id < 0: + return True + if self.current_view.modified: + return True + return False + + def reload(self, ids, written=False): + self.group.reload(ids) + if written: + self.group.written(ids) + if self.parent: + self.parent.root_parent.reload() + self.display() + + def unremove(self): + records = self.selected_records + for record in records: + self.group.unremove(record) + + def remove(self, delete=False, remove=False, force_remove=False, + records=None): + records = records or self.selected_records + if not records: + return + if delete: + # Must delete children records before parent + records.sort(key=lambda r: r.depth, reverse=True) + if not self.group.delete(records): + return False + + top_record = records[0] + top_group = top_record.group + idx = top_group.index(top_record) + path = top_record.get_path(self.group) + + for record in records: + # set current model to None to prevent __select_changed + # to save the previous_model as it can be already deleted. + self.current_record = None + record.group.remove(record, remove=remove, signal=False, + force_remove=force_remove) + # send record-changed only once + record.signal('record-changed') + + if delete: + for record in records: + if record in record.group.record_deleted: + record.group.record_deleted.remove(record) + if record in record.group.record_removed: + record.group.record_removed.remove(record) + if record.parent: + # Save parent without deleted children + record.parent.save(force_reload=False) + record.destroy() + + if idx > 0: + record = top_group[idx - 1] + path = path[:-1] + ((path[-1][0], record.id,),) + else: + path = path[:-1] + if path: + self.current_record = self.group.get_by_path(path) + elif len(self.group): + self.current_record = self.group[0] + self.set_cursor() + self.display() + return True + + def copy(self): + ids = [r.id for r in self.selected_records] + try: + new_ids = RPCExecute('model', self.model_name, 'copy', ids, {}, + context=self.context) + except RPCException: + return False + self.load(new_ids, position=self.new_position) + return True + + def set_tree_state(self): + view = self.current_view + if view.view_type not in ('tree', 'form'): + return + if id(view) in self.tree_states_done: + return + if view.view_type == 'form' and self.tree_states_done: + return + if (view.view_type == 'tree' + and not view.attributes.get('tree_state', False)): + # Mark as done to not set later when the view_type change + self.tree_states_done.add(id(view)) + parent = self.parent.id if self.parent else None + if parent is not None and parent < 0: + return + expanded_nodes, selected_nodes = [], [] + state = self.tree_states[parent][view.children_field] + if state: + expanded_nodes, selected_nodes = state + if state is None and CONFIG['client.save_tree_state']: + json_domain = self.get_tree_domain(parent) + try: + expanded_nodes, selected_nodes = RPCExecute('model', + 'ir.ui.view_tree_state', 'get', + self.model_name, json_domain, + view.children_field) + expanded_nodes = json.loads(expanded_nodes) + selected_nodes = json.loads(selected_nodes) + except RPCException: + logger.warn( + _('Unable to get view tree state for %s') + % self.model_name) + self.tree_states[parent][view.children_field] = ( + expanded_nodes, selected_nodes) + if view.view_type == 'tree': + view.expand_nodes(expanded_nodes) + view.select_nodes(selected_nodes) + else: + if selected_nodes: + record = None + for node in selected_nodes[0]: + new_record = self.group.get(node) + if node < 0 and -node < len(self.group): + # Negative id is the index of the new record + new_record = self.group[-node] + if not new_record: + break + else: + record = new_record + if record and record != self.current_record: + self.current_record = record + # Force a display of the view to synchronize the + # widgets with the new record + view.display() + self.tree_states_done.add(id(view)) + + def save_tree_state(self, store=True): + parent = self.parent.id if self.parent else None + for view in self.views: + if view.view_type == 'form': + for widgets in view.widgets.values(): + for widget in widgets: + if hasattr(widget, 'screen'): + widget.screen.save_tree_state(store) + if len(self.views) == 1 and self.current_record: + path = self.current_record.id + if path < 0: + path = -self.current_record.group.index( + self.current_record) + self.tree_states[parent][view.children_field] = ( + [], [[path]]) + elif view.view_type == 'tree': + paths = view.get_expanded_paths() + selected_paths = view.get_selected_paths() + self.tree_states[parent][view.children_field] = ( + paths, selected_paths) + if (store + and int(view.attributes.get('tree_state', False)) + and CONFIG['client.save_tree_state']): + json_domain = self.get_tree_domain(parent) + json_paths = json.dumps(paths, separators=(',', ':')) + json_selected_path = json.dumps( + selected_paths, separators=(',', ':')) + try: + RPCExecute('model', 'ir.ui.view_tree_state', 'set', + self.model_name, json_domain, view.children_field, + json_paths, json_selected_path, + process_exception=False) + clear_cache('model.ir.ui.view_tree_state.get') + except Exception: + logger.warn( + _('Unable to set view tree state'), exc_info=True) + + def get_tree_domain(self, parent): + if parent: + domain = (self.domain + [(self.exclude_field, '=', parent)]) + else: + domain = self.domain + json_domain = json.dumps( + domain, cls=JSONEncoder, separators=(',', ':')) + return json_domain + + def load(self, ids, set_cursor=True, modified=False, position=-1): + self.group.load(ids, modified=modified, position=position) + self.current_view.reset() + if ids and self.current_view.view_type != 'calendar': + self.display(ids[0]) + else: + self.current_record = None + self.display() + if set_cursor: + self.set_cursor() + + def display(self, res_id=None, set_cursor=False): + if res_id: + self.current_record = self.group.get(res_id) + else: + if (self.current_record + and self.current_record in self.current_record.group): + pass + elif self.group and self.current_view.view_type != 'calendar': + self.current_record = self.group[0] + else: + self.current_record = None + if self.views: + self.search_active(self.current_view.view_type + in ('tree', 'graph', 'calendar')) + for view in self.views: + # Always display tree view to update model + # because view can be used even if it is not shown + # like for save_tree_state + if (view == self.current_view + or view.view_type == 'tree' + or view.widget.get_parent()): + view.display() + self.current_view.widget.set_sensitive( + bool(self.group + or (self.current_view.view_type != 'form') + or self.current_record)) + if set_cursor: + self.set_cursor(reset_view=False) + self.set_tree_state() + # Force record-message signal + self.current_record = self.current_record + + def display_next(self): + view = self.current_view + view.set_value() + self.set_cursor(reset_view=False) + if view.view_type == 'tree' and len(self.group): + range_ = view.treeview.get_visible_range() + if range_: + start, end = range_ + vadjustment = view.treeview.get_vadjustment() + vadjustment.props.value = min( + vadjustment.props.value + vadjustment.props.page_increment, + vadjustment.props.upper) + model = view.treeview.get_model() + iter_ = model.get_iter(end) + self.current_record = model.get_value(iter_, 0) + elif (view.view_type == 'form' + and self.current_record + and self.current_record.group): + group = self.current_record.group + record = self.current_record + while group: + children = record.children_group(view.children_field) + if children: + record = children[0] + break + idx = group.index(record) + 1 + if idx < len(group): + record = group[idx] + break + parent = record.parent + if not parent or record.model_name != parent.model_name: + break + next = parent.next.get(id(parent.group)) + while not next: + parent = parent.parent + if not parent: + break + next = parent.next.get(id(parent.group)) + if not next: + break + record = next + break + self.current_record = record + elif (view.view_type == 'list-form' and len(self.group) + and self.current_record in self.group): + idx = self.group.index(self.current_record) + if 0 <= idx < len(self.group) - 1: + self.current_record = self.group[idx + 1] + elif view.view_type == 'calendar': + record = self.current_record + goocalendar = view.widgets.get('goocalendar') + if goocalendar: + date = goocalendar.selected_date + year = date.year + month = date.month + start = datetime.datetime(year, month, 1) + nb_days = calendar.monthrange(year, month)[1] + delta = datetime.timedelta(days=nb_days) + end = start + delta + events = goocalendar.event_store.get_events(start, end) + events.sort() + if not record: + if events: + self.current_record = events[0].record + else: + self.current_record = None + else: + for idx, event in enumerate(events): + if event.record == record: + next_id = idx + 1 + if next_id < len(events): + self.current_record = events[next_id].record + break + else: + self.current_record = self.group[0] if len(self.group) else None + self.set_cursor(reset_view=False) + view.display() + + def display_prev(self): + view = self.current_view + view.set_value() + self.set_cursor(reset_view=False) + if view.view_type == 'tree' and len(self.group): + range_ = view.treeview.get_visible_range() + if range_: + start, end = range_ + vadjustment = view.treeview.get_vadjustment() + vadjustment.props.value = min( + vadjustment.props.value - vadjustment.props.page_increment, + vadjustment.props.lower) + model = view.treeview.get_model() + iter_ = model.get_iter(start) + self.current_record = model.get_value(iter_, 0) + elif (view.view_type == 'form' + and self.current_record + and self.current_record.group): + group = self.current_record.group + record = self.current_record + idx = group.index(record) - 1 + if idx >= 0: + record = group[idx] + children = True + while children: + children = record.children_group(view.children_field) + if children: + record = children[-1] + else: + parent = record.parent + if parent and record.model_name == parent.model_name: + record = parent + self.current_record = record + elif view.view_type == 'calendar': + record = self.current_record + goocalendar = view.widgets.get('goocalendar') + if goocalendar: + date = goocalendar.selected_date + year = date.year + month = date.month + start = datetime.datetime(year, month, 1) + nb_days = calendar.monthrange(year, month)[1] + delta = datetime.timedelta(days=nb_days) + end = start + delta + events = goocalendar.event_store.get_events(start, end) + events.sort() + if not record: + if events: + self.current_record = events[0].record + else: + self.current_record = None + else: + for idx, event in enumerate(events): + if event.record == record: + prev_id = idx - 1 + if prev_id >= 0: + self.current_record = events[prev_id].record + break + elif (view.view_type == 'list-form' and len(self.group) + and self.current_record in self.group): + idx = self.group.index(self.current_record) + if 0 < idx <= len(self.group) - 1: + self.current_record = self.group[idx - 1] + else: + self.current_record = self.group[-1] if len(self.group) else None + self.set_cursor(reset_view=False) + view.display() + + def invalid_message(self, record=None): + if record is None: + record = self.current_record + domain_string = _('"%s" is not valid according to its domain.') + domain_parser = DomainParser( + {n: f.attrs for n, f in record.group.fields.items()}) + fields = [] + for field, invalid in sorted(record.invalid_fields.items()): + string = record.group.fields[field].attrs['string'] + if invalid == 'required' or invalid == [[field, '!=', None]]: + fields.append(_('"%s" is required.') % string) + elif invalid == 'domain': + fields.append(domain_string % string) + elif invalid == 'children': + fields.append(_('The values of "%s" are not valid.') % string) + else: + if domain_parser.stringable(invalid): + fields.append(domain_parser.string(invalid)) + else: + fields.append(domain_string % string) + if len(fields) > 5: + fields = fields[:5] + ['...'] + return '\n'.join(fields) + + @property + def selected_records(self): + return self.current_view.selected_records if self.current_view else [] + + @property + def selected_paths(self): + if self.current_view and self.current_view.view_type == 'tree': + return self.current_view.get_selected_paths() + + @property + def listed_records(self): + if self.current_view and self.current_view.view_type == 'tree': + return self.current_view.listed_records + elif self.current_record: + return [self.current_record] + else: + return [] + + @property + def listed_paths(self): + if self.current_view and self.current_view.view_type == 'tree': + return self.current_view.get_listed_paths() + + def clear(self): + self.current_record = None + self.group.clear() + self.tree_states_done.clear() + for view in self.views: + view.reset() + + def on_change(self, fieldname, attr): + self.current_record.on_change(fieldname, attr) + self.display() + + def get_buttons(self): + 'Return active buttons for the current view' + def is_active(record, button): + if button.attrs.get('type', 'class') == 'instance': + return False + states = record.expr_eval(button.attrs.get('states', {})) + return not (states.get('invisible') or states.get('readonly')) + + if not self.selected_records: + return [] + + buttons = self.current_view.get_buttons() + + for record in self.selected_records: + buttons = [b for b in buttons if is_active(record, b)] + if not buttons: + break + return buttons + + def button(self, button): + 'Execute button on the selected records' + self.current_view.set_value() + fields = self.current_view.get_fields() + for record in self.selected_records: + domain = record.expr_eval( + button.get('states', {})).get('pre_validate', []) + if not record.validate(fields, pre_validate=domain): + warning(self.invalid_message(record), _('Pre-validation')) + self.display(set_cursor=True) + if domain: + # Reset valid state with normal domain + record.validate(fields) + return + if button.get('confirm', False) and not sur(button['confirm']): + return + if button.get('type', 'class') == 'class': + if not self.current_record.save(force_reload=False): + return + if button.get('type', 'class') == 'class': + self._button_class(button) + else: + self._button_instance(button) + + def _button_instance(self, button): + record = self.current_record + args = record.expr_eval(button.get('change', [])) + values = record._get_on_change_args(args) + try: + changes = RPCExecute('model', self.model_name, button['name'], + values, context=self.context) + except RPCException: + return + record.set_on_change(changes) + record.signal('record-changed') + + def _button_class(self, button): + ids = [r.id for r in self.selected_records] + context = self.context + context['_timestamp'] = {} + for record in self.selected_records: + context['_timestamp'].update(record.get_timestamp()) + try: + action = RPCExecute('model', self.model_name, button['name'], + ids, context=context) + except RPCException: + action = None + self.reload(ids, written=True) + if isinstance(action, str): + self.client_action(action) + elif action: + Action.execute(action, { + 'model': self.model_name, + 'id': self.current_record.id, + 'ids': ids, + }, context=self.context, keyword=True) + + def client_action(self, action): + access = MODELACCESS[self.model_name] + if action == 'new': + if access['create']: + self.new() + elif action == 'delete': + if (access['delete'] + and (self.current_record.deletable + if self.current_record else True)): + self.remove(delete=not self.parent, + force_remove=not self.parent) + elif action == 'remove': + if access['write'] and access['read'] and self.parent: + self.remove(remove=True) + elif action == 'copy': + if access['create']: + self.copy() + elif action == 'next': + self.display_next() + elif action == 'previous': + self.display_prev() + elif action == 'close': + from tryton.gui import Main + Main().sig_win_close() + elif action.startswith('switch'): + self.switch_view(*action.split(None, 2)[1:]) + elif action == 'reload': + if (self.current_view.view_type in ['tree', 'graph', 'calendar'] + and not self.parent): + self.search_filter() + elif action == 'reload menu': + from tryton.gui import Main + RPCContextReload(Main().sig_win_menu) + elif action == 'reload context': + RPCContextReload() + + def get_url(self, name=''): + query_string = [] + if self.domain: + query_string.append(('domain', json.dumps( + self.domain, cls=JSONEncoder, separators=(',', ':')))) + context = self.local_context # Avoid rpc context + if context: + query_string.append(('context', json.dumps( + context, cls=JSONEncoder, separators=(',', ':')))) + if self.context_screen: + query_string.append( + ('context_model', self.context_screen.model_name)) + if name: + query_string.append( + ('name', json.dumps(name, separators=(',', ':')))) + if self.screen_container.tab_domain: + query_string.append(('tab_domain', json.dumps( + self.screen_container.tab_domain, + cls=JSONEncoder, separators=(',', ':')))) + path = [CONFIG['login.db'], 'model', self.model_name] + view_ids = [v.view_id for v in self.views] + self.view_ids + if self.current_view.view_type != 'form': + if self.search_value: + search_value = self.search_value + else: + search_string = self.screen_container.get_text() + search_value = self.domain_parser.parse(search_string) + if search_value: + query_string.append(('search_value', json.dumps( + search_value, cls=JSONEncoder, + separators=(',', ':')))) + elif self.current_record and self.current_record.id > -1: + path.append(str(self.current_record.id)) + i = view_ids.index(self.current_view.view_id) + view_ids = view_ids[i:] + view_ids[:i] + if view_ids: + query_string.append(('views', json.dumps( + view_ids, separators=(',', ':')))) + query_string = urllib.parse.urlencode(query_string) + return urllib.parse.urlunparse(('tryton', + CONFIG['login.host'], + '/'.join(path), query_string, '', '')) diff --git a/tryton/tryton/gui/window/view_form/view/__init__.py b/tryton/tryton/gui/window/view_form/view/__init__.py new file mode 100644 index 0000000000..3175ae64b1 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/__init__.py @@ -0,0 +1,131 @@ +# 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 gettext +from collections import defaultdict + +from tryton.common import node_attributes + +_ = gettext.gettext +list_ = list # list builtins is overridden by import .list + + +class View(object): + view_type = None + widget = None + mnemonic_widget = None + view_id = None + modified = None + editable = None + children_field = None + scroll = None + xml_parser = None + + def __init__(self, view_id, screen, xml): + self.view_id = view_id + self.screen = screen + self.widgets = defaultdict(list_) + self.state_widgets = [] + self.attributes = node_attributes(xml) + screen.set_on_write(self.attributes.get('on_write')) + + if self.xml_parser: + self.xml_parser( + self, self.screen.exclude_field, + {k: f.attrs for k, f in self.screen.group.fields.items()} + ).parse(xml) + + def set_value(self): + raise NotImplementedError + + def get_fields(self): + raise NotImplementedError + + @property + def record(self): + return self.screen.current_record + + @record.setter + def record(self, value): + self.screen.current_record = value + + @property + def group(self): + return self.screen.group + + @property + def selected_records(self): + return [] + + def get_buttons(self): + raise NotImplementedError + + @staticmethod + def parse(screen, view_id, view_type, xml, children_field): + from .list import ViewTree + from .form import ViewForm + from .graph import ViewGraph + from .calendar_ import ViewCalendar + from .list_form import ViewListForm + + root, = xml.childNodes + if view_type == 'tree': + return ViewTree(view_id, screen, root, children_field) + elif view_type == 'form': + return ViewForm(view_id, screen, root) + elif view_type == 'graph': + return ViewGraph(view_id, screen, root) + elif view_type == 'calendar': + return ViewCalendar(view_id, screen, root) + elif view_type == 'list-form': + return ViewListForm(view_id, screen, root) + + +class XMLViewParser: + + def __init__(self, view, exclude_field, field_attrs): + self.view = view + self.exclude_field = exclude_field + self.field_attrs = field_attrs + + def _node_attributes(self, node): + node_attrs = node_attributes(node) + if 'name' in node_attrs: + field = self.field_attrs.get(node_attrs['name'], {}) + else: + field = {} + + for name in ['readonly', 'homogeneous']: + if name in node_attrs: + node_attrs[name] = bool(int(node_attrs[name])) + for name in [ + 'yexpand', 'yfill', 'xexpand', 'xfill', 'colspan', 'position', + 'height', 'width', 'expand']: + if name in node_attrs: + node_attrs[name] = int(node_attrs[name]) + for name in ['xalign', 'yalign']: + if name in node_attrs: + node_attrs[name] = float(node_attrs[name]) + + if field: + node_attrs.setdefault('widget', field['type']) + if node.tagName == 'label' and 'string' not in node_attrs: + node_attrs['string'] = field['string'] + _(':') + if node.tagName == 'field' and 'help' not in node_attrs: + node_attrs['help'] = field['help'] + for name in [ + 'relation', 'domain', 'selection', 'string', 'states', + 'relation_field', 'views', 'invisible', 'add_remove', + 'sort', 'context', 'size', 'filename', 'autocomplete', + 'translate', 'create', 'delete', 'selection_change_with', + 'schema_model', 'required', 'help_selection', + 'help_field', 'order']: + if name in field: + node_attrs.setdefault(name, field[name]) + return node_attrs + + def parse(self, node): + node_attrs = self._node_attributes(node) + if node.nodeType != node.ELEMENT_NODE: + return + parser = getattr(self, '_parse_%s' % node.tagName) + parser(node, node_attrs) diff --git a/tryton/tryton/gui/window/view_form/view/calendar_.py b/tryton/tryton/gui/window/view_form/view/calendar_.py new file mode 100644 index 0000000000..664209e497 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/calendar_.py @@ -0,0 +1,171 @@ +# 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 functools import wraps + +import datetime +import gettext + +from gi.repository import Gtk + +from . import View, XMLViewParser +try: + from .calendar_gtk.calendar_ import Calendar_ + from .calendar_gtk.toolbar import Toolbar +except ImportError: + Calendar_ = None + Toolbar = None + +from tryton.common import MODELACCESS + +_ = gettext.gettext + + +def goocalendar_required(func): + "Decorator for goocalendar required" + @wraps(func) + def wrapper(self, *args, **kwargs): + if 'goocalendar' not in self.widgets: + return + return func(self, *args, **kwargs) + return wrapper + + +class CalendarXMLViewParser(XMLViewParser): + + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self.calendar_fields = [] + + def _parse_calendar(self, node, attributes): + for child in node.childNodes: + self.parse(child) + goocalendar = Calendar_( + self.view.attributes, self.view, self.calendar_fields) + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.add(goocalendar) + toolbar = Toolbar(goocalendar) + self.view.widget.pack_start( + toolbar, expand=False, fill=False, padding=0) + self.view.widget.pack_start( + scrolledwindow, expand=True, fill=True, padding=0) + self.view.widgets['goocalendar'] = goocalendar + self.view.widgets['toolbar'] = toolbar + + if attributes.get('height') or attributes.get('width'): + scrolledwindow.set_size_request( + int(attributes.get('width', -1)), + int(attributes.get('height', -1))) + + def _parse_field(self, node, attributes): + self.calendar_fields.append(attributes) + + +class ViewCalendar(View): + editable = False + view_type = 'calendar' + xml_parser = CalendarXMLViewParser + + def __init__(self, view_id, screen, xml): + self.widget = Gtk.VBox() + + if not Calendar_: + self.widgets = {} + return + + super().__init__(view_id, screen, xml) + + goocalendar = self.widgets['goocalendar'] + toolbar = self.widgets['toolbar'] + goocalendar.connect('view-changed', self.on_view_changed, toolbar) + goocalendar.connect('page-changed', self.on_page_changed, toolbar) + goocalendar.connect('event-pressed', self.on_event_pressed) + goocalendar.connect('event-activated', self.on_event_activated) + goocalendar.connect('event-released', self.on_event_released) + goocalendar.connect('day-pressed', self.on_day_pressed) + goocalendar.connect('day-activated', self.on_day_activated) + + def on_page_changed(self, goocalendar, day, toolbar): + toolbar.update_displayed_date() + if goocalendar.update_domain(): + self.screen.search_filter() + + def on_view_changed(self, goocalendar, view, toolbar): + toolbar.update_displayed_date() + if goocalendar.update_domain(): + self.screen.search_filter() + + def on_event_pressed(self, goocalendar, event): + self.record = event.record + + def on_event_activated(self, goocalendar, event): + self.screen.switch_view('form') + + def on_event_released(self, goocalendar, event): + dtstart = self.attributes['dtstart'] + dtend = self.attributes.get('dtend') + record = event.record + group = record.group + previous_start = record[dtstart].get(record) + new_start = event.start + new_end = event.end + if not isinstance(previous_start, datetime.datetime): + new_start = event.start.date() + new_end = event.end.date() if event.end else None + if previous_start <= new_start: + if dtend: + group.fields[dtend].set_client(record, new_end) + group.fields[dtstart].set_client(record, new_start) + else: + group.fields[dtstart].set_client(record, new_start) + if dtend: + group.fields[dtend].set_client(record, new_end) + goocalendar.select(new_start) + record.save() + + def on_day_pressed(self, goocalendar, day): + self.record = None + + def on_day_activated(self, goocalendar, day): + model_access = MODELACCESS[self.screen.model_name] + if (bool(int(self.attributes.get('editable', 1))) + and model_access['create']): + self.screen.new() + + def __getitem__(self, name): + return None + + @goocalendar_required + def destroy(self): + self.widget.destroy() + self.widgets['goocalendar'].destroy() + + @goocalendar_required + def get_selected_date(self): + return self.widgets['goocalendar'].selected_date + + @goocalendar_required + def set_default_date(self, record, selected_date): + self.widgets['goocalendar'].set_default_date(record, selected_date) + + def current_domain(self): + if 'goocalendar' in self.widgets: + return self.widgets['goocalendar'].current_domain() + else: + # No need to load any record as nothing will be shown + return [('id', '=', -1)] + + def set_value(self): + pass + + def reset(self): + pass + + @goocalendar_required + def display(self): + self.widgets['goocalendar'].display(self.group) + + def set_cursor(self, new=False, reset_view=True): + pass + + def get_fields(self): + return [] diff --git a/tryton/tryton/gui/window/view_form/view/calendar_gtk/__init__.py b/tryton/tryton/gui/window/view_form/view/calendar_gtk/__init__.py new file mode 100644 index 0000000000..4effdfa4a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/calendar_gtk/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/tryton/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py b/tryton/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py new file mode 100644 index 0000000000..fc3f3e7539 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/calendar_gtk/calendar_.py @@ -0,0 +1,144 @@ +# 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 calendar +import datetime +import goocalendar +from .dates_period import DatesPeriod + +from tryton.common import MODELACCESS +from tryton.config import CONFIG + +_colors = CONFIG['calendar.colors'].split(',') + + +class Calendar_(goocalendar.Calendar): + 'Calendar' + + def __init__(self, attrs, view, fields, event_store=None): + super(Calendar_, self).__init__( + event_store, attrs.get('mode', 'month')) + self.props.selected_border_color = _colors[1] + if hasattr(self.props, 'selected_text_color'): + self.props.selected_text_color = _colors[0] + self.attrs = attrs + self.view_calendar = view + self.fields = fields + self.event_store = event_store + self.current_domain_period = self.get_displayed_period() + + def set_default_date(self, record, selected_date): + dtstart = self.attrs['dtstart'] + record[dtstart].set(record, datetime.datetime.combine(selected_date, + datetime.time(0))) + record.on_change([dtstart]) + record.on_change_with([dtstart]) + + def get_displayed_period(self): + cal = calendar.Calendar(self.firstweekday) + if self.view == 'day': + first_date = self.selected_date + last_date = self.selected_date + datetime.timedelta(1) + if self.view == 'week': + week = goocalendar.util.my_weekdatescalendar(cal, + self.selected_date) + first_date = week[0] + last_date = week[6] + last_date += datetime.timedelta(1) + elif self.view == 'month': + weeks = goocalendar.util.my_monthdatescalendar(cal, + self.selected_date) + first_date = weeks[0][0] + last_date = weeks[5][6] + last_date += datetime.timedelta(1) + displayed_period = DatesPeriod(first_date, last_date) + return displayed_period + + def update_domain(self): + displayed_period = self.get_displayed_period() + if not displayed_period.is_in(self.current_domain_period): + self.current_domain_period = displayed_period + return True + return False + + def current_domain(self): + first_datetime, last_datetime = \ + self.current_domain_period.get_dates(True) + dtstart = self.attrs['dtstart'] + dtend = self.attrs.get('dtend') or dtstart + domain = ['OR', + ['AND', (dtstart, '>=', first_datetime), + (dtstart, '<', last_datetime)], + ['AND', (dtend, '>=', first_datetime), + (dtend, '<', last_datetime)], + ['AND', (dtstart, '<', first_datetime), + (dtend, '>', last_datetime)]] + return domain + + def get_colors(self, record): + text_color = _colors[0] + if self.attrs.get('color'): + text_color = record[self.attrs['color']].get(record) + bg_color = _colors[1] + if self.attrs.get('background_color'): + bg_color = record[self.attrs['background_color']].get( + record) + return text_color, bg_color + + def display(self, group): + def is_date_only(value): + return (isinstance(value, datetime.date) + and not isinstance(value, datetime.datetime)) + dtstart = self.attrs['dtstart'] + dtend = self.attrs.get('dtend') + if self.view_calendar.record: + record = self.view_calendar.record + date = record[dtstart].get(record) + if date: # select the day of the current record + self.select(date) + + event_store = goocalendar.EventStore() + + model_access = MODELACCESS[self.view_calendar.screen.model_name] + editable = ( + bool(int(self.view_calendar.attributes.get('editable', 1))) + and model_access['write']) + + for record in group: + if not record[dtstart].get(record): + continue + + start = record[dtstart].get_client(record) + record[dtstart].state_set(record) + if dtend: + end = record[dtend].get_client(record) + record[dtend].state_set(record) + else: + end = None + midnight = datetime.time(0) + all_day = is_date_only(start) and (not end or is_date_only(end)) + if not isinstance(start, datetime.datetime): + start = datetime.datetime.combine(start, midnight) + if end and not isinstance(end, datetime.datetime): + end = datetime.datetime.combine(end, midnight) + + # Skip invalid event + if end is not None and start > end: + continue + + text_color, bg_color = self.get_colors(record) + label = '\n'.join(record[attrs['name']].get_client(record) + for attrs in self.fields).rstrip() + event_editable = ( + editable + and not record[dtstart].get_state_attrs(record).get( + 'readonly', False) + and (not dtend + or not record[dtend].get_state_attrs(record).get( + 'readonly', False))) + event = goocalendar.Event(label, start, end, text_color=text_color, + bg_color=bg_color, all_day=all_day, editable=event_editable) + event.record = record + event_store.add(event) + self.event_store = event_store + + self.grab_focus(self.get_root_item()) diff --git a/tryton/tryton/gui/window/view_form/view/calendar_gtk/dates_period.py b/tryton/tryton/gui/window/view_form/view/calendar_gtk/dates_period.py new file mode 100644 index 0000000000..ba5addd159 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/calendar_gtk/dates_period.py @@ -0,0 +1,30 @@ +# 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 datetime + + +class DatesPeriod(): + """ + This class represents a period of time between two dates or two datetimes. + """ + + def __init__(self, start, end): + assert type(start) == type(end) + self.start = start + self.end = end + + def is_in(self, period): + return self.start >= period.start and self.end <= period.end + + def get_dates(self, format_datetime=False): + if not format_datetime: + return self.start, self.end + + midnight = datetime.time(0) + start = datetime.datetime.combine(self.start, midnight) + end = datetime.datetime.combine(self.end, midnight) + return start, end + + def __str__(self): + string = self.start.__str__() + ' => ' + self.end.__str__() + return string diff --git a/tryton/tryton/gui/window/view_form/view/calendar_gtk/toolbar.py b/tryton/tryton/gui/window/view_form/view/calendar_gtk/toolbar.py new file mode 100644 index 0000000000..866402d91b --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/calendar_gtk/toolbar.py @@ -0,0 +1,246 @@ +# 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 datetime +import calendar +import gettext + +from gi.repository import Gdk, Gtk + +from tryton.gui import Main +from tryton.common import IconFactory +from tryton.common.datetime_ import popup_position, popup_show, popup_hide + +_ = gettext.gettext + + +class Toolbar(Gtk.Toolbar): + + def __init__(self, goocalendar): + super(Toolbar, self).__init__() + self.goocalendar = goocalendar + self.accel_group = Main().accel_group + + today_button = Gtk.ToolButton() + today_button.set_label(_('Today')) + today_button.set_homogeneous(False) + today_button.connect("clicked", self.on_today_button_clicked) + today_button.add_accelerator( + "clicked", self.accel_group, Gdk.KEY_t, + Gdk.ModifierType.MODIFIER_MASK, Gtk.AccelFlags.VISIBLE) + self.insert(today_button, -1) + + arrow_left = IconFactory.get_image('tryton-arrow-left') + go_back = Gtk.ToolButton() + go_back.set_icon_widget(arrow_left) + go_back.set_label(_("go back")) + go_back.set_expand(False) + go_back.set_homogeneous(False) + go_back.connect("clicked", self.on_go_back_clicked) + self.insert(go_back, -1) + + self.current_page_label = Gtk.Label( + width_chars=10, max_width_chars=10, ellipsize=True) + self.current_page = Gtk.ToggleToolButton() + self.current_page.set_label_widget(self.current_page_label) + self.current_page.connect("clicked", self.on_current_page_clicked) + self.insert(self.current_page, -1) + + self.__cal_popup = Gtk.Window(type=Gtk.WindowType.POPUP) + self.__cal_popup.set_events( + self.__cal_popup.get_events() | Gdk.EventMask.KEY_PRESS_MASK) + self.__cal_popup.set_resizable(False) + self.__cal_popup.connect('delete-event', self.on_cal_popup_closed) + self.__cal_popup.connect( + 'key-press-event', self.on_cal_popup_key_pressed) + self.__cal_popup.connect( + 'button-press-event', self.on_cal_popup_button_pressed) + + gtkcal = Gtk.Calendar() + gtkcal.connect('day-selected', self.on_gtkcal_day_selected) + gtkcal.connect( + 'day-selected-double-click', + self.on_gtkcal_day_selected_double_click) + gtkcal.set_display_options( + Gtk.CalendarDisplayOptions.SHOW_HEADING + | Gtk.CalendarDisplayOptions.SHOW_WEEK_NUMBERS + | Gtk.CalendarDisplayOptions.SHOW_DAY_NAMES) + gtkcal.set_no_show_all(True) + self.__cal_popup.add(gtkcal) + gtkcal.show() + self.gtkcal = gtkcal + self.goocalendar.connect('day-selected', + self.on_goocalendar_day_selected) + + arrow_right = IconFactory.get_image('tryton-arrow-right') + go_forward = Gtk.ToolButton() + go_forward.set_icon_widget(arrow_right) + go_forward.set_label(_("go forward")) + go_forward.set_expand(False) + go_forward.set_homogeneous(False) + go_forward.connect("clicked", self.on_go_forward_clicked) + self.insert(go_forward, -1) + + arrow_left = IconFactory.get_image('tryton-arrow-left') + previous_year = Gtk.ToolButton() + previous_year.set_icon_widget(arrow_left) + previous_year.set_label(_("previous year")) + previous_year.set_expand(False) + previous_year.set_homogeneous(False) + previous_year.connect("clicked", self.on_previous_year_clicked) + self.insert(previous_year, -1) + + self.current_year_label = Gtk.Label(width_chars=4) + current_year = Gtk.ToolItem() + current_year.add(self.current_year_label) + self.insert(current_year, -1) + + arrow_right = IconFactory.get_image('tryton-arrow-right') + next_year = Gtk.ToolButton() + next_year.set_icon_widget(arrow_right) + next_year.set_label(_("next year")) + next_year.set_expand(False) + next_year.set_homogeneous(False) + next_year.connect("clicked", self.on_next_year_clicked) + self.insert(next_year, -1) + + blank_widget = Gtk.ToolItem() + blank_widget.set_expand(True) + self.insert(blank_widget, -1) + + day_button = Gtk.RadioToolButton() + day_button.set_label(_('Day View')) + day_button.connect("clicked", self.on_day_button_clicked) + day_button.add_accelerator( + "clicked", self.accel_group, Gdk.KEY_d, + Gdk.ModifierType.MODIFIER_MASK, Gtk.AccelFlags.VISIBLE) + self.insert(day_button, -1) + + week_button = Gtk.RadioToolButton.new_from_widget(day_button) + week_button.set_label(_('Week View')) + week_button.connect("clicked", self.on_week_button_clicked) + week_button.add_accelerator( + "clicked", self.accel_group, Gdk.KEY_w, + Gdk.ModifierType.MODIFIER_MASK, Gtk.AccelFlags.VISIBLE) + self.insert(week_button, -1) + + month_button = Gtk.RadioToolButton.new_from_widget(week_button) + month_button.set_label_widget(Gtk.Label(label=_('Month View'))) + month_button.connect("clicked", self.on_month_button_clicked) + month_button.add_accelerator( + "clicked", self.accel_group, Gdk.KEY_m, + Gdk.ModifierType.MODIFIER_MASK, Gtk.AccelFlags.VISIBLE) + self.insert(month_button, -1) + buttons = { + 'month': month_button, + 'week': week_button, + 'day': day_button, + } + buttons[self.goocalendar.view].set_active(True) + self.update_displayed_date() + self.set_style(Gtk.ToolbarStyle.ICONS) + + def update_displayed_date(self): + date = self.goocalendar.selected_date + year = date.timetuple()[0] + month = date.timetuple()[1] + day = date.timetuple()[2] + month_label = calendar.month_name[month] + self.current_year_label.set_text(str(year)) + + if self.goocalendar.view == "month": + self.current_page_label.set_text(month_label) + elif self.goocalendar.view == "week": + week_number = datetime.date(year, month, day).isocalendar()[1] + new_label = _('Week') + ' ' + str(week_number) + new_label += ' (' + month_label + ')' + self.current_page_label.set_text(new_label) + elif self.goocalendar.view == "day": + new_label = date.strftime('%x') + self.current_page_label.set_text(new_label) + + def on_today_button_clicked(self, widget): + self.goocalendar.select(datetime.date.today()) + + def on_go_back_clicked(self, widget): + self.goocalendar.previous_page() + + def on_current_page_clicked(self, widget): + if widget.get_active(): + self.__cal_popup.set_transient_for(widget.get_toplevel()) + popup_position(widget, self.__cal_popup) + popup_show(self.__cal_popup) + + def on_cal_popup_closed(self, widget): + self.cal_popup_hide() + return True + + def on_cal_popup_key_pressed(self, widget, event): + if event.keyval != Gdk.KEY_Escape: + return False + widget.stop_emission_by_name('key-press-event') + self.cal_popup_hide() + return True + + def on_cal_popup_button_pressed(self, widget, event): + child = event.window + if child != widget.props.window: + while child: + if child == widget.props.window: + return False + child = child.get_parent() + self.cal_popup_hide() + return True + + def cal_popup_hide(self): + popup_hide(self.__cal_popup) + self.current_page.set_active(False) + + def on_gtkcal_day_selected(self, gtkcal): + year, month, day = gtkcal.get_date() + month += 1 # months go from 1 to 12 instead of from 0 to 11 + self.goocalendar.select(datetime.date(year, month, day)) + + def on_gtkcal_day_selected_double_click(self, gtkcal): + self.cal_popup_hide() + + def on_goocalendar_day_selected(self, goocalendar, day): + # months go from 0 to 11 in Gtk.Calendar instead of 1 to 12 + new_date = self.goocalendar.selected_date + self.gtkcal.select_month(new_date.month - 1, new_date.year) + self.gtkcal.handler_block_by_func(self.on_gtkcal_day_selected) + self.gtkcal.select_day(new_date.day) + self.gtkcal.handler_unblock_by_func(self.on_gtkcal_day_selected) + + def on_go_forward_clicked(self, widget): + self.goocalendar.next_page() + + def on_previous_year_clicked(self, widget): + date = datetime.datetime.combine(self.goocalendar.selected_date, + datetime.time(0)) + year, month, day = date.timetuple()[:3] + year -= 1 + cal = calendar.Calendar(self.goocalendar.firstweekday) + next_month_days = [d for d in cal.itermonthdays(year, month)] + if day not in next_month_days: + day = max(next_month_days) + self.goocalendar.select(datetime.datetime(year, month, day)) + + def on_next_year_clicked(self, widget): + date = datetime.datetime.combine(self.goocalendar.selected_date, + datetime.time(0)) + year, month, day = date.timetuple()[:3] + year += 1 + cal = calendar.Calendar(self.goocalendar.firstweekday) + next_month_days = [d for d in cal.itermonthdays(year, month)] + if day not in next_month_days: + day = max(next_month_days) + self.goocalendar.select(datetime.datetime(year, month, day)) + + def on_day_button_clicked(self, widget): + self.goocalendar.set_view("day") + + def on_week_button_clicked(self, widget): + self.goocalendar.set_view("week") + + def on_month_button_clicked(self, widget): + self.goocalendar.set_view("month") diff --git a/tryton/tryton/gui/window/view_form/view/form.py b/tryton/tryton/gui/window/view_form/view/form.py new file mode 100644 index 0000000000..246b327028 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form.py @@ -0,0 +1,591 @@ +# 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 operator +import gettext + +from gi.repository import Gtk + +from . import View, XMLViewParser +from tryton.common.focus import (get_invisible_ancestor, find_focused_child, + next_focus_widget, find_focusable_child, find_first_focus_widget) +from tryton.common import Tooltips, node_attributes, IconFactory, get_align +from tryton.common.underline import set_underline +from tryton.common.button import Button +from tryton.config import CONFIG +from .form_gtk.calendar_ import Date, Time, DateTime +from .form_gtk.document import Document +from .form_gtk.float import Float +from .form_gtk.integer import Integer +from .form_gtk.selection import Selection +from .form_gtk.char import Char, Password +from .form_gtk.timedelta import TimeDelta +from .form_gtk.checkbox import CheckBox +from .form_gtk.reference import Reference +from .form_gtk.binary import Binary +from .form_gtk.textbox import TextBox +from .form_gtk.one2many import One2Many +from .form_gtk.many2many import Many2Many +from .form_gtk.many2one import Many2One +from .form_gtk.url import Email, URL, CallTo, SIP, HTML +from .form_gtk.image import Image as Image2 +from .form_gtk.progressbar import ProgressBar +from .form_gtk.one2one import One2One +from .form_gtk.richtextbox import RichTextBox +from .form_gtk.dictionary import DictWidget +from .form_gtk.multiselection import MultiSelection +from .form_gtk.pyson import PYSON +from .form_gtk.state_widget import (Label, VBox, Image, Frame, ScrolledWindow, + Notebook, Expander, Link) + +_ = gettext.gettext + + +class _Container(object): + + def __init__(self, col=4, homogeneous=False): + super().__init__() + if col < 0: + col = 0 + self.col = col + self.tooltips = Tooltips() + self.tooltips.enable() + + def add_row(self): + raise NotImplementedError + + def add_col(self): + raise NotImplementedError + + def add(self, widget, attributes): + if widget and attributes.get('help'): + self.tooltips.set_tip(widget, attributes['help']) + + @staticmethod + def constructor(col=4, homogeneous=False): + if CONFIG['client.modepda']: + col = 1 + if col <= 0: + return HContainer(col, homogeneous) + elif col == 1: + return VContainer(col, homogeneous) + else: + return Container(col, homogeneous) + + +class Container(_Container): + + def __init__(self, col=4, homogeneous=False): + super().__init__(col=col, homogeneous=homogeneous) + self.container = Gtk.Grid( + column_spacing=3, row_spacing=3, + column_homogeneous=homogeneous, row_homogeneous=homogeneous, + border_width=3) + self.last = (0, 0) + + def add_row(self): + height, width = self.last + self.last = (height + 1, 0) + + def add_col(self): + height, width = self.last + self.last = (height, width + 1) + + def add(self, widget, attributes): + super().add(widget, attributes) + + colspan = attributes.get('colspan', 1) + if self.col > 0: + height, width = self.last + if colspan > self.col: + colspan = self.col + + if width + colspan > self.col: + self.add_row() + else: + self.add_col() + height, width = self.last + self.last = height, width + colspan + + if widget: + widget.set_vexpand(bool(attributes.get('yexpand'))) + widget.set_hexpand(bool(attributes.get('xexpand', True))) + widget.show_all() + self.container.attach(widget, width, height, colspan, 1) + + +class VContainer(_Container): + def __init__(self, col=1, homogeneous=False): + col = 1 + super().__init__(col=col, homogeneous=homogeneous) + self.container = Gtk.VBox() + self.container.set_homogeneous(homogeneous) + + def add_row(self): + pass + + def add_col(self): + pass + + def add(self, widget, attributes): + super().add(widget, attributes) + if widget: + expand = bool(int(attributes.get('yexpand', False))) + fill = bool(int(attributes.get('yfill', False))) + self.container.pack_start( + widget, expand=expand, fill=fill, padding=2) + + +class HContainer(_Container): + def __init__(self, col=0, homogeneous=False): + col = 0 + super().__init__(col=col, homogeneous=homogeneous) + self.container = Gtk.HBox() + self.container.set_homogeneous(homogeneous) + + def add_row(self): + pass + + def add_col(self): + pass + + def add(self, widget, attributes): + super().add(widget, attributes) + if widget: + expand = bool(int(attributes.get('xexpand', True))) + fill = bool(int(attributes.get('xfill', True))) + self.container.pack_start( + widget, expand=expand, fill=fill, padding=1) + + +class FormXMLViewParser(XMLViewParser): + + WIDGETS = { + 'biginteger': Integer, + 'binary': Binary, + 'boolean': CheckBox, + 'callto': CallTo, + 'char': Char, + 'date': Date, + 'datetime': DateTime, + 'dict': DictWidget, + 'document': Document, + 'email': Email, + 'float': Float, + 'html': HTML, + 'image': Image2, + 'integer': Integer, + 'many2many': Many2Many, + 'many2one': Many2One, + 'multiselection': MultiSelection, + 'numeric': Float, + 'one2many': One2Many, + 'one2one': One2One, + 'password': Password, + 'progressbar': ProgressBar, + 'pyson': PYSON, + 'reference': Reference, + 'richtext': RichTextBox, + 'selection': Selection, + 'sip': SIP, + 'text': TextBox, + 'time': Time, + 'timedelta': TimeDelta, + 'timestamp': DateTime, + 'url': URL, + } + + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self._containers = [] + self._mnemonics = {} + + @property + def container(self): + if self._containers: + return self._containers[-1] + return None + + def _parse_form(self, node, attributes): + container_attributes = node_attributes(node) + container = Container.constructor( + int(container_attributes.get('col', 4)), + container_attributes.get('homogeneous', False)) + self.view.viewport.add(container.container) + self.parse_child(node, container) + assert not self._containers + + def parse_child(self, node, container=None): + if container: + self._containers.append(container) + for child in node.childNodes: + self.parse(child) + if container: + self._containers.pop() + + def _parse_field(self, node, attributes): + name = attributes['name'] + if name and name == self.exclude_field: + self.container.add(None, attributes) + return + + widget = self.WIDGETS[attributes['widget']](self.view, attributes) + self.view.widgets[name].append(widget) + + if widget.expand: + attributes.setdefault('yexpand', True) + attributes.setdefault('yfill', True) + + if attributes.get('height') or attributes.get('width'): + widget.widget.set_size_request( + int(attributes.get('width', -1)), + int(attributes.get('height', -1))) + + widget.widget.set_halign(get_align( + attributes.get('xalign', 0.5), + bool(attributes.get('xexpand', True)))) + widget.widget.set_valign(get_align( + attributes.get('yalign', 0.5), + bool(attributes.get('yexpand')))) + self.container.add(widget.widget, attributes) + + if name in self._mnemonics and widget.mnemonic_widget: + label = self._mnemonics.pop(name) + label.set_label(set_underline(label.get_label())) + label.set_use_underline(True) + label.set_mnemonic_widget(widget.mnemonic_widget) + + def _parse_button(self, node, attributes): + button = Button(attributes) + button.connect('clicked', self.view.button_clicked) + self.view.state_widgets.append(button) + self.container.add(button, attributes) + + def _parse_link(self, node, attributes): + link = Link(attrs=attributes) + self.view.state_widgets.append(link) + self.container.add(link, attributes) + + def _parse_image(self, node, attributes): + image = Image(attrs=attributes) + self.view.state_widgets.append(image) + self.container.add(image, attributes) + + def _parse_separator(self, node, attributes): + name = attributes.get('name') + if name and name == self.exclude_field: + self.container.add(None, attributes) + return + vbox = VBox(attrs=attributes) + if attributes.get('string'): + label = Label(label=attributes['string'], attrs=attributes) + label.set_halign(get_align( + attributes.get('xalign', 0.0), + bool(attributes.get('xexpand', True)))) + label.set_valign(get_align( + attributes.get('yalign', 0.5), + bool(attributes.get('yexpand', False)))) + vbox.pack_start(label, expand=True, fill=True, padding=0) + self.view.state_widgets.append(label) + if name: + self._mnemonics[name] = label + vbox.pack_start(Gtk.HSeparator(), expand=True, fill=True, padding=0) + self.view.state_widgets.append(vbox) + self.container.add(vbox, attributes) + + def _parse_label(self, node, attributes): + name = attributes.get('name') + if name and name == self.exclude_field: + self.container.add(None, attributes) + return + if CONFIG['client.modepda']: + attributes['xalign'] = 0.0 + + attributes.setdefault('xexpand', 0) + label = Label(label=attributes.get('string', ''), attrs=attributes) + label.set_halign(get_align( + attributes.get('xalign', 1.0), + bool(attributes.get('xexpand')))) + label.set_valign(get_align( + attributes.get('yalign', 0.5), + bool(attributes.get('yexpand')))) + label.set_angle(int(attributes.get('angle', 0))) + self.view.state_widgets.append(label) + self.container.add(label, attributes) + if name: + self._mnemonics[name] = label + + def _parse_newline(self, node, attributes): + self.container.add_row() + + def _parse_notebook(self, node, attributes): + attributes.setdefault('yexpand', True) + attributes.setdefault('yfill', True) + attributes.setdefault('colspan', 4) + notebook = Notebook(attrs=attributes) + notebook.set_scrollable(True) + notebook.set_border_width(3) + if attributes.get('height') or attributes.get('width'): + notebook.set_size_request( + int(attributes.get('width', -1)), + int(attributes.get('height', -1))) + + # Force to display the first time it switches on a page + # This avoids glitch in position of widgets + def switch(notebook, page, page_num): + if not self.view.widget: + # Not yet finish to parse + return + notebook.grab_focus() + self.view.display() + notebook.disconnect(handler_id) + handler_id = notebook.connect('switch-page', switch) + self.view.state_widgets.append(notebook) + + self.view.notebooks.append(notebook) + self.container.add(notebook, attributes) + self.parse_child(node, notebook) + + def _parse_page(self, node, attributes): + tab_box = Gtk.HBox(spacing=3) + if 'name' in attributes and attributes['name'] == self.exclude_field: + return + label = Gtk.Label(label=set_underline(attributes['string'])) + label.set_use_underline(True) + + if 'icon' in attributes: + tab_box.pack_start(IconFactory.get_image( + attributes['icon'], Gtk.IconSize.SMALL_TOOLBAR), + expand=True, fill=True, padding=0) + tab_box.pack_start(label, expand=True, fill=True, padding=0) + tab_box.show_all() + + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + scrolledwindow = ScrolledWindow(attrs=attributes) + scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolledwindow.add(viewport) + scrolledwindow.show_all() + self.view.state_widgets.append(scrolledwindow) + self.container.append_page(scrolledwindow, tab_box) + container = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, container) + viewport.add(container.container) + + def _parse_group(self, node, attributes): + group = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, group) + + if 'name' in attributes and attributes['name'] == self.exclude_field: + self.container.add(None, attributes) + return + + can_expand = attributes.get('expandable') + if can_expand: + widget = Expander(label=attributes.get('string'), attrs=attributes) + widget.add(group.container) + widget.set_expanded(can_expand == '1') + self.view.expandables.append(widget) + else: + widget = Frame(label=attributes.get('string'), attrs=attributes) + widget.add(group.container) + + widget.set_halign(get_align( + attributes.get('xalign', 0.5), + bool(attributes.get('xexpand', True)))) + widget.set_valign(get_align( + attributes.get('yalign', 0.5), + bool(attributes.get('yexpand')))) + self.view.state_widgets.append(widget) + self.container.add(widget, attributes) + + def _parse_hpaned(self, node, attributes): + self._parse_paned(node, attributes, Gtk.HPaned) + + def _parse_vpaned(self, node, attributes): + self._parse_paned(node, attributes, Gtk.VPaned) + + def _parse_paned(self, node, attributes, Paned): + attributes.setdefault('yexpand', True) + attributes.setdefault('yfill', True) + paned = Paned() + if 'position' in attributes: + paned.set_position(attributes['position']) + self.container.add(paned, attributes) + self.parse_child(node, paned) + + def _parse_child(self, node, attributes): + paned = self.container + container = Container.constructor( + int(attributes.get('col', 4)), + attributes.get('homogeneous', False)) + self.parse_child(node, container) + if not paned.get_child1(): + pack = paned.pack1 + else: + pack = paned.pack2 + pack(container.container, resize=True, shrink=True) + + +class ViewForm(View): + editable = True + view_type = 'form' + xml_parser = FormXMLViewParser + + def __init__(self, view_id, screen, xml): + self.notebooks = [] + self.expandables = [] + + vbox = Gtk.VBox() + vp = Gtk.Viewport() + vp.set_shadow_type(Gtk.ShadowType.NONE) + self.scroll = scroll = Gtk.ScrolledWindow() + scroll.add(vp) + scroll.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.set_placement(Gtk.CornerType.TOP_LEFT) + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + viewport.add(scroll) + vbox.pack_start(viewport, expand=True, fill=True, padding=0) + + self.widget = vbox + self.viewport = vp + + super().__init__(view_id, screen, xml) + + def get_fields(self): + return list(self.widgets.keys()) + + def __getitem__(self, name): + return self.widgets[name][0] + + def destroy(self): + for widget_name in list(self.widgets.keys()): + for widget in self.widgets[widget_name]: + widget.destroy() + self.widget.destroy() + + def set_value(self, focused_widget=False): + record = self.record + if record: + for name, widgets in self.widgets.items(): + if name in record.group.fields: + for widget in widgets: + if (not focused_widget + or widget.widget.is_focus() + or (isinstance(widget.widget, Gtk.Container) + and widget.widget.get_focus_child())): + widget.set_value() + + @property + def selected_records(self): + if self.record: + return [self.record] + return [] + + @property + def modified(self): + return any(w.modified for widgets in self.widgets.values() + for w in widgets) + + def get_buttons(self): + return [b for b in self.state_widgets if isinstance(b, Button)] + + def reset(self): + record = self.record + if record: + for name, widgets in self.widgets.items(): + field = record.group.fields.get(name) + if field and 'invalid' in field.get_state_attrs(record): + for widget in widgets: + field.get_state_attrs(record)['invalid'] = False + widget.display() + + def display(self): + record = self.record + if record: + # Force to set fields in record + # Get first the lazy one from the view to reduce number of requests + field_names = set() + for name in self.widgets: + field = record.group.fields[name] + field_names.add(name) + field_names.update(f for f in field.attrs.get('depends', []) + if (not f.startswith('_parent') + and f in record.group.fields)) + fields = [] + for name in field_names: + field = record.group.fields[name] + fields.append( + (name, field.attrs.get('loading', 'eager') == 'eager', + len(field.views))) + fields = sorted(fields, key=operator.itemgetter(1, 2)) + for field, _, _ in fields: + record[field].get(record) + focused_widget = find_focused_child(self.widget) + for name, widgets in self.widgets.items(): + field = None + if record: + field = record.group.fields.get(name) + if field: + field.state_set(record) + for widget in widgets: + widget.display() + for widget in self.state_widgets: + widget.state_set(record) + if focused_widget: + invisible_ancestor = get_invisible_ancestor(focused_widget) + if invisible_ancestor: + new_focused_widget = next_focus_widget(invisible_ancestor) + if new_focused_widget: + new_focused_widget.grab_focus() + return True + + def set_cursor(self, new=False, reset_view=True): + focus_widget = None + if reset_view or not self.widget.has_focus(): + if reset_view: + for notebook in self.notebooks: + notebook.set_current_page(0) + if self.attributes.get('cursor') in self.widgets: + focus_widget = find_focusable_child(self.widgets[ + self.attributes['cursor']][0].widget) + else: + child = find_focusable_child(self.viewport) + if child: + child.grab_focus() + record = self.record + if record: + invalid_widgets = [] + for name in record.invalid_fields: + widgets = self.widgets.get(name, []) + for widget in widgets: + invalid_widget = find_focusable_child(widget.widget) + if invalid_widget: + invalid_widgets.append(invalid_widget) + if invalid_widgets: + focus_widget = find_first_focus_widget( + self.viewport, invalid_widgets) + if focus_widget: + for notebook in self.notebooks: + for i in range(notebook.get_n_pages()): + child = notebook.get_nth_page(i) + if focus_widget.is_ancestor(child): + notebook.set_current_page(i) + for group in self.expandables: + if focus_widget.is_ancestor(group): + group.set_expanded(True) + focus_widget.grab_focus() + + def button_clicked(self, widget): + widget.handler_block_by_func(self.button_clicked) + try: + self.screen.button(widget.attrs) + finally: + widget.handler_unblock_by_func(self.button_clicked) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/__init__.py b/tryton/tryton/gui/window/view_form/view/form_gtk/__init__.py new file mode 100644 index 0000000000..4effdfa4a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py new file mode 100644 index 0000000000..9a9a632abc --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py @@ -0,0 +1,235 @@ +# 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 gettext +import os +from urllib.request import urlopen +from urllib.parse import urlparse, unquote + +from gi.repository import Gdk, Gtk + +from tryton.common import common +from tryton.common import file_selection, Tooltips, file_open, file_write +from tryton.common.entry_position import reset_position +from .widget import Widget + +_ = gettext.gettext + + +class BinaryMixin(Widget): + + def __init__(self, view, attrs): + super(BinaryMixin, self).__init__(view, attrs) + self.filename = attrs.get('filename') + + def toolbar(self): + 'Return HBox with the toolbar' + hbox = Gtk.HBox(spacing=0) + tooltips = Tooltips() + + self.but_save_as = Gtk.Button() + self.but_save_as.set_image(common.IconFactory.get_image( + 'tryton-save', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_save_as.set_relief(Gtk.ReliefStyle.NONE) + self.but_save_as.connect('clicked', self.save_as) + tooltips.set_tip(self.but_save_as, _('Save As...')) + hbox.pack_start(self.but_save_as, expand=False, fill=False, padding=0) + + self.but_select = Gtk.Button() + self.but_select.set_image(common.IconFactory.get_image( + 'tryton-search', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_select.set_relief(Gtk.ReliefStyle.NONE) + self.but_select.connect('clicked', self.select) + target_entry = Gtk.TargetEntry.new('text/uri-list', 0, 0) + self.but_select.drag_dest_set(Gtk.DestDefaults.ALL, [ + target_entry, + ], + Gdk.DragAction.MOVE | Gdk.DragAction.COPY) + self.but_select.connect( + 'drag-data-received', self.select_drag_data_received) + tooltips.set_tip(self.but_select, _('Select...')) + hbox.pack_start(self.but_select, expand=False, fill=False, padding=0) + + self.but_clear = Gtk.Button() + self.but_clear.set_image(common.IconFactory.get_image( + 'tryton-clear', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_clear.set_relief(Gtk.ReliefStyle.NONE) + self.but_clear.connect('clicked', self.clear) + tooltips.set_tip(self.but_clear, _('Clear')) + hbox.pack_start(self.but_clear, expand=False, fill=False, padding=0) + + tooltips.enable() + return hbox + + @property + def filename_field(self): + return self.record.group.fields.get(self.filename) + + @property + def filters(self): + filter_all = Gtk.FileFilter() + filter_all.set_name(_('All files')) + filter_all.add_pattern("*") + return [filter_all] + + @property + def preview(self): + return False + + def update_buttons(self, value): + if value: + self.but_save_as.show() + self.but_select.hide() + self.but_clear.show() + else: + self.but_save_as.hide() + self.but_select.show() + self.but_clear.hide() + + def select(self, widget=None): + if not self.field: + return + filename = file_selection( + _('Select'), preview=self.preview, filters=self.filters) + if filename: + self._set_uri('file:///' + filename) + + def select_drag_data_received( + self, widget, context, x, y, selection, info, timestamp): + if not self.field: + return + for uri in selection.get_uris(): + self._set_uri(uri) + + def _set_uri(self, uri): + uri = unquote(uri) + self.field.set_client(self.record, urlopen(uri).read()) + if self.filename_field: + self.filename_field.set_client(self.record, + os.path.basename(urlparse(uri).path)) + + def get_data(self): + if hasattr(self.field, 'get_data'): + data = self.field.get_data(self.record) + else: + data = self.field.get(self.record) + if isinstance(data, str): + data = data.encode('utf-8') + return data + + def open_(self, widget=None): + if not self.filename_field: + return + filename = self.filename_field.get(self.record) + if not filename: + return + file_path = file_write(filename, self.get_data()) + root, type_ = os.path.splitext(filename) + if type_: + type_ = type_[1:] + file_open(file_path, type_) + + def save_as(self, widget=None): + filename = '' + if self.filename_field: + filename = self.filename_field.get(self.record) + filename = file_selection(_('Save As...'), filename=filename, + action=Gtk.FileChooserAction.SAVE) + if filename: + with open(filename, 'wb') as fp: + fp.write(self.get_data()) + + def clear(self, widget=None): + if self.filename_field: + self.filename_field.set_client(self.record, None) + self.field.set_client(self.record, None) + + +class Binary(BinaryMixin, Widget): + "Binary" + + def __init__(self, view, attrs): + super(Binary, self).__init__(view, attrs) + + self.widget = Gtk.HBox(spacing=0) + self.wid_size = Gtk.Entry() + self.wid_size.set_width_chars(self.default_width_chars) + self.wid_size.set_alignment(1.0) + self.wid_size.props.sensitive = False + if self.filename and attrs.get('filename_visible'): + self.wid_text = Gtk.Entry() + self.wid_text.set_property('activates_default', True) + self.wid_text.connect('focus-out-event', + lambda x, y: self._focus_out()) + self.wid_text.connect_after('key_press_event', self.sig_key_press) + self.wid_text.connect('icon-press', self.sig_icon_press) + self.widget.pack_start( + self.wid_text, expand=True, fill=True, padding=0) + else: + self.wid_text = None + self.mnemonic_widget = self.wid_text + self.widget.pack_start( + self.wid_size, expand=not self.filename, fill=True, padding=0) + + self.widget.pack_start( + self.toolbar(), expand=False, fill=False, padding=0) + + def _readonly_set(self, value): + self.but_select.set_sensitive(not value) + self.but_clear.set_sensitive(not value) + if self.wid_text: + self.wid_text.set_editable(not value) + + def sig_key_press(self, widget, event, *args): + editable = self.wid_text and self.wid_text.get_editable() + if event.keyval == Gdk.KEY_F3 and editable: + self.sig_new(widget) + return True + elif event.keyval == Gdk.KEY_F2: + if self.filename: + self.sig_open(widget) + else: + self.sig_save_as(widget) + return True + return False + + def sig_icon_press(self, widget, icon_pos, event): + if icon_pos == Gtk.EntryIconPosition.PRIMARY: + self.open_() + + def display(self): + super(Binary, self).display() + if not self.field: + if self.wid_text: + self.wid_text.set_text('') + self.wid_size.set_text('') + self.but_save_as.hide() + return False + if hasattr(self.field, 'get_size'): + size = self.field.get_size(self.record) + else: + size = len(self.field.get(self.record)) + self.wid_size.set_text(common.humanize(size or 0)) + reset_position(self.wid_size) + if self.wid_text: + self.wid_text.set_text(self.filename_field.get(self.record) or '') + reset_position(self.wid_text) + if size: + icon, tooltip = 'tryton-open', _("Open...") + else: + icon, tooltip = None, '' + pos = Gtk.EntryIconPosition.PRIMARY + if icon: + pixbuf = common.IconFactory.get_pixbuf( + icon, Gtk.IconSize.MENU) + else: + pixbuf = None + self.wid_text.set_icon_from_pixbuf(pos, pixbuf) + self.wid_text.set_icon_tooltip_text(pos, tooltip) + self.update_buttons(bool(size)) + return True + + def set_value(self): + if self.wid_text: + self.filename_field.set_client(self.record, + self.wid_text.get_text() or False) + return diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py b/tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py new file mode 100644 index 0000000000..bbf8426453 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/calendar_.py @@ -0,0 +1,161 @@ +# 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 datetime +import gettext + +from gi.repository import GLib, Gtk + +from .widget import Widget +from tryton import common +from tryton.common.datetime_ import (Date as DateEntry, Time as TimeEntry, + DateTime as DateTimeEntry, add_operators) + +_ = gettext.gettext + + +class Date(Widget): + _changed_signal = 'date-changed' + + def __init__(self, view, attrs, _entry=DateEntry): + super(Date, self).__init__(view, attrs) + + self.widget = Gtk.HBox() + self.entry = self.mnemonic_widget = add_operators(_entry()) + self.real_entry.set_property('activates_default', True) + self.real_entry.connect('key_press_event', self.sig_key_press) + self.real_entry.connect('activate', self.sig_activate) + self.real_entry.connect('changed', lambda _: self.send_modified()) + self.real_entry.connect( + 'focus-out-event', lambda *a: self._focus_out()) + self.entry.connect(self._changed_signal, self.changed) + self.widget.pack_start(self.entry, expand=False, fill=False, padding=0) + + @property + def real_entry(self): + return self.entry + + def _set_editable(self, value): + self.entry.set_editable(value) + self.entry.set_icon_sensitive(Gtk.EntryIconPosition.PRIMARY, value) + + def _readonly_set(self, value): + self._set_editable(not value) + + @classmethod + def cast(cls, value): + if isinstance(value, datetime.datetime): + value = value.date() + return value + + @property + def modified(self): + if self.record and self.field: + field_value = self.cast(self.field.get_client(self.record)) + return field_value != self.get_value() + return False + + def changed(self, widget): + def focus_out(): + if widget.props.window: + self._focus_out() + # Must be deferred because it triggers a display of the form + GLib.idle_add(focus_out) + + def sig_key_press(self, widget, event): + self.send_modified() + + def set_value(self): + self.field.set_client(self.record, self.get_value()) + + def get_value(self): + self.entry.parse() + return self.entry.props.value + + def set_format(self): + if self.field and self.record: + format_ = self.field.date_format(self.record) + else: + format_ = common.date_format( + self.view.screen.context.get('date_format')) + self.entry.props.format = format_ + + def display(self): + super(Date, self).display() + if self.field and self.record: + value = self.field.get_client(self.record) + else: + value = '' + self.entry.props.value = value + self.set_format() + + +class Time(Date): + _changed_signal = 'time-changed' + + def __init__(self, view, attrs): + super(Time, self).__init__(view, attrs, _entry=TimeEntry) + + def _set_editable(self, value): + self.entry.set_sensitive(value) + + @classmethod + def cast(cls, value): + if isinstance(value, datetime.datetime): + value = value.time() + return value + + @property + def real_entry(self): + return self.entry.get_child() + + def display(self): + super(Time, self).display() + + def set_format(self): + if self.field and self.record: + format_ = self.field.time_format(self.record) + else: + format_ = '%X' + self.entry.props.format = format_ + + +class DateTime(Date): + _changed_signal = 'datetime-changed' + + def __init__(self, view, attrs): + Widget.__init__(self, view, attrs) + + self.widget = Gtk.HBox() + self.entry = self.mnemonic_widget = DateTimeEntry() + for child in self.entry.get_children(): + add_operators(child) + if isinstance(child, Gtk.ComboBox): + child = child.get_child() + child.set_property('activates_default', True) + child.connect('key_press_event', self.sig_key_press) + child.connect('activate', self.sig_activate) + child.connect('changed', lambda _: self.send_modified()) + self.widget.pack_start(self.entry, expand=False, fill=False, padding=0) + + @classmethod + def cast(cls, value): + return value + + def _set_editable(self, value): + for child in self.entry.get_children(): + if isinstance(child, Gtk.Entry): + child.set_editable(value) + child.set_icon_sensitive(Gtk.EntryIconPosition.PRIMARY, value) + elif isinstance(child, Gtk.ComboBox): + child.set_sensitive(value) + + def set_format(self): + if self.field and self.record: + date_format = self.field.date_format(self.record) + time_format = self.field.time_format(self.record) + else: + date_format = common.date_format( + self.view.screen.context.get('date_format')) + time_format = '%X' + self.entry.props.date_format = date_format + self.entry.props.time_format = time_format diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/char.py b/tryton/tryton/gui/window/view_form/view/form_gtk/char.py new file mode 100644 index 0000000000..1cdf30dffa --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/char.py @@ -0,0 +1,179 @@ +# 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 gettext + +from gi.repository import GLib, Gtk + +from .widget import Widget, TranslateMixin +from tryton.common import Tooltips, IconFactory +from tryton.common.entry_position import reset_position +from tryton.common.selection import PopdownMixin, selection_shortcuts + +_ = gettext.gettext + + +class Char(Widget, TranslateMixin, PopdownMixin): + "Char" + + def __init__(self, view, attrs): + super(Char, self).__init__(view, attrs) + + self.widget = Gtk.HBox() + self.autocomplete = bool(attrs.get('autocomplete')) + if self.autocomplete: + self.entry = Gtk.ComboBox(has_entry=True) + selection_shortcuts(self.entry) + focus_entry = self.entry.get_child() + self.set_popdown([], self.entry) + self.entry.connect('changed', self.changed) + self.entry.connect('move-active', self._move_active) + self.entry.connect( + 'scroll-event', + lambda c, e: c.stop_emission_by_name('scroll-event')) + else: + self.entry = Gtk.Entry() + focus_entry = self.entry + self.mnemonic_widget = focus_entry + + focus_entry.set_property('activates_default', True) + focus_entry.connect('activate', self.sig_activate) + focus_entry.connect('focus-out-event', lambda x, y: self._focus_out()) + focus_entry.connect('key-press-event', self.send_modified) + expand, fill = True, True + if attrs.get('size'): + expand, fill = False, False + self.widget.pack_start(self.entry, expand=expand, fill=fill, padding=0) + + if attrs.get('translate'): + self.entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.SECONDARY, + IconFactory.get_pixbuf('tryton-translate', Gtk.IconSize.MENU)) + self.entry.connect('icon-press', self.translate) + + def translate_widget(self): + entry = Gtk.Entry() + entry.set_property('activates_default', True) + if self.record: + field_size = self.record.expr_eval(self.attrs.get('size')) + entry.set_width_chars( + min(120, field_size or self.default_width_chars)) + entry.set_max_length(field_size or 0) + return entry + + def translate_widget_set(self, widget, value): + widget.set_text(value) + reset_position(widget) + + def translate_widget_get(self, widget): + return widget.get_text() + + def translate_widget_set_readonly(self, widget, value): + widget.set_editable(not value) + widget.props.sensitive = not value + + def changed(self, combobox): + def focus_out(): + if combobox.props.window: + self._focus_out() + # Only when changed from pop list + if not combobox.get_child().has_focus(): + # Must be deferred because it triggers a display of the form + GLib.idle_add(focus_out) + + @property + def modified(self): + if self.record and self.field: + value = self.get_client_value() + return value != self.get_value() + return False + + def set_value(self): + entry = self.entry.get_child() if self.autocomplete else self.entry + value = entry.get_text() or '' + return self.field.set_client(self.record, value) + + def get_value(self): + entry = self.entry.get_child() if self.autocomplete else self.entry + return entry.get_text() + + def get_client_value(self): + if not self.field: + value = '' + else: + value = self.field.get_client(self.record) + return value + + def display(self): + super(Char, self).display() + if self.autocomplete: + if self.record: + if self.field_name not in self.record.autocompletion: + self.record.do_autocomplete(self.field_name) + selection = self.record.autocompletion.get(self.field_name, []) + else: + selection = [] + self.set_popdown([(x, x) for x in selection], self.entry) + + # Set size + if self.autocomplete: + size_entry = self.entry.get_child() + else: + size_entry = self.entry + if self.record: + field_size = self.record.expr_eval(self.attrs.get('size')) + size_entry.set_width_chars( + min(120, field_size or self.default_width_chars)) + size_entry.set_max_length(field_size or 0) + else: + size_entry.set_width_chars(self.default_width_chars) + size_entry.set_max_length(0) + + value = self.get_client_value() + if not self.autocomplete: + self.entry.set_text(value) + reset_position(self.entry) + else: + self.entry.handler_block_by_func(self.changed) + if not self.set_popdown_value(self.entry, value) or not value: + child = self.entry.get_child() + child.set_text(value) + reset_position(child) + self.entry.handler_unblock_by_func(self.changed) + + def _move_active(self, combobox, scroll_type): + if not combobox.get_child().get_editable(): + combobox.stop_emission_by_name('move-active') + + def _readonly_set(self, value): + sensitivity = { + True: Gtk.SensitivityType.OFF, + False: Gtk.SensitivityType.AUTO, + } + super(Char, self)._readonly_set(value) + if self.autocomplete: + entry_editable = self.entry.get_child() + self.entry.set_button_sensitivity(sensitivity[value]) + else: + entry_editable = self.entry + entry_editable.set_editable(not value) + + +class Password(Char): + + def __init__(self, view, attrs): + super(Password, self).__init__(view, attrs) + self.entry.props.visibility = False + + self.visibility_checkbox = Gtk.CheckButton() + self.visibility_checkbox.connect('toggled', self.toggle_visibility) + Tooltips().set_tip(self.visibility_checkbox, _('Show plain text')) + self.widget.pack_start( + self.visibility_checkbox, expand=False, fill=True, padding=0) + + def _readonly_set(self, value): + super(Char, self)._readonly_set(value) + self.entry.set_editable(not value) + self.visibility_checkbox.props.visible = not value + + def toggle_visibility(self, button): + self.entry.props.visibility = not self.entry.props.visibility diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/checkbox.py b/tryton/tryton/gui/window/view_form/view/form_gtk/checkbox.py new file mode 100644 index 0000000000..3498c6ee8e --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/checkbox.py @@ -0,0 +1,37 @@ +# 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 gettext + +from gi.repository import Gtk + +from .widget import Widget + +_ = gettext.gettext + + +class CheckBox(Widget): + + def __init__(self, view, attrs): + super(CheckBox, self).__init__(view, attrs) + self.widget = self.mnemonic_widget = Gtk.CheckButton() + self.widget.connect('focus-out-event', lambda x, y: self._focus_out()) + self.widget.connect_after('toggled', self.sig_activate) + + def _readonly_set(self, value): + super(CheckBox, self)._readonly_set(value) + # TODO find a better solution to accept focus + self.widget.set_sensitive(not value) + + def set_value(self): + self.field.set_client(self.record, self.widget.get_active()) + + def display(self): + super(CheckBox, self).display() + if not self.field: + self.widget.set_active(False) + return False + self.widget.handler_block_by_func(self.sig_activate) + try: + self.widget.set_active(bool(self.field.get(self.record))) + finally: + self.widget.handler_unblock_by_func(self.sig_activate) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py new file mode 100644 index 0000000000..8523496634 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -0,0 +1,611 @@ +# 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 operator +import locale +import decimal +import gettext +from decimal import Decimal + +from gi.repository import GLib, GObject, Gtk + +from .widget import Widget + +from tryton.gui.window.win_search import WinSearch +from tryton.common import Tooltips, timezoned_date, untimezoned_date, \ + IconFactory +from tryton.common.selection import selection_shortcuts +from tryton.common.completion import get_completion, update_completion +from tryton.common.datetime_ import Date, DateTime, add_operators +from tryton.common.domain_parser import quote +from tryton.common.entry_position import reset_position +from tryton.common.number_entry import NumberEntry +from tryton.common.underline import set_underline +from tryton.common.domain_inversion import eval_domain +from tryton.common.widget_style import widget_class +from tryton.common.treeviewcontrol import TreeViewControl +from tryton.pyson import PYSONDecoder + +_ = gettext.gettext + + +class DictEntry(object): + expand = True + fill = True + + def __init__(self, name, parent_widget): + self.name = name + self.definition = parent_widget.field.keys[name] + self.parent_widget = parent_widget + self.widget = self.create_widget() + if self.definition.get('help'): + parent_widget.tooltips.set_tip( + self.widget, self.definition['help']) + + def create_widget(self): + widget = Gtk.Entry() + widget.connect('key-press-event', self.parent_widget.send_modified) + widget.connect('focus-out-event', + lambda w, e: self.parent_widget._focus_out()) + widget.props.activates_default = True + widget.connect('activate', self.parent_widget.sig_activate) + return widget + + def modified(self, value): + return self.get_value() != value.get(self.name) + + def get_value(self): + return self.widget.get_text() + + def set_value(self, value): + self.widget.set_text(value or '') + reset_position(self.widget) + + def set_readonly(self, readonly): + self.widget.set_editable(not readonly) + + +class DictBooleanEntry(DictEntry): + + def create_widget(self): + widget = Gtk.CheckButton() + widget.connect('toggled', self.parent_widget.sig_activate) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget + + def get_value(self): + return self.widget.props.active + + def set_value(self, value): + self.widget.handler_block_by_func(self.parent_widget.sig_activate) + try: + self.widget.props.active = bool(value) + finally: + self.widget.handler_unblock_by_func( + self.parent_widget.sig_activate) + + def set_readonly(self, readonly): + self.widget.set_sensitive(not readonly) + + +class DictSelectionEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = Gtk.ComboBox(has_entry=True) + + # customizing entry + child = widget.get_child() + child.props.activates_default = True + child.connect('changed', self._changed) + child.connect('focus-out-event', + lambda w, e: self.parent_widget._focus_out()) + child.connect('activate', + lambda w: self.parent_widget._focus_out()) + widget.connect( + 'scroll-event', + lambda c, e: c.stop_emission_by_name('scroll-event')) + selection_shortcuts(widget) + + # setting completion and selection + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_PYOBJECT) + model.append(('', "")) + width = 10 + selection = self.definition['selection'] + if self.definition.get('sort', True): + selection.sort(key=operator.itemgetter(1)) + for value, name in selection: + name = str(name) + model.append((name, value)) + width = max(width, len(name)) + widget.set_model(model) + widget.set_entry_text_column(0) + child.set_width_chars(width) + completion = Gtk.EntryCompletion() + completion.set_inline_selection(True) + completion.set_model(model) + child.set_completion(completion) + completion.set_text_column(0) + completion.connect('match-selected', self.match_selected) + return widget + + def match_selected(self, completion, model, iter_): + value, = model.get(iter_, 1) + model = self.widget.get_model() + for i, selection in enumerate(model): + if selection[1] == value: + GLib.idle_add(self.widget.set_active, i) + break + + def get_value(self): + active = self.widget.get_active() + if active < 0: + return None + else: + model = self.widget.get_model() + return model[active][1] + + def set_value(self, value): + active = -1 + model = self.widget.get_model() + for i, selection in enumerate(model): + if selection[1] == value: + active = i + break + child = self.widget.get_child() + child.handler_block_by_func(self._changed) + try: + self.widget.set_active(active) + if active == -1: + # When setting no item GTK doesn't clear the entry + child.set_text('') + finally: + child.handler_unblock_by_func(self._changed) + + def _changed(self, selection): + GLib.idle_add(self.parent_widget._focus_out) + + def set_readonly(self, readonly): + self.widget.set_sensitive(not readonly) + + +class DictMultiSelectionEntry(DictEntry): + expand = False + + def create_widget(self): + widget = Gtk.ScrolledWindow() + widget.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + widget.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + widget.set_size_request(100, 100) + + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) + self.tree = TreeViewControl() + self.tree.set_model(model) + self.tree.set_search_column(1) + self.tree.connect( + 'focus-out-event', lambda w, e: self.parent_widget._focus_out()) + self.tree.set_headers_visible(False) + selection = self.tree.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + selection.connect('changed', self._changed) + widget.add(self.tree) + + self.selection = self.definition['selection'] + if self.definition.get('sort', True): + self.selection.sort(key=operator.itemgetter(1)) + for value, name in self.selection: + name = str(name) + model.append((value, name)) + + name_column = Gtk.TreeViewColumn() + name_cell = Gtk.CellRendererText() + name_column.pack_start(name_cell, expand=True) + name_column.add_attribute(name_cell, 'text', 1) + self.tree.append_column(name_column) + + return widget + + def get_value(self): + model, paths = self.tree.get_selection().get_selected_rows() + return [model[path][0] for path in paths] + + def set_value(self, value): + value2path = {v: idx for idx, (v, _) in enumerate(self.selection)} + selection = self.tree.get_selection() + selection.handler_block_by_func(self._changed) + try: + selection.unselect_all() + for v in value: + if v in value2path: + selection.select_path(value2path[v]) + finally: + selection.handler_unblock_by_func(self._changed) + + def _changed(self, selection): + GLib.idle_add(self.parent_widget._focus_out) + + def set_readonly(self, readonly): + selection = self.tree.get_selection() + selection.set_select_function(lambda *a: not readonly) + + +class DictIntegerEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = NumberEntry() + widget.connect('key-press-event', self.parent_widget.send_modified) + widget.connect('focus-out-event', + lambda w, e: self.parent_widget._focus_out()) + widget.props.activates_default = True + widget.connect('activate', self.parent_widget.sig_activate) + return widget + + def get_value(self): + if self.widget.value is not None: + return int(self.widget.value) + return None + + def set_value(self, value): + if value is not None: + txt_val = locale.format_string('%d', value, True) + else: + txt_val = '' + self.widget.set_text(txt_val) + reset_position(self.widget) + + +class DictFloatEntry(DictIntegerEntry): + + @property + def digits(self): + record = self.parent_widget.record + if record: + digits = record.expr_eval(self.definition.get('digits')) + if not digits or any(d is None for d in digits): + return + return digits + + @property + def width(self): + digits = self.digits + if digits: + return sum(digits) + else: + return 18 + + def get_value(self): + return self.widget.value + + def set_value(self, value): + digits = self.digits + if digits: + self.widget.digits = digits[1] + else: + self.widget.digits = None + self.widget.set_width_chars(self.width) + if value is not None: + txt_val = locale.localize( + '{0:.{1}f}'.format(value, digits[1]), True) + else: + txt_val = '' + self.widget.set_text(txt_val) + reset_position(self.widget) + + +class DictNumericEntry(DictFloatEntry): + + def get_value(self): + txt_value = self.widget.get_text() + if txt_value: + try: + return Decimal(locale.delocalize(txt_value)) + except decimal.InvalidOperation: + pass + return None + + +class DictDateTimeEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = add_operators(DateTime()) + record = self.parent_widget.record + field = self.parent_widget.field + if record and field: + date_format = field.date_format(record) + time_format = field.time_format(record) + widget.props.date_format = date_format + widget.props.time_format = time_format + widget.connect('key_press_event', self.parent_widget.send_modified) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget + + def get_value(self): + return untimezoned_date(self.widget.props.value) + + def set_value(self, value): + self.widget.props.value = timezoned_date(value) + + def set_readonly(self, readonly): + for child in self.widget.get_children(): + if isinstance(child, Gtk.Entry): + child.set_editable(not readonly) + child.set_icon_sensitive( + Gtk.EntryIconPosition.PRIMARY, not readonly) + elif isinstance(child, Gtk.ComboBox): + child.set_sensitive(not readonly) + + +class DictDateEntry(DictEntry): + expand = False + fill = False + + def create_widget(self): + widget = add_operators(Date()) + record = self.parent_widget.record + field = self.parent_widget.field + if record and field: + format_ = field.date_format(record) + widget.props.format = format_ + widget.connect('key_press_event', self.parent_widget.send_modified) + widget.connect('focus-out-event', lambda w, e: + self.parent_widget._focus_out()) + return widget + + def get_value(self): + return self.widget.props.value + + def set_value(self, value): + self.widget.props.value = value + + def set_readonly(self, readonly): + super().set_readonly(readonly) + self.widget.set_icon_sensitive( + Gtk.EntryIconPosition.PRIMARY, not readonly) + + +DICT_ENTRIES = { + 'char': DictEntry, + 'boolean': DictBooleanEntry, + 'selection': DictSelectionEntry, + 'multiselection': DictMultiSelectionEntry, + 'datetime': DictDateTimeEntry, + 'date': DictDateEntry, + 'integer': DictIntegerEntry, + 'float': DictFloatEntry, + 'numeric': DictNumericEntry, + } + + +class DictWidget(Widget): + + def __init__(self, view, attrs): + super(DictWidget, self).__init__(view, attrs) + self.schema_model = attrs['schema_model'] + self.fields = {} + self.buttons = {} + self.rows = {} + + self.widget = Gtk.Frame() + label = Gtk.Label(label=set_underline(attrs.get('string', ''))) + label.set_use_underline(True) + self.widget.set_label_widget(label) + self.widget.set_shadow_type(Gtk.ShadowType.OUT) + + vbox = Gtk.VBox() + self.widget.add(vbox) + + self.grid = Gtk.Grid(column_spacing=3, row_spacing=3) + vbox.pack_start(self.grid, expand=True, fill=True, padding=0) + + hbox = Gtk.HBox() + hbox.set_border_width(2) + self.wid_text = Gtk.Entry() + self.wid_text.set_placeholder_text(_('Search')) + self.wid_text.props.width_chars = 13 + self.wid_text.connect('activate', self._sig_activate) + hbox.pack_start(self.wid_text, expand=True, fill=True, padding=0) + label.set_mnemonic_widget(self.wid_text) + + if int(self.attrs.get('completion', 1)): + self.wid_completion = get_completion(search=False, create=False) + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + else: + self.wid_completion = None + + self.but_add = Gtk.Button(can_focus=False) + self.but_add.connect('clicked', self._sig_add) + self.but_add.add( + IconFactory.get_image('tryton-add', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_add.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_add, expand=False, fill=False, padding=0) + vbox.pack_start(hbox, expand=True, fill=True, padding=0) + + self.tooltips = Tooltips() + self.tooltips.set_tip(self.but_add, _('Add value')) + self.tooltips.enable() + + self._readonly = False + self._record_id = None + + @property + def _invalid_widget(self): + return self.wid_text + + def _new_remove_btn(self): + but_remove = Gtk.Button() + but_remove.add( + IconFactory.get_image('tryton-remove', Gtk.IconSize.SMALL_TOOLBAR)) + but_remove.set_relief(Gtk.ReliefStyle.NONE) + return but_remove + + def _sig_activate(self, *args): + if self.wid_text.get_editable(): + self._sig_add() + + def _sig_add(self, *args): + context = self.field.get_context(self.record) + value = self.wid_text.get_text() + domain = self.field.domain_get(self.record) + + def callback(result): + if result: + self.add_new_keys([r[0] for r in result]) + self.wid_text.set_text('') + + win = WinSearch(self.schema_model, callback, sel_multi=True, + context=context, domain=domain, new=False) + win.screen.search_filter(quote(value)) + win.show() + + def add_new_keys(self, ids): + new_keys = self.field.add_new_keys(ids, self.record) + self.send_modified() + focus = False + for key_name in new_keys: + if key_name not in self.fields: + self.add_line(key_name) + if not focus: + # Use idle add because it can be called from the callback + # of WinSearch while the popup is still there + GLib.idle_add(self.fields[key_name].widget.grab_focus) + focus = True + + def _sig_remove(self, button, key, modified=True): + del self.fields[key] + del self.buttons[key] + for widget in self.rows[key]: + self.grid.remove(widget) + widget.destroy() + del self.rows[key] + if modified: + self.send_modified() + self.set_value() + + def set_value(self): + self.field.set_client(self.record, self.get_value()) + + def get_value(self): + return dict((key, widget.get_value()) + for key, widget in list(self.fields.items())) + + @property + def modified(self): + if self.record and self.field: + value = self.field.get_client(self.record) + return any(widget.modified(value) + for widget in self.fields.values()) + return False + + def _readonly_set(self, readonly): + self._readonly = readonly + self._set_button_sensitive() + for widget in list(self.fields.values()): + widget.set_readonly(readonly) + self.wid_text.set_sensitive(not readonly) + self.wid_text.set_editable(not readonly) + + def _set_button_sensitive(self): + self.but_add.set_sensitive(bool( + not self._readonly + and self.attrs.get('create', True))) + for button in self.buttons.values(): + button.set_sensitive(bool( + not self._readonly + and self.attrs.get('delete', True))) + + def add_line(self, key): + key_schema = self.field.keys[key] + self.fields[key] = DICT_ENTRIES[key_schema['type']](key, self) + field = self.fields[key] + text = key_schema['string'] + _(':') + label = Gtk.Label( + label=set_underline(text), + use_underline=True, halign=Gtk.Align.END) + self.grid.attach_next_to( + label, None, Gtk.PositionType.BOTTOM, 1, 1) + label.set_mnemonic_widget(field.widget) + label.show() + hbox = Gtk.HBox(hexpand=True) + hbox.pack_start( + field.widget, expand=field.expand, fill=field.fill, padding=0) + self.grid.attach_next_to( + hbox, label, Gtk.PositionType.RIGHT, 1, 1) + hbox.show_all() + remove_but = self._new_remove_btn() + self.tooltips.set_tip(remove_but, _('Remove "%s"') % + key_schema['string']) + self.grid.attach_next_to( + remove_but, hbox, Gtk.PositionType.RIGHT, 1, 1) + remove_but.connect('clicked', self._sig_remove, key) + remove_but.show_all() + self.rows[key] = [label, hbox, remove_but] + self.buttons[key] = remove_but + + def display(self): + super(DictWidget, self).display() + + if not self.field: + return + + record_id = self.record.id if self.record else None + if record_id != self._record_id: + for key in list(self.fields.keys()): + self._sig_remove(None, key, modified=False) + self._record_id = record_id + + value = self.field.get_client(self.record) if self.field else {} + new_key_names = set(value.keys()) - set(self.field.keys) + if new_key_names: + self.field.add_keys(list(new_key_names), self.record) + decoder = PYSONDecoder() + + def filter_func(item): + key, value = item + return key in self.field.keys + + def key(item): + key, value = item + return self.field.keys[key]['sequence'] or 0 + + for key, val in sorted(filter(filter_func, value.items()), key=key): + if key not in self.fields: + self.add_line(key) + widget = self.fields[key] + widget.set_value(val) + widget.set_readonly(self._readonly) + key_domain = decoder.decode( + self.field.keys[key].get('domain') or '[]') + widget_class( + widget.widget, 'invalid', not eval_domain(key_domain, value)) + for key in set(self.fields.keys()) - set(value.keys()): + self._sig_remove(None, key, modified=False) + + self._set_button_sensitive() + + def _completion_match_selected(self, completion, model, iter_): + record_id, = model.get(iter_, 1) + self.add_new_keys([record_id]) + self.wid_text.set_text('') + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if not self.wid_text.get_editable(): + return + if not self.record: + return + update_completion(self.wid_text, self.record, self.field, + self.schema_model) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/document.py b/tryton/tryton/gui/window/view_form/view/form_gtk/document.py new file mode 100644 index 0000000000..f6c2ea6b1a --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/document.py @@ -0,0 +1,94 @@ +# 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 pathlib import Path +from tempfile import NamedTemporaryFile + +from gi.repository import Gtk, Gdk, GLib +try: + from gi.repository import EvinceDocument, EvinceView + EvinceDocument.init() +except ImportError: + EvinceDocument = EvinceView = None + +from tryton.common import data2pixbuf, resize_pixbuf + +from .widget import Widget +from .binary import BinaryMixin + + +class Document(BinaryMixin, Widget): + expand = True + + def __init__(self, view, attrs): + super().__init__(view, attrs) + + self.widget = Gtk.VBox(spacing=3) + self.image = Gtk.DrawingArea() + self.widget.pack_start( + self.image, expand=True, fill=True, padding=0) + + if EvinceView: + self.evince_view = EvinceView.View() + self.evince_scroll = Gtk.ScrolledWindow() + self.evince_scroll.add(self.evince_view) + self.widget.pack_start( + self.evince_scroll, expand=True, fill=True, padding=0) + else: + self.evince_view = self.evince_scroll = None + + def _draw(self, drawing_area, cr, pixbuf): + width = drawing_area.get_allocated_width() + height = drawing_area.get_allocated_height() + pixbuf = resize_pixbuf(pixbuf, width, height) + width = (width - pixbuf.get_width()) / 2 + height = (height - pixbuf.get_height()) / 2 + Gdk.cairo_set_source_pixbuf(cr, pixbuf, width, height) + cr.paint() + + def display(self): + super().display() + if self.field and self.record: + data = self.field.get_data(self.record) + else: + data = None + + if not data: + self.image.hide() + if self.evince_view: + self.evince_view.set_model(EvinceView.DocumentModel()) + self.evince_scroll.hide() + return + + pixbuf = data2pixbuf(data) + if pixbuf: + try: + self.image.disconnect_by_func(self._draw) + except TypeError: + pass + self.image.connect('draw', self._draw, pixbuf) + self.image.queue_draw() + self.image.show() + if self.evince_view: + self.evince_view.set_model(EvinceView.DocumentModel()) + self.evince_scroll.hide() + else: + self.image.hide() + if self.evince_view: + self.evince_scroll.show() + if self.filename_field: + suffix = self.filename_field.get(self.record) + else: + suffix = None + try: + with NamedTemporaryFile(suffix=suffix) as fp: + fp.write(data) + path = Path(fp.name) + document = ( + EvinceDocument.Document.factory_get_document( + path.as_uri())) + model = EvinceView.DocumentModel() + model.set_document(document) + self.evince_view.set_model(model) + except GLib.GError: + self.evince_view.set_model(EvinceView.DocumentModel()) + self.evince_scroll.hide() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/float.py b/tryton/tryton/gui/window/view_form/view/form_gtk/float.py new file mode 100644 index 0000000000..5fb1e7bc09 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/float.py @@ -0,0 +1,28 @@ +# 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 .integer import Integer + + +class Float(Integer): + "Float" + + @property + def digits(self): + if self.field and self.record: + return self.field.digits(self.record, factor=self.factor) + + @property + def width(self): + digits = self.digits + if digits: + return sum(digits) + else: + return 18 + + def display(self): + digits = self.digits + if digits: + self.entry.digits = digits[1] + else: + self.entry.digits = None + super(Float, self).display() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/image.py b/tryton/tryton/gui/window/view_form/view/form_gtk/image.py new file mode 100644 index 0000000000..05c2188b61 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/image.py @@ -0,0 +1,113 @@ +# 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 gettext +import urllib.request + +from gi.repository import Gdk, Gtk + +from tryton.common import resize_pixbuf, data2pixbuf +from tryton.config import CONFIG +from .widget import Widget +from .binary import BinaryMixin + +_ = gettext.gettext + + +class Image(BinaryMixin, Widget): + + def __init__(self, view, attrs): + super(Image, self).__init__(view, attrs) + + self.height = int(attrs.get('height', 100)) + self.width = int(attrs.get('width', 300)) + + self.widget = Gtk.VBox(spacing=3) + self.event = Gtk.EventBox() + self.event.drag_dest_set(Gtk.DestDefaults.ALL, [ + Gtk.TargetEntry.new('text/plain', 0, 0), + Gtk.TargetEntry.new('text/uri-list', 0, 1), + Gtk.TargetEntry.new("image/x-xpixmap", 0, 2)], + Gdk.DragAction.MOVE) + self.event.connect('drag_motion', self.drag_motion) + self.event.connect('drag_data_received', self.drag_data_received) + + self.image = Gtk.Image() + self.event.add(self.image) + self.widget.pack_start(self.event, expand=True, fill=True, padding=0) + + toolbar = self.toolbar() # Set button attributes even if not display + if not attrs.get('readonly'): + toolbar.set_halign(Gtk.Align.CENTER) + self.widget.pack_start( + toolbar, expand=False, fill=False, padding=0) + + self._readonly = False + + @property + def filters(self): + filters = super(Image, self).filters + filter_image = Gtk.FileFilter() + filter_image.set_name(_('Images')) + for mime in ("image/png", "image/jpeg", "image/gif"): + filter_image.add_mime_type(mime) + for pat in ("*.png", "*.jpg", "*.gif", "*.tif", "*.xpm"): + filter_image.add_pattern(pat) + filters.insert(0, filter_image) + return filters + + def _readonly_set(self, value): + self._readonly = value + self.but_select.set_sensitive(not value) + self.but_clear.set_sensitive(not value) + + def clear(self, widget=None): + super(Image, self).clear(widget=widget) + self.update_img() + + def drag_motion(self, widget, context, x, y, timestamp): + if self._readonly: + return False + context.drag_status(Gdk.DragAction.COPY, timestamp) + return True + + def drag_data_received(self, widget, context, x, y, selection, + info, timestamp): + if self._readonly: + return + if info == 0: + uri = selection.get_text().split('\n')[0] + if uri: + self.field.set_client( + self.record, urllib.request.urlopen(uri).read()) + self.update_img() + elif info == 1: + uri = selection.data.split('\r\n')[0] + if uri: + self.field.set_client( + self.record, urllib.request.urlopen(uri).read()) + self.update_img() + elif info == 2: + data = selection.get_pixbuf() + if data: + self.field.set_client(self.record, data) + self.update_img() + + def update_img(self): + value = None + if self.field: + value = self.field.get_client(self.record) + if isinstance(value, int): + if value > CONFIG['image.max_size']: + value = False + else: + value = self.field.get_data(self.record) + pixbuf = data2pixbuf(value) + if pixbuf: + pixbuf = resize_pixbuf(pixbuf, self.width, self.height) + self.image.set_from_pixbuf(pixbuf) + return bool(value) + + def display(self): + super(Image, self).display() + value = self.update_img() + self.update_buttons(bool(value)) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/integer.py b/tryton/tryton/gui/window/view_form/view/form_gtk/integer.py new file mode 100644 index 0000000000..80269a953c --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/integer.py @@ -0,0 +1,83 @@ +# 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 gi.repository import Gtk + +from tryton.common.entry_position import reset_position +from tryton.common.number_entry import NumberEntry +from .widget import Widget + + +class Integer(Widget): + "Integer" + + def __init__(self, view, attrs): + super(Integer, self).__init__(view, attrs) + self.widget = Gtk.HBox() + self.entry = self.mnemonic_widget = NumberEntry() + self.entry.props.activates_default = True + self.entry.connect('activate', self.sig_activate) + self.entry.connect('focus-out-event', lambda *a: self._focus_out()) + self.entry.connect('key-press-event', self.send_modified) + self.symbol = attrs.get('symbol') + if self.symbol: + self.symbol_start = Gtk.Entry(editable=False) + self.widget.pack_start( + self.symbol_start, expand=False, fill=False, padding=1) + self.widget.pack_start(self.entry, expand=False, fill=False, padding=0) + self.factor = float(attrs.get('factor', 1)) + if self.symbol: + self.symbol_end = Gtk.Entry(editable=False) + self.widget.pack_start( + self.symbol_end, expand=False, fill=False, padding=1) + + @property + def modified(self): + if self.record and self.field: + value = self.get_client_value() + return value != self.get_value() + return False + + def set_value(self): + return self.field.set_client( + self.record, self.entry.get_text(), factor=self.factor) + + def get_value(self): + return self.entry.get_text() + + def get_client_value(self): + if not self.field: + value = '' + else: + value = self.field.get_client(self.record, factor=self.factor) + return value + + @property + def width(self): + return 8 + + def display(self): + def set_symbol(entry, text): + if text: + entry.set_text(text) + entry.set_width_chars(len(text) + 1) + entry.show() + else: + entry.set_text('') + entry.hide() + super().display() + self.entry.set_width_chars(self.width) + if self.field and self.symbol: + symbol, position = self.field.get_symbol(self.record, self.symbol) + if position < 0.5: + set_symbol(self.symbol_start, symbol) + set_symbol(self.symbol_end, '') + else: + set_symbol(self.symbol_start, '') + set_symbol(self.symbol_end, symbol) + value = self.get_client_value() + self.entry.set_text(value) + reset_position(self.entry) + + def _readonly_set(self, value): + super()._readonly_set(value) + self.entry.set_editable(not value) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py new file mode 100644 index 0000000000..c7442b2c53 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2many.py @@ -0,0 +1,311 @@ +# 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 gettext + +from gi.repository import Gdk, Gtk + +import tryton.common as common +from .widget import Widget +from tryton.common.completion import get_completion, update_completion +from tryton.common.domain_parser import quote +from tryton.common.underline import set_underline +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_search import WinSearch +from tryton.gui.window.win_form import WinForm + +_ = gettext.gettext + + +class Many2Many(Widget): + expand = True + + def __init__(self, view, attrs): + super(Many2Many, self).__init__(view, attrs) + + self.widget = Gtk.Frame() + self.widget.set_shadow_type(Gtk.ShadowType.NONE) + self.widget.get_accessible().set_name(attrs.get('string', '')) + vbox = Gtk.VBox(homogeneous=False, spacing=5) + self.widget.add(vbox) + self._readonly = True + self._required = False + self._position = 0 + + hbox = Gtk.HBox(homogeneous=False, spacing=0) + hbox.set_border_width(2) + + self.title = Gtk.Label( + label=set_underline(attrs.get('string', '')), + use_underline=True, halign=Gtk.Align.START) + hbox.pack_start(self.title, expand=True, fill=True, padding=0) + + hbox.pack_start(Gtk.VSeparator(), expand=False, fill=True, padding=0) + + tooltips = common.Tooltips() + + self.wid_text = Gtk.Entry() + self.wid_text.set_placeholder_text(_('Search')) + self.wid_text.set_property('width_chars', 13) + self.wid_text.connect('focus-out-event', self._focus_out) + self.focus_out = True + hbox.pack_start(self.wid_text, expand=True, fill=True, padding=0) + + if int(self.attrs.get('completion', 1)): + self.wid_completion = get_completion( + create=self.attrs.get('create', True) + and common.MODELACCESS[self.attrs['relation']]['create']) + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + else: + self.wid_completion = None + + self.but_add = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_add, _('Add existing record')) + self.but_add.connect('clicked', self._sig_add) + self.but_add.add(common.IconFactory.get_image( + 'tryton-add', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_add.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_add, expand=False, fill=False, padding=0) + + self.but_remove = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_remove, _('Remove selected record')) + self.but_remove.connect('clicked', self._sig_remove) + self.but_remove.add(common.IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_remove.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_remove, expand=False, fill=False, padding=0) + + tooltips.enable() + + frame = Gtk.Frame() + frame.add(hbox) + frame.set_shadow_type(Gtk.ShadowType.OUT) + vbox.pack_start(frame, expand=False, fill=True, padding=0) + + self.screen = Screen(attrs['relation'], + view_ids=attrs.get('view_ids', '').split(','), + mode=['tree'], views_preload=attrs.get('views', {}), + order=attrs.get('order'), + row_activate=self._on_activate, + readonly=True, + limit=None) + self.screen.signal_connect(self, 'record-message', self._sig_label) + + vbox.pack_start(self.screen.widget, expand=True, fill=True, padding=0) + + self.title.set_mnemonic_widget( + self.screen.current_view.mnemonic_widget) + + self.screen.widget.connect('key_press_event', self.on_keypress) + self.wid_text.connect('key_press_event', self.on_keypress) + + def on_keypress(self, widget, event): + editable = self.wid_text.get_editable() + activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab] + remove_keys = [Gdk.KEY_Delete, Gdk.KEY_KP_Delete] + if not self.wid_completion: + activate_keys.append(Gdk.KEY_Return) + if widget == self.screen.widget: + if event.keyval == Gdk.KEY_F3 and editable: + self._sig_add() + return True + elif event.keyval == Gdk.KEY_F2: + self._sig_edit() + return True + elif event.keyval in remove_keys and editable: + self._sig_remove() + return True + elif widget == self.wid_text: + if event.keyval == Gdk.KEY_F3: + self._sig_new() + return True + elif event.keyval == Gdk.KEY_F2: + self._sig_add() + return True + elif event.keyval in activate_keys and self.wid_text.get_text(): + self._sig_add() + self.wid_text.grab_focus() + return False + + def destroy(self): + self.wid_text.disconnect_by_func(self._focus_out) + self.screen.destroy() + + def _sig_add(self, *args): + if not self.focus_out: + return + domain = self.field.domain_get(self.record) + add_remove = self.record.expr_eval(self.attrs.get('add_remove')) + if add_remove: + domain = [domain, add_remove] + context = self.field.get_search_context(self.record) + order = self.field.get_search_order(self.record) + value = self.wid_text.get_text() + + self.focus_out = False + + def callback(result): + self.focus_out = True + if result: + ids = [x[0] for x in result] + self.screen.load(ids, modified=True) + self.screen.display(res_id=ids[0]) + self.screen.set_cursor() + self.wid_text.set_text('') + win = WinSearch(self.attrs['relation'], callback, sel_multi=True, + context=context, domain=domain, order=order, + view_ids=self.attrs.get('view_ids', '').split(','), + views_preload=self.attrs.get('views', {}), + new=self.attrs.get('create', True), + title=self.attrs.get('string')) + win.screen.search_filter(quote(value)) + if len(win.screen.group) == 1: + callback([ + (r.id, r.value.get('rec_name', '')) + for r in win.screen.group]) + win.destroy() + else: + win.show() + + def _sig_remove(self, *args): + self.screen.remove(remove=True) + + def _on_activate(self): + self._sig_edit() + + def _get_screen_form(self): + domain = self.field.domain_get(self.record) + add_remove = self.record.expr_eval(self.attrs.get('add_remove')) + if add_remove: + domain = [domain, add_remove] + context = self.field.get_context(self.record) + # Remove the first tree view as mode is form only + view_ids = self.attrs.get('view_ids', '').split(',')[1:] + model = self.attrs['relation'] + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + self.attrs.get('string') or common.MODELNAME.get(model)) + return Screen(model, domain=domain, + view_ids=view_ids, + mode=['form'], views_preload=self.attrs.get('views', {}), + context=context, breadcrumb=breadcrumb) + + def _sig_edit(self): + if not self.screen.current_record: + return + # Create a new screen that is not linked to the parent otherwise on the + # save of the record will trigger the save of the parent + screen = self._get_screen_form() + screen.load([self.screen.current_record.id]) + + def callback(result): + if result: + screen.current_record.save() + # Force a reload on next display + self.screen.current_record.cancel() + # Force a display to clear the CellCache + self.screen.display() + WinForm(screen, callback) + + def _sig_new(self): + screen = self._get_screen_form() + + def callback(result): + self.focus_out = True + if result: + record = screen.current_record + self.screen.load([record.id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + self.focus_out = False + WinForm(screen, callback, new=True, save_current=True, + rec_name=self.wid_text.get_text()) + + def _readonly_set(self, value): + self._readonly = value + self._set_button_sensitive() + self.wid_text.set_sensitive(not value) + self.wid_text.set_editable(not value) + self._set_label_state() + + def _required_set(self, value): + self._required = value + self._set_label_state() + + def _set_label_state(self): + common.apply_label_attributes( + self.title, self._readonly, self._required) + + def _set_button_sensitive(self): + if self.record and self.field: + field_size = self.record.expr_eval(self.attrs.get('size')) + m2m_size = len(self.field.get_eval(self.record)) + size_limit = (field_size is not None + and m2m_size >= field_size >= 0) + else: + size_limit = False + + self.but_add.set_sensitive(bool( + not self._readonly + and not size_limit)) + self.but_remove.set_sensitive(bool( + not self._readonly + and self._position)) + + def _sig_label(self, screen, signal_data): + self._position = signal_data[0] + self._set_button_sensitive() + + def display(self): + super(Many2Many, self).display() + if not self.field: + self.screen.new_group() + self.screen.current_record = None + self.screen.parent = None + self.screen.display() + return False + new_group = self.field.get_client(self.record) + if id(self.screen.group) != id(new_group): + self.screen.group = new_group + self.screen.display() + return True + + def set_value(self): + self.screen.current_view.set_value() + return True + + def _completion_match_selected(self, completion, model, iter_): + record_id, = model.get(iter_, 1) + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + model = self.attrs['relation'] + domain = self.field.domain_get(self.record) + add_remove = self.record.expr_eval(self.attrs.get('add_remove')) + if add_remove: + domain = [domain, add_remove] + update_completion( + self.wid_text, self.record, self.field, model, domain) + + def _completion_action_activated(self, completion, index): + if index == 0: + self._sig_add() + self.wid_text.grab_focus() + elif index == 1: + self._sig_new() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py new file mode 100644 index 0000000000..1dff81b434 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -0,0 +1,379 @@ +# 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 gettext + +from gi.repository import Gdk, GLib, Gtk + +from .widget import Widget +import tryton.common as common +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_search import WinSearch +from tryton.gui.window.win_form import WinForm +from tryton.common.popup_menu import populate +from tryton.common.completion import get_completion, update_completion +from tryton.common.entry_position import reset_position +from tryton.common.domain_parser import quote + +_ = gettext.gettext + + +class Many2One(Widget): + + def __init__(self, view, attrs): + super(Many2One, self).__init__(view, attrs) + + self.widget = Gtk.HBox(spacing=0) + self.widget.set_property('sensitive', True) + + self.wid_text = self.mnemonic_widget = Gtk.Entry() + self.wid_text.set_property('width-chars', self.default_width_chars) + self.wid_text.set_property('activates_default', True) + self.wid_text.connect('key-press-event', self.send_modified) + self.wid_text.connect('key_press_event', self.sig_key_press) + self.wid_text.connect('populate-popup', self._populate_popup) + self.wid_text.connect('focus-out-event', + lambda x, y: self._focus_out()) + self.wid_text.connect('changed', self.sig_changed) + self.changed = True + self.focus_out = True + + if int(self.attrs.get('completion', 1)): + self.wid_text.connect('changed', self._update_completion) + self.wid_completion = None + + self.wid_text.connect('icon-press', self.sig_edit) + + self.widget.pack_end(self.wid_text, expand=True, fill=True, padding=0) + + self._readonly = False + + def get_model(self): + return self.attrs['relation'] + + def _readonly_set(self, value): + self._readonly = value + self._set_button_sensitive() + + def _set_button_sensitive(self): + self.wid_text.set_editable(not self._readonly) + self.wid_text.set_icon_sensitive( + Gtk.EntryIconPosition.PRIMARY, self.read_access) + self.wid_text.set_icon_sensitive( + Gtk.EntryIconPosition.SECONDARY, not self._readonly) + + def get_access(self, type_): + model = self.get_model() + if model: + return common.MODELACCESS[model][type_] + else: + return True + + @property + def read_access(self): + return self.get_access('read') + + @property + def create_access(self): + return self.attrs.get('create', True) and self.get_access('create') + + @property + def modified(self): + if self.record and self.field: + value = self.wid_text.get_text() + return self.field.get_client(self.record) != value + return False + + @staticmethod + def has_target(value): + return value is not None + + @staticmethod + def value_from_id(id_, str_=None): + if str_ is None: + str_ = '' + return id_, str_ + + @staticmethod + def id_from_value(value): + return value + + def sig_activate(self): + model = self.get_model() + if not model or not common.MODELACCESS[model]['read']: + return + if not self.focus_out or not self.field: + return + self.changed = False + value = self.field.get(self.record) + model = self.get_model() + + self.focus_out = False + if model and not self.has_target(value): + if (not self._readonly + and (self.wid_text.get_text() + or self.field.get_state_attrs( + self.record)['required'])): + domain = self.field.domain_get(self.record) + context = self.field.get_search_context(self.record) + order = self.field.get_search_order(self.record) + text = self.wid_text.get_text() + + def callback(result): + if result: + self.field.set_client(self.record, + self.value_from_id(*result[0]), force_change=True) + else: + self.wid_text.set_text('') + self.focus_out = True + self.changed = True + + win = WinSearch(model, callback, sel_multi=False, + context=context, domain=domain, order=order, + view_ids=self.attrs.get('view_ids', '').split(','), + views_preload=self.attrs.get('views', {}), + new=self.create_access, + title=self.attrs.get('string'), + exclude_field=self.attrs.get('relation_field')) + win.screen.search_filter(quote(text)) + if len(win.screen.group) == 1: + callback([ + (r.id, r.value.get('rec_name', '')) + for r in win.screen.group]) + win.destroy() + else: + win.show() + return + self.focus_out = True + self.changed = True + return + + def get_screen(self, search=False): + domain = self.field.domain_get(self.record) + if search: + context = self.field.get_search_context(self.record) + else: + context = self.field.get_context(self.record) + # Remove first tree view as mode is form only + view_ids = self.attrs.get('view_ids', '').split(',')[1:] + model = self.get_model() + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + self.attrs.get('string') or common.MODELNAME.get(model)) + return Screen(model, domain=domain, context=context, + mode=['form'], view_ids=view_ids, + views_preload=self.attrs.get('views', {}), readonly=self._readonly, + exclude_field=self.attrs.get('relation_field'), + breadcrumb=breadcrumb) + + def sig_new(self, *args): + model = self.get_model() + if not model or not common.MODELACCESS[model]['create']: + return + self.focus_out = False + screen = self.get_screen(search=True) + + def callback(result): + if result: + self.field.set_client(self.record, + self.value_from_id(screen.current_record.id, + screen.current_record.rec_name())) + self.focus_out = True + WinForm(screen, callback, new=True, save_current=True, + rec_name=self.wid_text.get_text()) + + def sig_edit(self, entry=None, icon_pos=None, *args): + model = self.get_model() + if not model or not common.MODELACCESS[model]['read']: + return + if not self.focus_out or not self.field: + return + self.changed = False + self.focus_out = False + value = self.field.get(self.record) + + if (icon_pos == Gtk.EntryIconPosition.SECONDARY + and not self._readonly + and self.has_target(value)): + self.field.set_client(self.record, self.value_from_id(None, '')) + self.wid_text.set_text('') + self.changed = True + self.focus_out = True + return + + if self.has_target(value): + screen = self.get_screen() + screen.load([self.id_from_value(self.field.get(self.record))]) + + def callback(result): + if result: + self.field.set_client(self.record, + self.value_from_id(screen.current_record.id, + screen.current_record.rec_name()), + force_change=True) + self.focus_out = True + self.changed = True + WinForm(screen, callback, save_current=True) + return + if not self._readonly: + domain = self.field.domain_get(self.record) + context = self.field.get_search_context(self.record) + order = self.field.get_search_order(self.record) + text = self.wid_text.get_text() + + def callback(result): + if result: + self.field.set_client(self.record, + self.value_from_id(*result[0]), force_change=True) + self.focus_out = True + self.changed = True + win = WinSearch(model, callback, sel_multi=False, + context=context, domain=domain, order=order, + view_ids=self.attrs.get('view_ids', '').split(','), + views_preload=self.attrs.get('views', {}), + new=self.create_access, title=self.attrs.get('string'), + exclude_field=self.attrs.get('relation_field')) + win.screen.search_filter(quote(text)) + win.show() + return + self.focus_out = True + self.changed = True + + def sig_key_press(self, widget, event, *args): + editable = self.wid_text.get_editable() + activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab] + if not self.wid_completion: + activate_keys.append(Gdk.KEY_Return) + if (event.keyval == Gdk.KEY_F3 + and editable + and self.create_access): + self.sig_new(widget, event) + return True + elif event.keyval == Gdk.KEY_F2 and self.read_access: + self.sig_edit(widget) + return True + elif (event.keyval in activate_keys + and editable): + self.sig_activate() + elif (self.has_target(self.field.get(self.record)) + and editable + and event.keyval in [Gdk.KEY_Delete, Gdk.KEY_BackSpace]): + self.wid_text.set_text('') + return False + + def sig_changed(self, *args): + if not self.changed: + return False + value = self.field.get(self.record) + if self.has_target(value) and self.modified: + def clean(): + if not self.wid_text.props.window: + return + text = self.wid_text.get_text() + position = self.wid_text.get_position() + self.field.set_client(self.record, + self.value_from_id(None, '')) + # The value of the field could be different of None + # in such case, the original text should not be restored + if not self.wid_text.get_text(): + # Restore text and position after display + self.wid_text.set_text(text) + self.wid_text.set_position(position) + GLib.idle_add(clean) + return False + + def get_value(self): + return self.wid_text.get_text() + + def set_value(self): + if self.field.get_client(self.record) != self.wid_text.get_text(): + self.field.set_client(self.record, self.value_from_id(None, '')) + self.wid_text.set_text('') + + def set_text(self, value): + if not value: + value = '' + self.wid_text.set_text(value) + reset_position(self.wid_text) + + def display(self): + self.changed = False + super(Many2One, self).display() + + self._set_button_sensitive() + self._set_completion() + + if not self.field: + self.set_text(None) + self.changed = True + return False + self.set_text(self.field.get_client(self.record)) + if self.has_target(self.field.get(self.record)): + icon1, tooltip1 = 'tryton-open', _('Open the record ') + icon2, tooltip2 = 'tryton-clear', _('Clear the field ') + else: + icon1, tooltip1 = None, '' + icon2, tooltip2 = 'tryton-search', _('Search a record ') + if not self.wid_text.get_editable(): + icon2, tooltip2 = None, '' + for pos, icon, tooltip in [ + (Gtk.EntryIconPosition.PRIMARY, icon1, tooltip1), + (Gtk.EntryIconPosition.SECONDARY, icon2, tooltip2)]: + if icon: + pixbuf = common.IconFactory.get_pixbuf( + icon, Gtk.IconSize.MENU) + else: + pixbuf = None + self.wid_text.set_icon_from_pixbuf(pos, pixbuf) + self.wid_text.set_icon_tooltip_text(pos, tooltip) + self.changed = True + + def _populate_popup(self, widget, menu): + value = self.field.get(self.record) + if self.has_target(value): + populate( + menu, self.get_model(), self.id_from_value(value), + '', self.field, self.field.get_context(self.record)) + return True + + def _set_completion(self): + if not int(self.attrs.get('completion', 1)): + return + self.wid_completion = get_completion( + search=self.read_access, + create=self.create_access) + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + + def _completion_match_selected(self, completion, model, iter_): + rec_name, record_id = model.get(iter_, 0, 1) + # GTK on win32 doesn't like synchronous call to set_client + # because it triggers a display which reset the completion + GLib.idle_add(self.field.set_client, self.record, + self.value_from_id(record_id, rec_name), True) + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = rec_name + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + value = self.field.get(self.record) + if self.has_target(value): + id_ = self.id_from_value(value) + if id_ is not None and id_ >= 0: + return + model = self.get_model() + update_completion(self.wid_text, self.record, self.field, model) + + def _completion_action_activated(self, completion, index): + if index == 0: + self.sig_edit() + elif index == 1: + self.sig_new() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py b/tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py new file mode 100644 index 0000000000..5328be395d --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/multiselection.py @@ -0,0 +1,96 @@ +# 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 gi.repository import GLib, GObject, Gtk + +from .widget import Widget +from tryton.common.selection import SelectionMixin +from tryton.common.treeviewcontrol import TreeViewControl + + +class MultiSelection(Widget, SelectionMixin): + expand = True + + def __init__(self, view, attrs): + super(MultiSelection, self).__init__(view, attrs) + + if int(attrs.get('yexpand', self.expand)): + self.widget = Gtk.ScrolledWindow() + self.widget.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.widget.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + else: + self.widget = Gtk.VBox() + self.widget.set_size_request(100, 100) + self.widget.get_accessible().set_name(attrs.get('string', '')) + + self.model = Gtk.ListStore(GObject.TYPE_PYOBJECT, GObject.TYPE_STRING) + self.tree = self.mnemonic_widget = TreeViewControl() + self.tree.set_model(self.model) + self.tree.set_search_column(1) + self.tree.connect('focus-out-event', lambda *a: self._focus_out()) + self.tree.set_headers_visible(False) + selection = self.tree.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + selection.connect('changed', self.changed) + self.widget.add(self.tree) + name_column = Gtk.TreeViewColumn() + name_cell = Gtk.CellRendererText() + name_column.pack_start(name_cell, expand=True) + name_column.add_attribute(name_cell, 'text', 1) + self.tree.append_column(name_column) + + self.nullable_widget = False + self.init_selection() + + def _readonly_set(self, readonly): + super(MultiSelection, self)._readonly_set(readonly) + selection = self.tree.get_selection() + selection.set_select_function(lambda *a: not readonly) + + @property + def modified(self): + if self.record and self.field: + group = set(self.field.get_eval(self.record)) + value = set(self.get_value()) + return value != group + return False + + def changed(self, selection): + def focus_out(): + if self.widget.props.window: + self._focus_out() + # Must be deferred because it triggers a display of the form + GLib.idle_add(focus_out) + + def get_value(self): + model, paths = self.tree.get_selection().get_selected_rows() + return [model[path][0] for path in paths] + + def set_value(self): + self.field.set_client(self.record, self.get_value()) + + def display(self): + selection = self.tree.get_selection() + selection.handler_block_by_func(self.changed) + try: + # Remove select_function to allow update, + # it will be set back in the super call + selection.set_select_function(lambda *a: True) + self.update_selection(self.record, self.field) + new_model = self.selection != [list(row) for row in self.model] + if new_model: + self.model.clear() + if not self.field: + return + value2path = {} + for idx, (value, name) in enumerate(self.selection): + if new_model: + self.model.append((value, name)) + value2path[value] = idx + selection.unselect_all() + values = self.field.get_eval(self.record) + for value in values: + selection.select_path(value2path[value]) + super(MultiSelection, self).display() + finally: + selection.handler_unblock_by_func(self.changed) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py new file mode 100644 index 0000000000..5d80af36ea --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/one2many.py @@ -0,0 +1,573 @@ +# 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 gettext +import itertools + +from gi.repository import Gdk, Gtk + +from .widget import Widget +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_search import WinSearch +from tryton.gui.window.win_form import WinForm +import tryton.common as common +from tryton.common.completion import get_completion, update_completion +from tryton.common.domain_parser import quote +from tryton.common.underline import set_underline + +_ = gettext.gettext + + +class One2Many(Widget): + expand = True + + def __init__(self, view, attrs): + super(One2Many, self).__init__(view, attrs) + + self.widget = Gtk.Frame() + self.widget.set_shadow_type(Gtk.ShadowType.NONE) + self.widget.get_accessible().set_name(attrs.get('string', '')) + vbox = Gtk.VBox(homogeneous=False, spacing=2) + self.widget.add(vbox) + self._readonly = True + self._required = False + self._position = 0 + self._length = 0 + + self.title_box = hbox = Gtk.HBox(homogeneous=False, spacing=0) + hbox.set_border_width(2) + + self.title = Gtk.Label( + label=set_underline(attrs.get('string', '')), + use_underline=True, halign=Gtk.Align.START) + hbox.pack_start(self.title, expand=True, fill=True, padding=0) + + hbox.pack_start(Gtk.VSeparator(), expand=False, fill=True, padding=0) + + tooltips = common.Tooltips() + + but_switch = Gtk.Button(can_focus=False) + tooltips.set_tip(but_switch, _('Switch')) + but_switch.connect('clicked', self.switch_view) + but_switch.add(common.IconFactory.get_image( + 'tryton-switch', Gtk.IconSize.SMALL_TOOLBAR)) + but_switch.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_switch, expand=False, fill=False, padding=0) + + self.but_pre = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_pre, _('Previous')) + self.but_pre.connect('clicked', self._sig_previous) + self.but_pre.add(common.IconFactory.get_image( + 'tryton-back', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_pre.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_pre, expand=False, fill=False, padding=0) + + self.label = Gtk.Label(label='(0,0)') + hbox.pack_start(self.label, expand=False, fill=False, padding=0) + + self.but_next = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_next, _('Next')) + self.but_next.connect('clicked', self._sig_next) + self.but_next.add(common.IconFactory.get_image( + 'tryton-forward', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_next.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_next, expand=False, fill=False, padding=0) + + hbox.pack_start(Gtk.VSeparator(), expand=False, fill=True, padding=0) + + self.focus_out = True + self.wid_completion = None + if attrs.get('add_remove'): + + self.wid_text = Gtk.Entry() + self.wid_text.set_placeholder_text(_('Search')) + self.wid_text.set_property('width_chars', 13) + self.wid_text.connect('focus-out-event', self._focus_out) + hbox.pack_start(self.wid_text, expand=True, fill=True, padding=0) + + if int(self.attrs.get('completion', 1)): + access = common.MODELACCESS[attrs['relation']] + self.wid_completion = get_completion( + search=access['read'] and access['write'], + create=attrs.get('create', True) and access['create']) + self.wid_completion.connect('match-selected', + self._completion_match_selected) + self.wid_completion.connect('action-activated', + self._completion_action_activated) + self.wid_text.set_completion(self.wid_completion) + self.wid_text.connect('changed', self._update_completion) + + self.but_add = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_add, _('Add existing record')) + self.but_add.connect('clicked', self._sig_add) + self.but_add.add(common.IconFactory.get_image( + 'tryton-add', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_add.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_add, expand=False, fill=False, padding=0) + + self.but_remove = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_remove, + _('Remove selected record')) + self.but_remove.connect('clicked', self._sig_remove, True) + self.but_remove.add(common.IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_remove.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start( + self.but_remove, expand=False, fill=False, padding=0) + + hbox.pack_start( + Gtk.VSeparator(), expand=False, fill=True, padding=0) + + self.but_new = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_new, _('Create a new record')) + self.but_new.connect('clicked', self._sig_new) + self.but_new.add(common.IconFactory.get_image( + 'tryton-create', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_new.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_new, expand=False, fill=False, padding=0) + + self.but_open = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_open, _('Edit selected record')) + self.but_open.connect('clicked', self._sig_edit) + self.but_open.add(common.IconFactory.get_image( + 'tryton-open', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_open.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_open, expand=False, fill=False, padding=0) + + self.but_del = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_del, _('Delete selected record')) + self.but_del.connect('clicked', self._sig_remove, False) + self.but_del.add(common.IconFactory.get_image( + 'tryton-delete', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_del.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_del, expand=False, fill=False, padding=0) + + self.but_undel = Gtk.Button(can_focus=False) + tooltips.set_tip(self.but_undel, _('Undelete selected record ')) + self.but_undel.connect('clicked', self._sig_undelete) + self.but_undel.add(common.IconFactory.get_image( + 'tryton-undo', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_undel.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_undel, expand=False, fill=False, padding=0) + + tooltips.enable() + + frame = Gtk.Frame() + frame.add(hbox) + frame.set_shadow_type(Gtk.ShadowType.OUT) + vbox.pack_start(frame, expand=False, fill=True, padding=0) + + model = attrs['relation'] + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + attrs.get('string') or common.MODELNAME.get(model)) + self.screen = Screen(model, + mode=attrs.get('mode', 'tree,form').split(','), + view_ids=attrs.get('view_ids', '').split(','), + views_preload=attrs.get('views', {}), + order=attrs.get('order'), + row_activate=self._on_activate, + exclude_field=attrs.get('relation_field', None), + limit=None, + breadcrumb=breadcrumb) + self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) + self.screen.signal_connect(self, 'record-message', self._sig_label) + + vbox.pack_start(self.screen.widget, expand=True, fill=True, padding=0) + + self.title.set_mnemonic_widget( + self.screen.current_view.mnemonic_widget) + + self.screen.widget.connect('key_press_event', self.on_keypress) + if self.attrs.get('add_remove'): + self.wid_text.connect('key_press_event', self.on_keypress) + + but_switch.props.sensitive = self.screen.number_of_views > 1 + + def on_keypress(self, widget, event): + if ((event.keyval == Gdk.KEY_F3) + and self.but_new.get_property('sensitive')): + self._sig_new(widget) + return True + if event.keyval == Gdk.KEY_F2: + if widget == self.screen.widget: + self._sig_edit(widget) + return True + elif widget == self.wid_text: + self._sig_add(widget) + return True + if event.keyval == Gdk.KEY_F4: + self.switch_view(widget) + if (event.keyval in [Gdk.KEY_Delete, Gdk.KEY_KP_Delete] + and widget == self.screen.widget): + remove = not (event.state & Gdk.ModifierType.CONTROL_MASK) + if remove and self.attrs.get('add_remove'): + but = self.but_remove + else: + remove = False + but = self.but_del + if but.get_property('sensitive'): + self._sig_remove(widget, remove) + return True + if event.keyval == Gdk.KEY_Insert and widget == self.screen.widget: + self._sig_undelete(widget) + return True + if self.attrs.get('add_remove'): + editable = self.wid_text.get_editable() + activate_keys = [Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab] + if not self.wid_completion: + activate_keys.append(Gdk.KEY_Return) + if (widget == self.wid_text + and event.keyval in activate_keys + and editable + and self.wid_text.get_text()): + self._sig_add() + self.wid_text.grab_focus() + return False + + def destroy(self): + if self.attrs.get('add_remove'): + self.wid_text.disconnect_by_func(self._focus_out) + self.screen.destroy() + + def _on_activate(self): + self._sig_edit() + + def switch_view(self, widget): + self.screen.switch_view() + mnemonic_widget = self.screen.current_view.mnemonic_widget + string = self.attrs.get('string', '') + if mnemonic_widget: + string = set_underline(string) + self.title.set_mnemonic_widget(mnemonic_widget) + self.title.set_label(string) + + @property + def modified(self): + return self.screen.current_view.modified + + def _readonly_set(self, value): + self._readonly = value + self._set_button_sensitive() + self._set_label_state() + + def _required_set(self, value): + self._required = value + self._set_label_state() + + def _set_label_state(self): + common.apply_label_attributes( + self.title, self._readonly, self._required) + + def _set_button_sensitive(self): + access = common.MODELACCESS[self.screen.model_name] + if self.record and self.field: + field_size = self.record.expr_eval(self.attrs.get('size')) + o2m_size = len(self.field.get_eval(self.record)) + size_limit = (field_size is not None + and o2m_size >= field_size >= 0) + else: + o2m_size = None + size_limit = False + + first = last = False + if isinstance(self._position, int): + first = self._position <= 1 + last = self._position >= self._length + deletable = self.screen.deletable + + self.but_new.set_sensitive(bool( + not self._readonly + and self.attrs.get('create', True) + and not size_limit + and access['create'])) + self.but_del.set_sensitive(bool( + not self._readonly + and self.attrs.get('delete', True) + and deletable + and self._position + and access['delete'])) + self.but_undel.set_sensitive(bool( + not self._readonly + and not size_limit + and self._position)) + self.but_open.set_sensitive(bool( + self._position + and access['read'])) + self.but_next.set_sensitive(bool( + self._position + and not last)) + self.but_pre.set_sensitive(bool( + self._position + and not first)) + if self.attrs.get('add_remove'): + self.but_add.set_sensitive(bool( + not self._readonly + and not size_limit + and access['write'] + and access['read'])) + self.but_remove.set_sensitive(bool( + not self._readonly + and self._position + and access['write'] + and access['read'])) + self.wid_text.set_sensitive(self.but_add.get_sensitive()) + self.wid_text.set_editable(self.but_add.get_sensitive()) + + def _validate(self): + self.view.set_value() + record = self.screen.current_record + if record: + fields = self.screen.current_view.get_fields() + if not record.validate(fields): + self.screen.display(set_cursor=True) + return False + if self.screen.pre_validate and not record.pre_validate(): + return False + return True + + def _sequence(self): + for view in self.screen.views: + if view.view_type == 'tree': + sequence = view.attributes.get('sequence') + if sequence: + return sequence + + def _sig_new(self, *args): + if not common.MODELACCESS[self.screen.model_name]['create']: + return + if not self._validate(): + return + + if self.attrs.get('product'): + self._new_product() + else: + self._new_single() + + def _new_single(self): + sequence = self._sequence() + + def update_sequence(): + if sequence: + self.screen.group.set_sequence( + field=sequence, position=self.screen.new_position) + + if self.screen.current_view.editable: + self.screen.new() + self.screen.current_view.widget.set_sensitive(True) + update_sequence() + else: + field_size = self.record.expr_eval(self.attrs.get('size')) or -1 + field_size -= len(self.field.get_eval(self.record)) + 1 + WinForm(self.screen, lambda a: update_sequence(), new=True, + many=field_size) + + def _new_product(self): + fields = self.attrs['product'].split(',') + product = {} + + first = self.screen.new(default=False) + default = first.default_get() + first.set_default(default) + + def search_set(*args): + if not fields: + return make_product() + field = self.screen.group.fields[fields.pop()] + relation = field.attrs.get('relation') + if not relation: + search_set() + + domain = field.domain_get(first) + context = field.get_search_context(first) + order = field.get_search_order(first) + + def callback(result): + if result: + product[field.name] = result + + win_search = WinSearch(relation, callback, sel_multi=True, + context=context, domain=domain, order=order, + title=self.attrs.get('string')) + win_search.win.connect('destroy', search_set) + win_search.screen.search_filter() + win_search.show() + + def make_product(): + self.screen.group.remove(first, remove=True) + if not product: + return + + fields = list(product.keys()) + for values in itertools.product(*list(product.values())): + record = self.screen.new(default=False) + default_value = default.copy() + for field, value in zip(fields, values): + id_, rec_name = value + default_value[field] = id_ + default_value[field + '.rec_name'] = rec_name + record.set_default(default_value) + + sequence = self._sequence() + if sequence: + self.screen.group.set_sequence( + field=sequence, position=self.screen.new_position) + + search_set() + + def _sig_edit(self, widget=None): + if not common.MODELACCESS[self.screen.model_name]['read']: + return + if not self._validate(): + return + record = self.screen.current_record + if record: + WinForm(self.screen, lambda a: None) + + def _sig_next(self, widget): + if not self._validate(): + return + self.screen.display_next() + + def _sig_previous(self, widget): + if not self._validate(): + return + self.screen.display_prev() + + def _sig_remove(self, widget, remove=False): + access = common.MODELACCESS[self.screen.model_name] + writable = not self.screen.readonly + deletable = self.screen.deletable + if remove: + if not access['write'] or not writable or not access['read']: + return + else: + if not access['delete'] or not deletable: + return + self.screen.remove(remove=remove) + + def _sig_undelete(self, button): + self.screen.unremove() + + def _sig_add(self, *args): + if not self.focus_out: + return + access = common.MODELACCESS[self.screen.model_name] + if not access['write'] or not access['read']: + return + self.view.set_value() + domain = self.field.domain_get(self.record) + context = self.field.get_search_context(self.record) + domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))] + removed_ids = self.field.get_removed_ids(self.record) + domain = ['OR', domain, ('id', 'in', removed_ids)] + text = self.wid_text.get_text() + + self.focus_out = False + + sequence = self._sequence() + + def callback(result): + self.focus_out = True + if result: + ids = [x[0] for x in result] + self.screen.load(ids, modified=True) + self.screen.display(res_id=ids[0]) + if sequence: + self.screen.group.set_sequence( + field=sequence, position=self.screen.new_position) + self.screen.set_cursor() + self.wid_text.set_text('') + + order = self.field.get_search_order(self.record) + win = WinSearch(self.attrs['relation'], callback, sel_multi=True, + context=context, domain=domain, order=order, + view_ids=self.attrs.get('view_ids', '').split(','), + views_preload=self.attrs.get('views', {}), + new=self.but_new.get_property('sensitive'), + title=self.attrs.get('string'), + exclude_field=self.attrs.get('relation_field')) + win.screen.search_filter(quote(text)) + win.show() + + def _sig_label(self, screen, signal_data): + self._position = signal_data[0] + self._length = signal_data[1] + if self._position: + name = str(self._position) + else: + name = '_' + line = '(%s/%s)' % (name, self._length) + self.label.set_text(line) + self._set_button_sensitive() + + def display(self): + super(One2Many, self).display() + + self._set_button_sensitive() + + if not self.field: + self.screen.new_group() + self.screen.current_record = None + self.screen.parent = None + self.screen.display() + return False + new_group = self.field.get_client(self.record) + + if id(self.screen.group) != id(new_group): + self.screen.group = new_group + if (self.screen.current_view.view_type == 'tree') \ + and self.screen.current_view.editable: + self.screen.current_record = None + domain = [] + size_limit = None + if self.record: + domain = self.field.domain_get(self.record) + size_limit = self.record.expr_eval(self.attrs.get('size')) + if self._readonly: + if size_limit is None: + size_limit = len(self.screen.group) + else: + size_limit = min(size_limit, len(self.screen.group)) + if self.screen.domain != domain: + self.screen.domain = domain + self.screen.size_limit = size_limit + self.screen.display() + return True + + def set_value(self): + self.screen.current_view.set_value() + if self.screen.modified(): # TODO check if required + self.record.modified_fields.setdefault(self.field.name) + self.record.signal('record-modified') + return True + + def _completion_match_selected(self, completion, model, iter_): + record_id, = model.get(iter_, 1) + self.screen.load([record_id], modified=True) + self.wid_text.set_text('') + self.wid_text.grab_focus() + + completion_model = self.wid_completion.get_model() + completion_model.clear() + completion_model.search_text = self.wid_text.get_text() + return True + + def _update_completion(self, widget): + if self._readonly: + return + if not self.record: + return + model = self.attrs['relation'] + domain = self.field.domain_get(self.record) + domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))] + removed_ids = self.field.get_removed_ids(self.record) + domain = ['OR', domain, ('id', 'in', removed_ids)] + update_completion(self.wid_text, self.record, self.field, model, + domain=domain) + + def _completion_action_activated(self, completion, index): + if index == 0: + self._sig_add() + self.wid_text.grab_focus() + elif index == 1: + self._sig_new() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/one2one.py b/tryton/tryton/gui/window/view_form/view/form_gtk/one2one.py new file mode 100644 index 0000000000..effb6a72a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/one2one.py @@ -0,0 +1,7 @@ +# 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 .many2one import Many2One + + +class One2One(Many2One): + pass diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/progressbar.py b/tryton/tryton/gui/window/view_form/view/form_gtk/progressbar.py new file mode 100644 index 0000000000..8d3a554686 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/progressbar.py @@ -0,0 +1,41 @@ +# 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 gettext + +from gi.repository import Gtk + +from .widget import Widget + +_ = gettext.gettext + + +class ProgressBar(Widget): + 'Progress Bar' + orientations = { + 'left_to_right': (Gtk.Orientation.HORIZONTAL, False), + 'right_to_left': (Gtk.Orientation.HORIZONTAL, True), + 'bottom_to_top': (Gtk.Orientation.VERTICAL, True), + 'top_to_bottom': (Gtk.Orientation.VERTICAL, False), + } + + def __init__(self, view, attrs): + super(ProgressBar, self).__init__(view, attrs) + self.widget = self.mnemonic_widget = Gtk.ProgressBar() + orientation, inverted = self.orientations.get( + attrs.get('orientation', 'left_to_right')) + self.widget.set_orientation(orientation) + self.widget.set_inverted(inverted) + self.widget.set_show_text(True) + + def display(self): + super(ProgressBar, self).display() + if not self.field: + self.widget.set_text('') + self.widget.set_fraction(0.0) + return False + text = self.field.get_client(self.record, factor=100) + if text: + text = _('%s%%') % text + self.widget.set_text(text) + value = self.field.get(self.record) or 0.0 + self.widget.set_fraction(value) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/pyson.py b/tryton/tryton/gui/window/view_form/view/form_gtk/pyson.py new file mode 100644 index 0000000000..e9cbc7297c --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/pyson.py @@ -0,0 +1,53 @@ +# 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 gi.repository import Gtk + +from .char import Char +from tryton.common import IconFactory +from tryton.pyson import CONTEXT, PYSONEncoder, PYSONDecoder + + +class PYSON(Char): + + def __init__(self, view, attrs): + super(PYSON, self).__init__(view, attrs) + self.encoder = PYSONEncoder() + self.decoder = PYSONDecoder(noeval=True) + self.entry.connect('key-release-event', self.validate_pyson) + + def get_encoded_value(self): + value = self.get_value() + if not value: + return value + try: + return self.encoder.encode(eval(value, CONTEXT)) + except Exception: + return None + + def set_value(self): + # avoid modification because different encoding + value = self.get_encoded_value() + previous = self.field.get_client(self.record) + if (previous + and value == self.encoder.encode( + self.decoder.decode(previous))): + value = previous + self.field.set_client(self.record, value) + + def get_client_value(self): + value = super(PYSON, self).get_client_value() + if value: + value = repr(self.decoder.decode(value)) + return value + + def validate_pyson(self, *args): + icon = 'tryton-ok' + if self.get_encoded_value() is None: + icon = 'tryton-error' + pixbuf = IconFactory.get_pixbuf(icon, Gtk.IconSize.MENU) + self.entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.SECONDARY, pixbuf) + + def _focus_out(self): + self.validate_pyson() + super(PYSON, self)._focus_out() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/reference.py b/tryton/tryton/gui/window/view_form/view/form_gtk/reference.py new file mode 100644 index 0000000000..bd6b326a69 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/reference.py @@ -0,0 +1,143 @@ +# 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 gettext + +from gi.repository import Gtk + +from .many2one import Many2One +from tryton.common.selection import SelectionMixin, PopdownMixin, \ + selection_shortcuts + +_ = gettext.gettext + + +class Reference(Many2One, SelectionMixin, PopdownMixin): + + def __init__(self, view, attrs): + super(Reference, self).__init__(view, attrs) + + self.widget_combo = Gtk.ComboBox(has_entry=True) + child = self.widget_combo.get_child() + child.connect('activate', lambda *a: self._focus_out()) + child.connect('focus-out-event', lambda *a: self._focus_out()) + child.get_accessible().set_name(attrs.get('string', '')) + self.widget_combo.connect('changed', self.sig_changed_combo) + self.widget_combo.connect('move-active', self._move_active) + self.widget_combo.connect( + 'scroll-event', + lambda c, e: c.stop_emission_by_name('scroll-event')) + selection_shortcuts(self.widget_combo) + + self.widget.pack_start( + self.widget_combo, expand=False, fill=True, padding=0) + + self.init_selection() + self.set_popdown(self.selection, self.widget_combo) + + def get_model(self): + active = self.widget_combo.get_active() + if active < 0: + return '' + else: + model = self.widget_combo.get_model() + return model[active][1] + + def get_empty_value(self): + for name, model in self.widget_combo.get_model(): + if model in (None, ''): + return model, name + return '', '' + + def _move_active(self, combobox, scroll_type): + if not combobox.get_child().get_editable(): + combobox.stop_emission_by_name('move-active') + + def _set_button_sensitive(self): + super(Reference, self)._set_button_sensitive() + self.widget_combo.get_child().set_editable(not self._readonly) + self.widget_combo.set_button_sensitivity( + Gtk.SensitivityType.OFF if self._readonly + else Gtk.SensitivityType.AUTO) + + @property + def modified(self): + if self.record and self.field: + try: + model, name = self.field.get_client(self.record) + except (ValueError, TypeError): + model, name = self.get_empty_value() + return (model != self.get_model() + or name != self.wid_text.get_text()) + return False + + def has_target(self, value): + if value is None: + return False + model, value = value.split(',') + if not value: + value = None + else: + try: + value = int(value) + except ValueError: + value = None + result = model == self.get_model() and value >= 0 + return result + + def value_from_id(self, id_, str_=None): + if str_ is None: + str_ = '' + return self.get_model(), (id_, str_) + + @staticmethod + def id_from_value(value): + _, value = value.split(',') + return int(value) + + def sig_changed_combo(self, *args): + if not self.changed: + return + self.wid_text.set_text('') + model = self.get_model() + if model: + value = (model, (-1, '')) + else: + value = ('', '') + self.field.set_client(self.record, value) + + def set_value(self): + if not self.get_model(): + value = self.wid_text.get_text() + if not value: + self.field.set_client(self.record, None) + else: + self.field.set_client(self.record, ('', value)) + return + else: + try: + model, name = self.field.get_client(self.record) + except (ValueError, TypeError): + model, name = self.get_empty_value() + if (model != self.get_model() + or name != self.wid_text.get_text()): + self.field.set_client(self.record, None) + self.set_text(None) + + def set_text(self, value): + if value: + model, value = value + else: + model, value = None, None + super(Reference, self).set_text(value) + self.widget_combo.handler_block_by_func(self.sig_changed_combo) + if not self.set_popdown_value(self.widget_combo, model): + text = self.get_inactive_selection(model) + self.set_popdown( + self.selection[:] + [(model, text)], self.widget_combo) + self.set_popdown_value(self.widget_combo, value) + self.widget_combo.handler_unblock_by_func(self.sig_changed_combo) + + def display(self): + self.update_selection(self.record, self.field) + self.set_popdown(self.selection, self.widget_combo) + super(Reference, self).display() diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/richtextbox.py b/tryton/tryton/gui/window/view_form/view/form_gtk/richtextbox.py new file mode 100644 index 0000000000..872f914149 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/richtextbox.py @@ -0,0 +1,78 @@ +# 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 weakref import WeakKeyDictionary + +from gi.repository import Gtk + +from .textbox import TextBox +from tryton.common.htmltextbuffer import normalize_markup +from tryton.common.richtext import ( + register_format, set_content, get_content, add_toolbar) +from tryton.config import CONFIG + + +class RichTextBox(TextBox): + + def __init__(self, view, attrs): + super(RichTextBox, self).__init__(view, attrs) + self.toolbar = None + self.tag_widgets = WeakKeyDictionary() + self.tags = {} + self.colors = {} + if int(self.attrs.get('toolbar', 1)): + self.toolbar = add_toolbar(self.textview) + self.toolbar.set_style({ + 'default': False, + 'both': Gtk.ToolbarStyle.BOTH, + 'text': Gtk.ToolbarStyle.TEXT, + 'icons': Gtk.ToolbarStyle.ICONS, + }[CONFIG['client.toolbar']]) + self.widget.pack_start( + self.toolbar, expand=False, fill=True, padding=0) + + def _get_textview(self): + textview = super(RichTextBox, self)._get_textview() + register_format(textview) + return textview + + def translate_widget(self): + widget = super(RichTextBox, self).translate_widget() + textview = widget.get_children()[-1].get_child() + if self.toolbar: + widget.pack_start( + add_toolbar(textview), expand=False, fill=True, padding=0) + return widget + + def translate_widget_set_readonly(self, widget, value): + super(RichTextBox, self).translate_widget_set_readonly(widget, value) + if self.toolbar: + toolbar = widget.get_children()[0] + for n in range(toolbar.get_n_items()): + tool = toolbar.get_nth_item(n) + tool.set_sensitive(not value) + + def set_value(self): + # avoid modification of not normalized value + value = self.get_value() + prev_value = self.field.get_client(self.record) or '' + if value == normalize_markup(prev_value): + value = prev_value + self.field.set_client(self.record, value) + + @property + def modified(self): + if self.record and self.field: + value = normalize_markup(self.field.get_client(self.record) or '') + return value != self.get_value() + return False + + def set_buffer(self, value, textview): + set_content(textview, value) + + def get_buffer(self, textview): + return get_content(textview) + + def _readonly_set(self, value): + super(RichTextBox, self)._readonly_set(value) + if self.toolbar: + self.toolbar.set_sensitive(not value) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/selection.py b/tryton/tryton/gui/window/view_form/view/form_gtk/selection.py new file mode 100644 index 0000000000..785b3dfe0a --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/selection.py @@ -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 gi.repository import GLib, Gtk + +from .widget import Widget +from tryton.common.selection import SelectionMixin, selection_shortcuts, \ + PopdownMixin + + +class Selection(Widget, SelectionMixin, PopdownMixin): + + def __init__(self, view, attrs): + super(Selection, self).__init__(view, attrs) + + self.widget = Gtk.HBox(spacing=3) + self.entry = Gtk.ComboBox(has_entry=True) + child = self.mnemonic_widget = self.entry.get_child() + child.set_property('activates_default', True) + child.set_max_length(int(attrs.get('size', 0))) + child.set_width_chars(self.default_width_chars) + + selection_shortcuts(self.entry) + child.connect('activate', lambda *a: self._focus_out()) + child.connect('focus-out-event', lambda *a: self._focus_out()) + self.entry.connect('changed', self.changed) + self.entry.connect('move-active', self._move_active) + self.entry.connect( + 'scroll-event', + lambda c, e: c.stop_emission_by_name('scroll-event')) + self.widget.pack_start(self.entry, expand=True, fill=True, padding=0) + + self.selection = attrs.get('selection', [])[:] + self.attrs = attrs + self.init_selection() + self.set_popdown(self.selection, self.entry) + + def changed(self, combobox): + def focus_out(): + if combobox.props.window: + self._focus_out() + # Must be deferred because it triggers a display of the form + GLib.idle_add(focus_out) + + def _move_active(self, combobox, scroll_type): + if not combobox.get_child().get_editable(): + combobox.stop_emission_by_name('move-active') + + def _readonly_set(self, value): + super(Selection, self)._readonly_set(value) + self.entry.get_child().set_editable(not value) + self.entry.set_button_sensitivity( + Gtk.SensitivityType.OFF if value else Gtk.SensitivityType.AUTO) + + def get_value(self): + if not self.entry.get_child(): # entry is destroyed + return + return self.get_popdown_value(self.entry) + + @property + def modified(self): + if self.record and self.field: + return self.field.get(self.record) != self.get_value() + return False + + def set_value(self): + value = self.get_value() + if 'relation' in self.attrs and value: + value = (value, self.get_popdown_text(self.entry)) + self.field.set_client(self.record, value) + + def display(self): + self.update_selection(self.record, self.field) + self.set_popdown(self.selection, self.entry) + if not self.field: + self.entry.set_active(-1) + # When setting no item GTK doesn't clear the entry + self.entry.get_child().set_text('') + return + super(Selection, self).display() + value = self.field.get(self.record) + if isinstance(value, (list, tuple)): + # Compatibility with Many2One + value = value[0] + + self.entry.handler_block_by_func(self.changed) + if not self.set_popdown_value(self.entry, value): + text = self.get_inactive_selection(value) + self.set_popdown(self.selection[:] + [(value, text)], self.entry) + self.set_popdown_value(self.entry, value) + self.entry.handler_unblock_by_func(self.changed) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/state_widget.py b/tryton/tryton/gui/window/view_form/view/form_gtk/state_widget.py new file mode 100644 index 0000000000..a3baa190ee --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/state_widget.py @@ -0,0 +1,213 @@ +# 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 functools + +from gi.repository import Gtk + +import tryton.common as common +from tryton.action import Action +from tryton.config import CONFIG +from tryton.pyson import PYSONDecoder + + +class StateMixin(object): + + def __init__(self, *args, **kwargs): + self.attrs = kwargs.pop('attrs') + super(StateMixin, self).__init__(*args, **kwargs) + + def state_set(self, record): + if record: + state_changes = record.expr_eval(self.attrs.get('states', {})) + else: + state_changes = {} + if state_changes.get('invisible', self.attrs.get('invisible')): + self.hide() + else: + self.show() + + +class Label(StateMixin, Gtk.Label): + + def state_set(self, record): + super(Label, self).state_set(record) + if 'name' in self.attrs and record: + field = record.group.fields[self.attrs['name']] + else: + field = None + if not self.attrs.get('string', True): + if field and record: + text = field.get_client(record) or '' + else: + text = '' + self.set_text(text) + if record: + state_changes = record.expr_eval(self.attrs.get('states', {})) + else: + state_changes = {} + required = ((field and field.attrs.get('required')) + or state_changes.get('required')) + readonly = ((field and field.attrs.get('readonly')) + or state_changes.get('readonly', not bool(field))) + common.apply_label_attributes(self, readonly, required) + + +class VBox(StateMixin, Gtk.VBox): + pass + + +class Image(StateMixin, Gtk.Image): + + def state_set(self, record): + super(Image, self).state_set(record) + if not record: + return + name = self.attrs['name'] + if name in record.group.fields: + field = record.group.fields[name] + name = field.get(record) + size = int(self.attrs.get('size', 48)) + if self.attrs.get('type') == 'url': + pixbuf = common.IconFactory.get_pixbuf_url( + name, size=size, size_param=self.attrs.get('url_size')) + else: + pixbuf = common.IconFactory.get_pixbuf(name, size) + self.set_from_pixbuf(pixbuf) + + +class Frame(StateMixin, Gtk.Frame): + + def __init__(self, label=None, attrs=None): + if not label: # label must be None to have no label widget + label = None + super(Frame, self).__init__(label=label, attrs=attrs) + if not label: + self.set_shadow_type(Gtk.ShadowType.NONE) + self.set_border_width(0) + + +class ScrolledWindow(StateMixin, Gtk.ScrolledWindow): + + def state_set(self, record): + # Force to show first to ensure it is displayed in the Notebook + self.show() + super(ScrolledWindow, self).state_set(record) + + +class Notebook(StateMixin, Gtk.Notebook): + pass + + +class Expander(StateMixin, Gtk.Expander): + + def __init__(self, label=None, attrs=None): + if not label: + label = None + super(Expander, self).__init__(label=label, attrs=attrs) + + +class Link(StateMixin, Gtk.Button): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_relief(Gtk.ReliefStyle.NONE) + self.set_can_focus(False) + if self.attrs.get('icon'): + self.set_always_show_image(True) + self.set_image(common.IconFactory.get_image( + self.attrs['icon'], Gtk.IconSize.LARGE_TOOLBAR)) + self.set_image_position(Gtk.PositionType.TOP) + self._current = None + + @property + def action_id(self): + return int(self.attrs['id']) + + def state_set(self, record): + super().state_set(record) + if not self.get_visible(): + return + if CONFIG['client.modepda']: + self.hide() + return + if record: + data = { + 'model': record.model_name, + 'id': record.id, + 'ids': [record.id], + } + context = record.get_context() + pyson_ctx = { + 'active_model': record.model_name, + 'active_id': record.id, + 'active_ids': [record.id], + } + self._current = record.id + else: + data = {} + context = {} + pyson_ctx = {} + self._current = None + pyson_ctx['context'] = context + try: + self.disconnect_by_func(self.__class__.clicked) + except TypeError: + pass + self.connect('clicked', self.__class__.clicked, [data, context]) + action = common.RPCExecute( + 'model', 'ir.action', 'get_action_value', self.action_id, + context=context) + self.set_label(action['name']) + + decoder = PYSONDecoder(pyson_ctx) + domain = decoder.decode(action['pyson_domain']) + if action.get('pyson_search_value'): + domain = [domain, decoder.decode(action['pyson_search_value'])] + tab_domains = [(n, decoder.decode(d)) + for n, d, c in action['domains'] if c] + if tab_domains: + label = ('%s\n' % action['name']) + '\n'.join( + '%s (%%d)' % n for n, _ in tab_domains) + else: + label = '%s (%%d)' % action['name'] + if record and self.action_id in record.links_counts: + counter = record.links_counts[self.action_id] + self._set_label_counter(label, counter) + else: + counter = [0] * (len(tab_domains) or 1) + if record: + record.links_counts[self.action_id] = counter + if tab_domains: + for i, (_, tab_domain) in enumerate(tab_domains): + common.RPCExecute( + 'model', action['res_model'], 'search_count', + ['AND', domain, tab_domain], context=context, + callback=functools.partial( + self._set_count, idx=i, current=self._current, + counter=counter, label=label)) + else: + common.RPCExecute( + 'model', action['res_model'], 'search_count', domain, + context=context, callback=functools.partial( + self._set_count, current=self._current, + counter=counter, label=label)) + + def _set_count(self, value, idx=0, current=None, counter=None, label=''): + if current != self._current or not self.get_parent(): + return + try: + counter[idx] = value() + except common.RPCException: + pass + self._set_label_counter(label, counter) + + def _set_label_counter(self, label, counter): + self.set_label(label % tuple(counter)) + if self.attrs.get('empty') == 'hide': + if any(counter): + self.show() + else: + self.hide() + + def clicked(self, data): + Action.execute(self.action_id, *data, keyword=True) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py b/tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py new file mode 100644 index 0000000000..940327b059 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/textbox.py @@ -0,0 +1,164 @@ +# 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 logging + +from gi.repository import Gtk, Gdk +try: + from gi.repository import GtkSpell +except ImportError: + GtkSpell = None + +from .widget import Widget, TranslateMixin +from tryton.config import CONFIG + + +logger = logging.getLogger(__name__) + + +class TextBox(Widget, TranslateMixin): + expand = True + + def __init__(self, view, attrs): + super(TextBox, self).__init__(view, attrs) + + self.widget = Gtk.VBox() + self.scrolledwindow = Gtk.ScrolledWindow() + self.scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + self.scrolledwindow.set_size_request(100, 100) + + self.textview = self.mnemonic_widget = self._get_textview() + self.textview.connect('focus-out-event', + lambda x, y: self._focus_out()) + self.textview.connect('key-press-event', self.send_modified) + # The click is grabbed by ListBox widget in this case user can never + # set the input with a click + self.textview.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) + self.textview.connect_after('button-press-event', self._button_press) + self.scrolledwindow.add(self.textview) + self.scrolledwindow.show_all() + + self.button = None + if attrs.get('translate'): + self.button = self.translate_button() + self.widget.pack_end( + self.button, expand=False, fill=False, padding=0) + + self.widget.pack_end( + self.scrolledwindow, expand=True, fill=True, padding=0) + + def _get_textview(self): + if self.attrs.get('size'): + textbuffer = TextBufferLimitSize(int(self.attrs['size'])) + textview = Gtk.TextView() + textview.set_buffer(textbuffer) + else: + textview = Gtk.TextView() + textview.set_wrap_mode(Gtk.WrapMode.WORD) + # TODO better tab solution + textview.set_accepts_tab(False) + return textview + + def _button_press(self, textview, event): + textview.grab_focus() + return True + + def translate_widget(self): + box = Gtk.VBox() + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + scrolledwindow.set_size_request(-1, 80) + + textview = self._get_textview() + scrolledwindow.add(textview) + box.pack_end(scrolledwindow, expand=True, fill=True, padding=0) + return box + + def translate_widget_set(self, widget, value): + textview = widget.get_children()[-1].get_child() + self.set_buffer(value, textview) + + def translate_widget_get(self, widget): + textview = widget.get_children()[-1].get_child() + return self.get_buffer(textview) + + def translate_widget_set_readonly(self, widget, value): + textview = widget.get_children()[-1].get_child() + textview.set_editable(not value) + textview.props.sensitive = not value + + def _readonly_set(self, value): + super(TextBox, self)._readonly_set(value) + self.textview.set_editable(not value) + if self.button: + self.button.set_sensitive(not value) + + @property + def modified(self): + if self.record and self.field: + return self.field.get_client(self.record) != self.get_value() + return False + + def get_value(self): + return self.get_buffer(self.textview) + + def set_value(self): + self.field.set_client(self.record, self.get_value()) + + def set_buffer(self, value, textview): + buf = textview.get_buffer() + buf.delete(buf.get_start_iter(), buf.get_end_iter()) + iter_start = buf.get_start_iter() + buf.insert(iter_start, value) + + def get_buffer(self, textview): + buf = textview.get_buffer() + iter_start = buf.get_start_iter() + iter_end = buf.get_end_iter() + return buf.get_text(iter_start, iter_end, False) + + def display(self): + super(TextBox, self).display() + value = self.field and self.field.get(self.record) + if not value: + value = '' + self.set_buffer(value, self.textview) + if (GtkSpell + and self.textview.get_editable() + and self.attrs.get('spell') + and CONFIG['client.spellcheck']): + checker = GtkSpell.Checker.get_from_text_view(self.textview) + + if self.record: + language = self.record.expr_eval(self.attrs['spell']) + if not checker: + checker = GtkSpell.Checker() + checker.attach(self.textview) + if checker.get_language() != language: + try: + checker.set_language(language) + except Exception: + logger.debug( + 'Could not set spell checker to "%s"', language) + checker.detach() + elif checker: + checker.detach() + + +class TextBufferLimitSize(Gtk.TextBuffer): + __gsignals__ = { + 'insert-text': 'override', + } + + def __init__(self, max_length): + super(TextBufferLimitSize, self).__init__() + self.max_length = max_length + + def do_insert_text(self, iter, text, length): + free_chars = self.max_length - self.get_char_count() + text = text[0:free_chars] + length = len(text) + return Gtk.TextBuffer.do_insert_text(self, iter, text, length) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py b/tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py new file mode 100644 index 0000000000..465c46f664 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/timedelta.py @@ -0,0 +1,49 @@ +# 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 gi.repository import Gtk + +from tryton.common.entry_position import reset_position +from .widget import Widget + + +class TimeDelta(Widget): + + def __init__(self, view, attrs): + super(TimeDelta, self).__init__(view, attrs) + + self.widget = Gtk.HBox() + self.entry = self.mnemonic_widget = Gtk.Entry() + self.entry.set_alignment(1.0) + self.entry.set_property('activates_default', True) + + self.entry.connect('activate', self.sig_activate) + self.entry.connect('focus-out-event', lambda x, y: self._focus_out()) + self.entry.connect('key-press-event', self.send_modified) + self.widget.pack_start(self.entry, expand=True, fill=True, padding=0) + + @property + def modified(self): + if self.record and self.field: + value = self.entry.get_text() + return self.field.get_client(self.record) != value + return False + + def set_value(self): + value = self.entry.get_text() + return self.field.set_client(self.record, value) + + def get_value(self): + return self.entry.get_text() + + def display(self): + super(TimeDelta, self).display() + if not self.field: + value = '' + else: + value = self.field.get_client(self.record) + self.entry.set_text(value) + reset_position(self.entry) + + def _readonly_set(self, value): + super(TimeDelta, self)._readonly_set(value) + self.entry.set_editable(not value) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/url.py b/tryton/tryton/gui/window/view_form/view/form_gtk/url.py new file mode 100644 index 0000000000..cd547c7dcc --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/url.py @@ -0,0 +1,169 @@ +# 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 gettext +import webbrowser +from urllib.parse import urljoin, urlencode + +from gi.repository import Gtk + +from .char import Char +from .widget import Widget, TranslateMixin +import tryton.common as common +from tryton.config import CONFIG +from tryton.common.common import selection +from tryton.common.underline import set_underline +from tryton.rpc import CONNECTION + +_ = gettext.gettext + + +class URL(Char): + "url" + + def __init__(self, view, attrs): + super(URL, self).__init__(view, attrs) + + self.tooltips = common.Tooltips() + self.button = Gtk.Button() + self.button.set_image(common.IconFactory.get_image( + 'tryton-public', Gtk.IconSize.SMALL_TOOLBAR)) + self.button.set_relief(Gtk.ReliefStyle.NONE) + self.button.connect('clicked', self.button_clicked) + self.widget.pack_start( + self.button, expand=False, fill=False, padding=0) + + def display(self): + super(URL, self).display() + self.set_tooltips() + if self.record and 'icon' in self.attrs: + icon = self.attrs['icon'] + if icon in self.record.group.fields: + value = self.record[icon].get_client(self.record) + value = value if value else 'tryton-public' + else: + value = icon + self.button.set_image(common.IconFactory.get_image( + value, Gtk.IconSize.SMALL_TOOLBAR)) + + def set_tooltips(self): + value = self.entry.get_text() + if value: + self.tooltips.enable() + self.tooltips.set_tip(self.button, value) + else: + self.tooltips.set_tip(self.button, '') + self.tooltips.disable() + + def _readonly_set(self, value): + super(URL, self)._readonly_set(value) + if value: + self.entry.hide() + else: + self.entry.show() + self.button.set_sensitive(True) + + def button_clicked(self, widget): + value = self.entry.get_text() + if value: + webbrowser.open(value, new=2) + + +class Email(URL): + "email" + + def button_clicked(self, widget): + value = self.entry.get_text() + if value: + webbrowser.open('mailto:%s' % value, new=2) + + def set_tooltips(self): + value = self.entry.get_text() + if value: + self.tooltips.enable() + self.tooltips.set_tip(self.button, 'mailto:%s' % value) + else: + self.tooltips.set_tip(self.button, '') + self.tooltips.disable() + + +class CallTo(URL): + "call to" + + def button_clicked(self, widget): + value = self.entry.get_text() + if value: + webbrowser.open('callto:%s' % value, new=2) + + def set_tooltips(self): + value = self.entry.get_text() + if value: + self.tooltips.enable() + self.tooltips.set_tip(self.button, 'callto:%s' % value) + else: + self.tooltips.set_tip(self.button, '') + self.tooltips.disable() + + +class SIP(URL): + "sip" + + def button_clicked(self, widget): + value = self.entry.get_text() + if value: + webbrowser.open('sip:%s' % value, new=2) + + def set_tooltips(self): + value = self.entry.get_text() + if value: + self.tooltips.enable() + self.tooltips.set_tip(self.button, 'sip:%s' % value) + else: + self.tooltips.set_tip(self.button, '') + self.tooltips.disable() + + +class HTML(Widget, TranslateMixin): + "HTML" + + def __init__(self, view, attrs): + super().__init__(view, attrs) + self.widget = Gtk.HBox() + self.mnemonic_widget = self.button = Gtk.LinkButton() + self.button.set_label(set_underline(attrs['string'])) + self.button.set_use_underline(True) + self.button.set_alignment(0, 0.5) + self.widget.pack_start( + self.button, expand=False, fill=False, padding=0) + + if attrs.get('translate'): + self.widget.pack_start( + self.translate_button(), expand=False, fill=False, padding=0) + + def uri(self, language=None): + if not self.record or self.record.id < 0 or CONNECTION.url is None: + uri = '' + else: + path = ['ir/html', self.model_name, str(self.record.id), + self.field_name] + params = { + 'language': language or CONFIG['client.lang'], + 'title': CONFIG['client.title'], + } + uri = urljoin( + CONNECTION.url + '/', '/'.join(path) + '?' + urlencode(params)) + return uri + + def display(self): + super().display() + self.button.set_uri(self.uri()) + + def _readonly_set(self, value): + super()._readonly_set(value) + for button in self.widget.get_children(): + button.set_sensitive(not value) + + def translate_dialog(self, languages): + languages = {l['name']: l['code'] for l in languages} + result = selection(_('Choose a language'), languages) + if result: + webbrowser.open(self.uri(language=result[1]), new=2) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/widget.py b/tryton/tryton/gui/window/view_form/view/form_gtk/widget.py new file mode 100644 index 0000000000..7f16fdb44b --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/widget.py @@ -0,0 +1,320 @@ +# 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 gettext + +from gi.repository import Gdk, GLib, Gtk + +import tryton.common as common +from tryton.common import RPCExecute, RPCException +from tryton.common import TRYTON_ICON +from tryton.common.underline import set_underline +from tryton.common.widget_style import widget_class +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal + +_ = gettext.gettext + + +class Widget(object): + expand = False + default_width_chars = 25 + + def __init__(self, view, attrs): + super(Widget, self).__init__() + self.view = view + self.attrs = attrs + self.widget = None + self.mnemonic_widget = None + self.visible = True + self._readonly = False + + @property + def field_name(self): + return self.attrs['name'] + + @property + def model_name(self): + return self.view.screen.model_name + + @property + def record(self): + return self.view.record + + @property + def field(self): + if self.record: + return self.record.group.fields[self.field_name] + return None + + def destroy(self): + pass + + def sig_activate(self, widget=None): + # emulate a focus_out so that the onchange is called if needed + self._focus_out() + + def _readonly_set(self, readonly): + self._readonly = readonly + + def _required_set(self, required): + pass + + def _invisible_widget(self): + return self.widget + + @property + def _invalid_widget(self): + return self.widget + + @property + def _required_widget(self): + return self.widget + + @property + def modified(self): + return False + + def send_modified(self, *args): + def send(value): + if not self.widget.props.window: + return + if self.record and self.get_value() == value: + self.record.signal('record-modified') + + def get_value(): + if not self.widget.props.window: + return + GLib.timeout_add(300, send, self.get_value()) + # Wait the current event is finished to retreive the value + GLib.idle_add(get_value) + return False + + def invisible_set(self, value): + widget = self._invisible_widget() + if value and value != '0': + self.visible = False + widget.hide() + else: + self.visible = True + widget.show() + + def _focus_out(self, *args): + if not self.field: + return False + if not self.visible: + return False + self.set_value() + + def display(self): + if not self.field: + self._readonly_set(self.attrs.get('readonly', True)) + self.invisible_set(self.attrs.get('invisible', False)) + self._required_set(False) + return + states = self.field.get_state_attrs(self.record) + readonly = self.attrs.get('readonly', states.get('readonly', False)) + if self.view.screen.readonly: + readonly = True + self._readonly_set(readonly) + widget_class(self.widget, 'readonly', readonly) + self._required_set(not readonly and states.get('required', False)) + widget_class( + self._required_widget, 'required', + not readonly and states.get('required', False)) + invalid = states.get('invalid', False) + widget_class(self._invalid_widget, 'invalid', not readonly and invalid) + self.invisible_set(self.attrs.get( + 'invisible', states.get('invisible', False))) + + def set_value(self): + pass + + +class TranslateDialog(NoModal): + + def __init__(self, widget, languages, readonly): + NoModal.__init__(self) + self.widget = widget + self.win = Gtk.Dialog( + title=_('Translation'), transient_for=self.parent, + destroy_with_parent=True) + Main().add_window(self.win) + self.win.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.win.set_icon(TRYTON_ICON) + self.win.connect('response', self.response) + self.win.set_default_size(*self.default_size()) + + self.accel_group = Gtk.AccelGroup() + self.win.add_accel_group(self.accel_group) + + cancel_button = self.win.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + cancel_button.set_image( + common.IconFactory.get_image('tryton-cancel', Gtk.IconSize.BUTTON)) + cancel_button.set_always_show_image(True) + ok_button = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + ok_button.set_image( + common.IconFactory.get_image('tryton-ok', Gtk.IconSize.BUTTON)) + ok_button.set_always_show_image(True) + ok_button.add_accelerator( + 'clicked', self.accel_group, Gdk.KEY_Return, + Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) + + tooltips = common.Tooltips() + + self.widgets = {} + grid = Gtk.Grid(column_spacing=3, row_spacing=3) + for i, language in enumerate(languages): + label = language['name'] + _(':') + label = Gtk.Label( + label=label, + halign=Gtk.Align.END, + valign=(Gtk.Align.START if self.widget.expand + else Gtk.Align.FILL)) + grid.attach(label, 0, i, 1, 1) + + context = dict( + language=language['code'], + fuzzy_translation=False, + ) + try: + value = RPCExecute('model', self.widget.record.model_name, + 'read', [self.widget.record.id], [self.widget.field_name], + context={'language': language['code']} + )[0][self.widget.field_name] + except RPCException: + return + context['fuzzy_translation'] = True + try: + fuzzy_value = RPCExecute('model', + self.widget.record.model_name, 'read', + [self.widget.record.id], [self.widget.field_name], + context=context)[0][self.widget.field_name] + except RPCException: + return + if fuzzy_value is None: + fuzzy_value = '' + widget = self.widget.translate_widget() + label.set_mnemonic_widget(widget) + self.widget.translate_widget_set(widget, fuzzy_value) + self.widget.translate_widget_set_readonly(widget, True) + widget.set_vexpand(self.widget.expand) + widget.set_hexpand(True) + grid.attach(widget, 1, i, 1, 1) + editing = Gtk.CheckButton() + editing.connect('toggled', self.editing_toggled, widget) + editing.props.sensitive = not readonly + tooltips.set_tip(editing, _('Edit')) + grid.attach(editing, 2, i, 1, 1) + fuzzy = Gtk.CheckButton() + fuzzy.set_active(value != fuzzy_value) + fuzzy.props.sensitive = False + tooltips.set_tip(fuzzy, _('Fuzzy')) + grid.attach(fuzzy, 4, i, 1, 1) + self.widgets[language['code']] = (widget, editing, fuzzy) + + tooltips.enable() + vbox = Gtk.VBox() + vbox.pack_start(grid, expand=self.widget.expand, fill=True, padding=0) + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + viewport.add(vbox) + scrolledwindow = Gtk.ScrolledWindow() + scrolledwindow.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + scrolledwindow.add(viewport) + self.win.vbox.pack_start( + scrolledwindow, expand=True, fill=True, padding=0) + self.win.show_all() + + self.register() + self.show() + + def editing_toggled(self, editing, widget): + self.widget.translate_widget_set_readonly(widget, + not editing.get_active()) + + def response(self, win, response): + if response == Gtk.ResponseType.OK: + for code, widget in self.widgets.items(): + widget, editing, fuzzy = widget + if not editing.get_active(): + continue + value = self.widget.translate_widget_get(widget) + context = dict( + language=code, + fuzzy_translation=False, + ) + try: + RPCExecute('model', self.widget.record.model_name, 'write', + [self.widget.record.id], { + self.widget.field_name: value, + }, context=context) + except RPCException: + pass + self.widget.record.cancel() + self.widget.view.display() + self.destroy() + + def destroy(self): + self.win.destroy() + NoModal.destroy(self) + + def show(self): + self.win.show() + + def hide(self): + self.win.hide() + + +class TranslateMixin: + + def translate_button(self): + button = Gtk.Button() + button.set_image(common.IconFactory.get_image( + 'tryton-translate', Gtk.IconSize.SMALL_TOOLBAR)) + button.set_relief(Gtk.ReliefStyle.NONE) + button.connect('clicked', self.translate) + return button + + def translate(self, *args): + self.view.set_value() + if self.record.id < 0 or self.record.modified: + common.message( + _('You need to save the record before adding translations.')) + return + + try: + lang_ids = RPCExecute('model', 'ir.lang', 'search', [ + ('translatable', '=', True), + ]) + except RPCException: + return + + if not lang_ids: + common.message(_('No other language available.')) + return + try: + languages = RPCExecute('model', 'ir.lang', 'read', lang_ids, + ['code', 'name']) + except RPCException: + return + + self.translate_dialog(languages) + + def translate_dialog(self, languages): + TranslateDialog(self, languages, self._readonly) + + def translate_widget(self): + raise NotImplementedError + + def translate_widget_set(self, widget, value): + raise NotImplementedError + + def translate_widget_get(self, widget): + raise NotImplementedError + + def translate_widget_set_readonly(self, widget, value): + raise NotImplementedError diff --git a/tryton/tryton/gui/window/view_form/view/graph.py b/tryton/tryton/gui/window/view_form/view/graph.py new file mode 100644 index 0000000000..b071807493 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph.py @@ -0,0 +1,193 @@ +# 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 sys + +import gettext + +from gi.repository import Gtk + +from . import View, XMLViewParser +from .graph_gtk.bar import VerticalBar, HorizontalBar +from .graph_gtk.line import Line +from .graph_gtk.pie import Pie +from tryton.common import file_selection, IconFactory +from tryton.common import node_attributes, get_toplevel_window, message +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON +from tryton.gui import Main + +_ = gettext.gettext + + +class GraphXMLViewParser(XMLViewParser): + + WIDGETS = { + 'hbar': HorizontalBar, + 'line': Line, + 'pie': Pie, + 'vbar': VerticalBar, + } + + def __init__(self, view, exclude_field, field_attrs): + super().__init__(view, exclude_field, field_attrs) + self._xfield = None + self._yfields = [] + + def _node_attributes(self, node): + node_attrs = node_attributes(node) + if 'name' in node_attrs: + if not node_attrs.get('string') and node_attrs['name'] != '#': + field = self.field_attrs[node_attrs['name']] + node_attrs['string'] = field['string'] + return node_attrs + + def _parse_graph(self, node, attributes): + for child in node.childNodes: + self.parse(child) + + Widget = self.WIDGETS.get(attributes.get('type', 'vbar')) + widget = Widget( + self.view, self._xfield, self._yfields) + self.view.widget.add(widget) + self.view.widgets['root'] = widget + + def _parse_x(self, node, attributes): + for child in node.childNodes: + if not child.nodeType == child.ELEMENT_NODE: + continue + self._xfield = self._node_attributes(child) + + def _parse_y(self, node, attributes): + for child in node.childNodes: + if not child.nodeType == child.ELEMENT_NODE: + continue + self._yfields.append(self._node_attributes(child)) + + +class ViewGraph(View): + view_type = 'graph' + editable = False + xml_parser = GraphXMLViewParser + + def __init__(self, view_id, screen, xml): + self.widget = event = Gtk.EventBox() + super().__init__(view_id, screen, xml) + event.connect('button-press-event', self.button_press) + + def __getitem__(self, name): + return None + + def destroy(self): + self.widget.destroy() + self.widgets['root'].destroy() + self.widgets.clear() + + def set_value(self): + pass + + def reset(self): + pass + + def display(self): + self.widgets['root'].display(self.screen.group) + return True + + def set_cursor(self, new=False, reset_view=True): + pass + + def get_fields(self): + return [] + + def get_buttons(self): + return [] + + def save(self, widget): + parent = get_toplevel_window() + dia = Gtk.Dialog( + title=_('Image Size'), transient_for=parent, modal=True, + destroy_with_parent=True) + Main().add_window(dia) + cancel_button = dia.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + cancel_button.set_image(IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + cancel_button.set_always_show_image(True) + ok_button = dia.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + ok_button.set_image(IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + ok_button.set_always_show_image(True) + dia.set_icon(TRYTON_ICON) + dia.set_default_response(Gtk.ResponseType.OK) + + hbox = Gtk.HBox(spacing=3) + dia.vbox.pack_start(hbox, expand=False, fill=True, padding=0) + + hbox.pack_start( + Gtk.Label(label=_('Width:')), expand=False, fill=True, padding=0) + spinwidth = Gtk.SpinButton() + spinwidth.configure(Gtk.Adjustment( + value=400.0, lower=0.0, upper=sys.maxsize, + step_increment=1.0, page_increment=10.0), + climb_rate=1, digits=0) + spinwidth.set_numeric(True) + spinwidth.set_activates_default(True) + hbox.pack_start(spinwidth, expand=True, fill=True, padding=0) + + hbox.pack_start( + Gtk.Label(label=_('Height:')), expand=False, fill=True, padding=0) + spinheight = Gtk.SpinButton() + spinheight.configure(Gtk.Adjustment( + value=200.0, lower=0.0, upper=sys.maxsize, + step_increment=1.0, page_increment=10.0), + climb_rate=1, digits=0) + spinheight.set_numeric(True) + spinheight.set_activates_default(True) + hbox.pack_start(spinheight, expand=True, fill=True, padding=0) + dia.show_all() + + filter = Gtk.FileFilter() + filter.set_name(_('PNG image (*.png)')) + filter.add_mime_type('image/png') + filter.add_pattern('*.png') + + while True: + response = dia.run() + width = spinwidth.get_value_as_int() + height = spinheight.get_value_as_int() + if response == Gtk.ResponseType.OK: + filename = file_selection( + _('Save As'), + action=Gtk.FileChooserAction.SAVE, + preview=False, + filters=[filter]) + if width and height and filename: + if not filename.endswith('.png'): + filename = filename + '.png' + try: + self.widgets['root'].export_png( + filename, width, height) + break + except MemoryError: + message( + _('Image size too large.'), dia, + Gtk.MessageType.ERROR) + else: + break + parent.present() + dia.destroy() + + def button_press(self, widget, event): + if event.button == 3: + menu = Gtk.Menu() + item = Gtk.MenuItem(label=_('Save As...')) + item.connect('activate', self.save) + item.show() + menu.append(item) + if hasattr(menu, 'popup_at_pointer'): + menu.popup_at_pointer(event) + else: + menu.popup(None, None, None, event.button, event.time) + return True + elif event.button == 1: + self.widgets['root'].action() diff --git a/tryton/tryton/gui/window/view_form/view/graph_gtk/__init__.py b/tryton/tryton/gui/window/view_form/view/graph_gtk/__init__.py new file mode 100644 index 0000000000..4effdfa4a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph_gtk/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/tryton/tryton/gui/window/view_form/view/graph_gtk/bar.py b/tryton/tryton/gui/window/view_form/view/graph_gtk/bar.py new file mode 100644 index 0000000000..0574734fb6 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph_gtk/bar.py @@ -0,0 +1,263 @@ +# 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 code is inspired by the pycha project +# (http://www.lorenzogil.com/projects/pycha/) +import locale +import math +import datetime + +import cairo + +from .graph import Graph +import tryton.common as common +import tryton.rpc as rpc + + +class Bar(Graph): + + def __init__(self, *args, **kwargs): + super(Bar, self).__init__(*args, **kwargs) + self.bars = [] + + def drawGraph(self, cr, width, height): + + def drawBar(bar): + cr.set_line_width(0.5) + + x = self.area.w * bar.x + self.area.x + y = self.area.h * bar.y + self.area.y + w = self.area.w * bar.w + h = self.area.h * bar.h + + if w < 1 or h < 1: + return # don't draw too small + + self.drawRectangle(cr, x, y, w, h) + r, g, b = self.colorScheme[bar.yname] + if bar.highlight: + r, g, b = common.highlight_rgb(r, g, b) + cr.set_source(self.sourceRectangle(x, y, w, h, r, g, b)) + cr.fill_preserve() + cr.stroke() + + cr.save() + for bar in self.bars: + drawBar(bar) + cr.restore() + + def drawRectangle(self, cr, x, y, w, h): + radius = 2.5 + cr.arc(x + radius, y + radius, radius, 0, 2 * math.pi) + cr.arc(x + w - radius, y + radius, radius, 0, 2 * math.pi) + cr.arc(x + w - radius, y + h - radius, radius, 0, 2 * math.pi) + cr.arc(x + radius, y + h - radius, radius, 0, 2 * math.pi) + cr.rectangle(x + radius, y, w - radius * 2, h) + cr.rectangle(x, y + radius, w, h - radius * 2) + + def sourceRectangle(self, x, y, w, h, r, g, b): + linear = cairo.LinearGradient((x + w) / 2, y, (x + w) / 2, y + h) + linear.add_color_stop_rgb(0, 3.5 * r / 5.0, 3.5 * g / 5.0, + 3.5 * b / 5.0) + linear.add_color_stop_rgb(1, r, g, b) + return linear + + def motion(self, widget, event): + super(Bar, self).motion(widget, event) + + if not getattr(self, 'area', None): + return + + def intersect(bar, event): + x = self.area.w * bar.x + self.area.x + y = self.area.h * bar.y + self.area.y + w = self.area.w * bar.w + h = self.area.h * bar.h + + if x <= event.x <= x + w and y <= event.y <= y + h: + return True + return False + + highlight = False + draw_bars = [] + yfields_timedelta = {x.get('key', x['name']): x.get('timedelta') + for x in self.yfields if 'timedelta' in x} + for bar in self.bars: + if intersect(bar, event): + if not bar.highlight: + bar.highlight = True + if bar.yname in yfields_timedelta: + converter = None + if yfields_timedelta[bar.yname]: + converter = rpc.CONTEXT.get( + yfields_timedelta[bar.yname]) + label = common.timedelta.format( + datetime.timedelta(seconds=bar.yval), converter) + else: + label = locale.localize( + '{:.2f}'.format(bar.yval), True) + label += '\n' + label += str(self.labels[bar.xname]) + self.popup.set_text(label) + draw_bars.append(bar) + else: + if bar.highlight: + bar.highlight = False + draw_bars.append(bar) + if bar.highlight: + highlight = True + + if highlight: + self.popup.show() + else: + self.popup.hide() + + if draw_bars: + minx = self.area.w + self.area.x + miny = self.area.h + self.area.y + maxx = maxy = 0.0 + for bar in draw_bars: + x = self.area.w * bar.x + self.area.x + y = self.area.h * bar.y + self.area.y + minx = int(min(x, minx)) + miny = int(min(y, miny)) + maxx = int(max(x + self.area.w * bar.w, maxx)) + maxy = int(max(y + self.area.h * bar.h, maxy)) + self.queue_draw_area(minx - 1, miny - 1, + maxx - minx + 2, maxy - miny + 2) + + def action(self): + super(Bar, self).action() + for bar in self.bars: + if bar.highlight: + ids = self.ids[bar.xname] + self.action_keyword(ids) + + +class VerticalBar(Bar): + 'Vertical Bar Graph' + + def updateGraph(self): + + barWidth = self.xscale * 0.9 + barMargin = self.xscale * (1.0 - 0.9) / 2 + + self.bars = [] + i = 0 + keys = list(self.datas.keys()) + keys.sort() + for xfield in keys: + j = 0 + barWidthForSet = barWidth / len(self.datas[xfield]) + for yfield in self._getDatasKeys(): + xval = i + yval = self.datas[xfield][yfield] + + x = (xval - self.minxval) * self.xscale + \ + barMargin + (j * barWidthForSet) + y = 1.0 - (yval - self.minyval) * self.yscale + w = barWidthForSet + h = yval * self.yscale + + if h < 0: + h = abs(h) + y -= h + + rect = Rect(x, y, w, h, xval, yval, xfield, yfield) + if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): + self.bars.append(rect) + + j += 1 + i += 1 + + def XLabels(self): + xlabels = super(VerticalBar, self).XLabels() + return [(x[0] + (self.xscale / 2), x[1]) for x in xlabels] + + def YLabels(self): + ylabels = super(VerticalBar, self).YLabels() + if all('timedelta' in f for f in self.yfields): + converter = {f.get('timedelta') for f in self.yfields} + if len(converter) == 1: + converter = rpc.CONTEXT.get(converter.pop()) + else: + converter = None + return [ + (x[0], common.timedelta.format( + datetime.timedelta(seconds=locale.atof(x[1])), + converter)) + for x in ylabels] + return ylabels + + +class HorizontalBar(Bar): + 'Horizontal Bar Graph' + + def updateGraph(self): + + barWidth = self.xscale * 0.9 + barMargin = self.xscale * (1.0 - 0.9) / 2 + + self.bars = [] + i = 0 + keys = list(self.datas.keys()) + keys.sort() + for xfield in keys: + j = 0 + barWidthForSet = barWidth / len(self.datas[xfield]) + for yfield in self._getDatasKeys(): + xval = i + yval = self.datas[xfield][yfield] + + x = - self.minyval * self.yscale + y = (xval - self.minxval) * self.xscale + \ + barMargin + (j * barWidthForSet) + w = yval * self.yscale + h = barWidthForSet + + if w < 0: + w = abs(w) + x -= w + + rect = Rect(x, y, w, h, xval, yval, xfield, yfield) + if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): + self.bars.append(rect) + + j += 1 + i += 1 + + def YLabels(self): + xlabels = super(HorizontalBar, self).XLabels() + return [(1 - (x[0] + (self.xscale / 2)), x[1]) for x in xlabels] + + def XLabels(self): + ylabels = super(HorizontalBar, self).YLabels() + if all('timedelta' in f for f in self.yfields): + converter = {f.get('timedelta') for f in self.yfields} + if len(converter) == 1: + converter = rpc.CONTEXT.get(converter.pop()) + else: + converter = None + return [ + (x[0], common.timedelta.format( + datetime.timedelta(seconds=locale.atof(x[1])), + converter)) + for x in ylabels] + return [(x[0], x[1]) for x in ylabels] + + def _getLegendPosition(self, width, height): + return self.area.x + self.area.w * 0.95 - width, \ + self.area.y + self.area.h * 0.05 + + def drawLines(self, cr, width, height): + for w, label in self.XLabels(): + self.drawLine(cr, w, 0) + + +class Rect(object): + + def __init__(self, x, y, w, h, xval, yval, xname, yname): + self.x, self.y, self.w, self.h = x, y, w, h + self.xval, self.yval = xval, yval + self.yname = yname + self.xname = xname + self.highlight = False diff --git a/tryton/tryton/gui/window/view_form/view/graph_gtk/graph.py b/tryton/tryton/gui/window/view_form/view/graph_gtk/graph.py new file mode 100644 index 0000000000..370286739a --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph_gtk/graph.py @@ -0,0 +1,481 @@ +# 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 code is inspired by the pycha project +# (http://www.lorenzogil.com/projects/pycha/) +import datetime +import locale +import math +from dateutil.relativedelta import relativedelta +from functools import reduce + +import cairo +from gi.repository import Gdk, Gtk + +import tryton.rpc as rpc +from tryton.action import Action +from tryton.common import hex2rgb, generateColorscheme, COLOR_SCHEMES +from tryton.config import CONFIG +from tryton.gui.window import Window +from tryton.pyson import PYSONDecoder + + +class Popup(object): + + def __init__(self, widget): + self.win = Gtk.Window(type=Gtk.WindowType.POPUP) + self.win.set_name('gtk-tooltips') + self.win.set_resizable(False) + self.win.set_border_width(1) + self.win.set_transient_for(widget.props.window) + self.label = Gtk.Label() + self.win.add(self.label) + self.win.connect('enter-notify-event', self.enter) + + def set_text(self, text): + self.label.set_text(text) + + def set_position(self, widget, x, y): + origin = widget.props.window.get_origin() + allocation = widget.get_allocation() + width, height = allocation.width, allocation.height + popup_width, popup_height = self.win.get_size() + if x < popup_width // 2: + x = popup_width // 2 + if x > width - popup_width // 2: + x = width - popup_width // 2 + pos_x = origin.x + x - popup_width // 2 + if pos_x < 0: + pos_x = 0 + if y < popup_height + 5: + y = popup_height + 5 + if y > height: + y = height + pos_y = origin.y + y - popup_height - 5 + if pos_y < 0: + pos_y = 0 + self.win.move(int(pos_x), int(pos_y)) + + def show(self): + self.win.show_all() + + def hide(self): + self.win.hide() + + def destroy(self): + self.win.destroy() + + def enter(self, widget, event): + self.win.hide() + + +class Graph(Gtk.DrawingArea): + 'Graph' + + __gsignals__ = {"draw": "override"} + + def __init__(self, view, xfield, yfields): + super(Graph, self).__init__() + self.view = view + self.xfield = xfield + self.yfields = yfields + self.datas = {} + self.topPadding = 15 + self.bottomPadding = 15 + self.rightPadding = 30 + self.leftPadding = 30 + self.set_events(Gdk.EventMask.POINTER_MOTION_MASK) + self.connect('motion-notify-event', self.motion) + self.connect('leave-notify-event', self.leave) + self.popup = Popup(self) + + @property + def attrs(self): + return self.view.attributes + + @property + def model(self): + return self.view.screen.model_name + + def destroy(self): + self.popup.destroy() + super(Graph, self).destroy() + + def motion(self, widget, event): + self.popup.set_position(self, event.x, event.y) + + def leave(self, widget, event): + self.popup.hide() + + def do_draw(self, cr): + width = self.get_allocated_width() + height = self.get_allocated_height() + + self.updateArea(cr, width, height) + self.drawBackground(cr, width, height) + self.drawLines(cr, width, height) + self.drawGraph(cr, width, height) + self.drawAxis(cr, width, height) + self.drawLegend(cr, width, height) + + def export_png(self, filename, width, height): + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + cx = cairo.Context(surface) + + self.updateArea(cx, width, height) + self.drawBackground(cx, width, height) + self.drawLines(cx, width, height) + self.drawGraph(cx, width, height) + self.drawAxis(cx, width, height) + self.drawLegend(cx, width, height) + surface.write_to_png(filename) + + self.queue_draw() + + def action(self): + self.popup.hide() + + def action_keyword(self, ids): + if not ids: + return + ctx = self.group._context.copy() + if 'active_ids' in ctx: + del ctx['active_ids'] + if 'active_id' in ctx: + del ctx['active_id'] + event = Gtk.get_current_event() + allow_similar = False + if (event.state & Gdk.ModifierType.CONTROL_MASK + or event.state & Gdk.ModifierType.MOD1_MASK): + allow_similar = True + with Window(hide_current=True, allow_similar=allow_similar): + return Action.exec_keyword('graph_open', { + 'model': self.model, + 'id': ids[0], + 'ids': ids, + }, context=ctx, warning=False) + + def drawBackground(self, cr, width, height): + # Fill the background + cr.save() + r, g, b = hex2rgb(self.attrs.get('background', '#d5d5d5')) + linear = cairo.LinearGradient(width // 2, 0, width // 2, height) + linear.add_color_stop_rgb(0, 1, 1, 1) + linear.add_color_stop_rgb(1, r, g, b) + cr.set_source(linear) + cr.rectangle(0, 0, width, height) + cr.fill() + cr.stroke() + cr.restore() + + def drawGraph(self, cr, width, height): + pass + + def YLabels(self): + ylabels = [] + if self.yrange == 0.0: + base = 1 + else: + base = 10 ** int(math.log(self.yrange, 10)) + for i in range(int(self.yrange / base) + 1): + val = int(self.minyval / base) * base + i * base + h = (val - self.minyval) * self.yscale + label = locale.localize('{:.2f}'.format(val), True) + ylabels.append((h, label)) + return ylabels + + def XLabels(self): + xlabels = [] + i = 0.0 + keys = list(self.datas.keys()) + keys.sort() + for key in keys: + if self.xrange == 0: + w = 1.0 + else: + w = i / self.xrange + xlabels.append((w, str(self.labels[key]))) + i += 1 + return xlabels + + def drawAxis(self, cr, width, height): + cr.set_source_rgb(*hex2rgb('#000000')) + cr.set_line_width(0.5) + + # Y axis + def drawYLabel(h, label): + x = self.area.x + y = self.area.y + self.area.h - h * self.area.h + + cr.new_path() + cr.move_to(x, y) + cr.line_to(x - 3.0, y) + cr.close_path() + cr.stroke() + + extends = cr.text_extents(label) + labelWidth = extends[2] + labelHeight = extends[3] + if labelWidth <= self.area.x: + cr.move_to(x - 3.0 - labelWidth - 5, y + labelHeight / 2.0) + cr.show_text(label) + + for h, label in self.YLabels(): + drawYLabel(h, label) + cr.new_path() + cr.move_to(self.area.x, self.area.y) + cr.line_to(self.area.x, self.area.y + self.area.h) + cr.close_path() + cr.stroke() + + # X axis + def drawXLabel(w, label): + x = self.area.x + w * self.area.w + y = self.area.y + self.area.h + + cr.new_path() + cr.move_to(x, y) + cr.line_to(x, y + 3.0) + cr.close_path() + cr.stroke() + + extends = cr.text_extents(label) + labelWidth = extends[2] + labelHeight = extends[3] + if labelWidth <= self.xscale * self.area.w: + cr.move_to(x - labelWidth / 2.0, y + labelHeight + 5) + cr.show_text(label) + + for w, label in self.XLabels(): + drawXLabel(w, label) + cr.new_path() + cr.move_to(self.area.x, self.area.y + self.area.h) + cr.line_to(self.area.x + self.area.w, self.area.y + self.area.h) + cr.close_path() + cr.stroke() + + def drawLines(self, cr, width, height): + for h, label in self.YLabels(): + self.drawLine(cr, 0, h) + + def drawLine(self, cr, x, y): + if x: + x1 = x2 = self.area.x + x * self.area.w + y1 = self.area.y + y2 = y1 + self.area.h + else: + y1 = y2 = self.area.y + self.area.h - y * self.area.h + x1 = self.area.x + x2 = x1 + self.area.w + + cr.save() + cr.set_source_rgb(*hex2rgb('#A0A0A0')) + cr.set_line_width(0.3) + cr.new_path() + cr.set_dash([5.0, 4.0]) + cr.move_to(x1, y1) + cr.line_to(x2, y2) + cr.close_path() + cr.stroke() + cr.restore() + + def drawLegend(self, cr, width, height): + if not int(self.attrs.get('legend', 1)): + return + + padding = 4 + bullet = 15 + width = 0 + height = padding + keys = self._getDatasKeys() + if not keys: + return + keys2txt = {} + for yfield in self.yfields: + keys2txt[yfield.get('key', yfield['name'])] = yfield['string'] + for key in keys: + extents = cr.text_extents(keys2txt[key]) + width = max(extents[2], width) + height += max(extents[3], bullet) + padding + width = padding + bullet + padding + width + padding + + pos_x, pos_y = self._getLegendPosition(width, height) + + cr.save() + cr.rectangle(pos_x, pos_y, width, height) + cr.set_source_rgba(1, 1, 1, 0.8) + cr.fill_preserve() + cr.set_line_width(0.5) + cr.set_source_rgb(*hex2rgb('#000000')) + cr.stroke() + + def drawKey(key, x, y, text_height): + cr.rectangle(x, y, bullet, bullet) + cr.set_source_rgb(*self.colorScheme[key]) + cr.fill_preserve() + cr.set_source_rgb(0, 0, 0) + cr.stroke() + cr.move_to(x + bullet + padding, + y + bullet / 2.0 + text_height / 2.0) + cr.show_text(keys2txt[key]) + + cr.set_line_width(0.5) + x = pos_x + padding + y = pos_y + padding + for key in keys: + extents = cr.text_extents(keys2txt[key]) + drawKey(key, x, y, extents[3]) + y += max(extents[3], bullet) + padding + + cr.restore() + + def _getLegendPosition(self, width, height): + return self.area.x + self.area.w * 0.05, \ + self.area.y + self.area.h * 0.05 + + def display(self, group): + self.updateDatas(group) + self.setColorScheme() + self.updateXY() + self.updateGraph() + self.queue_draw() + + def updateDatas(self, group): + self.datas = {} + self.labels = {} + self.ids = {} + self.group = group + minx = None + maxx = None + for model in group: + x = model[self.xfield['name']].get(model) + if not minx: + minx = x + if not maxx: + maxx = x + if minx is None and maxx is None: + if isinstance(x, datetime.datetime): + minx, maxx = datetime.datetime.min, datetime.datetime.max + elif isinstance(x, datetime.date): + minx, maxx = datetime.date.min, datetime.date.max + elif isinstance(x, datetime.timedelta): + minx, maxx = datetime.timedelta.min, datetime.timedelta.max + try: + minx = min(minx, x) + maxx = max(maxx, x) + except TypeError: + continue + self.labels[x] = model[self.xfield['name']].get_client(model) + self.ids.setdefault(x, []) + self.ids[x].append(model.id) + self.datas.setdefault(x, {}) + for yfield in self.yfields: + key = yfield.get('key', yfield['name']) + if yfield.get('domain'): + context = rpc.CONTEXT.copy() + context['context'] = context.copy() + context['_user'] = rpc._USER + for field in model.group.fields: + context[field] = model[field].get(model) + if not PYSONDecoder(context).decode(yfield['domain']): + continue + self.datas[x].setdefault(key, 0.0) + if yfield['name'] == '#': + self.datas[x][key] += 1 + else: + value = model[yfield['name']].get(model) + if isinstance(value, datetime.timedelta): + value = value.total_seconds() + self.datas[x][key] += float(value or 0) + date_format = self.view.screen.context.get('date_format', '%x') + datetime_format = date_format + ' %X' + if isinstance(minx, datetime.datetime): + date = minx + while date <= maxx: + self.labels[date] = date.strftime(datetime_format) + self.datas.setdefault(date, {}) + for yfield in self.yfields: + self.datas[date].setdefault( + yfield.get('key', yfield['name']), 0.0) + date += relativedelta(days=1) + elif isinstance(minx, datetime.date): + date = minx + while date <= maxx: + self.labels[date] = date.strftime(date_format) + self.datas.setdefault(date, {}) + for yfield in self.yfields: + self.datas[date].setdefault( + yfield.get('key', yfield['name']), 0.0) + date += relativedelta(days=1) + + def updateArea(self, cr, width, height): + maxylabel = '' + for value, label in self.YLabels(): + if len(maxylabel) < len(label): + maxylabel = label + extends = cr.text_extents(maxylabel) + yLabelWidth = extends[2] + + maxxlabel = '' + for value, label in self.XLabels(): + if len(maxxlabel) < len(label): + maxxlabel = label + extends = cr.text_extents(maxxlabel) + xLabelHeight = extends[3] + + if yLabelWidth > width / 3.0: + yLabelWidth = 0 + width = width - self.leftPadding - yLabelWidth - self.rightPadding + height = height - self.topPadding - self.bottomPadding - xLabelHeight + self.area = Area(self.leftPadding + yLabelWidth, self.topPadding, + width, height) + + def updateXY(self): + self.maxxval = len(self.datas) + self.minxval = 0.0 + + self.xrange = self.maxxval - self.minxval + if self.xrange == 0: + self.xscale = 1.0 + else: + self.xscale = 1.0 / self.xrange + + if not list(self.datas.values()): + self.maxyval = 0.0 + self.minyval = 0.0 + else: + self.maxyval = max([reduce(lambda x, y: max(x, y), x.values()) + for x in self.datas.values()]) + self.minyval = min([reduce(lambda x, y: min(x, y), x.values()) + for x in self.datas.values()]) + if self.minyval > 0: + self.minyval = 0.0 + + self.yrange = self.maxyval - self.minyval + if self.yrange == 0: + self.yscale = 1.0 + else: + self.yscale = 1.0 / self.yrange + + def updateGraph(self): + pass + + def setColorScheme(self): + keys = self._getDatasKeys() + color = self.attrs.get('color', CONFIG['graph.color']) + r, g, b = hex2rgb(COLOR_SCHEMES.get(color, color)) + maxcolor = max(max(r, g), b) + self.colorScheme = generateColorscheme(color, keys, + maxcolor / (len(keys) or 1)) + for yfield in self.yfields: + if yfield.get('color'): + self.colorScheme[yfield.get('key', yfield['name'])] = hex2rgb( + COLOR_SCHEMES.get(yfield['color'], yfield['color'])) + + def _getDatasKeys(self): + return [x.get('key', x['name']) for x in self.yfields] + + +class Area(object): + + def __init__(self, x, y, w, h): + self.x, self.y, self.w, self.h = x, y, w, h diff --git a/tryton/tryton/gui/window/view_form/view/graph_gtk/line.py b/tryton/tryton/gui/window/view_form/view/graph_gtk/line.py new file mode 100644 index 0000000000..f1ae48b852 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph_gtk/line.py @@ -0,0 +1,279 @@ +# 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 code is inspired by the pycha project +# (http://www.lorenzogil.com/projects/pycha/) +import locale +import math +import datetime + +import cairo + +from .graph import Graph +import tryton.common as common +import tryton.rpc as rpc + + +class Line(Graph): + + def updateGraph(self): + + yfield2attrs = {} + for yfield in self.yfields: + yfield2attrs[yfield.get('key', yfield['name'])] = yfield + + self.points = [] + i = 0 + keys = list(self.datas.keys()) + keys.sort() + for xfield in keys: + j = 0 + for yfield in self.datas[xfield]: + xval = i + yval = self.datas[xfield][yfield] + + x = (xval - self.minxval) * self.xscale + y = 1.0 - (yval - self.minyval) * self.yscale + + if self.xrange == 0: + x = 1.0 + + if (not bool(int(yfield2attrs[yfield].get('empty', 1))) + and yval == 0): + continue + + point = Point(x, y, xval, yval, xfield, yfield) + if (0.0 <= point.x <= 1.0) and (0.0 <= point.y <= 1.0): + self.points.append(point) + + j += 1 + i += 1 + + def drawGraph(self, cr, width, height): + key2fill = {} + key2interpolation = {} + for yfield in self.yfields: + key = yfield.get('key', yfield['name']) + key2fill[key] = bool(int(yfield.get('fill', 0))) + key2interpolation[key] = yfield.get('interpolation', 'linear') + + def preparePath(key): + interpolation = key2interpolation[key] + points = (p for p in self.points if p.yname == key) + zero = 1.0 + self.minyval * self.yscale + cr.new_path() + + cr.move_to(self.area.x, zero * self.area.h + self.area.y) + if interpolation == 'linear': + for point in points: + cr.line_to(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + else: + previous = Point(0, zero, None, None, None, None) + + def breakage(previous, point): + if interpolation == 'constant-center': + return previous.x + ((point.x - previous.x) / 2.0) + elif interpolation == 'constant-left': + return point.x + elif interpolation == 'constant-right': + return previous.x + for point in points: + cr.line_to( + breakage(previous, point) * self.area.w + self.area.x, + previous.y * self.area.h + self.area.y) + cr.line_to( + breakage(previous, point) * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + cr.line_to(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + previous = point + cr.line_to(breakage(previous, + Point(1, zero, None, None, None, None)) + * self.area.w + self.area.x, + previous.y * self.area.h + self.area.y) + cr.line_to(self.area.w + self.area.x, + zero * self.area.h + self.area.y) + cr.move_to(self.area.x, zero * self.area.h + self.area.y) + + if key2fill[key]: + cr.close_path() + else: + cr.set_source_rgb(*self.colorScheme[key]) + cr.stroke() + + cr.save() + cr.set_line_width(2) + + if self._getDatasKeys(): + transparency = 0.8 / len(self._getDatasKeys()) + for key in self._getDatasKeys(): + if key2fill[key]: + cr.save() + + r, g, b = self.colorScheme[key] + preparePath(key) + cr.set_source_rgb(r, g, b) + cr.stroke_preserve() + cr.restore() + + # Add soft transparency the area when line is filled + cr.set_source_rgba(r, g, b, transparency) + cr.fill() + + # Add gradient top to bottom + linear = cairo.LinearGradient( + width / 2, 0, width / 2, height) + linear.add_color_stop_rgba( + 0, r * 0.65, g * 0.65, b * 0.65, transparency) + linear.add_color_stop_rgba(1, r, g, b, 0.1) + cr.set_source(linear) + preparePath(key) + cr.fill() + else: + preparePath(key) + + for point in self.points: + cr.set_source_rgb(*self.colorScheme[point.yname]) + cr.move_to(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + cr.arc(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y, + 3, 0, 2 * math.pi) + cr.fill() + + cr.restore() + + def drawLegend(self, cr, widht, height): + super(Line, self).drawLegend(cr, widht, height) + cr.save() + for point in self.points: + if point.highlight: + cr.set_line_width(2) + cr.set_source_rgb(*common.hex2rgb('#000000')) + cr.move_to(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + cr.arc(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y, + 3, 0, 2 * math.pi) + cr.stroke() + cr.set_source_rgb(*common.highlight_rgb( + *self.colorScheme[point.yname])) + cr.arc(point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y, + 3, 0, 2 * math.pi) + cr.fill() + cr.restore() + + def motion(self, widget, event): + if not getattr(self, 'area', None): + return + + nearest = None + for point in self.points: + x = point.x * self.area.w + self.area.x + y = point.y * self.area.h + self.area.y + + square = (event.x - x) ** 2 + (event.y - y) ** 2 + + if not nearest or square < nearest[1]: + nearest = (point, square) + + dia = self.area.w ** 2 + self.area.h ** 2 + + keys2txt = {} + for yfield in self.yfields: + keys2txt[yfield.get('key', yfield['name'])] = yfield['string'] + + highlight = False + draw_points = [] + yfields_timedelta = {x.get('key', x['name']): x.get('timedelta') + for x in self.yfields if 'timedelta' in x} + for point in self.points: + if point == nearest[0] and nearest[1] < dia / 100: + if not point.highlight: + point.highlight = True + label = keys2txt[point.yname] + label += '\n' + if point.yval in yfields_timedelta: + converter = None + if yfields_timedelta[point.yname]: + converter = rpc.CONTEXT.get( + yfields_timedelta[point.yname]) + label += common.timedelta.format(point.yval, converter) + else: + label += locale.localize( + '{:.2f}'.format(point.yval), True) + label += '\n' + label += str(self.labels[point.xname]) + self.popup.set_text(label) + draw_points.append(point) + else: + if point.highlight: + point.highlight = False + draw_points.append(point) + if point.highlight: + self.popup.set_position(self, + point.x * self.area.w + self.area.x, + point.y * self.area.h + self.area.y) + highlight = True + if highlight: + self.popup.show() + else: + self.popup.hide() + + if draw_points: + minx = self.area.w + self.area.x + miny = self.area.h + self.area.y + maxx = maxy = 0.0 + for point in draw_points: + x = self.area.w * point.x + self.area.x + y = self.area.h * point.y + self.area.y + minx = min(x - 5, minx) + miny = min(y - 5, miny) + maxx = max(x + 5, maxx) + maxy = max(y + 5, maxy) + self.queue_draw_area(int(minx - 1), int(miny - 1), + int(maxx - minx + 1), int(maxy - miny + 1)) + + def updateXY(self): + super(Line, self).updateXY() + if self.xrange != 0: + self.xrange -= 1 + if self.xrange == 0: + self.xscale = 1.0 + else: + self.xscale = 1.0 / self.xrange + + def drawAxis(self, cr, width, height): + super(Line, self).drawAxis(cr, width, height) + self.drawLine(cr, 1.0, 0) + + def action(self): + super(Line, self).action() + for point in self.points: + if point.highlight: + ids = self.ids[point.xname] + self.action_keyword(ids) + + def YLabels(self): + ylabels = super(Line, self).YLabels() + if all('timedelta' in f for f in self.yfields): + converter = {f.get('timedelta') for f in self.yfields} + if len(converter) == 1: + converter = rpc.CONTEXT.get(converter.pop()) + return [ + (x[0], common.timedelta.format( + datetime.timedelta(seconds=locale.atof(x[1])), + converter)) + for x in ylabels] + return ylabels + + +class Point(object): + + def __init__(self, x, y, xval, yval, xname, yname): + self.x, self.y = x, y + self.xval, self.yval = xval, yval + self.xname = xname + self.yname = yname + self.highlight = False diff --git a/tryton/tryton/gui/window/view_form/view/graph_gtk/pie.py b/tryton/tryton/gui/window/view_form/view/graph_gtk/pie.py new file mode 100644 index 0000000000..467e0f6d71 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/graph_gtk/pie.py @@ -0,0 +1,210 @@ +# 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 code is inspired by the pycha project +# (http://www.lorenzogil.com/projects/pycha/) +import math +import locale +import datetime + +import cairo + +from .graph import Graph, Area +import tryton.common as common +import tryton.rpc as rpc + + +class Pie(Graph): + + def _getDatasKeys(self): + return list(self.datas.keys()) + + def drawLegend(self, cr, width, height): + pass + + def drawAxis(self, cr, width, height): + cr.set_source_rgb(*common.hex2rgb('#000000')) + + for slice in self.slices: + normalisedAngle = slice.normalisedAngle() + + labelx = self.centerx + \ + math.sin(normalisedAngle) * (self.radius + 10) + labely = self.centery - \ + math.cos(normalisedAngle) * (self.radius + 10) + + label = '%s (%s%%)' % (self.labels[slice.xname], + locale.localize('{:.2f}'.format(slice.fraction * 100))) + extents = cr.text_extents(label) + labelWidth = extents[2] + labelHeight = extents[3] + + x = y = 0 + + if normalisedAngle <= math.pi * 0.5: + x = labelx + y = labely - labelHeight + elif math.pi / 2 < normalisedAngle <= math.pi: + x = labelx + y = labely + elif math.pi < normalisedAngle <= math.pi * 1.5: + x = labelx - labelWidth + y = labely + else: + x = labelx - labelWidth + y = labely - labelHeight + + cr.move_to(x, y) + cr.show_text(label) + + def drawLines(self, cr, width, height): + pass + + def updateArea(self, cr, width, height): + width = width - self.leftPadding - self.rightPadding + height = height - self.topPadding - self.bottomPadding + self.area = Area(self.leftPadding, self.topPadding, width, height) + + self.centerx = self.area.x + self.area.w * 0.5 + self.centery = self.area.y + self.area.h * 0.5 + self.radius = min(self.area.w * 0.4, self.area.h * 0.4) + + def updateGraph(self): + + self.sum = 0.0 + for xkey in self.datas.keys(): + key = self.yfields[0].get('key', self.yfields[0]['name']) + if self.datas[xkey][key] > 0: + self.sum += self.datas[xkey][key] + + fraction = angle = 0.0 + + self.slices = [] + for xkey in self.datas.keys(): + key = self.yfields[0].get('key', self.yfields[0]['name']) + value = self.datas[xkey][key] + if value > 0: + angle += fraction + fraction = value / self.sum + slice = Slice(xkey, fraction, value, angle) + self.slices.append(slice) + + def drawGraph(self, cr, width, height): + cr.set_line_join(cairo.LINE_JOIN_ROUND) + + cr.save() + for slice in self.slices: + if slice.isBigEnough(): + if bool(int(self.yfields[0].get('fill', 1))): + color = self.colorScheme[slice.xname] + if slice.highlight: + color = common.highlight_rgb(*color) + cr.set_source_rgba(*color) + slice.draw(cr, self.centerx, self.centery, self.radius) + cr.fill() + cr.set_source_rgb(*common.hex2rgb( + self.attrs.get('background', '#f5f5f5'))) + slice.draw(cr, self.centerx, self.centery, self.radius) + cr.set_line_width(2) + cr.stroke() + cr.restore() + + def motion(self, widget, event): + super(Pie, self).motion(widget, event) + + if not getattr(self, 'area', None): + return + + d = (event.x - self.centerx) ** 2 + (event.y - self.centery) ** 2 + if d > self.radius ** 2: + self.popup.hide() + for slice in self.slices: + if slice.highlight: + self.queue_draw() + slice.highlight = False + return + + self.popup.show() + + if event.y == self.centery: + angle = math.pi / 2 + else: + angle = math.atan((event.x - self.centerx) + / (self.centery - event.y)) + if event.x >= self.centerx: + if event.y <= self.centery: + pass + else: + angle += math.pi + else: + if event.y < self.centery: + angle += 2 * math.pi + else: + angle += math.pi + + for slice in self.slices: + if slice.startAngle <= angle <= slice.endAngle: + if not slice.highlight: + slice.highlight = True + if 'timedelta' in self.yfields[0]: + converter = self.yfields[0].get('timedelta') + if converter: + converter = rpc.CONTEXT.get(converter) + value = common.timedelta.format( + datetime.timedelta( + seconds=slice.fraction * self.sum), + converter) + sum = common.timedelta.format( + datetime.timedelta(seconds=self.sum), converter) + else: + value = locale.localize( + '{:.2f}'.format(slice.fraction * self.sum)) + sum = locale.localize('{:.2f}'.format(self.sum)) + label = '%s (%s%%)\n%s/%s' % ( + self.labels[slice.xname], + locale.localize('{:.2f}'.format(slice.fraction * 100)), + value, sum) + self.popup.set_text(label) + self.queue_draw() + else: + if slice.highlight: + slice.highlight = False + self.queue_draw() + + def action(self): + super(Pie, self).action() + for slice in self.slices: + if slice.highlight: + ids = self.ids[slice.xname] + self.action_keyword(ids) + + +class Slice(object): + + def __init__(self, xname, fraction, value, angle): + self.xname = xname + self.fraction = fraction + self.value = value + self.startAngle = 2 * angle * math.pi + self.endAngle = 2 * (angle + fraction) * math.pi + self.highlight = False + + def isBigEnough(self): + return abs(self.startAngle - self.endAngle) > 0.001 + + def draw(self, cr, centerx, centery, radius): + cr.new_path() + cr.move_to(centerx, centery) + cr.arc(centerx, centery, radius, + self.startAngle - (math.pi / 2), + self.endAngle - (math.pi / 2)) + cr.line_to(centerx, centery) + cr.close_path() + + def normalisedAngle(self): + normalisedAngle = (self.startAngle + self.endAngle) / 2 + + if normalisedAngle > 2 * math.pi: + normalisedAngle -= 2 * math.pi + elif normalisedAngle < 0: + normalisedAngle += 2 * math.pi + return normalisedAngle diff --git a/tryton/tryton/gui/window/view_form/view/list.py b/tryton/tryton/gui/window/view_form/view/list.py new file mode 100644 index 0000000000..3aeb3b9827 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/list.py @@ -0,0 +1,1274 @@ +# 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 sys +import json +import locale +import gettext +from functools import wraps + +from gi.repository import Gdk, GLib, GObject, Gtk +from pygtkcompat.generictreemodel import GenericTreeModel + +from tryton.config import CONFIG +from tryton.common.cellrendererbutton import CellRendererButton +from tryton.gui.window import Window +from tryton.common.popup_menu import populate +from tryton.common import RPCExecute, RPCException, node_attributes, Tooltips +from tryton.common import domain_inversion, simplify, unique_value +from tryton.pyson import PYSONDecoder +import tryton.common as common +from . import View, XMLViewParser +from .list_gtk.editabletree import EditableTreeView, TreeView +from .list_gtk.widget import (Affix, Char, Text, Int, Boolean, URL, Date, + Time, Float, TimeDelta, Binary, M2O, O2O, O2M, M2M, Selection, + MultiSelection, Reference, Dict, ProgressBar, Button, Image) + +_ = gettext.gettext + + +def delay(func): + """Decorator for ViewTree method to delay execution when idle and if + display counter did not change""" + @wraps(func) + def wrapper(self, *args, **kwargs): + def wait(): + if not self.treeview.props.window: + return + if self.treeview.display_counter == display_counter: + func(self, *args, **kwargs) + display_counter = self.treeview.display_counter + GLib.idle_add(wait) + return wrapper + + +def path_convert_id2pos(model, id_path): + "This function will transform a path of id into a path of position" + group = model.group + id_path = id_path[:] + indexes = [] + while id_path: + current_id = id_path.pop(0) + try: + record = group.get(current_id) + indexes.append(group.index(record)) + group = record.children_group(model.children_field) + except (KeyError, AttributeError, ValueError): + return None + return tuple(indexes) + + +class AdaptModelGroup(GenericTreeModel): + + def __init__(self, group, children_field=None): + super(AdaptModelGroup, self).__init__() + self.group = group + self.set_property('leak_references', False) + self.children_field = children_field + self.__removed = None # XXX dirty hack to allow update of has_child + + def added(self, group, record): + if (group is self.group + and (record.group is self.group + or record.group.child_name == self.children_field)): + path = record.get_index_path(self.group) + iter_ = self.get_iter(path) + self.row_inserted(path, iter_) + if record.children_group(self.children_field): + self.row_has_child_toggled(path, iter_) + if (record.parent + and record.group is not self.group): + path = record.parent.get_index_path(self.group) + iter_ = self.get_iter(path) + self.row_has_child_toggled(path, iter_) + + def removed(self, group, record): + if (group is self.group + and (record.group is self.group + or record.group.child_name == self.children_field)): + path = record.get_index_path(self.group) + self.row_deleted(path) + + def append(self, model): + self.group.add(model) + + def prepend(self, model): + self.group.add(model, 0) + + def remove(self, iter_): + record = self.get_value(iter_, 0) + record.group.remove(record) + self.invalidate_iters() + + def __move(self, record, path, offset=0): + iter_ = self.get_iter(path) + record_pos = self.get_value(iter_, 0) + group = record_pos.group + pos = group.index(record_pos) + offset + if group is not record.group: + prev_group = record.group + record.group.remove(record, remove=True, force_remove=True) + # Don't remove record from previous group + # as the new parent will change the parent + # This prevents concurrency conflict + record.group.record_removed.remove(record) + group.add(record) + if not record.parent_name: + record.modified_fields.setdefault(prev_group.parent_name) + record.value[prev_group.parent_name] = None + else: + record.modified_fields.setdefault(record.parent_name) + group.move(record, pos) + + def move_before(self, record, path): + self.__move(record, path) + + def move_after(self, record, path): + self.__move(record, path, 1) + + def move_into(self, record, path): + iter_ = self.get_iter(path) + parent = self.get_value(iter_, 0) + group = parent.children_group(self.children_field) + if group is not record.group: + record.group.remove(record, remove=True, force_remove=True) + # Don't remove record from previous group + # as the new parent will change the parent + # This prevents concurrency conflict + if record in record.group.record_removed: + record.group.record_removed.remove(record) + group.add(record) + record.modified_fields.setdefault(record.parent_name or 'id') + group.move(record, 0) + + def sort(self, ids): + old_idx = {record.id: i for i, record in enumerate(self.group)} + new_idx = {id_: i for i, id_ in enumerate(ids)} + size = len(self.group) + self.group.sort(key=lambda r: new_idx.get(r.id, size)) + new_order = [] + prev = None + for record in self.group: + new_order.append(old_idx.get(record.id)) + if prev: + prev.next[id(self.group)] = record + prev = record + if prev: + prev.next[id(self.group)] = None + path = Gtk.TreePath() + # XXX pygobject does not allow to create empty TreePath, + # it is always a path of 0 + # see: https://bugzilla.gnome.org/show_bug.cgi?id=770665 + if hasattr(path, 'get_depth'): + while path.get_depth(): + path.up() + self.rows_reordered(path, None, new_order) + + def __len__(self): + return len(self.group) + + def on_get_flags(self): + if not self.children_field: + return Gtk.TreeModelFlags.LIST_ONLY + return 0 + + def on_get_n_columns(self): + # XXX + return 1 + + def on_get_column_type(self, index): + # XXX + return GObject.TYPE_PYOBJECT + + def on_get_path(self, record): + return record.get_index_path(self.group) + + def on_get_iter(self, path): + group = self.group + record = None + for i in path: + if group is None or i >= len(group): + return None + record = group[i] + if not self.children_field: + break + group = record.children_group(self.children_field) + return record + + def on_get_value(self, record, column): + return record + + def on_iter_next(self, record): + if record is None: + return None + return record.next.get(id(record.group)) + + def on_iter_has_child(self, record): + if record is None or not self.children_field: + return False + children = record.children_group(self.children_field) + if children is None: + return False + length = len(children) + if self.__removed and self.__removed in children: + length -= 1 + return bool(length) + + def on_iter_children(self, record): + if record is None: + if self.group: + return self.group[0] + else: + return None + if self.children_field: + children = record.children_group(self.children_field) + if children: + return children[0] + return None + + def on_iter_n_children(self, record): + if record is None: + return len(self.group) + if not self.children_field: + return 0 + return len(record.children_group(self.children_field)) + + def on_iter_nth_child(self, record, nth): + if record is None: + if nth < len(self.group): + return self.group[nth] + return None + if not self.children_field: + return None + if nth < len(record.children_group(self.children_field)): + return record.children_group(self.children_field)[nth] + return None + + def on_iter_parent(self, record): + if record is None: + return None + return record.parent + + +class TreeXMLViewParser(XMLViewParser): + + WIDGETS = { + 'biginteger': Int, + 'binary': Binary, + 'boolean': Boolean, + 'callto': URL, + 'char': Char, + 'date': Date, + 'dict': Dict, + 'email': URL, + 'float': Float, + 'image': Image, + 'integer': Int, + 'many2many': M2M, + 'many2one': M2O, + 'numeric': Float, + 'one2many': O2M, + 'one2one': O2O, + 'progressbar': ProgressBar, + 'reference': Reference, + 'selection': Selection, + 'multiselection': MultiSelection, + 'sip': URL, + 'text': Text, + 'time': Time, + 'timedelta': TimeDelta, + 'url': URL, + } + + def _parse_tree(self, node, attributes): + for child in node.childNodes: + self.parse(child) + + def _parse_field(self, node, attributes): + name = attributes['name'] + widget = self.WIDGETS[attributes['widget']](self.view, attributes) + self.view.widgets[name].append(widget) + + column = Gtk.TreeViewColumn(attributes['string']) + column._type = 'field' + column.name = name + + prefixes = [] + suffixes = list(widget.suffixes) + if attributes['widget'] in ['url', 'email', 'callto', 'sip']: + prefixes.append( + Affix(self.view, attributes, protocol=attributes['widget'])) + if 'icon' in attributes: + prefixes.append(Affix(self.view, attributes)) + + for affix in node.childNodes: + affix_attrs = node_attributes(affix) + if 'name' not in affix_attrs: + affix_attrs['name'] = attributes['name'] + if affix.tagName == 'prefix': + list_ = prefixes + else: + list_ = suffixes + list_.append(Affix(self.view, affix_attrs)) + prefixes.extend(widget.prefixes) + + for prefix in prefixes: + column.pack_start(prefix.renderer, expand=prefix.expand) + column.set_cell_data_func(prefix.renderer, prefix.setter) + column.pack_start(widget.renderer, expand=True) + column.set_cell_data_func(widget.renderer, widget.setter) + for suffix in suffixes: + column.pack_start(suffix.renderer, expand=suffix.expand) + column.set_cell_data_func(suffix.renderer, suffix.setter) + + self._set_column_widget(column, attributes, align=widget.align) + self._set_column_width(column, attributes) + + if (not self.view.attributes.get('sequence') + and not self.view.children_field + and self.field_attrs[name].get('sortable', True)): + column.connect('clicked', self.view.sort_model) + + self.view.treeview.append_column(column) + + if 'sum' in attributes: + text = attributes['sum'] + _(':') + label, sum_ = Gtk.Label(label=text), Gtk.Label() + + hbox = Gtk.HBox() + hbox.pack_start(label, expand=True, fill=False, padding=2) + hbox.pack_start(sum_, expand=True, fill=False, padding=2) + hbox.show_all() + self.view.sum_box.pack_start( + hbox, expand=False, fill=False, padding=0) + + self.view.sum_widgets.append((attributes['name'], sum_)) + + def _parse_button(self, node, attributes): + button = Button(self.view, attributes) + self.view.state_widgets.append(button) + + column = Gtk.TreeViewColumn( + attributes.get('string', ''), button.renderer) + column._type = 'button' + column.name = None + column.set_cell_data_func(button.renderer, button.setter) + + self._set_column_widget(column, attributes, arrow=False) + self._set_column_width(column, attributes) + + decoder = PYSONDecoder(self.view.screen.context) + column.set_visible( + not decoder.decode(attributes.get('tree_invisible', '0'))) + + self.view.treeview.append_column(column) + + def _set_column_widget(self, column, attributes, arrow=True, align=0.5): + hbox = Gtk.HBox(homogeneous=False, spacing=2) + label = Gtk.Label(label=attributes['string']) + field = self.field_attrs.get(attributes['name'], {}) + if field and self.view.editable: + required = field.get('required') + readonly = field.get('readonly') + common.apply_label_attributes(label, readonly, required) + label.show() + help = attributes.get('help') + if help: + tooltips = Tooltips() + tooltips.set_tip(label, help) + tooltips.enable() + if arrow: + arrow_widget = Gtk.Image() + arrow_widget.show() + column.arrow = arrow_widget + hbox.pack_start(label, expand=True, fill=True, padding=0) + if arrow: + hbox.pack_start(arrow_widget, expand=False, fill=False, padding=0) + column.set_clickable(True) + hbox.show() + column.set_widget(hbox) + column.set_alignment(align) + + def _set_column_width(self, column, attributes): + default_width = { + 'integer': 80, + 'biginteger': 80, + 'selection': 90, + 'reference': 200, + 'one2many': 50, + 'many2many': 50, + 'boolean': 30, + 'binary': 200, + } + + screen = self.view.screen + width = screen.tree_column_width[screen.model_name].get(column.name) + field_attrs = self.field_attrs.get(attributes['name'], {}) + if not width: + if 'width' in attributes: + width = int(attributes['width']) + elif field_attrs: + width = default_width.get(field_attrs['type'], 100) + if attributes.get('symbol'): + width += 20 + else: + width = 80 + column.width = width + if width > 0: + column.set_fixed_width(width) + column.set_min_width(1) + + expand = bool(attributes.get('expand', False)) + column.set_expand(expand) + column.set_resizable(True) + if attributes.get('widget') != 'text': + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + + +class ViewTree(View): + view_type = 'tree' + xml_parser = TreeXMLViewParser + draggable = False + + def __init__(self, view_id, screen, xml, children_field): + self.children_field = children_field + self.sum_widgets = [] + self.sum_box = Gtk.HBox() + self.treeview = None + self._editable = bool(int(xml.getAttribute('editable') or 0)) + if self._editable: + self.treeview = EditableTreeView(self) + grid_lines = Gtk.TreeViewGridLines.BOTH + else: + self.treeview = TreeView(self) + grid_lines = Gtk.TreeViewGridLines.VERTICAL + + super().__init__(view_id, screen, xml) + self.set_drag_and_drop() + + self.mnemonic_widget = self.treeview + + # Add last column if necessary + for column in self.treeview.get_columns(): + if column.get_expand(): + break + else: + column = Gtk.TreeViewColumn() + column._type = 'fill' + column.name = None + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + self.treeview.append_column(column) + + self.treeview.set_property('enable-grid-lines', grid_lines) + self.treeview.set_fixed_height_mode( + all(c.get_sizing() == Gtk.TreeViewColumnSizing.FIXED + for c in self.treeview.get_columns())) + self.treeview.connect('button-press-event', self.__button_press) + self.treeview.connect('key-press-event', self.on_keypress) + self.treeview.connect_after('row-activated', self.__sig_switch) + if self.children_field: + self.treeview.connect('test-expand-row', self.test_expand_row) + self.treeview.set_rubber_banding(True) + + selection = self.treeview.get_selection() + selection.set_mode(Gtk.SelectionMode.MULTIPLE) + selection.connect('changed', self.__select_changed) + + self.widget = Gtk.VBox() + self.scroll = scroll = Gtk.ScrolledWindow() + scroll.add(self.treeview) + scroll.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.set_placement(Gtk.CornerType.TOP_LEFT) + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + viewport.add(scroll) + self.widget.pack_start(viewport, expand=True, fill=True, padding=0) + + self.sum_box.show() + self.widget.pack_start( + self.sum_box, expand=False, fill=False, padding=0) + self.treeview.set_search_equal_func(self.search_equal_func) + + self.display() + + def get_column_widget(self, column): + 'Return the widget of the column' + idx = [c for c in self.treeview.get_columns() + if c.name == column.name].index(column) + return self.widgets[column.name][idx] + + def search_equal_func(self, model, column, key, iter_): + key = key.lower() + record = model.get_value(iter_, 0) + for field_name in self.get_fields(visible_only=True): + field = record[field_name] + field.state_set(record, states=('invisible',)) + if not field.get_state_attrs(record).get('invisible', False): + for widget in self.widgets[field_name]: + text = str(widget.get_textual_value(record)).lower() + # Simple search by word + while text: + if text.startswith(key): + return False + text = ' '.join(text.split()[1:]) + return True + + def sort_model(self, column): + up = common.IconFactory.get_pixbuf('tryton-arrow-up') + down = common.IconFactory.get_pixbuf('tryton-arrow-down') + for col in self.treeview.get_columns(): + if col != column and getattr(col, 'arrow', None): + col.arrow.clear() + self.screen.order = self.screen.default_order + if not column.arrow.props.pixbuf: + column.arrow.set_from_pixbuf(down) + self.screen.order = [(column.name, 'ASC')] + else: + if column.arrow.props.pixbuf == down: + column.arrow.set_from_pixbuf(up) + self.screen.order = [(column.name, 'DESC')] + else: + column.arrow.clear() + model = self.treeview.get_model() + unsaved_records = [x for x in model.group if x.id < 0] + search_string = self.screen.screen_container.get_text() or '' + if (self.screen.search_count == len(model) + or unsaved_records + or self.screen.parent): + ids = self.screen.search_filter( + search_string=search_string, only_ids=True) + model.sort(ids) + else: + self.screen.search_filter(search_string=search_string) + + def update_arrow(self): + order = self.screen.order + if order and len(order) == 1: + (name, direction), = order + if direction: + direction = direction.split(None, 1)[0] + direction = { + 'ASC': common.IconFactory.get_pixbuf('tryton-arrow-down'), + 'DESC': common.IconFactory.get_pixbuf('tryton-arrow-up'), + }[direction] + else: + name, direction = None, None + + for col in self.treeview.get_columns(): + arrow = getattr(col, 'arrow', None) + if arrow: + if col.name != name: + arrow.clear() + else: + if direction: + arrow.set_from_pixbuf(direction) + else: + arrow.clear() + + def set_drag_and_drop(self): + dnd = False + if self.children_field: + children = self.group.fields.get(self.children_field) + if children: + parent_name = children.attrs.get('relation_field') + dnd = parent_name in self.widgets + elif self.attributes.get('sequence'): + dnd = True + # Disable DnD on mac until it is fully supported + if sys.platform == 'darwin': + dnd = False + if self.screen.readonly: + dnd = False + + self.draggable = dnd + if not dnd: + return + + self.treeview.enable_model_drag_dest( + [('MY_TREE_MODEL_ROW', Gtk.TargetFlags.SAME_WIDGET, 0)], + Gdk.DragAction.MOVE) + self.treeview.enable_model_drag_source( + Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON3_MASK, + [('MY_TREE_MODEL_ROW', Gtk.TargetFlags.SAME_WIDGET, 0)], + Gdk.DragAction.MOVE) + # XXX have to set manually because enable_model_drag_source + # does not set the mask + # https://bugzilla.gnome.org/show_bug.cgi?id=756177 + self.treeview.drag_source_set( + Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON3_MASK, + [Gtk.TargetEntry.new( + 'MY_TREE_MODEL_ROW', Gtk.TargetFlags.SAME_WIDGET, 0)], + Gdk.DragAction.MOVE) + + self.treeview.connect("drag-data-get", self.drag_data_get) + self.treeview.connect('drag-data-received', + self.drag_data_received) + self.treeview.connect('drag-drop', self.drag_drop) + self.treeview.connect('drag-data-delete', self.drag_data_delete) + + drag_column = Gtk.TreeViewColumn() + drag_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + drag_column._type = 'drag' + drag_column.name = None + cell_pixbuf = Gtk.CellRendererPixbuf() + cell_pixbuf.props.pixbuf = common.IconFactory.get_pixbuf('tryton-drag') + drag_column.pack_start(cell_pixbuf, expand=False) + self.treeview.insert_column(drag_column, 0) + + @property + def modified(self): + return False + + @property + def editable(self): + return self._editable and not self.screen.readonly + + def get_fields(self, visible_only=False): + return [col.name for col in self.treeview.get_columns() + if col.name and (not visible_only or col.get_visible())] + + def get_buttons(self): + return [b for b in self.state_widgets + if isinstance(b.renderer, CellRendererButton)] + + def on_keypress(self, widget, event): + control_mask = Gdk.ModifierType.CONTROL_MASK + if sys.platform == 'darwin': + control_mask = Gdk.ModifierType.MOD2_MASK + if (event.keyval == Gdk.KEY_c + and event.state & control_mask): + self.on_copy() + return False + if (event.keyval == Gdk.KEY_v + and event.state & control_mask): + self.on_paste() + return False + + def test_expand_row(self, widget, iter_, path): + model = widget.get_model() + if model.iter_n_children(iter_) > CONFIG['client.limit']: + self.record = model.get_value(iter_, 0) + self.screen.switch_view('form') + return True + iter_ = model.iter_children(iter_) + if not iter_: + return False + fields = [col.name for col in self.treeview.get_columns() + if col.name] + while iter_: + record = model.get_value(iter_, 0) + if not record.get_loaded(fields): + for field in fields: + record[field] + if record.exception: + return True + iter_ = model.iter_next(iter_) + return False + + def on_copy(self): + for clipboard_type in [ + Gdk.SELECTION_CLIPBOARD, Gdk.SELECTION_PRIMARY]: + clipboard = self.treeview.get_clipboard(clipboard_type) + selection = self.treeview.get_selection() + data = [] + selection.selected_foreach(self.copy_foreach, data) + clipboard.set_text('\n'.join(data), -1) + + def copy_foreach(self, treemodel, path, iter, data): + record = treemodel.get_value(iter, 0) + values = [] + for col in self.treeview.get_columns(): + if not col.get_visible() or not col.name: + continue + widget = self.get_column_widget(col) + values.append('"' + + str(widget.get_textual_value(record)).replace('"', '""') + + '"') + data.append('\t'.join(values)) + return + + def on_paste(self): + if not self.editable: + return + + def unquote(value): + if value[:1] == '"' and value[-1:] == '"': + return value[1:-1] + return value + data = [] + for clipboard_type in [ + Gdk.SELECTION_CLIPBOARD, Gdk.SELECTION_PRIMARY]: + clipboard = self.treeview.get_clipboard(clipboard_type) + text = clipboard.wait_for_text() + if not text: + continue + data = [[unquote(v) for v in l.split('\t')] + for l in text.splitlines()] + break + col = self.treeview.get_cursor()[1] + columns = [c for c in self.treeview.get_columns() + if c.get_visible() and c.name] + if col in columns: + idx = columns.index(col) + columns = columns[idx:] + if self.record: + record = self.record + group = record.group + idx = group.index(record) + else: + group = self.group + idx = len(group) + default = None + for line in data: + if idx >= len(group): + record = group.new(default=False) + if default is None: + default = record.default_get() + record.set_default(default) + group.add(record) + record = group[idx] + for col, value in zip(columns, line): + widget = self.get_column_widget(col) + if widget.get_textual_value(record) != value: + widget.value_from_text(record, value) + if value and not widget.get_textual_value(record): + # Stop setting value if a value is correctly set + idx = len(group) + break + if not record.validate(): + break + idx += 1 + self.record = record + self.screen.display(set_cursor=True) + + def drag_data_get(self, treeview, context, selection, target_id, + etime): + treeview.stop_emission_by_name('drag-data-get') + + def _func_sel_get(model, path, iter_, data): + value = model.get_value(iter_, 0) + data.append(json.dumps( + value.get_path(model.group), separators=(',', ':'))) + data = [] + treeselection = treeview.get_selection() + treeselection.selected_foreach(_func_sel_get, data) + if not data: + return + selection.set(selection.get_target(), 8, data[0].encode('utf-8')) + return True + + def drag_data_received(self, treeview, context, x, y, selection, + info, etime): + treeview.stop_emission_by_name('drag-data-received') + if self.attributes.get('sequence'): + field = self.group.fields[self.attributes['sequence']] + for record in self.group: + if field.get_state_attrs( + record).get('readonly', False): + return + try: + selection_data = selection.data + except AttributeError: + selection_data = selection.get_data() + if not selection_data: + return + selection_data = selection_data.decode('utf-8') + + # Don't received if the treeview was editing because it breaks the + # internal state of the cursor. + cursor, column = treeview.get_cursor() + if column: + for renderer in column.get_cells(): + if renderer.props.editing: + return + + model = treeview.get_model() + try: + data = json.loads(selection_data) + except ValueError: + return + record = model.group.get_by_path(data) + record_path = record.get_index_path(model.group) + drop_info = treeview.get_dest_row_at_pos(x, y) + + def check_recursion(from_, to): + if not from_ or not to: + return True + if from_ == to: + return False + length = min(len(from_), len(to)) + if len(from_) < len(to) and from_[:length] == to[:length]: + return False + return True + if drop_info: + path, position = drop_info + check_path = tuple(path) + if position in [ + Gtk.TreeViewDropPosition.BEFORE, + Gtk.TreeViewDropPosition.AFTER]: + check_path = path[:-1] + if not check_recursion(record_path, check_path): + return + if position == Gtk.TreeViewDropPosition.BEFORE: + model.move_before(record, path) + elif position == Gtk.TreeViewDropPosition.AFTER: + model.move_after(record, path) + elif self.children_field: + model.move_into(record, path) + else: + model.move_after(record, (len(model) - 1,)) + Gdk.drop_finish(context, False, etime) + selection = self.treeview.get_selection() + selection.unselect_all() + selection.select_path(record.get_index_path(model.group)) + if self.attributes.get('sequence'): + record.group.set_sequence( + field=self.attributes['sequence'], + position=self.screen.new_position) + return True + + def drag_drop(self, treeview, context, x, y, time): + treeview.stop_emission_by_name('drag-drop') + targets = treeview.drag_dest_get_target_list() + target = treeview.drag_dest_find_target(context, targets) + treeview.drag_get_data(context, target, time) + return True + + def drag_data_delete(self, treeview, context): + treeview.stop_emission_by_name('drag-data-delete') + + def __button_press(self, treeview, event): + if event.button == 3: + try: + path, col, x, y = treeview.get_path_at_pos( + int(event.x), int(event.y)) + except TypeError: + # Outside row + return False + menu = Gtk.Menu() + copy_item = Gtk.MenuItem(label=_('Copy')) + copy_item.connect('activate', lambda x: self.on_copy()) + menu.append(copy_item) + if self.editable: + paste_item = Gtk.MenuItem(label=_('Paste')) + paste_item.connect('activate', lambda x: self.on_paste()) + menu.append(paste_item) + + def pop(menu, group, record): + # Don't activate actions if parent is modified + parent = record.parent if record else None + while parent: + if parent.modified: + break + parent = parent.parent + else: + populate(menu, group.model_name, record) + for col in self.treeview.get_columns(): + if not col.get_visible() or not col.name: + continue + field = group.fields[col.name] + model = None + if field.attrs['type'] == 'many2one': + model = field.attrs['relation'] + record_id = field.get(record) + elif field.attrs['type'] == 'reference': + value = field.get(record) + if value: + model, record_id = value.split(',') + record_id = int(record_id) + if not model: + continue + label = field.attrs['string'] + context = field.get_context(record) + populate( + menu, model, record_id, title=label, field=field, + context=context) + + selection = treeview.get_selection() + if selection.count_selected_rows() == 1: + group = self.group + if selection.get_mode() == Gtk.SelectionMode.SINGLE: + model = selection.get_selected()[0] + elif selection.get_mode() == Gtk.SelectionMode.MULTIPLE: + model = selection.get_selected_rows()[0] + record = model.get_value(model.get_iter(path), 0) + pop(menu, group, record) + menu.show_all() + if hasattr(menu, 'popup_at_pointer'): + menu.popup_at_pointer(event) + else: + menu.popup(None, None, None, event.button, event.time) + return True # Don't change the selection + elif event.button == 2: + with Window(allow_similar=True): + self.screen.row_activate() + return True + return False + + def group_list_changed(self, group, signal): + model = self.treeview.get_model() + if model is not None: + if signal[0] == 'record-added': + model.added(group, signal[1]) + elif signal[0] == 'record-removed': + model.removed(group, signal[1]) + self.display() + + def __str__(self): + return 'ViewList (%d)' % id(self) + + def __getitem__(self, name): + return None + + def save_width(self): + if not CONFIG['client.save_tree_width']: + return + fields = {} + last_col = None + for col in self.treeview.get_columns(): + if col.get_visible(): + last_col = col + if not hasattr(col, 'name') or not hasattr(col, 'width'): + continue + if (col.get_width() != col.width and col.get_visible() + and not col.get_expand()): + fields[col.name] = col.get_width() + # Don't set width for last visible columns + # as it depends of the screen size + if last_col and last_col.name in fields: + del fields[last_col.name] + + if fields and any(fields.values()): + model_name = self.screen.model_name + try: + RPCExecute('model', 'ir.ui.view_tree_width', 'set_width', + model_name, fields) + except RPCException: + pass + self.screen.tree_column_width[model_name].update(fields) + + def destroy(self): + self.save_width() + self.treeview.destroy() + + def __sig_switch(self, treeview, path, column): + if column._type == 'button': + return + allow_similar = False + event = Gtk.get_current_event() + if (event.state & Gdk.ModifierType.MOD1_MASK + or event.state & Gdk.ModifierType.SHIFT_MASK): + allow_similar = True + with Window(allow_similar=allow_similar): + if not self.screen.row_activate() and self.children_field: + if treeview.row_expanded(path): + treeview.collapse_row(path) + else: + treeview.expand_row(path, False) + + def __select_changed(self, tree_sel): + previous_record = self.record + if previous_record and previous_record not in previous_record.group: + previous_record = None + + if tree_sel.get_mode() == Gtk.SelectionMode.SINGLE: + model, iter_ = tree_sel.get_selected() + if model and iter_: + record = model.get_value(iter_, 0) + self.record = record + else: + self.record = None + + elif tree_sel.get_mode() == Gtk.SelectionMode.MULTIPLE: + model, paths = tree_sel.get_selected_rows() + if model and paths: + iter_ = model.get_iter(paths[0]) + record = model.get_value(iter_, 0) + self.record = record + else: + self.record = None + + if self.editable and previous_record: + def go_previous(): + self.record = previous_record + self.set_cursor() + if not self.screen.parent and previous_record != self.record: + + def save(): + if not previous_record.destroyed: + if not previous_record.save(): + go_previous() + + if not previous_record.validate(self.get_fields()): + go_previous() + return True + # Delay the save to let GTK process the current event + GLib.idle_add(save) + elif previous_record != self.record and self.screen.pre_validate: + + def pre_validate(): + if not previous_record.destroyed: + if not previous_record.pre_validate(): + go_previous() + # Delay the pre_validate to let GTK process the current event + GLib.idle_add(pre_validate) + self.update_sum() + + def set_value(self): + if self.editable: + self.treeview.set_value() + + def reset(self): + pass + + def display(self, force=False): + self.treeview.display_counter += 1 + current_record = self.record + if (force + or not self.treeview.get_model() + or self.group != self.treeview.get_model().group): + model = AdaptModelGroup(self.group, self.children_field) + self.treeview.set_model(model) + # __select_changed resets current_record to None + self.record = current_record + if current_record: + selection = self.treeview.get_selection() + path = current_record.get_index_path(model.group) + selection.select_path(path) + # The search column must be set each time the model is changed + self.treeview.set_search_column(0) + if not current_record: + selection = self.treeview.get_selection() + selection.unselect_all() + self.treeview.queue_draw() + if self.editable: + self.set_state() + self.update_arrow() + self.update_sum() + + # Set column visibility depending on attributes and domain + domain = [] + if self.screen.domain: + domain.append(self.screen.domain) + tab_domain = self.screen.screen_container.get_tab_domain() + if tab_domain: + domain.append(tab_domain) + domain = simplify(domain) + decoder = PYSONDecoder(self.screen.context) + for column in self.treeview.get_columns(): + name = column.name + if not name: + continue + widget = self.get_column_widget(column) + widget.set_editable() + if decoder.decode(widget.attrs.get('tree_invisible', '0')): + column.set_visible(False) + elif name == self.screen.exclude_field: + column.set_visible(False) + else: + inv_domain = domain_inversion(domain, name) + if not isinstance(inv_domain, bool): + inv_domain = simplify(inv_domain) + unique, _, _ = unique_value(inv_domain) + column.set_visible(not unique or bool(self.children_field)) + if self.children_field: + for i, column in enumerate(self.treeview.get_columns()): + if self.draggable and not i: + continue + if column.get_visible(): + self.treeview.set_expander_column(column) + break + + def set_state(self): + record = self.record + if record: + for field in record.group.fields: + field = record.group.fields.get(field, None) + if field: + field.state_set(record) + + @delay + def update_sum(self): + selected_records = self.selected_records + for name, label in self.sum_widgets: + sum_ = None + selected_sum = None + loaded = True + digit = 0 + field = self.group.fields[name] + for record in self.group: + if not record.get_loaded([name]) and record.id >= 0: + loaded = False + break + value = field.get(record) + if value is not None: + if sum_ is None: + sum_ = value + else: + sum_ += value + if record in selected_records or not selected_records: + if selected_sum is None: + selected_sum = value + else: + selected_sum += value + if hasattr(field, 'digits'): + fdigits = field.digits(record) + if fdigits and digit is not None: + digit = max(fdigits[1], digit) + else: + digit = None + + if loaded: + if field.attrs['type'] == 'timedelta': + converter = field.converter(self.group) + selected_sum = common.timedelta.format( + selected_sum, converter) + sum_ = common.timedelta.format(sum_, converter) + elif digit is not None: + selected_sum = locale.localize( + '{0:.{1}f}'.format(selected_sum or 0, digit), True) + sum_ = locale.localize( + '{0:.{1}f}'.format(sum_ or 0, digit), True) + else: + selected_sum = locale.localize( + '{}'.format(selected_sum or 0), True) + sum_ = locale.localize('{}'.format(sum_ or 0), True) + + text = '%s / %s' % (selected_sum, sum_) + else: + text = '-' + label.set_text(text) + + def set_cursor(self, new=False, reset_view=True): + self.treeview.grab_focus() + model = self.treeview.get_model() + if self.record and model and self.treeview.get_realized(): + path = self.record.get_index_path(model.group) + if model.get_flags() & Gtk.TreeModelFlags.LIST_ONLY: + path = (path[0],) + focus_column, focus_cell = self.treeview.next_column( + path, editable=new) + if path[:-1]: + self.treeview.expand_to_path(Gtk.TreePath(path[:-1])) + self.treeview.scroll_to_cell(path, focus_column, + use_align=False) + current_path = self.treeview.get_cursor()[0] + selected_path = \ + self.treeview.get_selection().get_selected_rows()[1] + path = Gtk.TreePath(path) + if (current_path != path and path not in selected_path) or new: + self.treeview.set_cursor(path, focus_column, start_editing=new) + + @property + def selected_records(self): + def _func_sel_get(model, path, iter_, records): + records.append(model.get_value(iter_, 0)) + records = [] + sel = self.treeview.get_selection() + sel.selected_foreach(_func_sel_get, records) + return records + + def get_selected_paths(self): + selection = self.treeview.get_selection() + model, rows = selection.get_selected_rows() + id_paths = [] + for row in rows: + path = () + id_path = [] + for node in row: + path += (node,) + iter_ = model.get_iter(path) + id_path.append(model.get_value(iter_, 0).id) + id_paths.append(id_path) + return id_paths + + @property + def listed_records(self): + model = self.treeview.get_model() + if not self.children_field: + return list(model.group) + + def get_listed_records(start): + records = [] + if start: + iter_ = model.get_iter(Gtk.TreePath(start)) + else: + iter_ = None + for idx in range(model.iter_n_children(iter_)): + path = start + (idx,) + iter_ = model.get_iter(path) + record = model.get_value(iter_, 0) + records.append(record) + if self.treeview.row_expanded(Gtk.TreePath(path)): + records += get_listed_records(path) + return records + return get_listed_records(()) + + def get_listed_paths(self): + model = self.treeview.get_model() + if not self.children_field: + return [[r.id] for r in model.group] + + def get_listed_paths(start=None, start_path=None): + paths = [] + if start: + iter_ = model.get_iter(Gtk.TreePath(start)) + else: + iter_ = None + for idx in range(model.iter_n_children(iter_)): + path = start + (idx,) + iter_ = model.get_iter(path) + record = model.get_value(iter_, 0) + id_path = start_path + [record.id] + paths.append(id_path) + if self.treeview.row_expanded(Gtk.TreePath(path)): + paths += get_listed_paths(path, id_path) + return paths + return get_listed_paths((), []) + + def select_nodes(self, nodes): + selection = self.treeview.get_selection() + if not nodes: + return + selection.unselect_all() + scroll = False + model = self.treeview.get_model() + for node in nodes: + path = path_convert_id2pos(model, node) + if path: + selection.select_path(Gtk.TreePath(path)) + if not scroll: + self.treeview.scroll_to_cell(path) + scroll = True + + def get_expanded_paths(self, starting_path=None, starting_id_path=None): + # Use id instead of position + # because the position may change between load + if not starting_path: + starting_path = tuple() + if not starting_id_path: + starting_id_path = [] + id_paths = [] + model = self.treeview.get_model() + if starting_path: + iter_ = model.get_iter(Gtk.TreePath(starting_path)) + else: + iter_ = None + for path_idx in range(model.iter_n_children(iter_)): + path = starting_path + (path_idx,) + expanded = self.treeview.row_expanded(Gtk.TreePath(path)) + if expanded: + iter_ = model.get_iter(path) + expanded_record = model.get_value(iter_, 0) + id_path = starting_id_path + [expanded_record.id] + id_paths.append(id_path) + child_id_paths = self.get_expanded_paths(path, id_path) + id_paths += child_id_paths + return id_paths + + def expand_nodes(self, nodes): + model = self.treeview.get_model() + for node in nodes: + expand_path = path_convert_id2pos(model, node) + if expand_path: + self.treeview.expand_to_path(Gtk.TreePath(expand_path)) diff --git a/tryton/tryton/gui/window/view_form/view/list_form.py b/tryton/tryton/gui/window/view_form/view/list_form.py new file mode 100644 index 0000000000..856999d9a3 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/list_form.py @@ -0,0 +1,151 @@ +# 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 gi.repository import GObject, Gio, Gtk + +from tryton.common import common +from . import View +from .form import ViewForm + + +class ListBoxViewForm(ViewForm): + + def __init__(self, view_id, screen, xml): + self._record = None + super().__init__(view_id, screen, xml) + self.scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + + @property + def record(self): + return self._record + + @record.setter + def record(self, value): + self._record = value + + +class ListBoxItem(GObject.Object): + + def __init__(self, record): + super().__init__() + self.record = record + + +class ListBoxModel(GObject.Object, Gio.ListModel): + + def __init__(self, group): + super().__init__() + self.group = group + self._records = {} + + def do_get_item(self, position): + if position >= len(self.group): + return None + record = self.group[position] + if record.id not in self._records: + self._records[record.id] = ListBoxItem(record) + return self._records[record.id] + + def do_get_item_type(self): + return ListBoxItem + + def do_get_n_items(self): + return len(self.group) + + +class ViewListForm(View): + editable = True + xml_parser = None + + def __init__(self, view_id, screen, xml): + super().__init__(view_id, screen, xml) + self.view_type = 'list-form' + + self.form_xml = xml + self.listbox = Gtk.ListBox.new() + self.listbox.connect('row-selected', self._row_selected) + self.listbox.props.activate_on_single_click = False + self.listbox.set_selection_mode(Gtk.SelectionMode.MULTIPLE) + self.widget = Gtk.ScrolledWindow.new() + self.widget.add_with_viewport(self.listbox) + self._model = None + self._view_forms = [] + + def display(self): + if self._model is None or self._model.group is not self.group: + self._view_forms = [] + self._model = ListBoxModel(self.group) + self.listbox.bind_model(self._model, self._create_form) + for idx, view_form in enumerate(self._view_forms): + view_form.display() + + def _create_form(self, item): + view_form = ListBoxViewForm(self.view_id, self.screen, self.form_xml) + view_form.record = item.record + view_form.widget.props.margin = 3 + self._view_forms.append(view_form) + frame = Gtk.Frame.new() + frame.add(view_form.widget) + frame.show_all() + return frame + + def set_value(self): + for view_form in self._view_forms: + view_form.set_value() + + def get_fields(self): + if self._view_forms: + return self._view_forms[0].get_fields() + return [] + + def get_buttons(self): + if self._view_forms: + return self._view_forms[0].get_buttons() + return [] + + def destroy(self): + for view_form in self._view_forms: + view_form.destroy() + self.widget.destroy() + + @property + def selected_records(self): + selected_rows = self.listbox.get_selected_rows() + return [ + self._model.get_item(r.get_index()).record for r in selected_rows] + + def group_list_changed(self, group, signal): + action = signal[0] + if action == 'record-added': + position = signal[2] + self._model.emit('items-changed', position, 0, 1) + self._view_forms.insert(position, self._view_forms.pop()) + elif action == 'record-removed': + position = signal[2] + self._model.emit('items-changed', position, 1, 0) + self._view_forms.pop(position) + + def set_cursor(self, new=False, reset_view=True): + for idx, form in enumerate(self._view_forms): + if form.record == self.record: + self._select_show_row(idx) + break + + def _row_selected(self, listbox, row): + if not row: + return + self.record = self._model.get_item(row.get_index()).record + + @common.idle_add + def _select_show_row(self, index): + # translate_coordinates requires that both widgets are realized + if not self.listbox.get_realized(): + return + self.listbox.unselect_all() + row = self.listbox.get_row_at_index(index) + if not row or not row.get_realized(): + return + self.listbox.select_row(row) + y_position = row.translate_coordinates(self.listbox, 0, 0)[1] + y_size = row.get_allocated_height() + vadjustment = self.widget.get_vadjustment() + vadjustment.clamp_page(y_position, y_position + y_size) diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/__init__.py b/tryton/tryton/gui/window/view_form/view/list_gtk/__init__.py new file mode 100644 index 0000000000..4effdfa4a2 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py b/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py new file mode 100644 index 0000000000..f27be9d7c5 --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py @@ -0,0 +1,329 @@ +# 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 gettext +from itertools import islice, cycle, chain + +from gi.repository import Gdk, GLib, Gtk + +from tryton.common import MODELACCESS +from tryton.common.datetime_ import Date, Time + +_ = gettext.gettext + + +def focusable_cells(column, editable=True): + for cell in column.get_cells(): + if (not editable + or (isinstance(cell, ( + Gtk.CellRendererText, + Gtk.CellRendererCombo)) + and cell.get_property('editable')) + or (isinstance(cell, Gtk.CellRendererToggle) + and cell.get_property('activatable'))): + yield cell + + +class TreeView(Gtk.TreeView): + display_counter = 0 + + def __init__(self, view): + super(TreeView, self).__init__() + self.view = view + + def next_column( + self, path, column=None, cell=None, editable=True, _sign=1): + if cell: + cells = list(focusable_cells(column, editable)) + if len(cells) > 1: + idx = cells.index(cell) + _sign + if 0 <= idx < len(cells): + return (column, cells[idx]) + columns = self.get_columns() + if column is None: + column = columns[-1 if _sign > 0 else 0] + model = self.get_model() + record = model.get_value(model.get_iter(path), 0) + if _sign < 0: + columns.reverse() + current_idx = columns.index(column) + 1 + for column in islice(cycle(columns), current_idx, + len(columns) + current_idx): + if not column.name: + continue + widget = self.view.get_column_widget(column) + field = record[column.name] + field.state_set(record, states=('readonly', 'invisible')) + invisible = field.get_state_attrs(record).get('invisible', False) + if not column.get_visible(): + invisible = True + if editable: + readonly = widget.attrs.get('readonly', + field.get_state_attrs(record).get('readonly', False)) + else: + readonly = False + if not (invisible or readonly): + cells = list(focusable_cells(column, editable)) + if cells: + cell = cells[0 if _sign > 0 else -1] + else: + continue + return (column, cell) + return (None, None) + + def prev_column(self, path, column=None, cell=None, editable=True): + return self.next_column( + path, column=column, cell=cell, editable=editable, _sign=-1) + + +class EditableTreeView(TreeView): + leaving_record_events = ( + Gdk.KEY_Up, Gdk.KEY_Down, Gdk.KEY_Return) + leaving_events = leaving_record_events + ( + Gdk.KEY_Tab, Gdk.KEY_ISO_Left_Tab, Gdk.KEY_KP_Enter) + + def on_quit_cell( + self, current_record, column, renderer, value, callback=None): + field = current_record[column.name] + widget = self.view.get_column_widget(column) + + # The value has not changed and is valid ... do nothing. + if value == widget.get_textual_value(current_record) \ + and field.validate(current_record): + if callback: + callback() + return + if widget.renderer != renderer: + for widget in chain(widget.prefixes, widget.suffixes): + if widget.renderer == renderer: + break + else: + raise ValueError("Unknown renderer") + widget.value_from_text(current_record, value, callback=callback) + + def on_open_remote(self, current_record, column, create, value, + entry=None, callback=None): + widget = self.view.get_column_widget(column) + if value != widget.get_textual_value(current_record) or not value: + changed = True + else: + changed = False + try: + widget.open_remote(current_record, create, changed, value, + callback=callback) + except NotImplementedError: + pass + + def on_create_line(self): + access = MODELACCESS[self.view.screen.model_name] + model = self.get_model() + limit = (self.view.screen.size_limit is not None + and (len(model) >= self.view.screen.size_limit >= 0)) + if not access['create'] or limit: + return + if self.view.screen.new_position == 0: + method = model.prepend + else: + method = model.append + new_record = model.group.new() + res = method(new_record) + sequence = self.view.attributes.get('sequence') + if sequence: + model.group.set_sequence( + field=sequence, position=self.view.screen.new_position) + return res + + def set_cursor( + self, path, focus_column=None, cell=None, start_editing=False): + self.grab_focus() + if focus_column: + widget = self.view.get_column_widget(focus_column) + if isinstance(widget.renderer, Gtk.CellRendererToggle): + start_editing = False + self.scroll_to_cell(path, focus_column, use_align=False) + if cell: + self.set_cursor_on_cell(path, focus_column, cell, start_editing) + else: + super(EditableTreeView, self).set_cursor( + path, focus_column, start_editing) + + def set_value(self): + path, column = self.get_cursor() + if not path or not column or not column.name: + return True + for renderer in column.get_cells(): + if renderer.props.editing: + widget = self.view.get_column_widget(column) + self.on_editing_done(widget.editable, renderer) + return True + + def on_keypressed(self, entry, event, renderer): + path = self.get_cursor()[0] + column = self.get_column_from_renderer(renderer) + model = self.get_model() + record = model.get_value(model.get_iter(path), 0) + self.display_counter += 1 # Force a display + + leaving = False + if event.keyval == Gdk.KEY_Right: + if isinstance(entry, Gtk.Entry): + if entry.get_position() >= \ + len(entry.get_text()) \ + and not entry.get_selection_bounds(): + leaving = True + else: + leaving = True + elif event.keyval == Gdk.KEY_Left: + if isinstance(entry, Gtk.Entry): + if entry.get_position() <= 0 \ + and not entry.get_selection_bounds(): + leaving = True + else: + leaving = True + + if event.keyval in self.leaving_events or leaving: + if isinstance(entry, (Date, Time)): + entry.activate() + txt = entry.props.value + elif isinstance(entry, Gtk.Entry): + txt = entry.get_text() + elif isinstance(entry, Gtk.ComboBox): + active = entry.get_active() + if active < 0: + txt = None + else: + model = entry.get_model() + index = entry.get_property('entry-text-column') + txt = model[active][index] + else: + return True + keyval = event.keyval + entry.handler_block(entry.editing_done_id) + + def callback(): + entry.handler_unblock(entry.editing_done_id) + if (keyval in [Gdk.KEY_Tab, Gdk.KEY_KP_Enter] + or (keyval == Gdk.KEY_Right and leaving)): + GLib.idle_add(self.set_cursor, path, + *self.next_column(path, column, renderer), True) + elif (keyval == Gdk.KEY_ISO_Left_Tab + or (keyval == Gdk.KEY_Left and leaving)): + GLib.idle_add(self.set_cursor, path, + *self.prev_column(path, column, renderer), True) + elif keyval in self.leaving_record_events: + fields = list(self.view.widgets.keys()) + if not record.validate(fields): + invalid_fields = record.invalid_fields + col = None + for col in self.get_columns(): + if col.name in invalid_fields: + break + GLib.idle_add(self.set_cursor, path, col, None, True) + return + if (( + self.view.screen.pre_validate + and not record.pre_validate()) + or (not self.view.screen.parent + and not record.save())): + GLib.idle_add( + self.set_cursor, path, column, None, True) + return + entry.handler_block(entry.editing_done_id) + if keyval == Gdk.KEY_Up: + self._key_up(path, model, column) + elif keyval == Gdk.KEY_Down: + self._key_down(path, model, column) + elif keyval == Gdk.KEY_Return: + if self.view.screen.new_position == 0: + new_path = self._key_up(path, model) + else: + new_path = self._key_down(path, model) + GLib.idle_add(self.set_cursor, new_path, + *self.next_column(new_path), True) + entry.handler_unblock(entry.editing_done_id) + else: + GLib.idle_add(self.set_cursor, path, column, None, True) + self.on_quit_cell(record, column, renderer, txt, callback=callback) + return True + elif event.keyval in [Gdk.KEY_F3, Gdk.KEY_F2]: + if isinstance(entry, Gtk.Entry): + value = entry.get_text() + elif isinstance(entry, Gtk.ComboBox): + active = entry.get_active() + if active < 0: + value = None + else: + model = entry.get_model() + index = entry.get_property('entry-text-column') + value = model[active][index] + else: + return True + entry.handler_block(entry.editing_done_id) + + def callback(): + widget = self.view.get_column_widget(column) + value = widget.get_textual_value(record) + if isinstance(entry, Gtk.Entry): + entry.set_text(value) + else: + entry.set_active_text(value) + entry.handler_unblock(entry.editing_done_id) + self.on_open_remote(record, column, + create=(event.keyval == Gdk.KEY_F3), value=value, + callback=callback) + else: + field = record[column.name] + if isinstance(entry, Gtk.Entry): + entry.set_max_length(int(field.attrs.get('size', 0))) + record.modified_fields.setdefault(column.name) + return False + + return True + + def _key_down(self, path, model, column=None): + if path[0] == len(model) - 1 and self.view.screen.new_position == -1: + self.on_create_line() + new_path = Gtk.TreePath((path[0] + 1) % len(model)) + if not column: + column, cell = self.next_column(new_path) + self.set_cursor(new_path, column, start_editing=True) + self.scroll_to_cell(new_path) + return new_path + + def _key_up(self, path, model, column=None): + if path[0] == 0 and self.view.screen.new_position == 0: + self.on_create_line() + new_path = Gtk.TreePath(0) + else: + new_path = Gtk.TreePath((path[0] - 1) % len(model)) + if not column: + column, cell = self.next_column(new_path) + self.set_cursor(new_path, column, start_editing=True) + self.scroll_to_cell(new_path) + return new_path + + def on_editing_done(self, entry, renderer): + path = self.get_cursor()[0] + if not path: + return True + column = self.get_column_from_renderer(renderer) + model = self.get_model() + record = model.get_value(model.get_iter(path), 0) + if isinstance(entry, (Date, Time)): + entry.activate() + text = entry.props.value + elif isinstance(entry, Gtk.ComboBox): + model = entry.get_model() + iter_ = entry.get_active_iter() + if iter_: + text = model.get_value(iter_, entry.props.entry_text_column) + else: + text = '' + else: + text = entry.get_text() + self.on_quit_cell(record, column, renderer, text) + + def get_column_from_renderer(self, renderer): + for column in self.get_columns(): + for cell in column.get_cells(): + if cell == renderer: + return column diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py new file mode 100644 index 0000000000..3c0df1607c --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -0,0 +1,1321 @@ +# 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 datetime +import os +import gettext +import webbrowser +from functools import wraps, partial +from weakref import WeakKeyDictionary + +from gi.repository import Gdk, GLib, Gtk + +from tryton.gui.window.win_search import WinSearch +from tryton.gui.window.win_form import WinForm +from tryton.gui.window.view_form.screen import Screen +from tryton.common import file_selection, file_open, file_write +import tryton.common as common +from tryton.common.cellrendererbutton import CellRendererButton +from tryton.common.cellrenderertext import CellRendererText, \ + CellRendererTextCompletion +from tryton.common.cellrenderertoggle import CellRendererToggle +from tryton.common.cellrenderercombo import CellRendererCombo +from tryton.common.cellrendererinteger import CellRendererInteger +from tryton.common.cellrendererfloat import CellRendererFloat +from tryton.common.cellrendererclickablepixbuf import \ + CellRendererClickablePixbuf +from tryton.common import data2pixbuf +from tryton.common.completion import get_completion, update_completion +from tryton.common.selection import ( + SelectionMixin, PopdownMixin, selection_shortcuts) +from tryton.common.datetime_ import CellRendererDate, CellRendererTime +from tryton.common.domain_parser import quote +from tryton.config import CONFIG + +_ = gettext.gettext + +COLORS = {n: v for n, v in zip( + ['muted', 'success', 'warning', 'danger'], + CONFIG['tree.colors'].split(','))} + + +def send_keys(renderer, editable, position, treeview): + editable.connect('key_press_event', treeview.on_keypressed, renderer) + editable.editing_done_id = editable.connect('editing_done', + treeview.on_editing_done, renderer) + if isinstance(editable, Gtk.ComboBox): + def changed(combobox): + # "changed" signal is also triggered by text editing + # so only trigger editing-done if a row is active + if combobox.get_active_iter(): + treeview.on_editing_done(combobox, renderer) + editable.connect('changed', changed) + + +def realized(func): + + @wraps(func) + def wrapper(self, *args, **kwargs): + has_been_realized = _REALIZED.get(self.view.treeview, False) + if not has_been_realized: + has_been_realized = self.view.treeview.get_realized() + if has_been_realized: + self.view.treeview.queue_resize() + _REALIZED[self.view.treeview] = True + else: + return + return func(self, *args, **kwargs) + return wrapper + + +_REALIZED = WeakKeyDictionary() + + +class CellCache(list): + + methods = ('set_active', 'set_sensitive', 'set_property') + + def apply(self, cell): + for method, args, kwargs in self: + getattr(cell, method)(*args, **kwargs) + + def decorate(self, cell): + def decorate(func): + @wraps(func) + def wrapper(*args, **kwargs): + self.append((func.__name__, args, kwargs)) + return func(*args, **kwargs) + wrapper.previous = func + return wrapper + + for method in self.methods: + if getattr(cell, method, None): + setattr(cell, method, decorate(getattr(cell, method))) + cell.decorated = True + return cell + + def undecorate(self, cell): + for method in self.methods: + if getattr(cell, method, None): + setattr(cell, method, getattr(cell, method).previous) + del cell.decorated + + @classmethod + def cache(cls, func): + @wraps(func) + def wrapper(self, column, cell, store, iter_, user_data=None): + if not hasattr(self, 'display_counters'): + self.display_counters = {} + if not hasattr(self, 'cell_caches'): + self.cell_caches = {} + record = store.get_value(iter_, 0) + counter = self.view.treeview.display_counter + if (self.display_counters.get(record.id) != counter): + if getattr(cell, 'decorated', None): + func(self, column, cell, store, iter_, user_data) + else: + cache = cls() + cache.decorate(cell) + func(self, column, cell, store, iter_, user_data) + cache.undecorate(cell) + self.cell_caches[record.id] = cache + self.display_counters[record.id] = counter + else: + self.cell_caches[record.id].apply(cell) + return wrapper + + +class Cell(object): + renderer = None + setter = None + expand = True + attrs = None + view = None + prefixes = [] + suffixes = [] + + def _get_record_field_from_path(self, path, store=None): + if not store: + store = self.view.treeview.get_model() + record = store.get_value(store.get_iter(path), 0) + field = record.group.fields[self.attrs['name']] + return record, field + + def _get_record_field_from_iter(self, iter_, store=None): + if not store: + store = self.view.treeview.get_model() + record = store.get_value(iter_, 0) + field = record[self.attrs['name']] + return record, field + + def _set_visual(self, cell, record): + visual = record.expr_eval(self.attrs.get('visual')) + if not visual: + visual = record.expr_eval(self.view.attributes.get('visual')) + background = COLORS.get(visual) if visual != 'muted' else None + foreground = COLORS.get(visual) if visual == 'muted' else None + cell.set_property('cell-background', background) + if isinstance(cell, Gtk.CellRendererText): + cell.set_property('foreground', foreground) + cell.set_property('foreground-set', bool(foreground)) + + def set_editable(self): + pass + + +class Affix(Cell): + expand = False + + def __init__(self, view, attrs, protocol=None): + super(Affix, self).__init__() + self.attrs = attrs + self.protocol = protocol + self.icon = attrs.get('icon') + if protocol: + self.renderer = CellRendererClickablePixbuf() + self.renderer.connect('clicked', self.clicked) + if not self.icon: + self.icon = 'tryton-public' + elif self.icon: + self.renderer = Gtk.CellRendererPixbuf() + else: + self.renderer = Gtk.CellRendererText() + self.view = view + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + field = record[self.attrs['name']] + field.state_set(record, states=('invisible',)) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible) + if self.icon: + if self.icon in record.group.fields: + value = record[self.icon].get_client(record) or '' + else: + value = self.icon + if self.attrs.get('icon_type') == 'url': + pixbuf = common.IconFactory.get_pixbuf_url( + value, size_param=self.attrs.get('url_size')) + else: + pixbuf = common.IconFactory.get_pixbuf( + value, Gtk.IconSize.BUTTON) + cell.set_property('pixbuf', pixbuf) + else: + text = self.attrs.get('string', '') + if not text: + text = field.get_client(record) or '' + cell.set_property('text', text) + self._set_visual(cell, record) + + def clicked(self, renderer, path): + record, field = self._get_record_field_from_path(path) + value = record[self.attrs['name']].get(record) + if value: + if self.protocol == 'email': + value = 'mailto:%s' % value + elif self.protocol == 'callto': + value = 'callto:%s' % value + elif self.protocol == 'sip': + value = 'sip:%s' % value + webbrowser.open(value, new=2) + + +class Symbol(Cell): + expand = False + + def __init__(self, view, attrs, position): + super().__init__() + self.attrs = attrs + self.symbol = attrs.get('symbol') + self.position = position + self.renderer = Gtk.CellRendererText() + self.renderer.set_property('yalign', 0) + self.renderer.set_property('xalign', 0 if position else 1) + self.view = view + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + field.state_set(record, states=('invisible',)) + invisible = field.get_state_attrs(record).get('invisible', False) + if invisible: + cell.set_property('text', '') + cell.set_property('visible', not invisible) + return + symbol, position = field.get_symbol(record, self.symbol) + if round(position) == self.position: + cell.set_property('text', symbol) + cell.set_property('visible', True) + else: + cell.set_property('text', '') + cell.set_property('visible', False) + self._set_visual(cell, record) + + +class GenericText(Cell): + align = 0 + editable = None + editing = None + + def __init__(self, view, attrs, renderer=None): + super(GenericText, self).__init__() + self.attrs = attrs + if renderer is None: + renderer = CellRendererText + self.renderer = renderer() + self.renderer.connect('editing-started', self.editing_started) + self.renderer.connect_after( + 'editing-started', send_keys, view.treeview) + self.renderer.set_property('yalign', 0) + self.renderer.set_property('xalign', self.align) + self.view = view + + @property + def field_name(self): + return self.attrs['name'] + + @property + def model_name(self): + return self.view.screen.model_name + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + text = self.get_textual_value(record) + + if isinstance(cell, Gtk.CellRendererToggle): + cell.set_active(bool(text)) + else: + cell.set_sensitive(not (record.deleted or record.removed)) + if isinstance(cell, Gtk.CellRendererText): + cell.set_property('strikethrough', record.deleted) + cell.set_property('text', text) + + states = ('invisible',) + if self.view.editable: + states = ('readonly', 'required', 'invisible') + + field.state_set(record, states=states) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible) + # Sometimes, the treeview with fixed height mode computes a too big + # height for not visible cell with text + # We can force an empty text because not visible cell can not be edited + # and so value_from_text is never called. + if invisible and not isinstance(cell, CellRendererToggle): + cell.set_property('text', '') + + if self.view.editable: + readonly = self.attrs.get('readonly', + field.get_state_attrs(record).get('readonly', False)) + if invisible: + readonly = True + + if isinstance(cell, CellRendererToggle): + cell.set_property('activatable', not readonly) + elif isinstance(cell, + (Gtk.CellRendererProgress, CellRendererButton, + Gtk.CellRendererPixbuf)): + pass + else: + cell.set_property('editable', not readonly) + else: + if isinstance(cell, CellRendererToggle): + cell.set_property('activatable', False) + self._set_visual(cell, record) + + def open_remote(self, record, create, changed=False, text=None, + callback=None): + raise NotImplementedError + + def get_textual_value(self, record): + if not record: + return '' + return record[self.attrs['name']].get_client(record) + + def value_from_text(self, record, text, callback=None): + field = record[self.attrs['name']] + field.set_client(record, text) + if callback: + callback() + + def set_editable(self): + if not self.editable or not self.editing: + return + record, field = self.editing + self.editable.set_text(self.get_textual_value(record)) + + def editing_started(self, cell, editable, path): + def remove(editable): + self.editable = None + self.editing = None + self.editable = editable + self.editing = self._get_record_field_from_path(path) + editable.connect('remove-widget', remove) + return False + + +class Char(GenericText): + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + super(Char, self).setter(column, cell, store, iter_, user_data) + cell.set_property('single-paragraph-mode', True) + + +class Text(GenericText): + pass + + +class Int(GenericText): + align = 1 + + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererInteger + super(Int, self).__init__(view, attrs, renderer=renderer) + self.factor = float(attrs.get('factor', 1)) + self.symbol = attrs.get('symbol') + if self.symbol: + self.renderer_prefix = Symbol(view, attrs, 0) + self.renderer_suffix = Symbol(view, attrs, 1) + + @property + def prefixes(self): + if self.symbol: + return [self.renderer_prefix] + return [] + + @property + def suffixes(self): + if self.symbol: + return [self.renderer_suffix] + return [] + + def get_textual_value(self, record): + if not record: + return '' + return record[self.attrs['name']].get_client( + record, factor=self.factor) + + def value_from_text(self, record, text, callback=None): + field = record[self.attrs['name']] + field.set_client(record, text, factor=self.factor) + if callback: + callback() + + +class Boolean(GenericText): + align = 0.5 + + def __init__(self, view, attrs=None, + renderer=None): + if renderer is None: + renderer = CellRendererToggle + super(Boolean, self).__init__(view, attrs, renderer=renderer) + self.renderer.connect('toggled', self._sig_toggled) + + def _sig_toggled(self, renderer, path): + record, field = self._get_record_field_from_path(path) + if (self.view.record and self.view.record != record + and not self.view.record.validate(self.view.get_fields())): + renderer.stop_emission_by_name('toggled') + return True + if not self.attrs.get('readonly', + field.get_state_attrs(record).get('readonly', False)): + value = record[self.attrs['name']].get_client(record) + record[self.attrs['name']].set_client(record, int(not value)) + self.view.treeview.set_cursor(path) + return True + + +class URL(Char): + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + super(URL, self).setter(column, cell, store, iter_, user_data) + record, field = self._get_record_field_from_iter(iter_, store) + field.state_set(record, states=('readonly',)) + readonly = field.get_state_attrs(record).get('readonly', False) + cell.set_property('visible', not readonly) + + +class Date(GenericText): + + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererDate + super(Date, self).__init__(view, attrs, renderer=renderer) + + @realized + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + self.renderer.props.format = self.get_format(record, field) + super(Date, self).setter(column, cell, store, iter_, user_data) + + def get_format(self, record, field): + if field and record: + return field.date_format(record) + else: + return '%x' + + def get_textual_value(self, record): + if not record: + return '' + value = record[self.attrs['name']].get_client(record) + if value: + return value.strftime(self.renderer.props.format) + else: + return '' + + +class Time(Date): + + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererTime + super(Time, self).__init__(view, attrs, renderer=renderer) + + def get_format(self, record, field): + if field and record: + return field.time_format(record) + else: + return '%X' + + def get_textual_value(self, record): + if not record: + return '' + value = record[self.attrs['name']].get_client(record) + if value is not None: + if isinstance(value, datetime.datetime): + value = value.time() + return value.strftime(self.renderer.props.format) + else: + return '' + + def set_editable(self): + if not self.editable or not self.editing: + return + record, field = self.editing + self.editable.get_child().set_text(self.get_textual_value(record)) + + +class TimeDelta(GenericText): + align = 1 + + +class Float(Int): + + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererFloat + super(Float, self).__init__(view, attrs, renderer=renderer) + + @realized + def setter(self, column, cell, store, iter_, user_data=None): + super(Float, self).setter(column, cell, store, iter_, user_data) + record, field = self._get_record_field_from_iter(iter_, store) + digits = field.digits(record, factor=self.factor) + cell.digits = digits + + +class Binary(GenericText): + align = 1 + + def __init__(self, view, attrs, renderer=None): + if renderer is None: + renderer = CellRendererText + super(Binary, self).__init__(view, attrs, renderer=renderer) + self.renderer.set_property('editable', False) + self.renderer.set_property('xalign', self.align) + self.renderer_save = _BinarySave(self) + self.renderer_select = _BinarySelect(self) + if self.attrs.get('filename'): + self.renderer_open = _BinaryOpen(self) + else: + self.renderer_open = None + + @property + def prefixes(self): + return filter(None, [self.renderer_open]) + + @property + def suffixes(self): + return [self.renderer_save, self.renderer_select] + + def get_textual_value(self, record): + field = record[self.attrs['name']] + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + return common.humanize(size) if size else '' + + def value_from_text(self, record, text, callback=None): + if callback: + callback() + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + text = self.get_textual_value(record) + cell.set_property('text', text) + + states = ('invisible',) + if self.view.editable: + states = ('readonly', 'required', 'invisible') + + field.state_set(record, states=states) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible) + self._set_visual(cell, record) + + def get_data(self, record, field): + if hasattr(field, 'get_data'): + data = field.get_data(record) + else: + data = field.get(record) + if isinstance(data, str): + data = data.encode('utf-8') + return data + + +class _BinaryIcon(Cell): + expand = False + + def __init__(self, binary): + super().__init__() + self.binary = binary + self.renderer = CellRendererClickablePixbuf() + self.renderer.connect('clicked', self.clicked) + + @property + def attrs(self): + return self.binary.attrs + + @property + def view(self): + return self.binary.view + + +class _BinarySave(_BinaryIcon): + icon_name = 'tryton-save' + + def __init__(self, binary): + super().__init__(binary) + pixbuf = common.IconFactory.get_pixbuf( + self.icon_name, Gtk.IconSize.BUTTON) + self.renderer.set_property('pixbuf', pixbuf) + + @common.idle_add + def clicked(self, renderer, path): + filename = '' + record, field = self._get_record_field_from_path(path) + if self.attrs.get('filename'): + filename_field = record.group.fields.get(self.attrs['filename']) + filename = filename_field.get(record) + filename = file_selection(_('Save As...'), filename=filename, + action=Gtk.FileChooserAction.SAVE) + if filename: + with open(filename, 'wb') as fp: + fp.write(self.binary.get_data(record, field)) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + field.state_set(record, states=['invisible']) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible and size) + self._set_visual(cell, record) + + +class _BinarySelect(_BinaryIcon): + def clicked(self, renderer, path): + record, field = self._get_record_field_from_path(path) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + if self.attrs.get('filename'): + filename_field = record.group.fields[self.attrs['filename']] + else: + filename_field = None + if size: + if filename_field: + filename_field.set_client(record, None) + field.set_client(record, None) + else: + def _select(): + filename = file_selection(_('Open...')) + if filename: + with open(filename, 'rb') as fp: + field.set_client(record, fp.read()) + if filename_field: + filename_field.set_client( + record, os.path.basename(filename)) + GLib.idle_add(_select) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + if hasattr(field, 'get_size'): + size = field.get_size(record) + else: + size = len(field.get(record)) + if size: + icon = 'tryton-clear' + else: + icon = 'tryton-search' + pixbuf = common.IconFactory.get_pixbuf(icon, Gtk.IconSize.BUTTON) + cell.set_property('pixbuf', pixbuf) + field.state_set(record, states=['invisible', 'readonly']) + invisible = field.get_state_attrs(record).get('invisible', False) + readonly = self.attrs.get('readonly', + field.get_state_attrs(record).get('readonly', False)) + if readonly and size: + cell.set_property('visible', False) + else: + cell.set_property('visible', not invisible) + self._set_visual(cell, record) + + +class _BinaryOpen(_BinarySave): + icon_name = 'tryton-open' + + def clicked(self, renderer, path): + record, field = self._get_record_field_from_path(path) + filename_field = record.group.fields.get(self.attrs.get('filename')) + filename = filename_field.get(record) + file_path = file_write(filename, self.binary.get_data(record, field)) + root, type_ = os.path.splitext(filename) + if type_: + type_ = type_[1:] + GLib.idle_add(file_open, file_path, type_) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + super().setter(column, cell, store, iter_) + record, field = self._get_record_field_from_iter(iter_, store) + filename_field = record.group.fields.get(self.attrs.get('filename')) + filename = filename_field.get(record) + if not filename: + cell.set_property('visible', False) + + +class Image(GenericText): + align = 0.5 + + def __init__(self, view, attrs=None, renderer=None): + if renderer is None: + renderer = Gtk.CellRendererPixbuf + super(Image, self).__init__(view, attrs, renderer) + self.height = int(attrs.get('height', 100)) + self.width = int(attrs.get('width', 300)) + self.renderer.set_fixed_size(self.width, self.height) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + value = field.get_client(record) + if isinstance(value, int): + if value > CONFIG['image.max_size']: + value = None + else: + value = field.get_data(record) + pixbuf = data2pixbuf(value) + if pixbuf: + pixbuf = common.resize_pixbuf(pixbuf, self.width, self.height) + cell.set_property('pixbuf', pixbuf) + self._set_visual(cell, record) + + def get_textual_value(self, record): + if not record: + return '' + return str(record[self.attrs['name']].get_size(record)) + + +class M2O(GenericText): + + def __init__(self, view, attrs, renderer=None): + if renderer is None and int(attrs.get('completion', 1)): + renderer = partial(CellRendererTextCompletion, self.set_completion) + super(M2O, self).__init__(view, attrs, renderer=renderer) + + def get_model(self, record, field): + return self.attrs['relation'] + + def has_target(self, value): + return value is not None + + def value_from_id(self, model, id_, str_=None): + if str_ is None: + str_ = '' + return id_, str_ + + def id_from_value(self, value): + return value + + def value_from_text(self, record, text, callback=None): + field = record.group.fields[self.attrs['name']] + model = self.get_model(record, field) + if not text: + field.set_client( + record, self.value_from_id(model, None, '')) + if callback: + callback() + return + + if model and common.get_toplevel_window().get_focus(): + field = record[self.attrs['name']] + win = self.search_remote(record, field, text, callback=callback) + if len(win.screen.group) == 1: + win.response(None, Gtk.ResponseType.OK) + else: + win.show() + + def editing_started(self, cell, editable, path): + super(M2O, self).editing_started(cell, editable, path) + record, field = self._get_record_field_from_path(path) + model = self.get_model(record, field) + + def changed(editable): + text = editable.get_text() + if self.get_textual_value(record) != text: + field.set_client( + record, self.value_from_id(model, None, '')) + + if self.has_target(field.get(record)): + icon1, tooltip1 = 'tryton-open', _("Open the record ") + icon2, tooltip2 = 'tryton-clear', _("Clear the field ") + else: + icon1, tooltip1 = None, '' + icon2, tooltip2 = 'tryton-search', _("Search a record ") + for pos, icon, tooltip in [ + (Gtk.EntryIconPosition.PRIMARY, icon1, tooltip1), + (Gtk.EntryIconPosition.SECONDARY, icon2, tooltip2)]: + if icon: + pixbuf = common.IconFactory.get_pixbuf( + icon, Gtk.IconSize.MENU) + else: + pixbuf = None + editable.set_icon_from_pixbuf(pos, pixbuf) + editable.set_icon_tooltip_text(pos, tooltip) + + def icon_press(editable, icon_pos, event): + value = field.get(record) + if not model: + return + if (icon_pos == Gtk.EntryIconPosition.SECONDARY + and self.has_target(value)): + field.set_client( + record, self.value_from_id(model, None, '')) + editable.set_text('') + elif self.has_target(value): + self.open_remote(record, create=False, changed=False) + else: + self.open_remote( + record, create=False, changed=True, + text=editable.get_text()) + + editable.connect('icon-press', icon_press) + editable.connect('changed', changed) + changed(editable) + return False + + def open_remote(self, record, create=True, changed=False, text=None, + callback=None): + field = record.group.fields[self.attrs['name']] + model = self.get_model(record, field) + + access = common.MODELACCESS[model] + if (create + and not (self.attrs.get('create', True) and access['create'])): + return + elif not access['read']: + return + + domain = field.domain_get(record) + context = field.get_context(record) + if not create and changed: + self.search_remote(record, field, text, callback=callback).show() + return + target_id = self.id_from_value(field.get(record)) + + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + field.attrs.get('string') or common.MODELNAME.get(model)) + screen = Screen(model, domain=domain, context=context, + mode=['form'], view_ids=self.attrs.get('view_ids', '').split(','), + exclude_field=field.attrs.get('relation_field'), + breadcrumb=breadcrumb) + + def open_callback(result): + if result: + value = self.value_from_id( + model, + screen.current_record.id, + screen.current_record.rec_name()) + field.set_client(record, value, force_change=True) + if callback: + callback() + if target_id and target_id >= 0: + screen.load([target_id]) + WinForm(screen, open_callback, save_current=True) + else: + WinForm(screen, open_callback, new=True, save_current=True, + rec_name=text) + + def search_remote(self, record, field, text, callback=None): + model = self.get_model(record, field) + domain = field.domain_get(record) + context = field.get_search_context(record) + order = field.get_search_order(record) + access = common.MODELACCESS[model] + create_access = self.attrs.get('create', True) and access['create'] + + def search_callback(found): + value = None + if found: + value = self.value_from_id(model, *found[0]) + field.set_client(record, value) + if callback: + callback() + win = WinSearch(model, search_callback, sel_multi=False, + context=context, domain=domain, + order=order, view_ids=self.attrs.get('view_ids', '').split(','), + new=create_access, title=self.attrs.get('string')) + win.screen.search_filter(quote(text)) + return win + + def set_completion(self, entry, path): + record, field = self._get_record_field_from_path(path) + if entry.get_completion(): + entry.set_completion(None) + model = self.get_model(record, field) + if not model: + return + access = common.MODELACCESS[model] + completion = get_completion( + search=access['read'], + create=self.attrs.get('create', True) and access['create']) + completion.connect('match-selected', self._completion_match_selected, + record, field, model) + completion.connect('action-activated', + self._completion_action_activated, record, field) + entry.set_completion(completion) + entry.connect('key-press-event', self._key_press, record, field) + entry.connect('changed', self._update_completion, record, field) + + def _key_press(self, entry, event, record, field): + if (self.has_target(field.get(record)) + and event.keyval in [Gdk.KEY_Delete, Gdk.KEY_BackSpace]): + entry.set_text('') + return False + + def _completion_match_selected( + self, completion, model, iter_, record, field, model_name): + rec_name, record_id = model.get(iter_, 0, 1) + field.set_client( + record, self.value_from_id(model_name, record_id, rec_name)) + + completion.get_entry().set_text(rec_name) + completion_model = completion.get_model() + completion_model.clear() + completion_model.search_text = rec_name + return True + + def _update_completion(self, entry, record, field): + value = field.get(record) + if self.has_target(value): + id_ = self.id_from_value(value) + if id_ is not None and id_ >= 0: + return + model = self.get_model(record, field) + update_completion(entry, record, field, model) + + def _completion_action_activated(self, completion, index, record, field): + entry = completion.get_entry() + entry.handler_block(entry.editing_done_id) + + def callback(): + entry.handler_unblock(entry.editing_done_id) + entry.set_text(field.get_client(record)) + if index == 0: + self.open_remote(record, create=False, changed=True, + text=entry.get_text(), callback=callback) + elif index == 1: + self.open_remote(record, create=True, + text=entry.get_text(), callback=callback) + else: + entry.handler_unblock(entry.editing_done_id) + + +class O2O(M2O): + pass + + +class O2M(GenericText): + align = 0.5 + + def get_textual_value(self, record): + return '( ' + str(len(record[self.attrs['name']] + .get_eval(record))) + ' )' + + def value_from_text(self, record, text, callback=None): + if callback: + callback() + + def open_remote(self, record, create=True, changed=False, text=None, + callback=None): + group = record.value[self.attrs['name']] + field = record.group.fields[self.attrs['name']] + relation = field.attrs['relation'] + context = field.get_context(record) + + access = common.MODELACCESS[relation] + if not access['read']: + return + + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + field.attrs.get('string') or common.MODELNAME.get(relation)) + screen = Screen(relation, mode=['tree', 'form'], + view_ids=self.attrs.get('view_ids', '').split(','), + exclude_field=field.attrs.get('relation_field'), + breadcrumb=breadcrumb) + screen.pre_validate = bool(int(self.attrs.get('pre_validate', 0))) + screen.group = group + + def open_callback(result): + if callback: + callback() + WinForm(screen, open_callback, view_type='tree', context=context) + + +class M2M(O2M): + + def open_remote(self, record, create=True, changed=False, text=None, + callback=None): + group = record.value[self.attrs['name']] + field = record.group.fields[self.attrs['name']] + relation = field.attrs['relation'] + context = field.get_context(record) + domain = field.domain_get(record) + + breadcrumb = list(self.view.screen.breadcrumb) + breadcrumb.append( + field.attrs.get('string') or common.MODELNAME.get(relation)) + screen = Screen(relation, mode=['tree', 'form'], + view_ids=self.attrs.get('view_ids', '').split(','), + exclude_field=field.attrs.get('relation_field'), + breadcrumb=breadcrumb) + screen.group = group + + def open_callback(result): + if callback: + callback() + WinForm(screen, open_callback, view_type='tree', domain=domain, + context=context) + + +class Selection(GenericText, SelectionMixin, PopdownMixin): + + def __init__(self, *args, **kwargs): + if 'renderer' not in kwargs: + kwargs['renderer'] = CellRendererCombo + super(Selection, self).__init__(*args, **kwargs) + self.init_selection() + # Use a variable let Python holding reference when calling set_property + model = self.get_popdown_model(self.selection)[0] + self.renderer.set_property('model', model) + self.renderer.set_property('text-column', 0) + + def get_value(self, record, field): + return field.get(record) + + def get_textual_value(self, record): + field = record[self.attrs['name']] + self.update_selection(record, field) + value = self.get_value(record, field) + text = dict(self.selection).get(value, '') + if value and not text: + text = self.get_inactive_selection(value) + return text + + def value_from_text(self, record, text, callback=None): + if callback: + callback() + + def set_editable(self): + if not self.editable and not self.editing: + return + record, field = self.editing + value = self.get_value(record, field) + self.update_selection(record, field) + self.set_popdown_value(self.editable, value) + + def editing_started(self, cell, editable, path): + super(Selection, self).editing_started(cell, editable, path) + record, field = self._get_record_field_from_path(path) + # Combobox does not emit remove-widget when focus is changed + self.editable.connect( + 'editing-done', + lambda *a: self.editable.emit('remove-widget')) + + selection_shortcuts(editable) + + def set_value(*a): + return self.set_value(editable, record, field) + editable.get_child().connect('activate', set_value) + editable.get_child().connect('focus-out-event', set_value) + editable.connect('changed', set_value) + + self.update_selection(record, field) + self.set_popdown(self.selection, editable) + + value = self.get_value(record, field) + if not self.set_popdown_value(editable, value): + self.get_inactive_selection(value) + self.set_popdown_value(editable, value) + return False + + def set_value(self, editable, record, field): + value = self.get_popdown_value(editable) + if 'relation' in self.attrs and value: + active = editable.get_active() + if active < 0: + text = None + else: + model = editable.get_model() + index = editable.get_property('entry-text-column') + text = model[active][index] + value = (value, text) + field.set_client(record, value) + return False + + +class MultiSelection(GenericText, SelectionMixin): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.init_selection() + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + super().setter(column, cell, store, iter_, user_data=user_data) + cell.set_property('editable', False) + + def value_from_text(self, record, text, callback=None): + if callback: + callback() + + def get_textual_value(self, record): + field = record[self.attrs['name']] + self.update_selection(record, field) + selection = dict(self.selection) + values = [] + for value in field.get_eval(record): + text = selection.get(value, '') + values.append(text) + return ';'.join(values) + + +class Reference(M2O): + + def __init__(self, view, attrs, renderer=None): + super(Reference, self).__init__(view, attrs, renderer=renderer) + self.renderer_selection = _ReferenceSelection(view, attrs) + + @property + def prefixes(self): + return [self.renderer_selection] + + def get_model(self, record, field): + value = field.get_client(record) + if value: + model, value = value + return model + + def has_target(self, value): + if not value: + return False + model, value = value.split(',') + if not value: + value = None + else: + try: + value = int(value) + except ValueError: + value = None + return model and value >= 0 + + def value_from_id(self, model, id_, str_=None): + if str_ is None: + str_ = '' + return model, (id_, str_) + + def id_from_value(self, value): + _, value = value.split(',') + return int(value) + + def get_textual_value(self, record): + value = super().get_textual_value(record) + if value: + model, value = value + else: + value = '' + return value + + +class _ReferenceSelection(Selection): + + def get_value(self, record, field): + value = field.get_client(record) + if value: + model, value = value + else: + model = None + return model + + def set_value(self, editable, record, field): + value = self.get_popdown_value(editable) + if value != self.get_value(record, field): + if value: + value = (value, (-1, '')) + else: + value = ('', '') + field.set_client(record, value) + return False + + +class Dict(GenericText): + align = 0.5 + + def __init__(self, view, attrs): + super().__init__(view, attrs) + self.renderer.props.editable = False + + def setter(self, column, cell, store, iter_, user_data=None): + super().setter(column, cell, store, iter_, user_data=None) + cell.props.editable = False + + def get_textual_value(self, record): + return '(%s)' % len(record[self.attrs['name']].get_client(record)) + + +class ProgressBar(Cell): + align = 0.5 + orientations = { + 'left_to_right': (Gtk.Orientation.HORIZONTAL, False), + 'right_to_left': (Gtk.Orientation.HORIZONTAL, True), + 'bottom_to_top': (Gtk.Orientation.VERTICAL, True), + 'top_to_bottom': (Gtk.Orientation.VERTICAL, False), + } + + def __init__(self, view, attrs): + super(ProgressBar, self).__init__() + self.view = view + self.attrs = attrs + self.renderer = Gtk.CellRendererProgress() + orientation, inverted = self.orientations.get( + self.attrs.get('orientation', 'left_to_right')) + self.renderer.set_orientation(orientation) + self.renderer.set_property('inverted', inverted) + self.renderer.set_property('yalign', 0) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record, field = self._get_record_field_from_iter(iter_, store) + field.state_set(record, states=('invisible',)) + invisible = field.get_state_attrs(record).get('invisible', False) + cell.set_property('visible', not invisible) + text = self.get_textual_value(record) + if text: + text = _('%s%%') % text + cell.set_property('text', text) + value = field.get(record) or 0.0 + cell.set_property('value', value * 100) + self._set_visual(cell, record) + + def open_remote(self, record, create, changed=False, text=None, + callback=None): + raise NotImplementedError + + def get_textual_value(self, record): + return record[self.attrs['name']].get_client(record, factor=100) or '' + + def value_from_text(self, record, text, callback=None): + field = record[self.attrs['name']] + field.set_client(record, float(text)) + if callback: + callback() + + +class Button(Cell): + + def __init__(self, view, attrs): + super(Button, self).__init__() + self.attrs = attrs + self.renderer = CellRendererButton(attrs.get('string', _('Unknown'))) + self.view = view + + self.renderer.connect('clicked', self.button_clicked) + self.renderer.set_property('yalign', 0) + + @realized + @CellCache.cache + def setter(self, column, cell, store, iter_, user_data=None): + record = store.get_value(iter_, 0) + states = record.expr_eval(self.attrs.get('states', {})) + invisible = states.get('invisible', False) + cell.set_property('visible', not invisible) + readonly = states.get('readonly', False) + cell.set_property('sensitive', not readonly) + parent = record.parent if record else None + while parent: + if parent.modified: + cell.set_property('sensitive', False) + break + parent = parent.parent + # TODO icon + self._set_visual(cell, record) + + @common.idle_add + def button_clicked(self, widget, path): + if not path: + return True + store = self.view.treeview.get_model() + record = store.get_value(store.get_iter(path), 0) + + if self.view.record and self.view.record != record: + widget.stop_emission_by_name('clicked') + return True + + state_changes = record.expr_eval( + self.attrs.get('states', {})) + if state_changes.get('invisible') \ + or state_changes.get('readonly'): + return True + + self.view.treeview.set_cursor(path) + widget.handler_block_by_func(self.button_clicked) + try: + self.view.screen.button(self.attrs) + finally: + widget.handler_unblock_by_func(self.button_clicked) diff --git a/tryton/tryton/gui/window/view_form/view/screen_container.py b/tryton/tryton/gui/window/view_form/view/screen_container.py new file mode 100644 index 0000000000..e0293bcd9e --- /dev/null +++ b/tryton/tryton/gui/window/view_form/view/screen_container.py @@ -0,0 +1,678 @@ +# 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 datetime +import gettext + +from gi.repository import Gdk, GLib, GObject, Gtk + +import tryton.common as common +from tryton.common.domain_parser import quote +from tryton.common.treeviewcontrol import TreeViewControl +from tryton.common.datetime_ import Date, Time, DateTime, add_operators +from tryton.common.number_entry import NumberEntry +from tryton.pyson import PYSONDecoder + +_ = gettext.gettext + + +class Between(Gtk.HBox): + _changed_signal = None + + def __init__(self, _entry=Gtk.Entry): + super().__init__() + self.from_ = _entry() + self.pack_start(self.from_, expand=True, fill=True, padding=0) + self.pack_start( + Gtk.Label(label=_('..')), expand=False, fill=False, padding=0) + self.to = _entry() + self.pack_start(self.to, expand=True, fill=True, padding=0) + + if self._changed_signal: + self.from_.connect_after( + self._changed_signal, self._from_changed) + + @property + def _connect_widgets(self): + return [self.from_, self.to] + + def connect(self, signal, callback): + for widget in self._connect_widgets: + try: + widget.connect(signal, callback) + except TypeError: + pass + + def get_value(self): + from_ = self._get_value(self.from_) + to = self._get_value(self.to) + if from_ and to: + if from_ != to: + return '%s..%s' % (quote(from_), quote(to)) + else: + return quote(from_) + elif from_: + return '>=%s' % quote(from_) + elif to: + return '<=%s' % quote(to) + + def _get_value(self, entry): + raise NotImplementedError + + def set_value(self, from_, to): + self._set_value(self.from_, from_) + self._set_value(self.to, to) + + def _set_value(self, entry, value): + raise NotImplementedError + + def _from_changed(self, widget): + self._set_value(self.to, self._get_value(self.from_)) + + +class WithOperators: + def __init__(self, entry): + self.entry = entry + + def __call__(self): + return add_operators(self.entry()) + + +class BetweenDates(Between): + _entry = None + + def __init__(self, format_=None): + super().__init__(WithOperators(self._entry)) + if format_: + self.from_.props.format = format_ + self.to.props.format = format_ + + def _set_value(self, entry, value): + entry.props.value = value + + +class Dates(BetweenDates): + _entry = Date + _changed_signal = 'date-changed' + + def _get_value(self, widget): + value = widget.props.value + if value: + return value.strftime(widget.props.format) + + +class Times(BetweenDates): + _entry = Time + _changed_signal = 'time-changed' + + @property + def _connect_widgets(self): + return [self.from_.get_child(), self.to.get_child()] + + def _get_value(self, widget): + value = widget.props.value + if value: + return datetime.time.strftime(value, widget.props.format) + + +class DateTimes(BetweenDates): + _entry = DateTime + _changed_signal = 'datetime-changed' + + def __init__(self, date_format, time_format): + super().__init__() + self.from_.props.date_format = date_format + self.to.props.date_format = date_format + self.from_.props.time_format = time_format + self.to.props.time_format = time_format + + @property + def _connect_widgets(self): + return self.from_.get_children() + self.to.get_children() + + def _get_value(self, widget): + value = widget.props.value + if value: + return value.strftime( + widget.props.date_format + ' ' + widget.props.time_format) + + +class Numbers(Between): + _changed_signal = 'changed' + + def __init__(self): + super().__init__(NumberEntry) + + def _get_value(self, widget): + return widget.get_text() + + def _set_value(self, entry, value): + entry.set_text(value or '') + + +class Selection(Gtk.ScrolledWindow): + + def __init__(self, selections): + super(Selection, self).__init__() + self.treeview = TreeViewControl() + model = Gtk.ListStore(GObject.TYPE_STRING) + for selection in selections: + model.append((selection,)) + self.treeview.set_model(model) + + column = Gtk.TreeViewColumn() + cell = Gtk.CellRendererText() + column.pack_start(cell, expand=True) + column.add_attribute(cell, 'text', 0) + self.treeview.append_column(column) + self.treeview.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + self.treeview.set_headers_visible(False) + self.add(self.treeview) + self.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + self.set_min_content_height(min(20 * len(selections), 200)) + self.set_max_content_height(200) + + def get_value(self): + values = [] + model, paths = self.treeview.get_selection().get_selected_rows() + if not paths: + return + for path in paths: + iter_ = model.get_iter(path) + values.append(model.get_value(iter_, 0)) + return ';'.join(quote(v) for v in values) + + +class ScreenContainer(object): + + def __init__(self, tab_domain): + self.viewport = Gtk.Viewport() + self.viewport.set_shadow_type(Gtk.ShadowType.NONE) + self.vbox = Gtk.VBox(spacing=3) + self.alternate_viewport = Gtk.Viewport() + self.alternate_viewport.set_shadow_type(Gtk.ShadowType.NONE) + self.alternate_view = False + self.search_popover = None + self.search_grid = None + self.last_search_text = '' + self.tab_domain = tab_domain or [] + self.tab_counter = [] + + tooltips = common.Tooltips() + + self.filter_vbox = Gtk.VBox(spacing=0) + self.filter_vbox.set_border_width(0) + hbox = Gtk.HBox(homogeneous=False, spacing=0) + + self.search_entry = Gtk.Entry() + self.search_entry.set_placeholder_text(_('Search')) + self.search_entry.set_alignment(0.0) + self.search_entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.PRIMARY, + common.IconFactory.get_pixbuf('tryton-filter', Gtk.IconSize.MENU)) + self.search_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.PRIMARY, _('Open filters')) + self.completion = Gtk.EntryCompletion() + self.completion.set_model(Gtk.ListStore(str)) + self.completion.set_text_column(0) + self.completion.props.inline_selection = True + self.completion.props.popup_set_width = False + self.completion.set_match_func(lambda *a: True) + self.completion.connect('match-selected', self.match_selected) + self.search_entry.connect('activate', self.activate) + self.search_entry.set_completion(self.completion) + self.search_entry.connect('key-press-event', self.key_press) + self.search_entry.connect('focus-in-event', self.focus_in) + self.search_entry.connect('icon-press', self.icon_press) + + hbox.pack_start(self.search_entry, expand=True, fill=True, padding=0) + + def popup(widget): + menu = widget._menu + for child in menu.get_children(): + menu.remove(child) + if not widget.props.active: + menu.popdown() + return + + def menu_position(menu, data=None): + widget_allocation = widget.get_allocation() + x, y = widget.get_window().get_root_coords( + widget_allocation.x, widget_allocation.y) + return (x, y + widget_allocation.height, False) + + for id_, name, domain, access in self.bookmarks(): + menuitem = Gtk.MenuItem(label=name) + menuitem.connect('activate', self.bookmark_activate, domain) + menu.add(menuitem) + + menu.show_all() + if hasattr(menu, 'popup_at_widget'): + menu.popup_at_widget( + widget, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, + Gtk.get_current_event()) + else: + menu.popup(None, None, menu_position, 0, 0) + + def deactivate(menuitem, togglebutton): + togglebutton.props.active = False + + but_bookmark = Gtk.ToggleButton() + self.but_bookmark = but_bookmark + tooltips.set_tip(but_bookmark, _('Show bookmarks of filters')) + but_bookmark.add(common.IconFactory.get_image( + 'tryton-bookmarks', Gtk.IconSize.SMALL_TOOLBAR)) + but_bookmark.set_relief(Gtk.ReliefStyle.NONE) + menu = Gtk.Menu() + menu.set_property('reserve-toggle-size', False) + menu.connect('deactivate', deactivate, but_bookmark) + but_bookmark._menu = menu + but_bookmark.connect('toggled', popup) + hbox.pack_start(but_bookmark, expand=False, fill=False, padding=0) + + but_active = Gtk.ToggleButton() + self.but_active = but_active + self._set_active_tooltip() + but_active.add(common.IconFactory.get_image( + 'tryton-archive', Gtk.IconSize.SMALL_TOOLBAR)) + but_active.set_relief(Gtk.ReliefStyle.NONE) + but_active.connect('toggled', self.search_active) + hbox.pack_start(but_active, expand=False, fill=False, padding=0) + + but_prev = Gtk.Button() + self.but_prev = but_prev + tooltips.set_tip(but_prev, _('Previous')) + but_prev.connect('clicked', self.search_prev) + but_prev.add(common.IconFactory.get_image( + 'tryton-back', Gtk.IconSize.SMALL_TOOLBAR)) + but_prev.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_prev, expand=False, fill=False, padding=0) + + but_next = Gtk.Button() + self.but_next = but_next + tooltips.set_tip(but_next, _('Next')) + but_next.connect('clicked', self.search_next) + but_next.add(common.IconFactory.get_image( + 'tryton-forward', Gtk.IconSize.SMALL_TOOLBAR)) + but_next.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_next, expand=False, fill=False, padding=0) + + hbox.show_all() + self.filter_vbox.pack_start(hbox, expand=False, fill=False, padding=0) + + hseparator = Gtk.HSeparator() + hseparator.show() + self.filter_vbox.pack_start( + hseparator, expand=False, fill=False, padding=0) + + if self.tab_domain: + self.notebook = Gtk.Notebook() + try: + self.notebook.props.homogeneous = True + except AttributeError: + # No more supported by GTK+3 + pass + self.notebook.set_scrollable(True) + for name, domain, count in self.tab_domain: + hbox = Gtk.HBox(spacing=3) + label = Gtk.Label(label='_' + name) + label.set_use_underline(True) + hbox.pack_start(label, expand=True, fill=True, padding=0) + counter = Gtk.Label() + hbox.pack_start(counter, expand=False, fill=True, padding=0) + hbox.show_all() + self.notebook.append_page(Gtk.VBox(), hbox) + self.tab_counter.append(counter) + self.filter_vbox.pack_start( + self.notebook, expand=True, fill=True, padding=0) + self.notebook.show_all() + # Set the current page before connecting to switch-page to not + # trigger the search a second times. + self.notebook.set_current_page(0) + self.notebook.get_nth_page(0).pack_end( + self.viewport, expand=True, fill=True, padding=0) + self.notebook.connect('switch-page', self.switch_page) + self.notebook.connect_after('switch-page', self.switch_page_after) + filter_expand = True + else: + self.notebook = None + self.vbox.pack_end( + self.viewport, expand=True, fill=True, padding=0) + filter_expand = False + + self.vbox.pack_start( + self.filter_vbox, expand=filter_expand, fill=True, padding=0) + + self.but_next.set_sensitive(False) + self.but_prev.set_sensitive(False) + + tooltips.enable() + + def widget_get(self): + return self.vbox + + def set_screen(self, screen): + self.screen = screen + self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) + self.bookmark_match() + + def show_filter(self): + if self.filter_vbox: + self.filter_vbox.show() + if self.notebook: + self.notebook.set_show_tabs(True) + if self.viewport in self.vbox.get_children(): + self.vbox.remove(self.viewport) + self.notebook.get_nth_page(self.notebook.get_current_page() + ).pack_end( + self.viewport, expand=True, fill=True, padding=0) + + def hide_filter(self): + if self.filter_vbox: + self.filter_vbox.hide() + if self.notebook: + self.notebook.set_show_tabs(False) + if self.viewport not in self.vbox.get_children(): + self.notebook.get_nth_page(self.notebook.get_current_page() + ).remove(self.viewport) + self.vbox.pack_end( + self.viewport, expand=True, fill=True, padding=0) + + def set(self, widget): + viewport1 = self.viewport + viewport2 = self.alternate_viewport + if self.alternate_view: + viewport1, viewport2 = viewport2, viewport1 + + if viewport1.get_child(): + viewport1.remove(viewport1.get_child()) + if widget == viewport2.get_child(): + viewport2.remove(widget) + viewport1.add(widget) + viewport1.show_all() + + def update(self): + res = self.screen.search_complete(self.get_text()) + model = self.completion.get_model() + model.clear() + for r in res: + model.append([r.strip()]) + + def get_text(self): + return self.search_entry.get_text() + + def set_text(self, value): + self.search_entry.set_text(value) + self.bookmark_match() + + def bookmarks(self): + for id_, name, domain, access in common.VIEW_SEARCH[ + self.screen.model_name]: + if self.screen.domain_parser.stringable(domain): + yield id_, name, domain, access + + def bookmark_activate(self, menuitem, domain): + self.set_text(self.screen.domain_parser.string(domain)) + self.do_search() + + def bookmark_match(self): + current_text = self.get_text() + if current_text: + current_domain = self.screen.domain_parser.parse(current_text) + for id_, name, domain, access in self.bookmarks(): + text = self.screen.domain_parser.string(domain) + domain = self.screen.domain_parser.parse(text) + if (text == current_text + or domain == current_domain): + self.search_entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.SECONDARY, + common.IconFactory.get_pixbuf( + 'tryton-bookmark', Gtk.IconSize.MENU)) + self.search_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, + _('Remove this bookmark')) + self.search_entry.set_icon_activatable( + Gtk.EntryIconPosition.SECONDARY, access) + self.search_entry.set_icon_sensitive( + Gtk.EntryIconPosition.SECONDARY, access) + return id_ + self.search_entry.set_icon_activatable( + Gtk.EntryIconPosition.SECONDARY, bool(current_text)) + self.search_entry.set_icon_sensitive( + Gtk.EntryIconPosition.SECONDARY, bool(current_text)) + self.search_entry.set_icon_from_pixbuf( + Gtk.EntryIconPosition.SECONDARY, + common.IconFactory.get_pixbuf( + 'tryton-bookmark-border', Gtk.IconSize.MENU)) + if current_text: + self.search_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, _('Bookmark this filter')) + elif self.search_entry.get_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY): + self.search_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, None) + + def search_next(self, widget=None): + self.screen.search_next(self.get_text()) + + def search_prev(self, widget=None): + self.screen.search_prev(self.get_text()) + + def search_active(self, widget=None): + self._set_active_tooltip() + self.screen.search_filter(self.get_text()) + + def _set_active_tooltip(self): + if self.but_active.get_active(): + tooltip = _('Show active records') + else: + tooltip = _('Show inactive records') + tooltips = common.Tooltips() + tooltips.set_tip(self.but_active, tooltip) + + def switch_page(self, notebook, page, page_num): + current_page = notebook.get_nth_page(notebook.get_current_page()) + current_page.remove(self.viewport) + + new_page = notebook.get_nth_page(page_num) + new_page.pack_end(self.viewport, expand=True, fill=True, padding=0) + + def switch_page_after(self, notebook, page, page_num): + self.do_search() + notebook.grab_focus() + self.screen.count_tab_domain(True) + + def get_tab_index(self): + if not self.notebook: + return -1 + return self.notebook.get_current_page() + + def get_tab_domain(self): + idx = self.get_tab_index() + if idx < 0: + return [] + return self.tab_domain[idx][1] + + def set_tab_counter(self, count, idx=None): + if not self.tab_counter or not self.notebook: + return + if idx is None: + idx = self.notebook.get_current_page() + if idx < 0: + return + label = self.tab_counter[idx] + tooltip = common.Tooltips() + if count is None: + label.set_label('') + tooltip.set_tip(label, '') + else: + tooltip.set_tip(label, '%d' % count) + fmt = '(%d)' + if count > 99: + fmt = '(%d+)' + count = 99 + label.set_label(fmt % count) + + def match_selected(self, completion, model, iter): + def callback(): + if not self.search_entry.props.window: + return + self.update() + self.search_entry.emit('changed') + GLib.idle_add(callback) + + def activate(self, widget): + if not self.search_entry.get_selection_bounds(): + self.do_search(widget) + + def do_search(self, widget=None): + self.screen.search_filter(self.get_text()) + + def set_cursor(self, new=False, reset_view=True): + if self.filter_vbox: + self.search_entry.grab_focus() + + def key_press(self, widget, event): + def keypress(): + if not self.search_entry.props.window: + return + self.update() + self.bookmark_match() + GLib.idle_add(keypress) + + def icon_press(self, widget, icon_pos, event): + if icon_pos == Gtk.EntryIconPosition.PRIMARY: + self.search_box(widget) + elif icon_pos == Gtk.EntryIconPosition.SECONDARY: + model_name = self.screen.model_name + id_ = self.bookmark_match() + if not id_: + text = self.get_text() + if not text: + return + name = common.ask(_('Bookmark Name:')) + if not name: + return + domain = self.screen.domain_parser.parse(text) + common.VIEW_SEARCH.add(model_name, name, domain) + self.set_text(self.screen.domain_parser.string(domain)) + else: + common.VIEW_SEARCH.remove(model_name, id_) + # Refresh icon and bookmark button + self.bookmark_match() + self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) + + def focus_in(self, widget, event): + self.update() + self.search_entry.emit('changed') + + def search_box(self, widget): + def window_hide(window, *args): + window.hide() + self.search_entry.grab_focus() + + def search(): + self.search_popover.popdown() + text = '' + for label, entry in self.search_grid.fields: + if isinstance(entry, Gtk.ComboBoxText): + value = quote(entry.get_active_text()) or None + elif isinstance(entry, (Between, Selection)): + value = entry.get_value() + else: + value = quote(entry.get_text()) or None + if value is not None: + text += quote(label) + ': ' + value + ' ' + self.set_text(text) + self.do_search() + # Store text after doing the search + # because domain parser could simplify the text + self.last_search_text = self.get_text() + + if not self.search_popover: + self.search_popover = Gtk.Popover() + self.search_popover.set_relative_to(widget) + + vbox = Gtk.VBox() + fields = [f for f in self.screen.domain_parser.fields.values() + if f.get('searchable', True) and '.' not in f['name']] + self.search_grid = Gtk.Grid(column_spacing=3, row_spacing=3) + + # Fill table with fields + self.search_grid.fields = [] + for i, field in enumerate(fields): + label = Gtk.Label( + label=field['string'], + halign=Gtk.Align.START, valign=Gtk.Align.START) + self.search_grid.attach(label, 0, i, 1, 1) + if field['type'] == 'boolean': + entry = Gtk.ComboBoxText() + entry.append_text('') + selections = (_('True'), _('False')) + for selection in selections: + entry.append_text(selection) + elif field['type'] in ['selection', 'multiselection']: + selections = tuple(x[1] for x in field['selection']) + entry = Selection(selections) + entry.set_vexpand(True) + elif field['type'] in ('date', 'datetime', 'time'): + date_format = common.date_format( + self.screen.context.get('date_format')) + if field['type'] == 'date': + entry = Dates(date_format) + elif field['type'] in ('datetime', 'time'): + time_format = PYSONDecoder({}).decode(field['format']) + if field['type'] == 'time': + entry = Times(time_format) + elif field['type'] == 'datetime': + entry = DateTimes(date_format, time_format) + entry.connect('activate', lambda *a: search()) + elif field['type'] in ['integer', 'float', 'numeric']: + entry = Numbers() + entry.connect('activate', lambda *a: search()) + else: + entry = Gtk.Entry() + entry.connect('activate', lambda *a: search()) + label.set_mnemonic_widget(entry) + self.search_grid.attach(entry, 1, i, 1, 1) + self.search_grid.fields.append((field['string'], entry)) + + scrolled = Gtk.ScrolledWindow() + scrolled.add(self.search_grid) + scrolled.set_shadow_type(Gtk.ShadowType.NONE) + scrolled.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + vbox.pack_start(scrolled, expand=True, fill=True, padding=0) + find_button = Gtk.Button(label=_('Find')) + find_button.connect('clicked', lambda *a: search()) + find_button.set_image(common.IconFactory.get_image( + 'tryton-search', Gtk.IconSize.SMALL_TOOLBAR)) + find_button.set_can_default(True) + self.search_popover.set_default_widget(find_button) + hbuttonbox = Gtk.HButtonBox() + hbuttonbox.pack_start( + find_button, expand=False, fill=False, padding=0) + hbuttonbox.set_layout(Gtk.ButtonBoxStyle.END) + vbox.pack_start(hbuttonbox, expand=False, fill=True, padding=0) + self.search_popover.add(vbox) + vbox.show_all() + scrolled.set_size_request( + -1, min(self.search_grid.get_preferred_height()[1], 400)) + + self.search_popover.set_pointing_to( + widget.get_icon_area(Gtk.EntryIconPosition.PRIMARY)) + self.search_popover.popup() + if self.search_grid.fields: + self.search_grid.fields[0][1].grab_focus() + + if self.last_search_text.strip() != self.get_text().strip(): + for label, entry in self.search_grid.fields: + if isinstance(entry, Gtk.ComboBoxText): + entry.set_active(-1) + elif isinstance(entry, Between): + entry.set_value(None, None) + elif isinstance(entry, Selection): + entry.treeview.get_selection().unselect_all() + else: + entry.set_text('') diff --git a/tryton/tryton/gui/window/win_csv.py b/tryton/tryton/gui/window/win_csv.py new file mode 100644 index 0000000000..4480070791 --- /dev/null +++ b/tryton/tryton/gui/window/win_csv.py @@ -0,0 +1,330 @@ +# 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 locale +import os +import sys +import gettext + +from gi.repository import Gdk, GObject, Gtk + +from tryton.common import IconFactory +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal + +_ = gettext.gettext + +encodings = ["ascii", "big5", "big5hkscs", "cp037", "cp424", "cp437", "cp500", + "cp720", "cp737", "cp775", "cp850", "cp852", "cp855", "cp856", "cp857", + "cp858", "cp860", "cp861", "cp862", "cp863", "cp864", "cp865", "cp866", + "cp869", "cp874", "cp875", "cp932", "cp949", "cp950", "cp1006", "cp1026", + "cp1140", "cp1250", "cp1251", "cp1252", "cp1253", "cp1254", "cp1255", + "cp1256", "cp1257", "cp1258", "euc_jp", "euc_jis_2004", "euc_jisx0213", + "euc_kr", "gb2312", "gbk", "gb18030", "hz", "iso2022_jp", "iso2022_jp_1", + "iso2022_jp_2", "iso2022_jp_2004", "iso2022_jp_3", "iso2022_jp_ext", + "iso2022_kr", "latin_1", "iso8859_2", "iso8859_3", "iso8859_4", + "iso8859_5", "iso8859_6", "iso8859_7", "iso8859_8", "iso8859_9", + "iso8859_10", "iso8859_13", "iso8859_14", "iso8859_15", "iso8859_16", + "johab", "koi8_r", "koi8_u", "mac_cyrillic", "mac_greek", "mac_iceland", + "mac_latin2", "mac_roman", "mac_turkish", "ptcp154", "shift_jis", + "shift_jis_2004", "shift_jisx0213", "utf_32", "utf_32_be", "utf_32_le", + "utf_16", "utf_16_be", "utf_16_le", "utf_7", "utf_8", "utf_8_sig"] + + +class WinCSV(NoModal): + def __init__(self, *args, **kwargs): + super(WinCSV, self).__init__(*args, **kwargs) + + self.dialog = Gtk.Dialog( + transient_for=self.parent, destroy_with_parent=True) + Main().add_window(self.dialog) + self.dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.dialog.set_icon(TRYTON_ICON) + self.dialog.set_default_size(*self.default_size()) + self.dialog.connect('response', self.response) + + dialog_vbox = Gtk.VBox() + + hbox_mapping = Gtk.HBox(homogeneous=True) + dialog_vbox.pack_start(hbox_mapping, expand=True, fill=True, padding=0) + + frame_fields = Gtk.Frame() + frame_fields.set_shadow_type(Gtk.ShadowType.NONE) + viewport_fields = Gtk.Viewport() + scrolledwindow_fields = Gtk.ScrolledWindow() + scrolledwindow_fields.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + viewport_fields.add(scrolledwindow_fields) + frame_fields.add(viewport_fields) + label_all_fields = Gtk.Label( + label=_('All fields'), use_markup=True) + frame_fields.set_label_widget(label_all_fields) + hbox_mapping.pack_start( + frame_fields, expand=True, fill=True, padding=0) + + vbox_buttons = Gtk.VBox(homogeneous=False, spacing=10) + vbox_buttons.set_border_width(5) + hbox_mapping.pack_start( + vbox_buttons, expand=False, fill=True, padding=0) + + button_add = Gtk.Button( + label=_('_Add'), stock=None, use_underline=True) + button_add.set_image(IconFactory.get_image( + 'tryton-add', Gtk.IconSize.BUTTON)) + button_add.set_always_show_image(True) + button_add.connect_after('clicked', self.sig_sel) + vbox_buttons.pack_start( + button_add, expand=False, fill=False, padding=0) + + button_remove = Gtk.Button( + label=_('_Remove'), stock=None, use_underline=True) + button_remove.set_image(IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.BUTTON)) + button_remove.set_always_show_image(True) + button_remove.connect_after('clicked', self.sig_unsel) + vbox_buttons.pack_start( + button_remove, expand=False, fill=False, padding=0) + + button_remove_all = Gtk.Button( + label=_('_Clear'), stock=None, use_underline=True) + button_remove_all.set_image(IconFactory.get_image( + 'tryton-clear', Gtk.IconSize.BUTTON)) + button_remove_all.set_always_show_image(True) + button_remove_all.connect_after('clicked', self.sig_unsel_all) + vbox_buttons.pack_start( + button_remove_all, expand=False, fill=False, padding=0) + + hseparator_buttons = Gtk.HSeparator() + vbox_buttons.pack_start( + hseparator_buttons, expand=False, fill=False, padding=3) + + self.add_buttons(vbox_buttons) + + frame_fields_selected = Gtk.Frame() + frame_fields_selected.set_shadow_type(Gtk.ShadowType.NONE) + viewport_fields_selected = Gtk.Viewport() + scrolledwindow_fields_selected = Gtk.ScrolledWindow() + scrolledwindow_fields_selected.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + viewport_fields_selected.add(scrolledwindow_fields_selected) + frame_fields_selected.add(viewport_fields_selected) + label_fields_selected = Gtk.Label( + label=_('Fields selected'), use_markup=True) + frame_fields_selected.set_label_widget(label_fields_selected) + hbox_mapping.pack_start( + frame_fields_selected, expand=True, fill=True, padding=0) + + vbox_csv_param = Gtk.VBox() + vbox_csv_param.props.margin = 7 + dialog_vbox.pack_start( + vbox_csv_param, expand=False, fill=True, padding=0) + + self.add_chooser(vbox_csv_param) + + expander_csv = Gtk.Expander() + vbox_csv_param.pack_start( + expander_csv, expand=False, fill=True, padding=0) + label_csv_param = Gtk.Label(label=_('CSV Parameters')) + expander_csv.set_label_widget(label_csv_param) + + box = Gtk.HBox(spacing=3) + expander_csv.add(box) + + label_csv_delimiter = Gtk.Label( + label=_('Delimiter:'), halign=Gtk.Align.END) + box.pack_start(label_csv_delimiter, expand=False, fill=True, padding=0) + self.csv_delimiter = Gtk.Entry() + self.csv_delimiter.set_max_length(1) + if os.name == 'nt' and ',' == locale.localeconv()['decimal_point']: + delimiter = ';' + else: + delimiter = ',' + self.csv_delimiter.set_text(delimiter) + self.csv_delimiter.set_width_chars(1) + label_csv_delimiter.set_mnemonic_widget(self.csv_delimiter) + box.pack_start( + self.csv_delimiter, expand=False, fill=True, padding=0) + + label_csv_quotechar = Gtk.Label( + label=_("Quote char:"), halign=Gtk.Align.END) + box.pack_start(label_csv_quotechar, expand=False, fill=True, padding=0) + self.csv_quotechar = Gtk.Entry() + self.csv_quotechar.set_text("\"") + self.csv_quotechar.set_width_chars(1) + label_csv_quotechar.set_mnemonic_widget(self.csv_quotechar) + box.pack_start(self.csv_quotechar, expand=False, fill=True, padding=0) + + label_csv_enc = Gtk.Label( + label=_("Encoding:"), halign=Gtk.Align.END) + box.pack_start(label_csv_enc, expand=False, fill=True, padding=0) + self.csv_enc = Gtk.ComboBoxText() + for i, encoding in enumerate(encodings): + self.csv_enc.append_text(encoding) + if ((os.name == 'nt' and encoding == 'cp1252') + or (os.name != 'nt' and encoding == 'utf_8')): + self.csv_enc.set_active(i) + label_csv_enc.set_mnemonic_widget(self.csv_enc) + box.pack_start(self.csv_enc, expand=False, fill=True, padding=0) + + self.csv_locale = Gtk.CheckButton(label=_("Use locale format")) + self.csv_locale.set_active(True) + box.pack_start(self.csv_locale, expand=False, fill=True, padding=0) + + self.add_csv_header_param(box) + + button_cancel = self.dialog.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + button_cancel.set_image(IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + + button_ok = self.dialog.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + button_ok.set_image(IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + + self.dialog.vbox.pack_start( + dialog_vbox, expand=True, fill=True, padding=0) + + self.view1 = Gtk.TreeView() + self.view1.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + self.view1.connect('row-expanded', self.on_row_expanded) + scrolledwindow_fields.add(self.view1) + self.view2 = Gtk.TreeView() + self.view2.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) + scrolledwindow_fields_selected.add(self.view2) + self.view1.set_headers_visible(False) + self.view2.set_headers_visible(False) + + cell = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_('Field name'), cell, text=0) + self.view1.append_column(column) + + cell = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(_('Field name'), cell, text=0) + self.view2.append_column(column) + + self.model1 = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING) + self.model2 = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING) + + self.model_populate(self._get_fields(self.model)) + + self.view1.set_model(self.model1) + self.view1.connect('row-activated', self.sig_sel) + self.view2.set_model(self.model2) + self.view2.connect('row-activated', self.sig_unsel) + + self.dialog.show_all() + self.show() + + self.register() + + if sys.platform != 'darwin': + self.view2.drag_source_set( + Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON3_MASK, + [Gtk.TargetEntry.new( + 'EXPORT_TREE', Gtk.TargetFlags.SAME_WIDGET, 0)], + Gdk.DragAction.MOVE) + self.view2.drag_dest_set( + Gtk.DestDefaults.ALL, + [Gtk.TargetEntry.new( + 'EXPORT_TREE', Gtk.TargetFlags.SAME_WIDGET, 0)], + Gdk.DragAction.MOVE) + self.view2.connect('drag-begin', self.drag_begin) + self.view2.connect('drag-motion', self.drag_motion) + self.view2.connect('drag-drop', self.drag_drop) + self.view2.connect("drag-data-get", self.drag_data_get) + self.view2.connect('drag-data-received', self.drag_data_received) + self.view2.connect('drag-data-delete', self.drag_data_delete) + + drag_column = Gtk.TreeViewColumn() + drag_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + cell_pixbuf = Gtk.CellRendererPixbuf() + cell_pixbuf.props.pixbuf = IconFactory.get_pixbuf('tryton-drag') + drag_column.pack_start(cell_pixbuf, expand=False) + self.view2.insert_column(drag_column, 0) + + def drag_begin(self, treeview, context): + return True + + def drag_motion(self, treeview, context, x, y, time): + try: + treeview.set_drag_dest_row(*treeview.get_dest_row_at_pos(x, y)) + except TypeError: + treeview.set_drag_dest_row( + Gtk.TreePath(len(treeview.get_model()) - 1), + Gtk.TreeViewDropPosition.AFTER) + Gdk.drag_status(context, Gdk.DragAction.MOVE, time) + return True + + def drag_drop(self, treeview, context, x, y, time): + treeview.stop_emission_by_name('drag-drop') + return True + + def drag_data_get(self, treeview, context, selection, target_id, + etime): + treeview.stop_emission_by_name('drag-data-get') + + def _func_sel_get(store, path, iter_, data): + data.append(path[0]) + data = [] + treeselection = treeview.get_selection() + treeselection.selected_foreach(_func_sel_get, data) + if not data: + return + data = ','.join(str(x) for x in data) + selection.set(selection.get_target(), 8, data.encode('utf-8')) + return True + + def drag_data_received(self, treeview, context, x, y, selection, + info, etime): + treeview.stop_emission_by_name('drag-data-received') + try: + selection_data = selection.data + except AttributeError: + selection_data = selection.get_data() + if not selection_data: + return + selection_data = selection_data.decode('utf-8') + store = treeview.get_model() + + data_iters = [store.get_iter((int(i),)) + for i in selection_data.split(',')] + drop_info = treeview.get_dest_row_at_pos(x, y) + if drop_info: + path, position = drop_info + pos = store.get_iter(path) + else: + pos = store.get_iter((len(store) - 1,)) + position = Gtk.TreeViewDropPosition.AFTER + if position == Gtk.TreeViewDropPosition.AFTER: + data_iters = reversed(data_iters) + for item in data_iters: + if position == Gtk.TreeViewDropPosition.BEFORE: + store.move_before(item, pos) + else: + store.move_after(item, pos) + Gdk.drop_finish(context, False, etime) + return True + + def drag_data_delete(self, treeview, context): + treeview.stop_emission_by_name('drag-data-delete') + + def get_delimiter(self): + return self.csv_delimiter.get_text() or ',' + + def get_quotechar(self): + return self.csv_quotechar.get_text() or '"' + + def get_encoding(self): + return self.csv_enc.get_active_text() or 'utf_8' + + def destroy(self): + super(WinCSV, self).destroy() + self.dialog.destroy() + + def show(self): + self.dialog.show() + + def hide(self): + self.dialog.hide() diff --git a/tryton/tryton/gui/window/win_export.py b/tryton/tryton/gui/window/win_export.py new file mode 100644 index 0000000000..9f72896220 --- /dev/null +++ b/tryton/tryton/gui/window/win_export.py @@ -0,0 +1,519 @@ +# 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 +import datetime +import os +import tempfile +import gettext +import json +import locale +import urllib.parse +from itertools import zip_longest +from numbers import Number + +from gi.repository import Gdk, GObject, Gtk + +import tryton.common as common +from tryton.config import CONFIG +from tryton.common import RPCExecute, RPCException +from tryton.gui.window.win_csv import WinCSV +from tryton.jsonrpc import JSONEncoder +from tryton.rpc import clear_cache, CONNECTION + +_ = gettext.gettext + + +class WinExport(WinCSV): + "Window export" + + def __init__(self, name, screen): + self.name = name + self.screen = screen + self.fields = {} + super(WinExport, self).__init__() + self.dialog.set_title(_('CSV Export: %s') % name) + # Hide as selected record is the default + self.ignore_search_limit.hide() + + @property + def model(self): + return self.screen.model_name + + @property + def context(self): + return self.screen.context + + @property + def screen_is_tree(self): + return bool( + self.screen.current_view + and self.screen.current_view.view_type == 'tree' + and self.screen.current_view.children_field) + + def add_buttons(self, box): + button_save_export = Gtk.Button( + label=_('_Save Export'), stock=None, use_underline=True) + button_save_export.set_image(common.IconFactory.get_image( + 'tryton-save', Gtk.IconSize.BUTTON)) + button_save_export.set_always_show_image(True) + button_save_export.connect_after('clicked', self.addreplace_predef) + box.pack_start( + button_save_export, expand=False, fill=False, padding=0) + + button_url = Gtk.MenuButton( + label=_("_URL Export"), stock=None, use_underline=True) + button_url.set_image(common.IconFactory.get_image( + 'tryton-public', Gtk.IconSize.BUTTON)) + button_url.set_always_show_image(True) + box.pack_start(button_url, expand=False, fill=False, padding=0) + menu_url = Gtk.Menu() + menuitem_url = Gtk.MenuItem() + menuitem_url.connect('activate', self.copy_url) + menu_url.add(menuitem_url) + menu_url.show_all() + button_url.set_popup(menu_url) + button_url.connect('button-press-event', self.set_url, menuitem_url) + + button_del_export = Gtk.Button( + label=_('_Delete Export'), stock=None, use_underline=True) + button_del_export.set_image(common.IconFactory.get_image( + 'tryton-delete', Gtk.IconSize.BUTTON)) + button_del_export.set_always_show_image(True) + button_del_export.connect_after('clicked', self.remove_predef) + box.pack_start(button_del_export, expand=False, fill=False, padding=0) + + frame_predef_exports = Gtk.Frame() + frame_predef_exports.set_border_width(2) + frame_predef_exports.set_shadow_type(Gtk.ShadowType.NONE) + box.pack_start(frame_predef_exports, expand=True, fill=True, padding=0) + viewport_exports = Gtk.Viewport() + scrolledwindow_exports = Gtk.ScrolledWindow() + scrolledwindow_exports.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + label_predef_exports = Gtk.Label( + label=_("Predefined exports"), use_markup=True) + frame_predef_exports.set_label_widget(label_predef_exports) + viewport_exports.add(scrolledwindow_exports) + frame_predef_exports.add(viewport_exports) + + self.pref_export = Gtk.TreeView() + self.pref_export.append_column( + Gtk.TreeViewColumn(_('Name'), Gtk.CellRendererText(), text=2)) + scrolledwindow_exports.add(self.pref_export) + self.pref_export.connect("button-press-event", self.export_click) + self.pref_export.connect("key-press-event", self.export_keypress) + + self.predef_model = Gtk.ListStore( + GObject.TYPE_INT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_STRING) + self.fill_predefwin() + + def add_chooser(self, box): + hbox_csv_export = Gtk.HBox() + box.pack_start(hbox_csv_export, expand=False, fill=True, padding=0) + self.saveas = Gtk.ComboBoxText() + hbox_csv_export.pack_start( + self.saveas, expand=True, fill=True, padding=3) + self.saveas.append_text(_("Open")) + self.saveas.append_text(_("Save")) + self.saveas.set_active(0) + + self.selected_records = Gtk.ComboBoxText() + hbox_csv_export.pack_start( + self.selected_records, expand=True, fill=True, padding=3) + self.selected_records.append_text(_("Listed Records")) + self.selected_records.append_text(_("Selected Records")) + if not self.screen_is_tree: + self.selected_records.set_active(1) + else: + self.selected_records.set_active(0) + + self.ignore_search_limit = Gtk.CheckButton( + label=_("Ignore search limit")) + hbox_csv_export.pack_start( + self.ignore_search_limit, expand=False, fill=True, padding=3) + + self.selected_records.connect( + 'changed', + lambda w: self.ignore_search_limit.set_visible( + not w.get_active() and not self.screen_is_tree)) + + def add_csv_header_param(self, box): + self.add_field_names = Gtk.CheckButton(label=_("Add field names")) + self.add_field_names.set_active(True) + box.pack_start( + self.add_field_names, expand=False, fill=True, padding=0) + + def model_populate(self, fields, parent_node=None, prefix_field='', + prefix_name=''): + + def key(n_f): + return n_f[1].get('string') or n_f[0] + for name, field in sorted(list(fields.items()), key=key, reverse=True): + + string_ = field['string'] or name + + items = [(name, field, string_)] + if field['type'] == 'selection': + items.insert(0, ('%s.translated' % name, field, + _('%s (string)') % string_)) + elif field['type'] == 'reference': + items.insert(0, ('%s.translated' % name, field, + _('%s (model name)') % string_)) + items.insert(0, ('%s/rec_name' % name, field, + _("%s (record name)") % string_)) + + for name, field, string_ in items: + path = prefix_field + name + node = self.model1.insert(parent_node, 0, + [string_, path]) + string_ = prefix_name + string_ + + self.fields[path] = (string_, field.get('relation')) + # Insert relation only to real field + if '.' not in name: + if field.get('relation'): + self.model1.insert(node, 0, [None, '']) + + def _get_fields(self, model): + try: + return RPCExecute('model', model, 'fields_get', None, + context=self.context) + except RPCException: + return '' + + def on_row_expanded(self, treeview, iter, path): + child = self.model1.iter_children(iter) + if child and self.model1.get_value(child, 0) is None: + prefix_field = self.model1.get_value(iter, 1) + string_, relation = self.fields[prefix_field] + self.model_populate(self._get_fields(relation), iter, + prefix_field + '/', string_ + '/') + self.model1.remove(child) + + def sig_sel(self, *args): + sel = self.view1.get_selection() + sel.selected_foreach(self._sig_sel_add) + + def _sig_sel_add(self, store, path, iter): + name = store.get_value(iter, 1) + self.sel_field(name) + + def sig_unsel(self, *args): + store, paths = self.view2.get_selection().get_selected_rows() + # Convert first into TreeIter before removing from the store + iters = [store.get_iter(p) for p in paths] + for i in iters: + store.remove(i) + + def sig_unsel_all(self, *args): + self.model2.clear() + + def fill_predefwin(self): + try: + exports = RPCExecute( + 'model', 'ir.export', 'search_read', + [('resource', '=', self.model)], 0, None, None, + ['name', 'export_fields.name'], + context=self.context) + except RPCException: + return + for export in exports: + self.predef_model.append(( + export['id'], + [f['name'] for f in export['export_fields.']], + export['name'])) + self.pref_export.set_model(self.predef_model) + + def addreplace_predef(self, widget): + iter = self.model2.get_iter_first() + fields = [] + while iter: + field_name = self.model2.get_value(iter, 1) + fields.append(field_name) + iter = self.model2.iter_next(iter) + if not fields: + return + + selection = self.pref_export.get_selection().get_selected() + if selection is None: + return + model, iter_ = selection + if iter_ is None: + pref_id = None + name = common.ask(_('What is the name of this export?')) + if not name: + return + else: + pref_id = model.get_value(iter_, 0) + name = model.get_value(iter_, 2) + override = common.sur(_("Override '%s' definition?") % name) + if not override: + return + try: + if not pref_id: + pref_id, = RPCExecute('model', 'ir.export', 'create', [{ + 'name': name, + 'resource': self.model, + 'export_fields': [('create', [{ + 'name': x, + } for x in fields])], + }], context=self.context) + else: + RPCExecute('model', 'ir.export', 'update', [pref_id], fields, + context=self.context) + except RPCException: + return + clear_cache('model.%s.view_toolbar_get' % self.model) + if iter_ is None: + self.predef_model.append((pref_id, fields, name)) + else: + model.set_value(iter_, 0, pref_id) + model.set_value(iter_, 1, fields) + + def remove_predef(self, widget): + sel = self.pref_export.get_selection().get_selected() + if sel is None: + return None + (model, i) = sel + if not i: + return None + export_id = model.get_value(i, 0) + try: + RPCExecute('model', 'ir.export', 'delete', [export_id], + context=self.context) + except RPCException: + return + clear_cache('model.%s.view_toolbar_get' % self.model) + for i in range(len(self.predef_model)): + if self.predef_model[i][0] == export_id: + del self.predef_model[i] + break + self.pref_export.set_model(self.predef_model) + + def sel_predef(self, path): + self.model2.clear() + for name in self.predef_model[path[0]][1]: + if name not in self.fields: + iter = self.model1.get_iter_first() + prefix = '' + for parent in name.split('/')[:-1]: + while iter: + if self.model1.get_value(iter, 1) == \ + (prefix + parent): + self.on_row_expanded(self.view1, iter, + self.model1.get_path(iter)) + iter = self.model1.iter_children(iter) + prefix += parent + '/' + break + else: + iter = self.model1.iter_next(iter) + + if name not in self.fields: + continue + self.sel_field(name) + + def sel_field(self, name): + string_, relation = self.fields[name] + if relation: + name += '/rec_name' + self.model2.append((string_, name)) + + def response(self, dialog, response): + if response == Gtk.ResponseType.OK: + fields = [] + fields2 = [] + iter = self.model2.get_iter_first() + while iter: + fields.append(self.model2.get_value(iter, 1)) + fields2.append(self.model2.get_value(iter, 0)) + iter = self.model2.iter_next(iter) + + if self.selected_records.get_active(): + ids = [r.id for r in self.screen.selected_records] + paths = self.screen.selected_paths + try: + data = RPCExecute( + 'model', self.model, 'export_data', + ids, fields, + context=self.context) + except RPCException: + data = [] + elif self.screen_is_tree: + ids = [r.id for r in self.screen.listed_records] + paths = self.screen.listed_paths + try: + data = RPCExecute( + 'model', self.model, 'export_data', + ids, fields, + context=self.context) + except RPCException: + data = [] + else: + paths = None + domain = self.screen.search_domain( + self.screen.screen_container.get_text()) + if self.ignore_search_limit.get_active(): + offset, limit = 0, None + else: + offset, limit = self.screen.offset, self.screen.limit + try: + data = RPCExecute( + 'model', self.model, 'export_data_domain', + domain, fields, offset, limit, self.screen.order, + context=self.context) + except RPCException: + data = [] + + if self.saveas.get_active(): + fname = common.file_selection(_('Save As...'), + action=Gtk.FileChooserAction.SAVE) + if fname: + self.export_csv(fname, fields2, data, paths) + else: + fileno, fname = tempfile.mkstemp( + '.csv', common.slugify(self.name) + '_') + self.export_csv(fname, fields2, data, paths, popup=False) + os.close(fileno) + common.file_open(fname, 'csv') + self.destroy() + + def export_csv(self, fname, fields, data, paths, popup=True): + encoding = self.csv_enc.get_active_text() or 'UTF-8' + locale_format = self.csv_locale.get_active() + + try: + writer = csv.writer( + open(fname, 'w', encoding=encoding, newline=''), + quotechar=self.get_quotechar(), + delimiter=self.get_delimiter()) + if self.add_field_names.get_active(): + writer.writerow(fields) + for row, path in zip_longest(data, paths or []): + indent = len(path) - 1 if path else 0 + if row: + writer.writerow(self.format_row( + row, indent=indent, locale_format=locale_format)) + if popup: + if len(data) == 1: + common.message(_('%d record saved.') % len(data)) + else: + common.message(_('%d records saved.') % len(data)) + return True + except IOError as exception: + common.warning(_("Operation failed.\nError message:\n%s") + % exception, _('Error')) + return False + + @classmethod + def format_row(cls, line, indent=0, locale_format=True): + row = [] + for i, val in enumerate(line): + if locale_format: + if isinstance(val, Number): + val = locale.str(val) + elif isinstance(val, datetime.datetime): + val = val.strftime(common.date_format() + ' %X') + elif isinstance(val, datetime.date): + val = val.strftime(common.date_format()) + elif isinstance(val, datetime.timedelta): + val = common.timedelta.format( + val, {'s': 1, 'm': 60, 'h': 60 * 60}) + elif isinstance(val, datetime.timedelta): + val = val.total_seconds() + elif isinstance(val, bool): + val = int(val) + if i == 0 and indent and isinstance(val, str): + val = ' ' * indent + val + row.append(val) + return row + + def export_click(self, treeview, event): + path_at_pos = treeview.get_path_at_pos(int(event.x), int(event.y)) + if not path_at_pos or event.button != 1: + return + path, col, x, y = path_at_pos + selection = treeview.get_selection() + if event.type == Gdk.EventType._2BUTTON_PRESS: + self.sel_predef(path) + selection.select_path(path) + return True + elif selection.path_is_selected(path): + selection.unselect_path(path) + return True + + def export_keypress(self, treeview, event): + if event.keyval not in [Gdk.KEY_Return, Gdk.KEY_space]: + return + model, selected = treeview.get_selection().get_selected() + if not selected: + return + self.sel_predef(model.get_path(selected)) + + def get_url(self): + path = [CONFIG['login.db'], 'data', self.model] + protocol = 'https' if CONNECTION.ssl else 'http' + host = common.get_hostname(CONFIG['login.host']) + port = common.get_port(CONFIG['login.host']) + if protocol == 'https' and port == 445: + netloc = host + elif protocol == 'http' and port == 80: + netloc = host + else: + netloc = '%s:%s' % (host, port) + query_string = [] + if self.selected_records.get_active(): + domain = [r.id for r in self.screen.selected_records] + else: + domain = self.screen.search_domain( + self.screen.screen_container.get_text()) + if not self.ignore_search_limit.get_active(): + query_string.append(('s', str(self.screen.limit))) + query_string.append( + ('p', str(self.screen.offset // self.screen.limit))) + if self.screen.order: + for expr in self.screen.order: + query_string.append(('o', ','.join(filter(None, expr)))) + query_string.insert(0, ('d', json.dumps( + domain, cls=JSONEncoder, separators=(',', ':')))) + if self.screen.local_context: + query_string.append(('c', json.dumps( + self.screen.local_context, + cls=JSONEncoder, separators=(',', ':')))) + + iter_ = self.model2.get_iter_first() + while iter_: + query_string.append(('f', self.model2.get_value(iter_, 1))) + iter_ = self.model2.iter_next(iter_) + + encoding = self.csv_enc.get_active_text() + if encoding: + query_string.append(('enc', encoding)) + + query_string.append(('dl', self.get_delimiter())) + query_string.append(('qc', self.get_quotechar())) + if not self.add_field_names.get_active(): + query_string.append(('h', '0')) + if self.csv_locale.get_active(): + query_string.append(('loc', '1')) + + query_string = urllib.parse.urlencode(query_string) + return urllib.parse.urlunparse(( + protocol, netloc, '/'.join(path), '', query_string, '')) + + def set_url(self, button, event, menuitem): + url = self.get_url() + size = 80 + if len(url) > size: + url = url[:size // 2] + '...' + url[-size // 2:] + menuitem.set_label(url) + + def copy_url(self, menuitem): + url = self.get_url() + for selection in [ + Gdk.Atom.intern('PRIMARY', True), + Gdk.Atom.intern('CLIPBOARD', True), + ]: + clipboard = Gtk.Clipboard.get(selection) + clipboard.set_text(url, -1) diff --git a/tryton/tryton/gui/window/win_form.py b/tryton/tryton/gui/window/win_form.py new file mode 100644 index 0000000000..341889f152 --- /dev/null +++ b/tryton/tryton/gui/window/win_form.py @@ -0,0 +1,477 @@ +# 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 itertools import chain +import gettext + +from gi.repository import Gdk, Gtk, Pango + +from .infobar import InfoBar +import tryton.common as common +from tryton.common import TRYTON_ICON, MODELNAME +from tryton.common.domain_parser import quote +from tryton.common.underline import set_underline +from tryton.common.widget_style import widget_class +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal + +_ = gettext.gettext + + +class WinForm(NoModal, InfoBar): + "Form window" + + def __init__(self, screen, callback, view_type='form', + new=False, many=0, domain=None, context=None, + save_current=False, title='', rec_name=None): + tooltips = common.Tooltips() + NoModal.__init__(self) + self.screen = screen + self.callback = callback + self.many = many + self.domain = domain + self.context = context + self.save_current = save_current + if screen.breadcrumb: + breadcrumb = list(screen.breadcrumb) + if title: + breadcrumb.append(title) + self.title = ' › '.join(chain( + (common.ellipsize(x, 30) for x in breadcrumb[-3:-1]), + breadcrumb[-1:])) + if len(breadcrumb) > 3: + self.title = '... › ' + self.title + else: + if not title: + title = MODELNAME.get(screen.model_name) + self.title = title + self.prev_view = self.screen.current_view + self.screen.screen_container.alternate_view = True + self.screen.switch_view(view_type=view_type) + if self.screen.current_view.view_type != view_type: + self.destroy() + return + if new: + self.screen.new(rec_name=rec_name) + self.win = Gtk.Dialog( + title=_('Link'), transient_for=self.parent, + destroy_with_parent=True) + Main().add_window(self.win) + self.win.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.win.set_icon(TRYTON_ICON) + self.win.set_deletable(False) + self.win.connect('delete-event', lambda *a: True) + self.win.connect('close', self.close) + self.win.connect('response', self.response) + + self.win.set_default_size(*self.default_size()) + + self.accel_group = Gtk.AccelGroup() + self.win.add_accel_group(self.accel_group) + + readonly = self.screen.readonly or self.screen.group.readonly + + self.but_ok = None + self.but_new = None + + self._initial_value = None + if view_type == 'form': + if new: + label, icon = _("Delete"), 'tryton-delete' + else: + label, icon = _("Cancel"), 'tryton-cancel' + record = self.screen.current_record + self._initial_value = record.get_on_change_value() + if record.parent and record.parent_name in record.group.fields: + parent_field = record.group.fields[record.parent_name] + self._initial_value[record.parent_name] = ( + parent_field.get_eval(record)) + self.but_cancel = self.win.add_button( + set_underline(label), Gtk.ResponseType.CANCEL) + self.but_cancel.set_image(common.IconFactory.get_image( + icon, Gtk.IconSize.BUTTON)) + self.but_cancel.set_always_show_image(True) + + if new and self.many: + self.but_new = self.win.add_button( + set_underline(_("New")), Gtk.ResponseType.ACCEPT) + self.but_new.set_image(common.IconFactory.get_image( + 'tryton-create', Gtk.IconSize.BUTTON)) + self.but_new.set_always_show_image(True) + self.but_new.set_accel_path('/Form/New', self.accel_group) + + if self.save_current: + self.but_ok = Gtk.Button(label=_('_Save'), use_underline=True) + self.but_ok.set_image(common.IconFactory.get_image( + 'tryton-save', Gtk.IconSize.BUTTON)) + self.but_ok.set_always_show_image(True) + self.but_ok.set_accel_path('/Form/Save', self.accel_group) + self.but_ok.set_can_default(True) + self.but_ok.show() + self.win.add_action_widget(self.but_ok, Gtk.ResponseType.OK) + if not new: + self.but_ok.props.sensitive = False + else: + self.but_ok = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + self.but_ok.set_image(common.IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + self.but_ok.set_always_show_image(True) + self.but_ok.add_accelerator('clicked', self.accel_group, + Gdk.KEY_Return, Gdk.ModifierType.CONTROL_MASK, + Gtk.AccelFlags.VISIBLE) + self.win.set_default_response(Gtk.ResponseType.OK) + + self.win.set_title(self.title) + + revision = self.screen.context.get('_datetime') + if revision and self.screen.model_name in common.MODELHISTORY: + format_ = self.screen.context.get('date_format', '%x') + format_ += ' %H:%M:%S.%f' + revision_label = ' @ %s' % revision.strftime(format_) + label = common.ellipsize( + self.title, 80 - len(revision_label)) + revision_label + tooltip = self.title + revision_label + else: + label = common.ellipsize(self.title, 80) + tooltip = self.title + + title = Gtk.Label( + label=label, + halign=Gtk.Align.START, margin=5, + ellipsize=Pango.EllipsizeMode.END) + tooltips.set_tip(title, tooltip) + title.set_size_request(0, -1) # Allow overflow + title.show() + + hbox = Gtk.HBox() + hbox.pack_start(title, expand=True, fill=True, padding=0) + hbox.show() + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + widget_class(frame, 'window-title', True) + frame.add(hbox) + frame.show() + + self.win.vbox.pack_start(frame, expand=False, fill=True, padding=3) + + if view_type == 'tree': + hbox = Gtk.HBox(homogeneous=False, spacing=0) + hbox.set_halign(Gtk.Align.END) + access = common.MODELACCESS[screen.model_name] + + but_switch = Gtk.Button() + tooltips.set_tip(but_switch, _('Switch')) + but_switch.connect('clicked', self.switch_view) + but_switch.add(common.IconFactory.get_image( + 'tryton-switch', Gtk.IconSize.SMALL_TOOLBAR)) + but_switch.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(but_switch, expand=False, fill=False, padding=0) + + self.but_pre = Gtk.Button() + tooltips.set_tip(self.but_pre, _('Previous')) + self.but_pre.connect('clicked', self._sig_previous) + self.but_pre.add(common.IconFactory.get_image( + 'tryton-back', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_pre.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_pre, expand=False, fill=False, padding=0) + + self.label = Gtk.Label(label='(0,0)') + hbox.pack_start(self.label, expand=False, fill=False, padding=0) + + self.but_next = Gtk.Button() + tooltips.set_tip(self.but_next, _('Next')) + self.but_next.connect('clicked', self._sig_next) + self.but_next.add(common.IconFactory.get_image( + 'tryton-forward', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_next.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_next, expand=False, fill=False, padding=0) + + hbox.pack_start( + Gtk.VSeparator(), expand=False, fill=True, padding=0) + + if domain is not None: + self.wid_text = Gtk.Entry() + self.wid_text.set_property('width_chars', 13) + self.wid_text.connect('activate', self._sig_activate) + self.wid_text.connect('focus-out-event', self._focus_out) + hbox.pack_start( + self.wid_text, expand=True, fill=True, padding=0) + + self.but_add = Gtk.Button() + tooltips.set_tip(self.but_add, _('Add')) + self.but_add.connect('clicked', self._sig_add) + self.but_add.add(common.IconFactory.get_image( + 'tryton-add', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_add.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start( + self.but_add, expand=False, fill=False, padding=0) + if not access['read'] or readonly: + self.but_add.set_sensitive(False) + + self.but_remove = Gtk.Button() + tooltips.set_tip(self.but_remove, _('Remove ')) + self.but_remove.connect('clicked', self._sig_remove, True) + self.but_remove.add(common.IconFactory.get_image( + 'tryton-remove', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_remove.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start( + self.but_remove, expand=False, fill=False, padding=0) + if not access['read'] or readonly: + self.but_remove.set_sensitive(False) + + hbox.pack_start( + Gtk.VSeparator(), expand=False, fill=True, padding=0) + + self.but_new = Gtk.Button() + tooltips.set_tip(self.but_new, _('Create a new record ')) + self.but_new.connect('clicked', self._sig_new) + self.but_new.add(common.IconFactory.get_image( + 'tryton-create', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_new.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_new, expand=False, fill=False, padding=0) + if not access['create'] or readonly: + self.but_new.set_sensitive(False) + + self.but_del = Gtk.Button() + tooltips.set_tip(self.but_del, _('Delete selected record ')) + self.but_del.connect('clicked', self._sig_remove, False) + self.but_del.add(common.IconFactory.get_image( + 'tryton-delete', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_del.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start(self.but_del, expand=False, fill=False, padding=0) + if not access['delete'] or readonly: + self.but_del.set_sensitive(False) + + self.but_undel = Gtk.Button() + tooltips.set_tip(self.but_undel, + _('Undelete selected record ')) + self.but_undel.connect('clicked', self._sig_undelete) + self.but_undel.add(common.IconFactory.get_image( + 'tryton-undo', Gtk.IconSize.SMALL_TOOLBAR)) + self.but_undel.set_relief(Gtk.ReliefStyle.NONE) + hbox.pack_start( + self.but_undel, expand=False, fill=False, padding=0) + if not access['delete'] or readonly: + self.but_undel.set_sensitive(False) + + but_switch.props.sensitive = screen.number_of_views > 1 + + tooltips.enable() + hbox.show_all() + + self.win.vbox.pack_start( + hbox, expand=False, fill=True, padding=0) + + scroll = Gtk.ScrolledWindow() + scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scroll.set_placement(Gtk.CornerType.TOP_LEFT) + scroll.set_shadow_type(Gtk.ShadowType.NONE) + scroll.show() + self.win.vbox.pack_start(scroll, expand=True, fill=True, padding=0) + + scroll.add(self.screen.screen_container.alternate_viewport) + + self.create_info_bar() + self.win.vbox.pack_start( + self.info_bar, expand=False, fill=True, padding=0) + + if view_type == 'tree': + self.screen.signal_connect(self, 'record-message', self._sig_label) + self.screen.screen_container.alternate_viewport.connect( + 'key-press-event', self.on_keypress) + + if self.save_current and not new: + self.screen.signal_connect(self, 'record-message', + self.activate_save) + self.screen.signal_connect(self, 'record-modified', + self.activate_save) + + self.register() + self.show() + + self.screen.display() + self.screen.current_view.set_cursor() + + def on_keypress(self, widget, event): + if ((event.keyval == Gdk.KEY_F3) + and self.but_new.get_property('sensitive')): + self._sig_new(widget) + return False + if (event.keyval in [Gdk.KEY_Delete, Gdk.KEY_KP_Delete] + and widget == self.screen.screen_container.alternate_viewport): + self._sig_remove(widget) + return False + + def switch_view(self, widget): + self.screen.switch_view() + + def _sig_new(self, widget): + self.screen.new() + self.screen.current_view.widget.set_sensitive(True) + + def _sig_next(self, widget): + self.screen.display_next() + + def _sig_previous(self, widget): + self.screen.display_prev() + + def _sig_remove(self, widget, remove=False): + self.screen.remove(remove=remove) + + def _sig_undelete(self, button): + self.screen.unremove() + + def _sig_activate(self, *args): + self._sig_add() + self.wid_text.grab_focus() + + def _focus_out(self, *args): + if self.wid_text.get_text(): + self._sig_add() + + def _sig_add(self, *args): + from tryton.gui.window.win_search import WinSearch + domain = self.domain[:] + model_name = self.screen.model_name + value = self.wid_text.get_text() + + def callback(result): + if result: + ids = [x[0] for x in result] + self.screen.load(ids, modified=True) + self.screen.display(res_id=ids[0]) + self.screen.set_cursor() + self.wid_text.set_text('') + + win = WinSearch(model_name, callback, sel_multi=True, + context=self.context, domain=domain) + win.screen.search_filter(quote(value)) + win.show() + + def _sig_label(self, screen, signal_data): + name = '_' + access = common.MODELACCESS[screen.model_name] + deletable = True + if screen.current_record: + deletable = screen.current_record.deletable + readonly = screen.group.readonly + if signal_data[0] >= 1: + name = str(signal_data[0]) + if self.domain is not None: + self.but_remove.set_sensitive(True) + if signal_data[0] < signal_data[1]: + self.but_next.set_sensitive(True) + else: + self.but_next.set_sensitive(False) + if signal_data[0] > 1: + self.but_pre.set_sensitive(True) + else: + self.but_pre.set_sensitive(False) + if access['delete'] and not readonly and deletable: + self.but_del.set_sensitive(True) + self.but_undel.set_sensitive(True) + else: + self.but_del.set_sensitive(False) + self.but_undel.set_sensitive(False) + self.but_next.set_sensitive(False) + self.but_pre.set_sensitive(False) + if self.domain is not None: + self.but_remove.set_sensitive(False) + line = '(%s/%s)' % (name, signal_data[1]) + self.label.set_text(line) + + def activate_save(self, *args): + modified = self.screen.modified() + # Keep sensible as change could have been trigger by a Many2One edition + sensitive = modified or self.but_ok.props.sensitive + self.but_ok.props.sensitive = sensitive + self.win.set_default_response( + Gtk.ResponseType.OK if sensitive else Gtk.ResponseType.CANCEL) + + def close(self, widget): + widget.stop_emission_by_name('close') + self.response(self.win, Gtk.ResponseType.CANCEL) + return True + + def response(self, win, response_id): + validate = False + cancel_responses = [ + Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT] + self.screen.current_view.set_value() + readonly = self.screen.group.readonly + if (response_id not in cancel_responses + and not readonly + and self.screen.current_record is not None): + validate = self.screen.current_record.validate( + self.screen.current_view.get_fields()) + if validate and self.screen.pre_validate: + validate = self.screen.current_record.pre_validate() + if validate and self.save_current: + if not self.screen.save_current(): + validate = False + elif validate and self.screen.current_view.view_type == 'form': + view = self.screen.current_view + for widgets in view.widgets.values(): + for widget in widgets: + if (hasattr(widget, 'screen') + and widget.screen.pre_validate): + record = widget.screen.current_record + if record: + validate = record.pre_validate() + if not validate: + self.message_info(self.screen.invalid_message(), + Gtk.MessageType.ERROR) + self.screen.set_cursor() + self.screen.display() + return + self.message_info() + if response_id == Gtk.ResponseType.ACCEPT: + self.new() + return + if (self.screen.current_record + and not readonly + and response_id in cancel_responses): + record = self.screen.current_record + added = 'id' in record.modified_fields + if (self.screen.current_record.id < 0 + or self.save_current): + self.screen.cancel_current(self._initial_value) + elif record.modified: + record.cancel() + record.reload() + record.signal('record-changed') + if added: + record.modified_fields.setdefault('id') + result = False + else: + result = response_id not in cancel_responses + self.callback(result) + self.destroy() + + def new(self): + self.screen.new() + self._initial_value = None + self.screen.current_view.display() + self.screen.set_cursor(new=True) + self.many -= 1 + if self.many == 0: + self.but_new.set_sensitive(False) + self.win.set_default_response(Gtk.ResponseType.OK) + + def destroy(self): + self.screen.screen_container.alternate_view = False + viewport = self.screen.screen_container.alternate_viewport + if viewport and viewport.get_parent(): + viewport.get_parent().remove(viewport) + self.screen.switch_view(view_type=self.prev_view.view_type) + self.screen.signal_unconnect(self) + if getattr(self, 'win', None): + self.win.destroy() + NoModal.destroy(self) + + def show(self): + self.win.show() + + def hide(self): + self.win.hide() diff --git a/tryton/tryton/gui/window/win_import.py b/tryton/tryton/gui/window/win_import.py new file mode 100644 index 0000000000..1fb93d4446 --- /dev/null +++ b/tryton/tryton/gui/window/win_import.py @@ -0,0 +1,225 @@ +# 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 +import gettext +import locale +from decimal import Decimal + +from gi.repository import Gtk + +import tryton.common as common +from tryton.common import RPCExecute, RPCException +from tryton.common.datetime_ import date_parse +from tryton.gui.window.win_csv import WinCSV + +_ = gettext.gettext + + +class WinImport(WinCSV): + "Window import" + + def __init__(self, name, model, context): + self.name = name + self.model = model + self.context = context + self.fields_data = {} + self.fields = {} + self.fields_invert = {} + super(WinImport, self).__init__() + self.dialog.set_title(_('CSV Import: %s') % name) + + def add_buttons(self, box): + button_autodetect = Gtk.Button( + label=_('_Auto-Detect'), stock=None, use_underline=True) + button_autodetect.set_image(common.IconFactory.get_image( + 'tryton-search', Gtk.IconSize.BUTTON)) + button_autodetect.set_always_show_image(True) + button_autodetect.connect_after('clicked', self.sig_autodetect) + box.pack_start( + button_autodetect, expand=False, fill=False, padding=0) + + def add_chooser(self, box): + hbox_csv_import = Gtk.HBox() + box.pack_start(hbox_csv_import, expand=False, fill=True, padding=4) + label_csv_import = Gtk.Label(label=_("File to Import:")) + hbox_csv_import.pack_start(label_csv_import, False, False, 0) + self.import_csv_file = Gtk.FileChooserButton(title=_("Open...")) + label_csv_import.set_mnemonic_widget(self.import_csv_file) + hbox_csv_import.pack_start( + self.import_csv_file, expand=True, fill=True, padding=0) + + def add_csv_header_param(self, box): + label_csv_skip = Gtk.Label( + label=_('Lines to Skip:'), halign=Gtk.Align.START) + box.pack_start(label_csv_skip, expand=False, fill=True, padding=0) + + self.csv_skip = Gtk.SpinButton() + self.csv_skip.configure(Gtk.Adjustment( + value=0, lower=0, upper=100, + step_increment=1, page_increment=10), + 1, 0) + label_csv_skip.set_mnemonic_widget(self.csv_skip) + box.pack_start(self.csv_skip, expand=False, fill=True, padding=0) + + def model_populate(self, fields, parent_node=None, prefix_field='', + prefix_name=''): + fields_order = list(fields.keys()) + fields_order.sort( + key=lambda x: fields[x].get('string', ''), reverse=True) + for field in fields_order: + if not fields[field].get('readonly', False) or field == 'id': + self.fields_data[prefix_field + field] = fields[field] + name = fields[field]['string'] or field + node = self.model1.insert( + parent_node, 0, [name, prefix_field + field]) + name = prefix_name + name + # Only One2Many can be nested for import + if fields[field]['type'] == 'one2many': + relation = fields[field].get('relation') + else: + relation = None + self.fields[prefix_field + field] = (name, relation) + self.fields_invert[name] = prefix_field + field + if relation: + self.model1.insert(node, 0, [None, '']) + + def _get_fields(self, model): + try: + return RPCExecute('model', model, 'fields_get', None, + context=self.context) + except RPCException: + return '' + + def on_row_expanded(self, treeview, iter, path): + child = self.model1.iter_children(iter) + # autodetect could call for node without children + if child is None: + return + if self.model1.get_value(child, 0) is None: + prefix_field = self.model1.get_value(iter, 1) + name, model = self.fields[prefix_field] + self.model_populate( + self._get_fields(model), iter, + prefix_field + '/', name + '/') + self.model1.remove(child) + + def sig_autodetect(self, widget=None): + fname = self.import_csv_file.get_filename() + if not fname: + common.message(_('You must select an import file first.')) + return True + + encoding = self.get_encoding() + self.csv_skip.set_value(1) + try: + data = csv.reader( + open(fname, 'r', encoding=encoding, newline=''), + quotechar=self.get_quotechar(), + delimiter=self.get_delimiter()) + except IOError: + common.warning(_('Error opening CSV file'), _('Error')) + return True + self.sig_unsel_all() + word = '' + for line in data: + for word in line: + if word not in self.fields_invert and word not in self.fields: + iter = self.model1.get_iter_first() + prefix = '' + for parent in word.split('/')[:-1]: + while iter: + if self.model1.get_value(iter, 0) == parent or \ + self.model1.get_value(iter, 1) == \ + (prefix + parent): + self.on_row_expanded(self.view1, iter, + self.model1.get_path(iter)) + iter = self.model1.iter_children(iter) + prefix = parent + '/' + break + else: + iter = self.model1.iter_next(iter) + + if word in self.fields_invert: + name = word + field = self.fields_invert[word] + elif word in self.fields: + name = self.fields[word][0] + field = word + else: + common.warning(_('Error processing the file at field %s.') + % word, _('Error')) + return True + num = self.model2.append() + self.model2.set(num, 0, name, 1, field) + break + return True + + def sig_sel(self, *args): + sel = self.view1.get_selection() + sel.selected_foreach(self._sig_sel_add) + + def _sig_sel_add(self, store, path, iter): + num = self.model2.append() + name = self.fields[store.get_value(iter, 1)][0] + self.model2.set(num, 0, name, 1, store.get_value(iter, 1)) + + def sig_unsel(self, *args): + store, paths = self.view2.get_selection().get_selected_rows() + # Convert first into TreeIter before removing from the store + iters = [store.get_iter(p) for p in paths] + for i in iters: + store.remove(i) + + def sig_unsel_all(self, *args): + self.model2.clear() + + def response(self, dialog, response): + if response == Gtk.ResponseType.OK: + fields = [] + iter = self.model2.get_iter_first() + while iter: + fields.append(self.model2.get_value(iter, 1)) + iter = self.model2.iter_next(iter) + + fname = self.import_csv_file.get_filename() + if fname: + self.import_csv(fname, fields) + self.destroy() + + def import_csv(self, fname, fields): + # TODO: make it works with references + skip = self.csv_skip.get_value_as_int() + encoding = self.get_encoding() + locale_format = self.csv_locale.get_active() + reader = csv.reader( + open(fname, 'r', encoding=encoding), + quotechar=self.get_quotechar(), + delimiter=self.get_delimiter()) + data = [] + for i, line in enumerate(reader): + if i < skip or not line: + continue + row = [] + for field, val in zip(fields, line): + if locale_format and val: + type_ = self.fields_data[field]['type'] + if type_ in ['integer', 'biginteger']: + val = locale.atoi(val) + elif type_ == 'float': + val = locale.atof(val) + elif type_ == 'numeric': + val = Decimal(locale.delocalize(val)) + elif type_ in ['date', 'datetime']: + val = date_parse(val, common.date_format()) + row.append(val) + data.append(row) + try: + count = RPCExecute( + 'model', self.model, 'import_data', fields, data, + context=self.context) + except RPCException: + return + if count == 1: + common.message(_('%d record imported.') % count) + else: + common.message(_('%d records imported.') % count) diff --git a/tryton/tryton/gui/window/win_search.py b/tryton/tryton/gui/window/win_search.py new file mode 100644 index 0000000000..2003a49fc7 --- /dev/null +++ b/tryton/tryton/gui/window/win_search.py @@ -0,0 +1,155 @@ +# 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 gettext + +from gi.repository import Gtk, Gdk + +import tryton.common as common +from tryton.common.underline import set_underline +from tryton.config import TRYTON_ICON +from tryton.gui import Main +from tryton.gui.window.nomodal import NoModal +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_form import WinForm + +_ = gettext.gettext + + +class WinSearch(NoModal): + + def __init__(self, model, callback, sel_multi=True, context=None, + domain=None, order=None, view_ids=None, + views_preload=None, new=True, title='', exclude_field=None): + NoModal.__init__(self) + if view_ids is None: + view_ids = [] + if views_preload is None: + views_preload = {} + self.domain = domain or [] + self.context = context or {} + self.order = order + self.view_ids = view_ids + self.views_preload = views_preload + self.sel_multi = sel_multi + self.callback = callback + if not title: + title = common.MODELNAME.get(model) + self.title = title + self.exclude_field = exclude_field + + self.win = Gtk.Dialog( + title=_('Search'), transient_for=self.parent, + destroy_with_parent=True) + Main().add_window(self.win) + self.win.set_icon(TRYTON_ICON) + self.win.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.win.set_default_response(Gtk.ResponseType.APPLY) + self.win.connect('response', self.response) + + self.win.set_default_size(*self.default_size()) + + self.accel_group = Gtk.AccelGroup() + self.win.add_accel_group(self.accel_group) + + self.but_cancel = self.win.add_button( + set_underline(_("Cancel")), Gtk.ResponseType.CANCEL) + self.but_cancel.set_image(common.IconFactory.get_image( + 'tryton-cancel', Gtk.IconSize.BUTTON)) + self.but_cancel.set_always_show_image(True) + self.but_find = self.win.add_button( + set_underline(_("Search")), Gtk.ResponseType.APPLY) + self.but_find.set_image(common.IconFactory.get_image( + 'tryton-search', Gtk.IconSize.BUTTON)) + self.but_find.set_always_show_image(True) + if new and common.MODELACCESS[model]['create']: + self.but_new = self.win.add_button( + set_underline(_("New")), Gtk.ResponseType.ACCEPT) + self.but_new.set_image(common.IconFactory.get_image( + 'tryton-create', Gtk.IconSize.BUTTON)) + self.but_new.set_always_show_image(True) + self.but_new.set_accel_path('/Form/New', self.accel_group) + + self.but_ok = self.win.add_button( + set_underline(_("OK")), Gtk.ResponseType.OK) + self.but_ok.set_image(common.IconFactory.get_image( + 'tryton-ok', Gtk.IconSize.BUTTON)) + self.but_ok.set_always_show_image(True) + self.but_ok.add_accelerator( + 'clicked', self.accel_group, Gdk.KEY_Return, + Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) + + hbox = Gtk.HBox() + hbox.show() + self.win.vbox.pack_start(hbox, expand=False, fill=True, padding=0) + self.win.vbox.pack_start( + Gtk.HSeparator(), expand=False, fill=True, padding=0) + + self.screen = Screen(model, domain=domain, mode=['tree'], order=order, + context=context, view_ids=view_ids, views_preload=views_preload, + row_activate=self.sig_activate, readonly=True) + self.view = self.screen.current_view + # Prevent to set tree_state + self.screen.tree_states_done.add(id(self.view)) + sel = self.view.treeview.get_selection() + self.win.set_title(_('Search %s') % self.title) + + if not sel_multi: + sel.set_mode(Gtk.SelectionMode.SINGLE) + else: + sel.set_mode(Gtk.SelectionMode.MULTIPLE) + self.win.vbox.pack_start( + self.screen.widget, expand=True, fill=True, padding=0) + self.screen.widget.show() + + self.model_name = model + + self.register() + + def sig_activate(self, *args): + self.view.treeview.stop_emission_by_name('row_activated') + self.win.response(Gtk.ResponseType.OK) + return True + + def destroy(self): + self.screen.destroy() + self.win.destroy() + NoModal.destroy(self) + + def show(self): + self.win.show() + + def hide(self): + self.win.hide() + + def response(self, win, response_id): + res = None + if response_id == Gtk.ResponseType.OK: + res = [r.id for r in self.screen.selected_records] + elif response_id == Gtk.ResponseType.APPLY: + self.screen.search_filter(self.screen.screen_container.get_text()) + return + elif response_id == Gtk.ResponseType.ACCEPT: + # Remove first tree view as mode if form only + view_ids = self.view_ids[1:] + screen = Screen(self.model_name, domain=self.domain, + context=self.context, order=self.order, mode=['form'], + view_ids=view_ids, views_preload=self.views_preload, + exclude_field=self.exclude_field, + breadcrumb=[self.title]) + + def callback(result): + if result: + record = screen.current_record + res = [(record.id, record.value.get('rec_name', ''))] + self.callback(res) + else: + self.callback(None) + self.destroy() + WinForm(screen, callback, new=True, save_current=True) + return + if res: + group = self.screen.group + res = [(id_, group.get(id_).value.get('rec_name', '')) + for id_ in res] + self.callback(res) + self.destroy() diff --git a/tryton/tryton/gui/window/window.py b/tryton/tryton/gui/window/window.py new file mode 100644 index 0000000000..67d6ff83c3 --- /dev/null +++ b/tryton/tryton/gui/window/window.py @@ -0,0 +1,51 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + + +class Window(object): + + hide_current = False + allow_similar = False + + def __init__(self, hide_current=False, allow_similar=True): + Window.hide_current = hide_current + Window.allow_similar = allow_similar + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + Window.hide_current = False + Window.allow_similar = False + + @staticmethod + def create(model, **attributes): + from tryton.gui import Main + main = Main() + if not Window.allow_similar: + for other_page in main.pages: + if other_page.compare(model, attributes): + main.win_set(other_page) + return + if model: + from .form import Form + win = Form(model, **attributes) + else: + from .board import Board + win = Board(model, **attributes) + win.icon = attributes.get('icon') + main.win_add(win, hide_current=Window.hide_current) + + @staticmethod + def create_wizard(action, data, direct_print=False, email_print=False, + email=None, name='', context=None, icon=None, window=False): + from tryton.gui import Main + from .wizard import WizardForm, WizardDialog + if window: + win = WizardForm(name=name) + win.icon = icon + Main().win_add(win, Window.hide_current) + else: + win = WizardDialog(name=name) + win.run(action, data, direct_print=direct_print, + email_print=email_print, email=email, context=context) diff --git a/tryton/tryton/gui/window/wizard.py b/tryton/tryton/gui/window/wizard.py new file mode 100644 index 0000000000..02d2237342 --- /dev/null +++ b/tryton/tryton/gui/window/wizard.py @@ -0,0 +1,426 @@ +# 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 logging +import gettext + +from gi.repository import Gdk, Gtk, Pango + +from tryton.signal_event import SignalEvent +import tryton.common as common +from tryton.gui.window.view_form.screen import Screen +from tryton.gui import Main +from tryton.exceptions import TrytonServerError +from tryton.gui.window.nomodal import NoModal +from tryton.common.button import Button +from tryton.common import RPCExecute, RPCException, RPCContextReload +from tryton.common import TRYTON_ICON +from tryton.common.widget_style import widget_class +from .infobar import InfoBar +from .tabcontent import TabContent + +_ = gettext.gettext +logger = logging.getLogger(__name__) + + +class Wizard(InfoBar): + + def __init__(self, name=''): + super(Wizard, self).__init__() + self.widget = Gtk.VBox(spacing=3) + self.widget.show() + self.name = name or _('Wizard') + self.id = None + self.ids = None + self.action = None + self.action_id = None + self.direct_print = False + self.email_print = False + self.email = False + self.context = None + self.states = {} + self.response2button = {} + self.__processing = False + self.__waiting_response = False + self.session_id = None + self.start_state = None + self.end_state = None + self.screen = None + self.screen_state = None + self.state = None + + def run(self, action, data, direct_print=False, email_print=False, + email=None, context=None): + self.action = action + self.action_id = data.get('action_id') + self.id = data.get('id') + self.ids = data.get('ids') + self.model = data.get('model') + self.direct_print = direct_print + self.email_print = email_print + self.email = email + self.context = context.copy() if context is not None else {} + self.context['active_id'] = self.id + self.context['active_ids'] = self.ids + self.context['active_model'] = self.model + self.context['action_id'] = self.action_id + + def callback(result): + try: + result = result() + except RPCException: + self.destroy() + return + self.session_id, self.start_state, self.end_state = result + self.state = self.start_state + self.process() + RPCExecute('wizard', action, 'create', callback=callback) + + def process(self): + from tryton.action import Action + if self.__processing or self.__waiting_response: + return + self.__processing = True + + ctx = self.context.copy() + if self.screen: + data = { + self.screen_state: self.screen.get_on_change_value(), + } + else: + data = {} + + def callback(result): + try: + result = result() + except RPCException as rpc_exception: + if (not isinstance(rpc_exception.exception, + TrytonServerError) + or not self.screen): + self.state = self.end_state + self.end() + self.__processing = False + return + + if 'view' in result: + self.clean() + view = result['view'] + self.update(view['fields_view'], view['buttons']) + + self.screen.new(default=False) + self.screen.current_record.set_default(view['defaults']) + self.update_buttons(self.screen.current_record) + self.screen.set_cursor() + + self.screen_state = view['state'] + self.__waiting_response = True + else: + self.state = self.end_state + + def execute_actions(): + for action in result.get('actions', []): + for k, v in [ + ('direct_print', self.direct_print), + ('email_print', self.email_print), + ('email', self.email), + ]: + action[0].setdefault(k, v) + context = self.context.copy() + # Remove wizard keys added by run + del context['active_id'] + del context['active_ids'] + del context['active_model'] + del context['action_id'] + Action.execute(*action, context=context) + + if self.state == self.end_state: + self.end(lambda *a: execute_actions()) + else: + execute_actions() + self.__processing = False + + RPCExecute('wizard', self.action, 'execute', self.session_id, data, + self.state, context=ctx, callback=callback) + + def destroy(self, action=None): + if self.screen: + self.screen.destroy() + + def end(self, callback=None): + def end_callback(action): + self.destroy(action=action()) + if callback: + callback() + try: + RPCExecute('wizard', self.action, 'delete', self.session_id, + process_exception=False, callback=end_callback) + except Exception: + logger.warn( + _("Unable to delete wizard %s") % self.session_id, + exc_info=True) + + def clean(self): + for widget in self.widget.get_children(): + self.widget.remove(widget) + self.states = {} + + def response(self, widget, response): + self.__waiting_response = False + button_attrs = self.response2button[response].attrs + state = button_attrs.get('state', self.end_state) + self.screen.current_view.set_value() + if (button_attrs.get('validate', True) + and not self.screen.current_record.validate()): + self.screen.display(set_cursor=True) + self.message_info( + self.screen.invalid_message(), Gtk.MessageType.ERROR) + return + self.message_info() + self.state = state + self.process() + + def _get_button(self, definition): + button = Button(definition) + self.states[definition['state']] = button + response = len(self.states) + self.response2button[response] = button + button.show() + return button + + def _record_changed(self, screen, record): + self.update_buttons(record) + + def update_buttons(self, record): + for button in self.states.values(): + button.state_set(record) + + def update(self, view, buttons): + tooltips = common.Tooltips() + for button in buttons: + self._get_button(button) + + self.screen = Screen(view['model'], mode=[], context=self.context) + self.screen.add_view(view) + self.screen.switch_view() + self.screen.widget.show() + self.screen.signal_connect(self, 'group-changed', + self._record_changed) + + title = Gtk.Label( + label=common.ellipsize(self.name, 80), + halign=Gtk.Align.START, margin=5, + ellipsize=Pango.EllipsizeMode.END) + tooltips.set_tip(title, self.name) + title.set_size_request(0, -1) # Allow overflow + title.show() + + hbox = Gtk.HBox() + hbox.pack_start(title, expand=True, fill=True, padding=0) + hbox.show() + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + widget_class(frame, 'wizard-title', True) + frame.add(hbox) + frame.show() + + self.widget.pack_start(frame, expand=False, fill=True, padding=3) + + viewport = Gtk.Viewport() + viewport.set_shadow_type(Gtk.ShadowType.NONE) + viewport.add(self.screen.widget) + viewport.show() + self.scrolledwindow = Gtk.ScrolledWindow() + self.scrolledwindow.set_shadow_type(Gtk.ShadowType.NONE) + self.scrolledwindow.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + self.scrolledwindow.add(viewport) + self.scrolledwindow.show() + + self.widget.pack_start( + self.scrolledwindow, expand=True, fill=True, padding=0) + + self.create_info_bar() + self.widget.pack_start( + self.info_bar, expand=False, fill=True, padding=0) + + +class WizardForm(Wizard, TabContent, SignalEvent): + "Wizard" + + def __init__(self, name=''): + super(WizardForm, self).__init__(name=name) + self.hbuttonbox = Gtk.HButtonBox() + self.hbuttonbox.set_spacing(5) + self.hbuttonbox.set_layout(Gtk.ButtonBoxStyle.END) + self.hbuttonbox.show() + self.dialogs = [] + + self.handlers = { + 'but_close': self.sig_close + } + + def clean(self): + super(WizardForm, self).clean() + for button in self.hbuttonbox.get_children(): + self.hbuttonbox.remove(button) + + def _get_button(self, state): + button = super(WizardForm, self)._get_button(state) + response = len(self.states) + button.connect('clicked', self.response, response) + self.hbuttonbox.pack_start(button, expand=True, fill=True, padding=0) + return button + + def update(self, view, buttons): + super(WizardForm, self).update(view, buttons) + self.widget.pack_start( + self.hbuttonbox, expand=False, fill=True, padding=0) + + def sig_close(self): + if self.end_state in self.states: + self.states[self.end_state].clicked() + return self.state == self.end_state + + def destroy(self, action=None): + super(WizardForm, self).destroy(action=action) + if action == 'reload menu': + RPCContextReload(Main().sig_win_menu) + elif action == 'reload context': + RPCContextReload() + + def end(self, callback=None): + super(WizardForm, self).end(callback=callback) + Main()._win_del(self.widget) + + def set_cursor(self): + if self.screen: + self.screen.set_cursor() + + +class WizardDialog(Wizard, NoModal): + + def __init__(self, name=''): + Wizard.__init__(self, name=name) + NoModal.__init__(self) + self.dia = Gtk.Dialog( + title=self.name, transient_for=self.parent, + destroy_with_parent=True) + Main().add_window(self.dia) + self.dia.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + self.dia.set_icon(TRYTON_ICON) + self.dia.set_deletable(False) + self.dia.connect('delete-event', lambda *a: True) + self.dia.connect('close', self.close) + self.dia.connect('response', self.response) + + self.accel_group = Gtk.AccelGroup() + self.dia.add_accel_group(self.accel_group) + + self._buttons = set() + + self.dia.vbox.pack_start( + self.widget, expand=True, fill=True, padding=0) + + self.register() + + def clean(self): + super(WizardDialog, self).clean() + while self._buttons: + button = self._buttons.pop() + button.get_parent().remove(button) + + def _get_button(self, definition): + button = super(WizardDialog, self)._get_button(definition) + response = len(self.states) + self.dia.add_action_widget(button, response) + self._buttons.add(button) + if definition['default']: + button.add_accelerator( + 'clicked', self.accel_group, Gdk.KEY_Return, + Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE) + button.get_style_context().add_class( + Gtk.STYLE_CLASS_SUGGESTED_ACTION) + button.set_can_default(True) + button.grab_default() + self.dia.set_default_response(response) + return button + + def update(self, view, buttons): + super(WizardDialog, self).update(view, buttons) + current_view = self.screen.current_view + self.scrolledwindow.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + if current_view.scroll: + current_view.scroll.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) + self.show() + self.scrolledwindow.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + if current_view.scroll: + current_view.scroll.set_policy( + Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + def destroy(self, action=None): + super(WizardDialog, self).destroy(action=action) + self.dia.destroy() + NoModal.destroy(self) + main = Main() + if self.parent == main.window: + current_form = main.get_page() + if current_form: + for dialog in current_form.dialogs: + dialog.show() + if self.page.dialogs: + dialog = self.page.dialogs[-1] + else: + dialog = self.page + screen = getattr(dialog, 'screen', None) + if self.sensible_widget == main.window: + screen = main.menu_screen + if screen: + if (screen.current_record + and self.sensible_widget != main.window): + if screen.model_name == self.model: + ids = self.ids + else: + # Wizard run from a children record so reload parent record + ids = [screen.current_record.id] + screen.reload(ids, written=True) + if action: + screen.client_action(action) + + def close(self, widget, event=None): + widget.stop_emission_by_name('close') + if self.end_state in self.states: + self.states[self.end_state].clicked() + return True + + def show(self): + view = self.screen.current_view + if view.view_type == 'form': + expand = False + for name in view.get_fields(): + for widget in view.widgets[name]: + if widget.expand: + expand = True + break + if expand: + break + else: + expand = True + if expand: + width, height = self.default_size() + else: + width, height = -1, -1 + self.dia.set_default_size(width, height) + # reshow with initial size + self.dia.hide() + self.dia.unrealize() + self.dia.show() + width, height = self.dia.get_size() + screen = self.dia.get_screen() + self.dia.resize( + min(width, screen.width()), min(height, screen.height())) + + def hide(self): + self.dia.hide() diff --git a/tryton/tryton/jsonrpc.py b/tryton/tryton/jsonrpc.py new file mode 100644 index 0000000000..ed3b65b99c --- /dev/null +++ b/tryton/tryton/jsonrpc.py @@ -0,0 +1,414 @@ +# 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 copy +import xmlrpc.client +import json +import ssl +import http.client +from decimal import Decimal +import datetime +import socket +import hashlib +import base64 +import threading +import errno +import logging +from functools import partial +from collections import defaultdict +from contextlib import contextmanager +from functools import reduce +from urllib.parse import urljoin, quote + +__all__ = ["ResponseError", "Fault", "ProtocolError", "Transport", + "ServerProxy", "ServerPool"] +CONNECT_TIMEOUT = 5 +DEFAULT_TIMEOUT = None +logger = logging.getLogger(__name__) + + +class ResponseError(xmlrpc.client.ResponseError): + pass + + +class Fault(xmlrpc.client.Fault): + + def __init__(self, faultCode, faultString='', **extra): + super(Fault, self).__init__(faultCode, faultString, **extra) + self.args = faultString + + def __str__(self): + return str(self.faultCode) + + +class ProtocolError(xmlrpc.client.ProtocolError): + pass + + +def object_hook(dct): + if '__class__' in dct: + if dct['__class__'] == 'datetime': + return datetime.datetime(dct['year'], dct['month'], dct['day'], + dct['hour'], dct['minute'], dct['second'], dct['microsecond']) + elif dct['__class__'] == 'date': + return datetime.date(dct['year'], dct['month'], dct['day']) + elif dct['__class__'] == 'time': + return datetime.time(dct['hour'], dct['minute'], dct['second'], + dct['microsecond']) + elif dct['__class__'] == 'timedelta': + return datetime.timedelta(seconds=dct['seconds']) + elif dct['__class__'] == 'bytes': + return base64.decodebytes(dct['base64'].encode('utf-8')) + elif dct['__class__'] == 'Decimal': + return Decimal(dct['decimal']) + return dct + + +class JSONEncoder(json.JSONEncoder): + + def default(self, obj): + if isinstance(obj, datetime.date): + if isinstance(obj, datetime.datetime): + return {'__class__': 'datetime', + 'year': obj.year, + 'month': obj.month, + 'day': obj.day, + 'hour': obj.hour, + 'minute': obj.minute, + 'second': obj.second, + 'microsecond': obj.microsecond, + } + return {'__class__': 'date', + 'year': obj.year, + 'month': obj.month, + 'day': obj.day, + } + elif isinstance(obj, datetime.time): + return {'__class__': 'time', + 'hour': obj.hour, + 'minute': obj.minute, + 'second': obj.second, + 'microsecond': obj.microsecond, + } + elif isinstance(obj, datetime.timedelta): + return {'__class__': 'timedelta', + 'seconds': obj.total_seconds(), + } + elif isinstance(obj, bytes): + return {'__class__': 'bytes', + 'base64': base64.encodebytes(obj).decode('utf-8'), + } + elif isinstance(obj, Decimal): + return {'__class__': 'Decimal', + 'decimal': str(obj), + } + return super(JSONEncoder, self).default(obj) + + +class JSONParser(object): + + def __init__(self, target): + self.__targer = target + + def feed(self, data): + self.__targer.feed(data) + + def close(self): + pass + + +class JSONUnmarshaller(object): + def __init__(self): + self.data = [] + + def feed(self, data): + self.data.append(data.decode('utf-8')) + + def close(self): + return json.loads(''.join(self.data), object_hook=object_hook) + + +class Transport(xmlrpc.client.SafeTransport): + + accept_gzip_encoding = True + encode_threshold = 1400 # common MTU + + def __init__( + self, fingerprints=None, ca_certs=None, session=None): + xmlrpc.client.Transport.__init__(self) + self._connection = (None, None) + self.__fingerprints = fingerprints + self.__ca_certs = ca_certs + self.session = session + + def getparser(self): + target = JSONUnmarshaller() + parser = JSONParser(target) + return parser, target + + def parse_response(self, response): + cache = None + if hasattr(response, 'getheader'): + cache = int(response.getheader('X-Tryton-Cache', 0)) + response = super().parse_response(response) + if cache: + try: + response['cache'] = int(cache) + except ValueError: + pass + return response + + def get_host_info(self, host): + host, extra_headers, x509 = xmlrpc.client.Transport.get_host_info( + self, host) + if extra_headers is None: + extra_headers = [] + if self.session: + auth = base64.encodebytes( + self.session.encode('utf-8')).decode('ascii') + auth = ''.join(auth.split()) # get rid of whitespace + extra_headers.append( + ('Authorization', 'Session ' + auth), + ) + extra_headers.append(('Connection', 'keep-alive')) + return host, extra_headers, x509 + + def send_headers(self, connection, headers): + for key, val in headers: + if key == 'Content-Type': + val = 'application/json' + connection.putheader(key, val) + + def make_connection(self, host): + if self._connection and host == self._connection[0]: + return self._connection[1] + chost, self._extra_headers, x509 = self.get_host_info(host) + + ssl_ctx = ssl.create_default_context(cafile=self.__ca_certs) + + def http_connection(): + self._connection = host, http.client.HTTPConnection(chost, + timeout=CONNECT_TIMEOUT) + self._connection[1].connect() + sock = self._connection[1].sock + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + def https_connection(allow_http=False): + self._connection = host, http.client.HTTPSConnection(chost, + timeout=CONNECT_TIMEOUT, context=ssl_ctx) + try: + self._connection[1].connect() + sock = self._connection[1].sock + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + try: + peercert = sock.getpeercert(True) + except socket.error: + peercert = None + + def format_hash(value): + return reduce(lambda x, y: x + y[1].upper() + + ((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''), + enumerate(value), '') + return format_hash(hashlib.sha1(peercert).hexdigest()) + except (socket.error, ssl.SSLError, ssl.CertificateError): + if allow_http: + http_connection() + else: + raise + + fingerprint = '' + if (self.__fingerprints is not None + and self.__fingerprints.exists(chost)): + if self.__fingerprints.get(chost): + fingerprint = https_connection() + else: + http_connection() + else: + fingerprint = https_connection(allow_http=True) + + if self.__fingerprints is not None: + self.__fingerprints.set(chost, fingerprint) + self._connection[1].timeout = DEFAULT_TIMEOUT + self._connection[1].sock.settimeout(DEFAULT_TIMEOUT) + return self._connection[1] + + +class ServerProxy(xmlrpc.client.ServerProxy): + __id = 0 + + def __init__(self, host, port, database='', verbose=0, + fingerprints=None, ca_certs=None, session=None, cache=None): + self.__host = '%s:%s' % (host, port) + if database: + database = quote(database) + self.__handler = '/%s/' % database + else: + self.__handler = '/' + self.__transport = Transport(fingerprints, ca_certs, session) + self.__verbose = verbose + self.__cache = cache + + def __request(self, methodname, params): + dumper = partial(json.dumps, cls=JSONEncoder, separators=(',', ':')) + self.__id += 1 + id_ = self.__id + if self.__cache and self.__cache.cached(methodname): + try: + return self.__cache.get(methodname, dumper(params)) + except KeyError: + pass + request = dumper({ + 'id': id_, + 'method': methodname, + 'params': params, + }).encode('utf-8') + + try: + try: + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + except (socket.error, http.client.HTTPException) as v: + if (isinstance(v, socket.error) + and v.args[0] == errno.EPIPE): + raise + # try one more time + self.__transport.close() + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + except xmlrpc.client.ProtocolError as e: + raise Fault(str(e.errcode), e.errmsg) + except Exception: + self.__transport.close() + raise + if response['id'] != id_: + raise ResponseError('Invalid response id (%s) excpected %s' % + (response['id'], id_)) + if response.get('error'): + raise Fault(*response['error']) + if self.__cache and response.get('cache'): + self.__cache.set( + methodname, dumper(params), response['cache'], + response['result']) + return response['result'] + + def close(self): + self.__transport.close() + + @property + def ssl(self): + return isinstance(self.__transport.make_connection(self.__host), + http.client.HTTPSConnection) + + +class ServerPool(object): + keep_max = 4 + _cache = None + + def __init__(self, host, port, database, *args, **kwargs): + if kwargs.get('cache'): + self._cache = kwargs['cache'] = _Cache() + self.ServerProxy = partial( + ServerProxy, host, port, database, *args, **kwargs) + + self._host = host + self._port = port + self._database = database + + self._lock = threading.Lock() + self._pool = [] + self._used = {} + self.session = kwargs.get('session') + + def getconn(self): + with self._lock: + if self._pool: + conn = self._pool.pop() + else: + conn = self.ServerProxy() + self._used[id(conn)] = conn + return conn + + def putconn(self, conn): + with self._lock: + self._pool.append(conn) + del self._used[id(conn)] + + # Remove oldest connections + while len(self._pool) > self.keep_max: + conn = self._pool.pop() + conn.close() + + def close(self): + with self._lock: + for conn in self._pool + list(self._used.values()): + conn.close() + self._pool = [] + self._used.clear() + + @property + def ssl(self): + for conn in self._pool + list(self._used.values()): + return conn.ssl + return None + + @property + def url(self): + if self.ssl is None: + return None + scheme = 'https' if self.ssl else 'http' + return urljoin( + scheme + '://' + self._host + ':' + str(self._port), + self._database) + + @contextmanager + def __call__(self): + conn = self.getconn() + yield conn + self.putconn(conn) + + def clear_cache(self, prefix=None): + if self._cache: + self._cache.clear(prefix) + + +class _Cache: + + def __init__(self): + self.store = defaultdict(dict) + + def cached(self, prefix): + return prefix in self.store + + def set(self, prefix, key, expire, value): + if isinstance(expire, (int, float)): + expire = datetime.timedelta(seconds=expire) + if isinstance(expire, datetime.timedelta): + expire = datetime.datetime.now() + expire + self.store[prefix][key] = (expire, copy.deepcopy(value)) + + def get(self, prefix, key): + now = datetime.datetime.now() + try: + expire, value = self.store[prefix][key] + except ValueError: + raise KeyError + if expire < now: + self.store.pop(key) + raise KeyError + logger.info('(cached) %s %s', prefix, key) + return copy.deepcopy(value) + + def clear(self, prefix=None): + if prefix: + self.store[prefix].clear() + else: + self.store.clear() diff --git a/tryton/tryton/plugins/__init__.py b/tryton/tryton/plugins/__init__.py new file mode 100644 index 0000000000..db22c616cb --- /dev/null +++ b/tryton/tryton/plugins/__init__.py @@ -0,0 +1,45 @@ +# 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 os +import importlib +import gettext + +from tryton.config import get_config_dir, CURRENT_DIR + +__all__ = ['MODULES', 'register'] + +_ = gettext.gettext + +MODULES = [] + + +def register(): + global MODULES + paths = [ + os.path.join(get_config_dir(), 'plugins'), + os.path.join(CURRENT_DIR, 'plugins'), + ] + paths = list(filter(os.path.isdir, paths)) + + imported = set() + for path in paths: + finder = importlib.machinery.FileFinder( + path, ( + importlib.machinery.SourceFileLoader, + importlib.machinery.SOURCE_SUFFIXES)) + for plugin in os.listdir(path): + module = os.path.splitext(plugin)[0] + if (module.startswith('_') or module in imported): + continue + module = 'tryton.plugins.%s' % module + spec = finder.find_spec(module) + if not spec: + continue + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) + except ImportError: + continue + else: + MODULES.append(module) + imported.add(module.__name__) diff --git a/tryton/tryton/plugins/translation/__init__.py b/tryton/tryton/plugins/translation/__init__.py new file mode 100644 index 0000000000..9ee7306577 --- /dev/null +++ b/tryton/tryton/plugins/translation/__init__.py @@ -0,0 +1,27 @@ +# 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 gettext + +from tryton.gui.window import Window +from tryton.common import MODELACCESS + +_ = gettext.gettext + + +def translate_view(datas): + model = datas['model'] + Window.create('ir.translation', + res_id=False, + domain=[('model', '=', model)], + mode=['tree', 'form'], + name=_('Translate view')) + + +def get_plugins(model): + access = MODELACCESS['ir.translation'] + if access['read'] and access['write']: + return [ + (_('Translate view'), translate_view), + ] + else: + return [] diff --git a/tryton/tryton/pyson.py b/tryton/tryton/pyson.py new file mode 100644 index 0000000000..0f2ed608c6 --- /dev/null +++ b/tryton/tryton/pyson.py @@ -0,0 +1,755 @@ +# 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 json +import datetime +from decimal import Decimal +from dateutil.relativedelta import relativedelta +from functools import reduce + + +class PYSON(object): + + def pyson(self): + raise NotImplementedError + + def types(self): + raise NotImplementedError + + @staticmethod + def eval(dct, context): + raise NotImplementedError + + def __invert__(self): + if self.types() != {bool}: + return Not(Bool(self)) + else: + return Not(self) + + def __and__(self, other): + if (isinstance(other, PYSON) + and other.types() != {bool}): + other = Bool(other) + if (isinstance(self, And) + and not isinstance(self, Or)): + self._statements.append(other) + return self + if self.types() != {bool}: + return And(Bool(self), other) + else: + return And(self, other) + + __rand__ = __and__ + + def __or__(self, other): + if (isinstance(other, PYSON) + and other.types() != {bool}): + other = Bool(other) + if isinstance(self, Or): + self._statements.append(other) + return self + if self.types() != {bool}: + return Or(Bool(self), other) + else: + return Or(self, other) + + __ror__ = __or__ + + def __eq__(self, other): + return Equal(self, other) + + def __ne__(self, other): + return Not(Equal(self, other)) + + def __gt__(self, other): + return Greater(self, other) + + def __ge__(self, other): + return Greater(self, other, True) + + def __lt__(self, other): + return Less(self, other) + + def __le__(self, other): + return Less(self, other, True) + + def get(self, k, d=''): + return Get(self, k, d) + + def in_(self, obj): + return In(self, obj) + + def contains(self, k): + return In(k, self) + + def __repr__(self): + klass = self.__class__.__name__ + return '%s(%s)' % (klass, ', '.join(map(repr, self.__repr_params__))) + + @property + def __repr_params__(self): + return NotImplementedError + + +class PYSONEncoder(json.JSONEncoder): + + def default(self, obj): + if isinstance(obj, PYSON): + return obj.pyson() + elif isinstance(obj, datetime.date): + if isinstance(obj, datetime.datetime): + return DateTime(obj.year, obj.month, obj.day, + obj.hour, obj.minute, obj.second, obj.microsecond + ).pyson() + else: + return Date(obj.year, obj.month, obj.day).pyson() + elif isinstance(obj, Decimal): + return float(obj) + return super(PYSONEncoder, self).default(obj) + + +class PYSONDecoder(json.JSONDecoder): + + def __init__(self, context=None, noeval=False): + self.__context = context or {} + self.noeval = noeval + super(PYSONDecoder, self).__init__(object_hook=self._object_hook) + + def _object_hook(self, dct): + if '__class__' in dct: + klass = CONTEXT.get(dct['__class__']) + if klass: + if not self.noeval: + return klass.eval(dct, self.__context) + else: + dct = dct.copy() + del dct['__class__'] + return klass(**dct) + return dct + + +class Eval(PYSON): + + def __init__(self, v, d=''): + super(Eval, self).__init__() + self._value = v + self._default = d + + @property + def __repr_params__(self): + return self._value, self._default + + def pyson(self): + return { + '__class__': 'Eval', + 'v': self._value, + 'd': self._default, + } + + def types(self): + if isinstance(self._default, PYSON): + return self._default.types() + else: + return {type(self._default)} + + @staticmethod + def eval(dct, context): + if '.' in dct['v'] and dct['v'] not in context: + base, name = dct['v'].split('.', 1) + return Eval.eval({ + 'v': name, + 'd': dct['d'], + }, context.get(base) or {}) + return context.get(dct['v'], dct['d']) + + +class Not(PYSON): + + def __init__(self, v): + super(Not, self).__init__() + if isinstance(v, PYSON): + if v.types() != {bool}: + v = Bool(v) + elif not isinstance(v, bool): + v = bool(v) + self._value = v + + @property + def __repr_params__(self): + return (self._value,) + + def pyson(self): + return { + '__class__': 'Not', + 'v': self._value, + } + + def types(self): + return {bool} + + @staticmethod + def eval(dct, context): + return not dct['v'] + + +class Bool(PYSON): + + def __init__(self, v): + super(Bool, self).__init__() + self._value = v + + @property + def __repr_params__(self): + return (self._value,) + + def pyson(self): + return { + '__class__': 'Bool', + 'v': self._value, + } + + def types(self): + return {bool} + + @staticmethod + def eval(dct, context): + return bool(dct['v']) + + +class And(PYSON): + + def __init__(self, *statements, **kwargs): + super(And, self).__init__() + statements = list(statements) + kwargs.get('s', []) + for i, statement in enumerate(list(statements)): + if isinstance(statement, PYSON): + if statement.types() != {bool}: + statements[i] = Bool(statement) + elif not isinstance(statement, bool): + statements[i] = bool(statement) + assert len(statements) >= 2, 'must have at least 2 statements' + self._statements = statements + + @property + def __repr_params__(self): + return tuple(self._statements) + + def pyson(self): + return { + '__class__': 'And', + 's': self._statements, + } + + def types(self): + return {bool} + + @staticmethod + def eval(dct, context): + return bool(reduce(lambda x, y: x and y, dct['s'])) + + +class Or(And): + + def pyson(self): + res = super(Or, self).pyson() + res['__class__'] = 'Or' + return res + + @staticmethod + def eval(dct, context): + return bool(reduce(lambda x, y: x or y, dct['s'])) + + +class Equal(PYSON): + + def __init__(self, s1, s2): + statement1, statement2 = s1, s2 + super(Equal, self).__init__() + if isinstance(statement1, PYSON): + types1 = statement1.types() + else: + types1 = {type(s1)} + if isinstance(statement2, PYSON): + types2 = statement2.types() + else: + types2 = {type(s2)} + assert types1 == types2, 'statements must have the same type' + self._statement1 = statement1 + self._statement2 = statement2 + + @property + def __repr_params__(self): + return (self._statement1, self._statement2) + + def pyson(self): + return { + '__class__': 'Equal', + 's1': self._statement1, + 's2': self._statement2, + } + + def types(self): + return {bool} + + @staticmethod + def eval(dct, context): + return dct['s1'] == dct['s2'] + + +class Greater(PYSON): + + def __init__(self, s1, s2, e=False): + statement1, statement2, equal = s1, s2, e + super(Greater, self).__init__() + for i in (statement1, statement2): + if isinstance(i, PYSON): + assert i.types().issubset({ + int, float, type(None), + datetime.datetime, datetime.date}), \ + 'statement must be an integer, float, date or datetime' + else: + assert isinstance(i, ( + int, float, type(None), + datetime.datetime, datetime.date)), \ + 'statement must be an integer, float, date or datetime' + if isinstance(equal, PYSON): + if equal.types() != {bool}: + equal = Bool(equal) + elif not isinstance(equal, bool): + equal = bool(equal) + self._statement1 = statement1 + self._statement2 = statement2 + self._equal = equal + + @property + def __repr_params__(self): + return (self._statement1, self._statement2, self._equal) + + def pyson(self): + return { + '__class__': 'Greater', + 's1': self._statement1, + 's2': self._statement2, + 'e': self._equal, + } + + def types(self): + return {bool} + + @staticmethod + def _convert(dct): + for i in ('s1', 's2'): + if dct[i] is None: + dct[i] = 0.0 + if not isinstance(dct[i], (int, float)): + dct = dct.copy() + stmt = dct[i] + if isinstance(stmt, datetime.datetime): + stmt = stmt.timestamp() + elif isinstance(stmt, datetime.date): + time = datetime.time(0, 0) + stmt = datetime.datetime.combine(stmt, time).timestamp() + dct[i] = float(stmt) + return dct + + @staticmethod + def eval(dct, context): + if dct['s1'] is None or dct['s2'] is None: + return False + dct = Greater._convert(dct) + if dct['e']: + return dct['s1'] >= dct['s2'] + else: + return dct['s1'] > dct['s2'] + + +class Less(Greater): + + def pyson(self): + res = super(Less, self).pyson() + res['__class__'] = 'Less' + return res + + @staticmethod + def eval(dct, context): + if dct['s1'] is None or dct['s2'] is None: + return False + dct = Less._convert(dct) + if dct['e']: + return dct['s1'] <= dct['s2'] + else: + return dct['s1'] < dct['s2'] + + +class If(PYSON): + + def __init__(self, c, t, e=None): + condition, then_statement, else_statement = c, t, e + super(If, self).__init__() + if isinstance(condition, PYSON): + if condition.types() != {bool}: + condition = Bool(condition) + elif not isinstance(condition, bool): + condition = bool(condition) + if isinstance(then_statement, PYSON): + then_types = then_statement.types() + else: + then_types = {type(then_statement)} + if isinstance(else_statement, PYSON): + else_types = else_statement.types() + else: + else_types = {type(else_statement)} + assert then_types == else_types, \ + 'then and else statements must be the same type' + self._condition = condition + self._then_statement = then_statement + self._else_statement = else_statement + + @property + def __repr_params__(self): + return (self._condition, self._then_statement, self._else_statement) + + def pyson(self): + return { + '__class__': 'If', + 'c': self._condition, + 't': self._then_statement, + 'e': self._else_statement, + } + + def types(self): + if isinstance(self._then_statement, PYSON): + return self._then_statement.types() + else: + return {type(self._then_statement)} + + @staticmethod + def eval(dct, context): + if dct['c']: + return dct['t'] + else: + return dct['e'] + + +class Get(PYSON): + + def __init__(self, v, k, d=''): + obj, key, default = v, k, d + super(Get, self).__init__() + if isinstance(obj, PYSON): + assert obj.types() == {dict}, 'obj must be a dict' + else: + assert isinstance(obj, dict), 'obj must be a dict' + self._obj = obj + if isinstance(key, PYSON): + assert key.types() == {str}, 'key must be a string' + else: + assert isinstance(key, str), 'key must be a string' + self._key = key + self._default = default + + @property + def __repr_params__(self): + return (self._obj, self._key, self._default) + + def pyson(self): + return { + '__class__': 'Get', + 'v': self._obj, + 'k': self._key, + 'd': self._default, + } + + def types(self): + if isinstance(self._default, PYSON): + return self._default.types() + else: + return {type(self._default)} + + @staticmethod + def eval(dct, context): + return dct['v'].get(dct['k'], dct['d']) + + +class In(PYSON): + + def __init__(self, k, v): + key, obj = k, v + super(In, self).__init__() + if isinstance(key, PYSON): + assert key.types().issubset({str, int}), \ + 'key must be a string or an integer or a long' + else: + assert isinstance(key, (str, int)), \ + 'key must be a string or an integer or a long' + if isinstance(obj, PYSON): + assert obj.types().issubset({dict, list}), \ + 'obj must be a dict or a list' + if obj.types() == {dict}: + assert isinstance(key, str), 'key must be a string' + else: + assert isinstance(obj, (dict, list)) + if isinstance(obj, dict): + assert isinstance(key, str), 'key must be a string' + self._key = key + self._obj = obj + + @property + def __repr_params__(self): + return (self._key, self._obj) + + def pyson(self): + return { + '__class__': 'In', + 'k': self._key, + 'v': self._obj, + } + + def types(self): + return {bool} + + @staticmethod + def eval(dct, context): + if dct['v']: + return dct['k'] in dct['v'] + else: + return False + + +class Date(PYSON): + + def __init__(self, year=None, month=None, day=None, + delta_years=0, delta_months=0, delta_days=0, start=None, **kwargs): + year = kwargs.get('y', year) + month = kwargs.get('M', month) + day = kwargs.get('d', day) + delta_years = kwargs.get('dy', delta_years) + delta_months = kwargs.get('dM', delta_months) + delta_days = kwargs.get('dd', delta_days) + super(Date, self).__init__() + for i in (year, month, day, delta_years, delta_months, delta_days): + if isinstance(i, PYSON): + assert i.types().issubset({int, type(None)}), \ + '%s must be an integer or None' % (i,) + else: + assert isinstance(i, (int, type(None))), \ + '%s must be an integer or None' % (i,) + self._year = year + self._month = month + self._day = day + self._delta_years = delta_years + self._delta_months = delta_months + self._delta_days = delta_days + self._start = start + + @property + def __repr_params__(self): + return (self._year, self._month, self._day, + self._delta_years, self._delta_months, self._delta_days, + self._start) + + def pyson(self): + return { + '__class__': 'Date', + 'y': self._year, + 'M': self._month, + 'd': self._day, + 'dy': self._delta_years, + 'dM': self._delta_months, + 'dd': self._delta_days, + 'start': self._start, + } + + def types(self): + return {datetime.date} + + @staticmethod + def eval(dct, context): + today = dct.get('start') + if isinstance(today, datetime.datetime): + today = today.date() + if not isinstance(today, datetime.date): + today = datetime.date.today() + return today + relativedelta( + year=dct['y'], + month=dct['M'], + day=dct['d'], + years=dct['dy'], + months=dct['dM'], + days=dct['dd'], + ) + + +class DateTime(Date): + + def __init__(self, year=None, month=None, day=None, + hour=None, minute=None, second=None, microsecond=None, + delta_years=0, delta_months=0, delta_days=0, + delta_hours=0, delta_minutes=0, delta_seconds=0, + delta_microseconds=0, start=None, **kwargs): + hour = kwargs.get('h', hour) + minute = kwargs.get('m', minute) + second = kwargs.get('s', second) + microsecond = kwargs.get('ms', microsecond) + delta_hours = kwargs.get('dh', delta_hours) + delta_minutes = kwargs.get('dm', delta_minutes) + delta_seconds = kwargs.get('ds', delta_seconds) + delta_microseconds = kwargs.get('dms', delta_microseconds) + super(DateTime, self).__init__(year=year, month=month, day=day, + delta_years=delta_years, delta_months=delta_months, + delta_days=delta_days, start=start, **kwargs) + for i in (hour, minute, second, microsecond, + delta_hours, delta_minutes, delta_seconds, delta_microseconds): + if isinstance(i, PYSON): + assert i.types() == {int, type(None)}, \ + '%s must be an integer or None' % (i,) + else: + assert isinstance(i, (int, type(None))), \ + '%s must be an integer or None' % (i,) + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + self._delta_hours = delta_hours + self._delta_minutes = delta_minutes + self._delta_seconds = delta_seconds + self._delta_microseconds = delta_microseconds + + @property + def __repr_params__(self): + date_params = super(DateTime, self).__repr_params__ + return (date_params[:3] + + (self._hour, self._minute, self._second, self._microsecond) + + date_params[3:-1] + + (self._delta_hours, self._delta_minutes, self._delta_seconds, + self._delta_microseconds) + + date_params[-1:]) + + def pyson(self): + res = super(DateTime, self).pyson() + res['__class__'] = 'DateTime' + res['h'] = self._hour + res['m'] = self._minute + res['s'] = self._second + res['ms'] = self._microsecond + res['dh'] = self._delta_hours + res['dm'] = self._delta_minutes + res['ds'] = self._delta_seconds + res['dms'] = self._delta_microseconds + return res + + def types(self): + return {datetime.datetime} + + @staticmethod + def eval(dct, context): + now = dct.get('start') + if (isinstance(now, datetime.date) + and not isinstance(now, datetime.datetime)): + now = datetime.datetime.combine(now, datetime.time()) + if not isinstance(now, datetime.datetime): + now = datetime.datetime.utcnow() + return now + relativedelta( + year=dct['y'], + month=dct['M'], + day=dct['d'], + hour=dct['h'], + minute=dct['m'], + second=dct['s'], + microsecond=dct['ms'], + years=dct['dy'], + months=dct['dM'], + days=dct['dd'], + hours=dct['dh'], + minutes=dct['dm'], + seconds=dct['ds'], + microseconds=dct['dms'], + ) + + +class TimeDelta(PYSON): + + def __init__(self, days=0, seconds=0, microseconds=0): + for i in [days, seconds, microseconds]: + if isinstance(i, PYSON): + assert i.types().issubset({int, float}), \ + '%s must be an integer' % (i,) + else: + assert isinstance(i, (int, float)), \ + '%s must be an integer' % (i,) + self._days = days + self._seconds = seconds + self._microseconds = microseconds + + @property + def __repr_params__(self): + return self._days, self._seconds, self._microseconds + + def pyson(self): + return { + '__class__': 'TimeDelta', + 'd': self._days, + 's': self._seconds, + 'm': self._microseconds, + } + + def types(self): + return {datetime.timedelta} + + @staticmethod + def eval(dct, context): + return datetime.timedelta( + days=dct['d'], + seconds=dct['s'], + microseconds=dct['m'], + ) + + +class Len(PYSON): + + def __init__(self, v): + super(Len, self).__init__() + if isinstance(v, PYSON): + assert v.types().issubset({dict, list, str}), \ + 'value must be a dict or a list or a string' + else: + assert isinstance(v, (dict, list, str)), \ + 'value must be a dict or list or a string' + self._value = v + + @property + def __repr_params__(self): + return (self._value,) + + def pyson(self): + return { + '__class__': 'Len', + 'v': self._value, + } + + def types(self): + return {int} + + @staticmethod + def eval(dct, context): + return len(dct['v']) + + +CONTEXT = { + 'Eval': Eval, + 'Not': Not, + 'Bool': Bool, + 'And': And, + 'Or': Or, + 'Equal': Equal, + 'Greater': Greater, + 'Less': Less, + 'If': If, + 'Get': Get, + 'In': In, + 'Date': Date, + 'DateTime': DateTime, + 'TimeDelta': TimeDelta, + 'Len': Len, + 'true': True, + 'false': False, +} diff --git a/tryton/tryton/rpc.py b/tryton/tryton/rpc.py new file mode 100644 index 0000000000..f037b2aede --- /dev/null +++ b/tryton/tryton/rpc.py @@ -0,0 +1,129 @@ +# 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 http.client +import logging +import socket +import os +try: + from http import HTTPStatus +except ImportError: + from http import client as HTTPStatus + +from functools import partial + +from tryton import bus, device_cookie, fingerprints +from tryton.jsonrpc import ServerProxy, ServerPool, Fault +from tryton.config import get_config_dir +from tryton.exceptions import TrytonServerError, TrytonServerUnavailable +from tryton.config import CONFIG + +CONNECTION = None +_USER = None +CONTEXT = {} +_VIEW_CACHE = {} +_TOOLBAR_CACHE = {} +_KEYWORD_CACHE = {} +_CA_CERTS = os.path.join(get_config_dir(), 'ca_certs') +if not os.path.isfile(_CA_CERTS): + _CA_CERTS = None + +ServerProxy = partial(ServerProxy, fingerprints=fingerprints, + ca_certs=_CA_CERTS) +ServerPool = partial(ServerPool, fingerprints=fingerprints, + ca_certs=_CA_CERTS) + + +def context_reset(): + CONTEXT.clear() + CONTEXT['client'] = bus.ID + + +context_reset() + + +def db_list(host, port): + try: + connection = ServerProxy(host, port) + logging.getLogger(__name__).info('common.db.list()') + result = connection.common.db.list() + logging.getLogger(__name__).debug(repr(result)) + return result + except Fault as exception: + logging.getLogger(__name__).debug(exception.faultCode) + if exception.faultCode == str(HTTPStatus.FORBIDDEN.value): + return [] + else: + return None + + +def server_version(host, port): + try: + connection = ServerProxy(host, port) + logging.getLogger(__name__).info( + 'common.server.version(None, None)') + result = connection.common.server.version() + logging.getLogger(__name__).debug(repr(result)) + return result + except Exception as e: + logging.getLogger(__name__).error(e) + return None + + +def login(parameters): + from tryton import common + global CONNECTION, _USER + host = CONFIG['login.host'] + hostname = common.get_hostname(host) + port = common.get_port(host) + database = CONFIG['login.db'] + username = CONFIG['login.login'] + language = CONFIG['client.lang'] + parameters['device_cookie'] = device_cookie.get() + connection = ServerProxy(hostname, port, database) + logging.getLogger(__name__).info('common.db.login(%s, %s, %s)' + % (username, 'x' * 10, language)) + result = connection.common.db.login(username, parameters, language) + logging.getLogger(__name__).debug(repr(result)) + _USER = result[0] + session = ':'.join(map(str, [username] + result)) + if CONNECTION is not None: + CONNECTION.close() + CONNECTION = ServerPool( + hostname, port, database, session=session, cache=not CONFIG['dev']) + device_cookie.renew() + bus.listen(CONNECTION) + + +def logout(): + global CONNECTION, _USER + if CONNECTION is not None: + try: + logging.getLogger(__name__).info('common.db.logout()') + with CONNECTION() as conn: + conn.common.db.logout() + except (Fault, socket.error, http.client.CannotSendRequest): + pass + CONNECTION.close() + CONNECTION = None + _USER = None + + +def execute(*args): + global CONNECTION, _USER + if CONNECTION is None: + raise TrytonServerError('403') + try: + name = '.'.join(args[:3]) + args = args[3:] + logging.getLogger(__name__).info('%s%s' % (name, args)) + with CONNECTION() as conn: + result = getattr(conn, name)(*args) + except (http.client.CannotSendRequest, socket.error) as exception: + raise TrytonServerUnavailable(*exception.args) + logging.getLogger(__name__).debug(repr(result)) + return result + + +def clear_cache(prefix=None): + if CONNECTION: + CONNECTION.clear_cache(prefix) diff --git a/tryton/tryton/signal_event.py b/tryton/tryton/signal_event.py new file mode 100644 index 0000000000..699d2ef789 --- /dev/null +++ b/tryton/tryton/signal_event.py @@ -0,0 +1,42 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Signal event" + + +class SignalEvent(object): + "Signal event" + + def __init__(self, *args, **kwargs): + super(SignalEvent, self).__init__(*args, **kwargs) + self.__connects = {} + + def signal(self, signal, signal_data=None): + for fnct, data, key in self.__connects.get(signal, []): + fnct(self, signal_data, *data) + return True + + def signal_connected(self, signal): + return bool(self.__connects.get(signal)) + + def signal_connect(self, key, signal, fnct, *data): + self.__connects.setdefault(signal, []) + if (fnct, data, key) not in self.__connects[signal]: + self.__connects[signal].append((fnct, data, key)) + return True + + def signal_unconnect(self, key, signal=None): + if signal is None: + signal = list(self.__connects.keys()) + else: + signal = [signal] + for sig in signal: + i = 0 + while i < len(self.__connects[sig]): + if self.__connects[sig][i][2] is key: + del self.__connects[sig][i] + else: + i += 1 + return True + + def destroy(self): + self.__connects = {} diff --git a/tryton/tryton/tests/__init__.py b/tryton/tryton/tests/__init__.py new file mode 100644 index 0000000000..758ec4a9ec --- /dev/null +++ b/tryton/tryton/tests/__init__.py @@ -0,0 +1,29 @@ +# 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 os +import sys +import unittest + + +def test_suite(): + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for filename in os.listdir(os.path.dirname(__file__)): + if filename.startswith("test") and filename.endswith(".py"): + modname = "tryton.tests." + filename[:-3] + __import__(modname) + module = sys.modules[modname] + suite.addTests(loader.loadTestsFromModule(module)) + return suite + + +def main(): + suite = test_suite() + runner = unittest.TextTestRunner() + return runner.run(suite) + + +if __name__ == '__main__': + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))))) + sys.exit(not main().wasSuccessful()) diff --git a/tryton/tryton/tests/test_common_domain_parser.py b/tryton/tryton/tests/test_common_domain_parser.py new file mode 100644 index 0000000000..c46974c624 --- /dev/null +++ b/tryton/tryton/tests/test_common_domain_parser.py @@ -0,0 +1,1163 @@ +# 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 datetime as dt +from decimal import Decimal +from unittest import TestCase + +from tryton.common import untimezoned_date +from tryton.common.domain_parser import ( + group_operator, quote, split_target_value, convert_value, format_value, + complete_value, parenthesize, rlist, operatorize, DomainParser, udlex, + likify) + + +class DomainParserTestCase(TestCase): + "Test common domain_parser" + + def test_group_operator(self): + "Test group_operator" + self.assertEqual( + list(group_operator(iter(['a', '>', '=']))), ['a', '>=']) + self.assertEqual( + list(group_operator(iter(['>', '=', 'b']))), ['>=', 'b']) + self.assertEqual( + list(group_operator(iter(['a', '=', 'b']))), ['a', '=', 'b']) + self.assertEqual( + list(group_operator(iter(['a', '>', '=', 'b']))), ['a', '>=', 'b']) + self.assertEqual( + list(group_operator(iter(['a', '>', '=', '=']))), ['a', '>=', '=']) + + def test_likify(self): + "Test likify" + for value, result in [ + ('', '%'), + ('foo', '%foo%'), + ('foo%', 'foo%'), + ('foo_bar', 'foo_bar'), + ('foo\\%', '%foo\\%%'), + ('foo\\_bar', '%foo\\_bar%'), + ]: + with self.subTest(value=value): + self.assertEqual(likify(value), result) + + def test_quote(self): + "Test quote" + self.assertEqual(quote('test'), 'test') + self.assertEqual(quote('foo bar'), '"foo bar"') + self.assertEqual(quote('"foo"'), '\\\"foo\\\"') + self.assertEqual(quote('foo\\bar'), 'foo\\\\bar') + + def test_split_target_value(self): + "Test split_target_value" + field = { + 'type': 'reference', + 'selection': [ + ('spam', 'Spam'), + ('ham', 'Ham'), + ('e', 'Eggs'), + ] + } + for value, result in ( + ('Spam', (None, 'Spam')), + ('foo', (None, 'foo')), + ('Spam,', ('spam', '')), + ('Ham,bar', ('ham', 'bar')), + ('Eggs,foo', ('e', 'foo')), + ): + self.assertEqual( + split_target_value(field, value), result, + msg="split_target_value(%r, %r)" % (field, value)) + + def test_convert_boolean(self): + "Test convert boolean" + field = { + 'type': 'boolean', + } + for value, result in ( + ('Y', True), + ('yes', True), + ('t', True), + ('1', True), + ('N', False), + ('False', False), + ('no', False), + ('0', False), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_float(self): + "Test convert float" + field = { + 'type': 'float', + } + for value, result in ( + ('1', 1.0), + ('1.5', 1.5), + ('', None), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_float_factor(self): + "Test convert float with factor" + field = { + 'type': 'float', + 'factor': '100', + } + self.assertEqual(convert_value(field, '42'), 0.42) + + def test_convert_integer(self): + "Test convert integer" + field = { + 'type': 'integer', + } + for value, result in ( + ('1', 1), + ('1.5', 1), + ('', None), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_integer_factor(self): + "Test convert integer with factor" + field = { + 'type': 'integer', + 'factor': '2', + } + self.assertEqual(convert_value(field, '6'), 3) + + def test_convert_numeric(self): + "Test convert numeric" + field = { + 'type': 'numeric', + } + for value, result in ( + ('1', Decimal(1)), + ('1.5', Decimal('1.5')), + ('', None), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_numeric_factor(self): + "Test convert numeric with factor" + field = { + 'type': 'numeric', + 'factor': '5', + } + self.assertEqual(convert_value(field, '1'), Decimal('0.2')) + + def test_convert_selection(self): + "Test convert selection" + field = { + 'type': 'selection', + 'selection': [ + ('male', 'Male'), + ('female', 'Female'), + ], + } + field_with_empty = field.copy() + field_with_empty['selection'] = (field_with_empty['selection'] + + [('', '')]) + for value, result in ( + ('Male', 'male'), + ('male', 'male'), + ('test', 'test'), + (None, None), + ('', ''), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + self.assertEqual( + convert_value(field_with_empty, value), result, + msg="convert_value(%r, %r)" % (field_with_empty, value)) + + def test_convert_datetime(self): + "Test convert datetime" + field = { + 'type': 'datetime', + 'format': '"%H:%M:%S"', + } + for value, result in ( + ('12/04/2002', untimezoned_date(dt.datetime(2002, 12, 4))), + ('12/04/2002 12:30:00', untimezoned_date( + dt.datetime(2002, 12, 4, 12, 30))), + ('02/03/04', untimezoned_date(dt.datetime(2004, 2, 3))), + ('02/03/04 05:06:07', untimezoned_date( + dt.datetime(2004, 2, 3, 5, 6, 7))), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_date(self): + "Test convert date" + field = { + 'type': 'date', + } + for value, result in ( + ('12/04/2002', dt.date(2002, 12, 4)), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_time(self): + "Test convert time" + field = { + 'type': 'time', + 'format': '"%H:%M:%S"', + } + for value, result in ( + ('12:30:00', dt.time(12, 30, 0)), + ('test', None), + (None, None), + ): + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_convert_timedelta(self): + "Test convert timedelta" + field = { + 'type': 'timedelta', + } + for value, result in [ + ('1d 2:00', dt.timedelta(days=1, hours=2)), + ('foo', dt.timedelta()), + (None, None), + ]: + self.assertEqual( + convert_value(field, value), result, + msg="convert_value(%r, %r)" % (field, value)) + + def test_format_boolean(self): + "Test format boolean" + field = { + 'type': 'boolean', + } + for value, result in ( + (True, 'True'), + (False, 'False'), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_integer(self): + "Test format integer" + field = { + 'type': 'integer', + } + for value, result in ( + (1, '1'), + (1.5, '1'), + (0, '0'), + (0.0, '0'), + (False, ''), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_integer_factor(self): + "Test format integer with factor" + field = { + 'type': 'integer', + 'factor': '2', + } + self.assertEqual(format_value(field, 3), '6') + + def test_format_float(self): + "Test format float" + field = { + 'type': 'float', + } + for value, result in ( + (1, '1'), + (1.5, '1.5'), + (1.50, '1.5'), + (150.79, '150.79'), + (0, '0'), + (0.0, '0'), + (False, ''), + (None, ''), + (1e-12, '0.000000000001'), + (1.0579e-10, '0.00000000010579'), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_float_factor(self): + "Test format float with factor" + field = { + 'type': 'float', + 'factor': '100', + } + self.assertEqual(format_value(field, 0.42), '42') + + def test_format_numeric(self): + "Test format numeric" + field = { + 'type': 'numeric', + } + for value, result in ( + (Decimal(1), '1'), + (Decimal('1.5'), '1.5'), + (Decimal('1.50'), '1.5'), + (Decimal('150.79'), '150.79'), + (Decimal(0), '0'), + (Decimal('0.0'), '0'), + (False, ''), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_numeric_factor(self): + "Test format numeric with factor" + field = { + 'type': 'numeric', + 'factor': '5', + } + self.assertEqual(format_value(field, Decimal('0.2')), '1') + + def test_format_selection(self): + "Test format selection" + field = { + 'type': 'selection', + 'selection': [ + ('male', 'Male'), + ('female', 'Female'), + ], + } + field_with_empty = field.copy() + field_with_empty['selection'] = (field_with_empty['selection'] + + [('', '')]) + for value, result in ( + ('male', 'Male'), + ('test', 'test'), + (False, ''), + (None, ''), + ('', ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + self.assertEqual( + format_value(field_with_empty, value), result, + msg="format_value(%r, %r)" % (field_with_empty, value)) + + def test_format_datetime(self): + "Test format datetime" + field = { + 'type': 'datetime', + 'format': '"%H:%M:%S"', + } + for value, result in ( + (dt.date(2002, 12, 4), dt.date(2002, 12, 4).strftime('%x')), + (untimezoned_date(dt.datetime(2002, 12, 4)), + dt.date(2002, 12, 4).strftime('%x')), + (untimezoned_date(dt.datetime(2002, 12, 4, 12, 30)), + dt.datetime(2002, 12, 4, 12, 30).strftime( + '"%x %H:%M:%S"')), + (False, ''), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_date(self): + "Test format date" + field = { + 'type': 'date', + } + for value, result in ( + (dt.date(2002, 12, 4), dt.date(2002, 12, 4).strftime('%x')), + (False, ''), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_time(self): + "Test format time" + field = { + 'type': 'time', + 'format': '"%H:%M:%S"', + } + for value, result in ( + (dt.time(12, 30, 0), '"12:30:00"'), + (False, ''), + (None, ''), + ): + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_format_timedelta(self): + "Test format timedelta" + field = { + 'type': 'timedelta', + } + for value, result in [ + (dt.timedelta(days=1, hours=2), '"1d 02:00"'), + (dt.timedelta(), ''), + (None, ''), + ('', ''), + ]: + self.assertEqual( + format_value(field, value), result, + msg="format_value(%r, %r)" % (field, value)) + + def test_complete_boolean(self): + "Test complete boolean" + field = { + 'type': 'boolean', + } + for value, result in [ + (None, [True, False]), + (True, [False]), + (False, [True]), + ]: + self.assertEqual( + list(complete_value(field, value)), result, + msg="complete_value(%r, %r)" % (field, value)) + + def test_complete_selection(self): + "Test complete selection" + field = { + 'type': 'selection', + 'selection': [ + ('male', 'Male'), + ('female', 'Female'), + ], + } + for value, result in ( + ('m', ['male']), + ('test', []), + ('', ['male', 'female']), + (None, ['male', 'female']), + (['male', 'f'], [['male', 'female']]), + (['male', None], [['male', 'male'], ['male', 'female']]), + ): + self.assertEqual( + list(complete_value(field, value)), result, + msg="complete_value(%r, %r)" % (field, value)) + + field_with_empty = field.copy() + field_with_empty['selection'] = (field_with_empty['selection'] + + [('', '')]) + for value, result in ( + ('m', ['male']), + ('test', []), + ('', ['male', 'female', '']), + (None, ['male', 'female', '']), + (['male', 'f'], [['male', 'female']]), + ): + self.assertEqual( + list(complete_value(field_with_empty, value)), result, + msg="complete_value(%r, %r)" % (field_with_empty, value)) + + def test_complete_reference(self): + "Test complete reference" + field = { + 'type': 'reference', + 'selection': [ + ('spam', 'Spam'), + ('ham', 'Ham'), + ('', ''), + ], + } + for value, result in ( + ('s', ['%spam%']), + ('test', []), + ('', ['%spam%', '%ham%', '%']), + (None, ['%spam%', '%ham%', '%']), + (['spam', 'h'], [['spam', 'ham']]), + ): + self.assertEqual( + list(complete_value(field, value)), result, + msg="complete_value(%r, %r)" % (field, value)) + + def test_parenthesize(self): + for value, result in ( + (['a'], ['a']), + (['a', 'b'], ['a', 'b']), + (['(', 'a', ')'], [['a']]), + (['a', 'b', '(', 'c', '(', 'd', 'e', ')', 'f', ')', 'g'], + ['a', 'b', ['c', ['d', 'e'], 'f'], 'g']), + (['a', 'b', '(', 'c'], ['a', 'b', ['c']]), + (['a', 'b', '(', 'c', '(', 'd', 'e', ')', 'f'], + ['a', 'b', ['c', ['d', 'e'], 'f']]), + (['a', 'b', ')'], ['a', 'b']), + (['a', 'b', ')', 'c', ')', 'd)'], ['a', 'b']), + ): + self.assertEqual( + rlist(parenthesize(iter(value))), result, + msg="parenthesize(%r)" % value) + + def test_operatorize(self): + "Test operatorize" + a = ('a', 'a', 'a') + b = ('b', 'b', 'b') + c = ('c', 'c', 'c') + null_ = ('d', None, 'x') + double_null_ = ('e', None, None) + for value, result in ( + (['a'], ['a']), + (['a', 'or', 'b'], [['OR', 'a', 'b']]), + (['a', 'or', 'b', 'or', 'c'], [['OR', ['OR', 'a', 'b'], 'c']]), + (['a', 'b', 'or', 'c'], ['a', ['OR', 'b', 'c']]), + (['a', 'or', 'b', 'c'], [['OR', 'a', 'b'], 'c']), + (['a', iter(['b', 'c'])], ['a', ['b', 'c']]), + (['a', iter(['b', 'c']), 'd'], ['a', ['b', 'c'], 'd']), + (['a', 'or', iter(['b', 'c'])], [['OR', 'a', ['b', 'c']]]), + (['a', 'or', iter(['b', 'c']), 'd'], + [['OR', 'a', ['b', 'c']], 'd']), + (['a', iter(['b', 'c']), 'or', 'd'], + ['a', ['OR', ['b', 'c'], 'd']]), + (['a', 'or', iter(['b', 'or', 'c'])], + [['OR', 'a', [['OR', 'b', 'c']]]]), + (['or'], []), + (['or', 'a'], ['a']), + (['a', iter(['or', 'b'])], ['a', ['b']]), + (['a', 'or', 'or', 'b'], [['OR', 'a', 'b']]), + (['or', 'or', 'a'], ['a']), + (['or', 'or', 'a', 'b'], ['a', 'b']), + (['or', 'or', 'a', 'or', 'b'], [['OR', 'a', 'b']]), + (['a', iter(['b', 'or', 'c'])], ['a', [['OR', 'b', 'c']]]), + ([a, iter([b, ('or',), c])], [a, [['OR', b, c]]]), + (['a', iter(['b', 'or'])], ['a', [['OR', 'b']]]), + ([null_], [null_]), + ([null_, 'or', double_null_], [['OR', null_, double_null_]]), + ): + self.assertEqual( + rlist(operatorize(iter(value))), result, + msg="operatorize(%r)" % value) + + def test_stringable(self): + "Test stringable" + dom = DomainParser({ + 'name': { + 'string': 'Name', + 'type': 'char', + }, + 'multiselection': { + 'string': "MultiSelection", + 'type': 'multiselection', + 'selection': [ + ('foo', "Foo"), + ('bar', "Bar"), + ('baz', "Baz"), + ], + }, + 'relation': { + 'string': 'Relation', + 'type': 'many2one', + 'relation_fields': { + 'name': { + 'string': "Name", + 'type': 'char', + }, + }, + }, + 'relations': { + 'string': 'Relations', + 'type': 'many2many', + }, + }) + valid = ('name', '=', 'Doe') + invalid = ('surname', '=', 'John') + self.assertTrue(dom.stringable([valid])) + self.assertFalse(dom.stringable([invalid])) + self.assertTrue(dom.stringable(['AND', valid])) + self.assertFalse(dom.stringable(['AND', valid, invalid])) + self.assertTrue(dom.stringable([[valid]])) + self.assertFalse(dom.stringable([[valid], [invalid]])) + self.assertTrue(dom.stringable([('multiselection', '=', None)])) + self.assertTrue(dom.stringable([('multiselection', '=', '')])) + self.assertFalse(dom.stringable([('multiselection', '=', 'foo')])) + self.assertTrue(dom.stringable([('multiselection', '=', ['foo'])])) + self.assertTrue(dom.stringable([('relation', '=', None)])) + self.assertTrue(dom.stringable([('relation', '=', "Foo")])) + self.assertTrue(dom.stringable([('relation.rec_name', '=', "Foo")])) + self.assertFalse(dom.stringable([('relation', '=', 1)])) + self.assertTrue(dom.stringable([('relations', '=', "Foo")])) + self.assertTrue(dom.stringable([('relations', 'in', ["Foo"])])) + self.assertFalse(dom.stringable([('relations', 'in', [42])])) + self.assertTrue(dom.stringable([('relation.name', '=', "Foo")])) + + def test_string(self): + dom = DomainParser({ + 'name': { + 'string': 'Name', + 'type': 'char', + }, + 'surname': { + 'string': '(Sur)Name', + 'type': 'char', + }, + 'date': { + 'string': 'Date', + 'type': 'date', + }, + 'selection': { + 'string': 'Selection', + 'type': 'selection', + 'selection': [ + ('male', 'Male'), + ('female', 'Female'), + ('', ''), + ], + }, + 'multiselection': { + 'string': "MultiSelection", + 'type': 'multiselection', + 'selection': [ + ('foo', "Foo"), + ('bar', "Bar"), + ('baz', "Baz"), + ], + }, + 'reference': { + 'string': 'Reference', + 'type': 'reference', + 'selection': [ + ('spam', 'Spam'), + ('ham', 'Ham'), + ] + }, + 'many2one': { + 'string': 'Many2One', + 'name': 'many2one', + 'type': 'many2one', + 'relation_fields': { + 'name': { + 'string': "Name", + 'type': 'char', + }, + }, + }, + }) + self.assertEqual(dom.string([('name', '=', 'Doe')]), 'Name: =Doe') + self.assertEqual(dom.string([('name', '=', None)]), 'Name: =') + self.assertEqual(dom.string([('name', '=', '')]), 'Name: =""') + self.assertEqual(dom.string([('name', 'ilike', '%')]), 'Name: ') + self.assertEqual(dom.string([('name', 'ilike', '%Doe%')]), 'Name: Doe') + self.assertEqual( + dom.string([('name', 'ilike', '%<%')]), 'Name: "" "<"') + self.assertEqual(dom.string([('name', 'ilike', 'Doe')]), 'Name: =Doe') + self.assertEqual(dom.string([('name', 'ilike', 'Doe%')]), 'Name: Doe%') + self.assertEqual( + dom.string([('name', 'ilike', 'Doe\\%')]), 'Name: =Doe%') + self.assertEqual( + dom.string([('name', 'not ilike', '%Doe%')]), 'Name: !Doe') + self.assertEqual( + dom.string([('name', 'in', ['John', 'Jane'])]), 'Name: John;Jane') + self.assertEqual( + dom.string([('name', 'not in', ['John', 'Jane'])]), + 'Name: !John;Jane') + self.assertEqual( + dom.string([ + ('name', 'ilike', '%Doe%'), + ('name', 'ilike', '%Jane%')]), + 'Name: Doe Name: Jane') + self.assertEqual( + dom.string(['AND', + ('name', 'ilike', '%Doe%'), + ('name', 'ilike', '%Jane%')]), + 'Name: Doe Name: Jane') + self.assertEqual( + dom.string(['OR', + ('name', 'ilike', '%Doe%'), + ('name', 'ilike', '%Jane%')]), + 'Name: Doe or Name: Jane') + self.assertEqual( + dom.string([ + ('name', 'ilike', '%Doe%'), + ['OR', + ('name', 'ilike', '%John%'), + ('name', 'ilike', '%Jane%')]]), + 'Name: Doe (Name: John or Name: Jane)') + self.assertEqual(dom.string([]), '') + self.assertEqual( + dom.string([('surname', 'ilike', '%Doe%')]), '"(Sur)Name": Doe') + self.assertEqual( + dom.string([('date', '>=', dt.date(2012, 10, 24))]), + dt.date(2012, 10, 24).strftime('Date: >=%x')) + self.assertEqual(dom.string([('selection', '=', '')]), 'Selection: ') + self.assertEqual(dom.string([('selection', '=', None)]), 'Selection: ') + self.assertEqual( + dom.string([('selection', '!=', '')]), 'Selection: !""') + self.assertEqual( + dom.string([('selection', '=', 'male')]), 'Selection: Male') + self.assertEqual( + dom.string([('selection', '!=', 'male')]), 'Selection: !Male') + self.assertEqual( + dom.string([('multiselection', '=', None)]), "MultiSelection: =") + self.assertEqual( + dom.string([('multiselection', '=', '')]), "MultiSelection: =") + self.assertEqual( + dom.string([('multiselection', '!=', '')]), "MultiSelection: !=") + self.assertEqual( + dom.string([('multiselection', '=', ['foo'])]), + "MultiSelection: =Foo") + self.assertEqual( + dom.string([('multiselection', '!=', ['foo'])]), + "MultiSelection: !=Foo") + self.assertEqual( + dom.string([('multiselection', '=', ['foo', 'bar'])]), + "MultiSelection: =Foo;Bar") + self.assertEqual( + dom.string([('multiselection', '!=', ['foo', 'bar'])]), + "MultiSelection: !=Foo;Bar") + self.assertEqual( + dom.string([('multiselection', 'in', ['foo'])]), + "MultiSelection: Foo") + self.assertEqual( + dom.string([('multiselection', 'not in', ['foo'])]), + "MultiSelection: !Foo") + self.assertEqual( + dom.string([('multiselection', '=', ['foo', 'bar'])]), + "MultiSelection: =Foo;Bar") + self.assertEqual( + dom.string([('multiselection', '!=', ['foo', 'bar'])]), + "MultiSelection: !=Foo;Bar") + self.assertEqual( + dom.string([('multiselection', 'in', ['foo', 'bar'])]), + "MultiSelection: Foo;Bar") + self.assertEqual( + dom.string([('multiselection', 'not in', ['foo', 'bar'])]), + "MultiSelection: !Foo;Bar") + self.assertEqual( + dom.string([('reference', 'ilike', '%foo%')]), + 'Reference: foo') + self.assertEqual( + dom.string([('reference.rec_name', 'ilike', '%bar%', 'spam')]), + 'Reference: Spam,bar') + self.assertEqual( + dom.string([('reference', 'in', ['foo', 'bar'])]), + 'Reference: foo;bar') + self.assertEqual( + dom.string([('many2one', 'ilike', '%John%')]), 'Many2One: John') + self.assertEqual( + dom.string([('many2one.rec_name', 'in', ['John', 'Jane'])]), + 'Many2One: John;Jane') + self.assertEqual( + dom.string([('many2one.name', 'ilike', '%Foo%')]), + "Many2One.Name: Foo") + + def test_group(self): + "Test group" + dom = DomainParser({ + 'name': { + 'string': 'Name', + }, + 'firstname': { + 'string': 'First Name', + }, + 'surname': { + 'string': '(Sur)Name', + }, + 'relation': { + 'string': "Relation", + 'relation': 'relation', + 'relation_fields': { + 'name': { + 'string': "Name", + }, + }, + }, + }) + self.assertEqual( + rlist(dom.group(udlex('Name: Doe'))), [('Name', None, 'Doe')]) + self.assertEqual( + rlist(dom.group(udlex('"(Sur)Name": Doe'))), [ + ('(Sur)Name', None, 'Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: Doe Name: John'))), [ + ('Name', None, 'Doe'), + ('Name', None, 'John'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: Name: John'))), [ + ('Name', None, None), + ('Name', None, 'John')]) + self.assertEqual( + rlist(dom.group(udlex('First Name: John'))), [ + ('First Name', None, 'John'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: Doe First Name: John'))), [ + ('Name', None, 'Doe'), + ('First Name', None, 'John'), + ]) + self.assertEqual( + rlist(dom.group(udlex('First Name: John Name: Doe'))), [ + ('First Name', None, 'John'), + ('Name', None, 'Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('First Name: John First Name: Jane'))), [ + ('First Name', None, 'John'), + ('First Name', None, 'Jane'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: John Doe'))), [ + ('Name', None, 'John'), + ('Doe',), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: "John Doe"'))), [ + ('Name', None, 'John Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Doe Name: John'))), [ + ('Doe',), + ('Name', None, 'John'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: =Doe'))), [('Name', '=', 'Doe')]) + self.assertEqual( + rlist(dom.group(udlex('Name: =Doe Name: >John'))), [ + ('Name', '=', 'Doe'), + ('Name', '>', 'John'), + ]) + self.assertEqual( + rlist(dom.group(udlex('First Name: =John First Name: =Jane'))), [ + ('First Name', '=', 'John'), + ('First Name', '=', 'Jane'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: John;Jane'))), [ + ('Name', None, ['John', 'Jane']) + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: John;'))), [ + ('Name', None, ['John']) + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: John;Jane Name: Doe'))), [ + ('Name', None, ['John', 'Jane']), + ('Name', None, 'Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: John; Name: Doe'))), [ + ('Name', None, ['John']), + ('Name', None, 'Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name:'))), [ + ('Name', None, None), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: ='))), [ + ('Name', '=', None), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: =""'))), [ + ('Name', '=', ''), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: = ""'))), [ + ('Name', '=', ''), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: = Name: Doe'))), [ + ('Name', '=', None), + ('Name', None, 'Doe'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: \\"foo\\"'))), [ + ('Name', None, '"foo"'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Name: "" <'))), [ + ('Name', '', '<'), + ]) + self.assertEqual( + rlist(dom.group(udlex('Relation.Name: Test'))), [ + ('Relation.Name', None, "Test"), + ]) + + def test_parse_clause(self): + "Test parse clause" + dom = DomainParser({ + 'name': { + 'string': 'Name', + 'name': 'name', + 'type': 'char', + }, + 'integer': { + 'string': 'Integer', + 'name': 'integer', + 'type': 'integer', + }, + 'selection': { + 'string': 'Selection', + 'name': 'selection', + 'type': 'selection', + 'selection': [ + ('male', 'Male'), + ('female', 'Female'), + ], + }, + 'multiselection': { + 'string': "MultiSelection", + 'name': 'multiselection', + 'type': 'multiselection', + 'selection': [ + ('foo', "Foo"), + ('bar', "Bar"), + ('baz', "Baz"), + ], + }, + 'reference': { + 'string': 'Reference', + 'name': 'reference', + 'type': 'reference', + 'selection': [ + ('spam', 'Spam'), + ('ham', 'Ham'), + ] + }, + 'many2one': { + 'string': 'Many2One', + 'name': 'many2one', + 'type': 'many2one', + }, + 'relation': { + 'string': "Relation", + 'relation': 'relation', + 'name': 'relation', + 'relation_fields': { + 'name': { + 'string': "Name", + 'name': 'name', + 'type': 'char', + }, + }, + }, + }) + self.assertEqual( + rlist(dom.parse_clause([('John',)])), [ + ('rec_name', 'ilike', '%John%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', None, None)])), [ + ('name', 'ilike', '%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', '', None)])), [ + ('name', 'ilike', '%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', '=', None)])), [ + ('name', '=', None), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', '=', '')])), [ + ('name', '=', ''), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', None, 'Doe')])), [ + ('name', 'ilike', '%Doe%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', '!', 'Doe')])), [ + ('name', 'not ilike', '%Doe%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', None, ['John', 'Jane'])])), [ + ('name', 'in', ['John', 'Jane']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Name', '!', ['John', 'Jane'])])), [ + ('name', 'not in', ['John', 'Jane']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Selection', None, None)])), [ + ('selection', '=', None), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Selection', None, '')])), [ + ('selection', '=', ''), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Selection', None, ['Male', 'Female'])])), + [('selection', 'in', ['male', 'female'])]) + self.assertEqual( + rlist(dom.parse_clause([('MultiSelection', None, None)])), [ + ('multiselection', '=', None), + ]) + self.assertEqual( + rlist(dom.parse_clause([('MultiSelection', None, '')])), [ + ('multiselection', 'in', ['']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('MultiSelection', '=', '')])), [ + ('multiselection', '=', ['']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('MultiSelection', '!', '')])), [ + ('multiselection', 'not in', ['']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('MultiSelection', '!=', '')])), [ + ('multiselection', '!=', ['']), + ]) + self.assertEqual( + rlist(dom.parse_clause( + [('MultiSelection', None, ['Foo', 'Bar'])])), [ + ('multiselection', 'in', ['foo', 'bar']), + ]) + self.assertEqual( + rlist(dom.parse_clause( + [('MultiSelection', '=', ['Foo', 'Bar'])])), [ + ('multiselection', '=', ['foo', 'bar']), + ]) + self.assertEqual( + rlist(dom.parse_clause( + [('MultiSelection', '!', ['Foo', 'Bar'])])), [ + ('multiselection', 'not in', ['foo', 'bar']), + ]) + self.assertEqual( + rlist(dom.parse_clause( + [('MultiSelection', '!=', ['Foo', 'Bar'])])), [ + ('multiselection', '!=', ['foo', 'bar']), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Integer', None, None)])), [ + ('integer', '=', None), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Integer', None, '3..5')])), [[ + ('integer', '>=', 3), + ('integer', '<=', 5), + ]]) + self.assertEqual( + rlist(dom.parse_clause([('Reference', None, 'foo')])), [ + ('reference', 'ilike', '%foo%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Reference', None, 'Spam')])), [ + ('reference', 'ilike', '%spam%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Reference', None, 'Spam,bar')])), [ + ('reference.rec_name', 'ilike', '%bar%', 'spam'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Reference', None, ['foo', 'bar'])])), [ + ('reference', 'in', ['foo', 'bar']), + ]) + self.assertEqual( + rlist(dom.parse_clause(['OR', + ('Name', None, 'John'), ('Name', None, 'Jane')])), + ['OR', + ('name', 'ilike', '%John%'), + ('name', 'ilike', '%Jane%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Many2One', None, 'John')])), [ + ('many2one', 'ilike', '%John%'), + ]) + self.assertEqual( + rlist(dom.parse_clause([('Many2One', None, ['John', 'Jane'])])), [ + ('many2one.rec_name', 'in', ['John', 'Jane']), + ]) + self.assertEqual( + rlist(dom.parse_clause(iter([iter([['John']])]))), [ + [('rec_name', 'ilike', '%John%')]]) + self.assertEqual( + rlist(dom.parse_clause(iter([['Relation.Name', None, "Test"]]))), + [('relation.name', 'ilike', "%Test%")]) + self.assertEqual( + rlist(dom.parse_clause(iter([['OR']]))), + [('rec_name', 'ilike', "%OR%")]) + self.assertEqual( + rlist(dom.parse_clause(iter([['AND']]))), + [('rec_name', 'ilike', "%AND%")]) + + def test_completion_char(self): + "Test completion char" + dom = DomainParser({ + 'name': { + 'string': 'Name', + 'name': 'name', + 'type': 'char', + }, + }) + self.assertEqual(list(dom.completion('Nam')), ['Name: ']) + self.assertEqual(list(dom.completion('Name:')), ['Name: ']) + self.assertEqual(list(dom.completion('Name: foo')), []) + self.assertEqual(list(dom.completion('Name: !=')), []) + self.assertEqual(list(dom.completion('Name: !=foo')), []) + self.assertEqual(list(dom.completion('')), ['Name: ']) + self.assertEqual(list(dom.completion(' ')), ['', 'Name: ']) + + def test_completion_many2one(self): + "Test completion many2one" + dom = DomainParser({ + 'relation': { + 'name': 'relation', + 'string': "Relation", + 'type': 'many2one', + 'relation_fields': { + 'name': { + 'name': 'name', + 'string': "Name", + 'type': 'char', + }, + }, + }, + }) + self.assertEqual( + list(dom.completion('Relatio')), + ['Relation: ', 'Relation.Name: ']) + + def test_completion_boolean(self): + "Test completion boolean" + dom = DomainParser({ + 'name': { + 'string': "Active", + 'name': 'active', + 'type': 'boolean', + }, + }) + + self.assertEqual(list(dom.completion("Act")), ["Active: "]) + self.assertEqual(list(dom.completion("Active:")), + ["Active: ", "Active: True", "Active: False"]) + self.assertEqual( + list(dom.completion("Active: t")), + ["Active: True", "Active: False"]) + self.assertEqual( + list(dom.completion("Active: f")), + ["Active: False", "Active: True"]) diff --git a/tryton/tryton/tests/test_common_selection.py b/tryton/tryton/tests/test_common_selection.py new file mode 100644 index 0000000000..11cb2ba4b9 --- /dev/null +++ b/tryton/tryton/tests/test_common_selection.py @@ -0,0 +1,17 @@ +# 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 unittest import TestCase + +from tryton.common.selection import freeze_value + + +class SelectionTestCase(TestCase): + "Test common selection" + + def test_freeze_value(self): + "Test freeze_value" + self.assertEqual(freeze_value({'foo': 'bar'}), (('foo', 'bar'),)) + self.assertEqual(freeze_value([1, 42, 2, 3]), (1, 42, 2, 3)) + self.assertEqual(freeze_value('foo'), 'foo') + self.assertEqual( + freeze_value({'foo': {'bar': 42}}), (('foo', (('bar', 42),)),)) diff --git a/tryton/tryton/tests/test_common_timedelta.py b/tryton/tryton/tests/test_common_timedelta.py new file mode 100644 index 0000000000..3676ddee8b --- /dev/null +++ b/tryton/tryton/tests/test_common_timedelta.py @@ -0,0 +1,88 @@ +# 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 datetime as dt +from unittest import TestCase + +from tryton.common.timedelta import format, parse, DEFAULT_CONVERTER + + +class TimeDeltaTestCase(TestCase): + "Test common timedelta" + + def _format_values(self): + return [ + (None, ''), + (dt.timedelta(), '00:00'), + (dt.timedelta(days=3, hours=5, minutes=30), '3d 05:30'), + (dt.timedelta(weeks=48), '11M 6d'), + (dt.timedelta(weeks=50), '11M 2w 6d'), + (dt.timedelta(weeks=52), '12M 4d'), + (dt.timedelta(days=360), '12M'), + (dt.timedelta(days=364), '12M 4d'), + (dt.timedelta(days=365), '1Y'), + (dt.timedelta(days=366), '1Y 1d'), + (dt.timedelta(hours=2, minutes=5, seconds=10), '02:05:10'), + (dt.timedelta(minutes=15, microseconds=42), '00:15:00.000042'), + (dt.timedelta(days=1, microseconds=42), '1d .000042'), + (dt.timedelta(seconds=-1), '-00:00:01'), + (dt.timedelta(days=-1, hours=-5, minutes=-30), '-1d 05:30'), + ] + + def _time_only_converter(self): + converter = {} + converter['s'] = DEFAULT_CONVERTER['s'] + converter['m'] = DEFAULT_CONVERTER['m'] + converter['h'] = DEFAULT_CONVERTER['h'] + converter['d'] = 0 + return converter + + def _time_only_converter_values(self): + return [ + (None, ''), + (dt.timedelta(), '00:00'), + (dt.timedelta(days=5, hours=5, minutes=30), '125:30'), + (dt.timedelta(hours=2, minutes=5, seconds=10), '02:05:10'), + (dt.timedelta(minutes=15, microseconds=42), '00:15:00.000042'), + (dt.timedelta(days=1, microseconds=42), '24:00:00.000042'), + (dt.timedelta(seconds=-1), '-00:00:01'), + (dt.timedelta(days=-1, hours=-5, minutes=-30), '-29:30'), + ] + + def test_format(self): + "Test format" + for timedelta, text in self._format_values(): + self.assertEqual(format(timedelta), text, + msg="format(%r)" % timedelta) + + def test_format_time_only_converter(self): + "Test format with time only converter" + converter = self._time_only_converter() + for timedelta, text in self._time_only_converter_values(): + self.assertEqual(format(timedelta, converter), text, + msg="format(%r)" % timedelta) + + def _parse_values(self): + return self._format_values() + [ + (dt.timedelta(), ' '), + (dt.timedelta(), 'foo'), + (dt.timedelta(days=1.5), '1.5d'), + (dt.timedelta(days=-2), '1d -1d'), + (dt.timedelta(hours=1, minutes=5, seconds=10), '1:5:10:42'), + (dt.timedelta(hours=2), '1: 1:'), + (dt.timedelta(hours=.25), ':15'), + (dt.timedelta(hours=1), '1h'), + (dt.timedelta(hours=.25), '.25h'), + ] + + def test_parse(self): + "Test parse" + for timedelta, text in self._parse_values(): + self.assertEqual(parse(text), timedelta, + msg="parse(%r)" % text) + + def test_parse_time_only_converter(self): + "Test parse with time only converter" + converter = self._time_only_converter() + for timedelta, text in self._time_only_converter_values(): + self.assertEqual(parse(text, converter), timedelta, + msg="parse(%r)" % text) diff --git a/tryton/tryton/translate.py b/tryton/tryton/translate.py new file mode 100644 index 0000000000..6c6a2fffdd --- /dev/null +++ b/tryton/tryton/translate.py @@ -0,0 +1,204 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +"Translate" +import os +import locale +import gettext +import logging +import sys + +from gi.repository import Gtk + +from tryton.config import CURRENT_DIR + +_ = gettext.gettext + +_LOCALE2WIN32 = { + 'af_ZA': 'Afrikaans_South Africa', + 'ar_AE': 'Arabic_UAE', + 'ar_BH': 'Arabic_Bahrain', + 'ar_DZ': 'Arabic_Algeria', + 'ar_EG': 'Arabic_Egypt', + 'ar_IQ': 'Arabic_Iraq', + 'ar_JO': 'Arabic_Jordan', + 'ar_KW': 'Arabic_Kuwait', + 'ar_LB': 'Arabic_Lebanon', + 'ar_LY': 'Arabic_Libya', + 'ar_MA': 'Arabic_Morocco', + 'ar_OM': 'Arabic_Oman', + 'ar_QA': 'Arabic_Qatar', + 'ar_SA': 'Arabic_Saudi_Arabia', + 'ar_SY': 'Arabic_Syria', + 'ar_TN': 'Arabic_Tunisia', + 'ar_YE': 'Arabic_Yemen', + 'az-Cyrl-AZ': 'Azeri_Cyrillic', + 'az-Latn-AZ': 'Azeri_Latin', + 'be_BY': 'Belarusian_Belarus', + 'bg_BG': 'Bulgarian_Bulgaria', + 'bs_BA': 'Serbian (Latin)', + 'ca_ES': 'Catalan_Spain', + 'cs_CZ': 'Czech_Czech Republic', + 'da_DK': 'Danish_Denmark', + 'de_AT': 'German_Austrian', + 'de_CH': 'German_Swiss', + 'de_DE': 'German_Germany', + 'de_LI': 'German_Liechtenstein', + 'de_LU': 'German_Luxembourg', + 'el_GR': 'Greek_Greece', + 'en_AU': 'English_Australian', + 'en_BZ': 'English_Belize', + 'en_CA': 'English_Canadian', + 'en_IE': 'English_Irish', + 'en_JM': 'English_Jamaica', + 'en_TT': 'English_Trinidad', + 'en_US': 'English_USA', + 'en_ZW': 'English_Zimbabwe', + 'es_AR': 'Spanish_Argentina', + 'es_BO': 'Spanish_Bolivia', + 'es_CL': 'Spanish_Chile', + 'es_CO': 'Spanish_Colombia', + 'es_CR': 'Spanish_Costa_Rica', + 'es_DO': 'Spanish_Dominican_Republic', + 'es_EC': 'Spanish_Ecuador', + 'es_ES': 'Spanish_Spain', + 'es_ES_tradnl': 'Spanish_Traditional_Sort', + 'es_GT': 'Spanish_Guatemala', + 'es_HN': 'Spanish_Honduras', + 'es_MX': 'Spanish_Mexican', + 'es_NI': 'Spanish_Nicaragua', + 'es_PA': 'Spanish_Panama', + 'es_PE': 'Spanish_Peru', + 'es_PR': 'Spanish_Puerto_Rico', + 'es_PY': 'Spanish_Paraguay', + 'es_SV': 'Spanish_El_Salvador', + 'es_UY': 'Spanish_Uruguay', + 'es_VE': 'Spanish_Venezuela', + 'et_EE': 'Estonian_Estonia', + 'eu_ES': 'Basque_Spain', + 'fa_IR': 'Farsi_Iran', + 'fi_FI': 'Finnish_Finland', + 'fr_BE': 'French_Belgian', + 'fr_CA': 'French_Canadian', + 'fr_CH': 'French_Swiss', + 'fr_FR': 'French_France', + 'fr_LU': 'French_Luxembourg', + 'fr_MC': 'French_Monaco', + 'ga': 'Scottish Gaelic', + 'gl_ES': 'Galician_Spain', + 'gu': 'Gujarati_India', + 'he_IL': 'Hebrew_Israel', + 'hi_IN': 'Hindi', + 'hr_HR': 'Croatian', + 'hu_HU': 'Hungarian', + 'hu': 'Hungarian_Hungary', + 'hy_AM': 'Armenian', + 'id_ID': 'Indonesian_indonesia', + 'is_IS': 'Icelandic_Iceland', + 'it_CH': 'Italian_Swiss', + 'it_IT': 'Italian_Italy', + 'ja_JP': 'Japanese_Japan', + 'ka_GE': 'Georgian_Georgia', + 'kk_KZ': 'Kazakh', + 'km_KH': 'Khmer', + 'kn_IN': 'Kannada', + 'ko_IN': 'Konkani', + 'ko_KR': 'Korean_Korea', + 'lo_LA': 'Lao_Laos', + 'lt_LT': 'Lithuanian_Lithuania', + 'lv_LV': 'Latvian_Latvia', + 'mi_NZ': 'Maori', + 'mi_NZ': 'Maori', + 'mi_NZ': 'Maori', + 'mk_MK': 'Macedonian', + 'ml_IN': 'Malayalam_India', + 'mn': 'Cyrillic_Mongolian', + 'mr_IN': 'Marathi', + 'ms_BN': 'Malay_Brunei_Darussalam', + 'ms_MY': 'Malay_Malaysia', + 'nb_NO': 'Norwegian_Bokmal', + 'nl_BE': 'Dutch_Belgian', + 'nl_NL': 'Dutch_Netherlands', + 'nn_NO': 'Norwegian-Nynorsk_Norway', + 'ph_PH': 'Filipino_Philippines', + 'pl_PL': 'Polish_Poland', + 'pt_BR': 'Portuguese_Brazil', + 'pt_PT': 'Portuguese_Portugal', + 'ro_RO': 'Romanian_Romania', + 'ru_RU': 'Russian_Russia', + 'sa_IN': 'Sanskrit', + 'sk_SK': 'Slovak_Slovakia', + 'sl_SI': 'Slovenian_Slovenia', + 'sq_AL': 'Albanian_Albania', + 'sr_CS': 'Serbian (Cyrillic)_Serbia and Montenegro', + 'sv_FI': 'Swedish_Finland', + 'sv_SE': 'Swedish_Sweden', + 'sw_KE': 'Swahili', + 'ta_IN': 'Tamil', + 'th_TH': 'Thai_Thailand', + 'tr_IN': 'Urdu', + 'tr_TR': 'Turkish_Turkey', + 'tt_RU': 'Tatar', + 'uk_UA': 'Ukrainian_Ukraine', + 'uz-Cyrl_UZ': 'Uzbek_Cyrillic', + 'uz-Latn_UZ': 'Uzbek_Latin', + 'vi_VN': 'Vietnamese_Viet Nam', + 'zh_CN': 'Chinese_PRC', + 'zh_HK': 'Chinese_Hong_Kong', + 'zh_MO': 'Chinese_Macau', + 'zh_SG': 'Chinese_Singapore', + 'zh_TW': 'Chinese_Taiwan', +} + + +def setlang(lang=None, locale_dict=None): + "Set language" + locale_dir = os.path.join(CURRENT_DIR, 'data/locale') + if not os.path.isdir(locale_dir): + # do not import when frozen + import pkg_resources + locale_dir = pkg_resources.resource_filename( + 'tryton', 'data/locale') + if lang: + encoding = locale.getdefaultlocale()[1] + if not encoding: + encoding = 'UTF-8' + if encoding.lower() in ('utf', 'utf8'): + encoding = 'UTF-8' + if encoding == 'cp1252': + encoding = '1252' + try: + lang2 = locale.normalize(lang).split('.')[0] + if os.name == 'nt': + lang2 = _LOCALE2WIN32.get(lang2, lang2) + elif sys.platform == 'darwin': + encoding = 'UTF-8' + # ensure environment variable are str + lang, lang2, encoding = str(lang), str(lang2), str(encoding) + os.environ['LANGUAGE'] = lang + os.environ['LC_ALL'] = lang2 + '.' + encoding + os.environ['LC_MESSAGES'] = lang2 + '.' + encoding + os.environ['LANG'] = lang + '.' + encoding + locale.setlocale(locale.LC_ALL, lang2 + '.' + encoding) + except locale.Error: + logging.getLogger(__name__).info( + _('Unable to set locale %s') % lang2 + '.' + encoding) + + if os.path.isdir(locale_dir): + gettext.bindtextdomain('tryton', locale_dir) + gettext.textdomain('tryton') + + if locale_dict: + conv = locale.localeconv() + for field in list(locale_dict.keys()): + if field == 'date': + continue + conv[field] = locale_dict[field] + locale.localeconv = lambda: conv + + +def set_language_direction(direction): + if direction == 'rtl': + direction = Gtk.TextDirection.RTL + else: + direction = Gtk.TextDirection.LTR + Gtk.Widget.set_default_direction(direction) diff --git a/tryton/win32/gtk-3.0/gdk-pixbuf.loaders b/tryton/win32/gtk-3.0/gdk-pixbuf.loaders new file mode 100644 index 0000000000..6fd63640c3 --- /dev/null +++ b/tryton/win32/gtk-3.0/gdk-pixbuf.loaders @@ -0,0 +1,114 @@ +# GdkPixbuf Image Loader Modules file +# Automatically generated file, do not edit +# Created by gdk-pixbuf-query-loaders.exe from gdk-pixbuf-2.36.12 +# +# LoaderDir = lib\gdk-pixbuf-2.0\2.10.0\loaders +# +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-ani.dll" +"ani" 4 "gdk-pixbuf" "Windows animated cursor" "LGPL" +"application/x-navi-animation" "" +"ani" "" +"RIFF ACON" " xxxx " 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-bmp.dll" +"bmp" 5 "gdk-pixbuf" "BMP" "LGPL" +"image/bmp" "image/x-bmp" "image/x-MS-bmp" "" +"bmp" "" +"BM" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-gif.dll" +"gif" 4 "gdk-pixbuf" "GIF" "LGPL" +"image/gif" "" +"gif" "" +"GIF8" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-icns.dll" +"icns" 4 "gdk-pixbuf" "MacOS X icon" "GPL" +"image/x-icns" "" +"icns" "" +"icns" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-ico.dll" +"ico" 5 "gdk-pixbuf" "Windows icon" "LGPL" +"image/x-icon" "image/x-ico" "image/x-win-bitmap" "image/vnd.microsoft.icon" "application/ico" "image/ico" "image/icon" "text/ico" "" +"ico" "cur" "" +" \001 " "zz znz" 100 +" \002 " "zz znz" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-jasper.dll" +"jpeg2000" 4 "gdk-pixbuf" "JPEG 2000" "LGPL" +"image/jp2" "image/jpeg2000" "image/jpx" "" +"jp2" "jpc" "jpx" "j2k" "jpf" "" +" jP" "!!!! " 100 +"\377O\377Q" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-jpeg.dll" +"jpeg" 5 "gdk-pixbuf" "JPEG" "LGPL" +"image/jpeg" "" +"jpeg" "jpe" "jpg" "" +"\377\330" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-png.dll" +"png" 5 "gdk-pixbuf" "PNG" "LGPL" +"image/png" "" +"png" "" +"\211PNG\r\n\032\n" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-pnm.dll" +"pnm" 4 "gdk-pixbuf" "PNM/PBM/PGM/PPM" "LGPL" +"image/x-portable-anymap" "image/x-portable-bitmap" "image/x-portable-graymap" "image/x-portable-pixmap" "" +"pnm" "pbm" "pgm" "ppm" "" +"P1" "" 100 +"P2" "" 100 +"P3" "" 100 +"P4" "" 100 +"P5" "" 100 +"P6" "" 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-qtif.dll" +"qtif" 4 "gdk-pixbuf" "QuickTime" "LGPL" +"image/x-quicktime" "image/qtif" "" +"qtif" "qif" "" +"abcdidsc" "xxxx " 100 +"abcdidat" "xxxx " 100 + +"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-svg.dll" +"svg" 6 "gdk-pixbuf" "Scalable Vector Graphics" "LGPL" +"image/svg+xml" "image/svg" "image/svg-xml" "image/vnd.adobe.svg+xml" "text/xml-svg" "image/svg+xml-compressed" "" +"svg" "svgz" "svg.gz" "" +" > /root/.hgrc + - echo "hgext.share =" >> /root/.hgrc + - echo "[share]" >> /root/.hgrc + - echo "pool = /root/.cache/hg" >> /root/.hgrc + - pip install tox + - tox -e "${TOXENV}-${DATABASE}" + volumes: + - cache:/root/.cache + check_dist: + image: ${IMAGE} + commands: + - pip install twine + - python setup.py sdist + - twine check dist/* + +services: + postgresql: + image: postgres + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: "-c fsync=off -c synchronous_commit=off -c full_page_writes=off" + when: + matrix: + DATABASE: postgresql + +matrix: + include: + - IMAGE: python:3.6 + TOXENV: py36 + DATABASE: sqlite + - IMAGE: python:3.6 + TOXENV: py36 + DATABASE: postgresql + - IMAGE: python:3.7 + TOXENV: py37 + DATABASE: sqlite + - IMAGE: python:3.7 + TOXENV: py37 + DATABASE: postgresql + - IMAGE: python:3.8 + TOXENV: py38 + DATABASE: sqlite + - IMAGE: python:3.8 + TOXENV: py38 + DATABASE: postgresql + - IMAGE: python:3.9 + TOXENV: py39 + DATABASE: sqlite + - IMAGE: python:3.9 + TOXENV: py39 + DATABASE: postgresql diff --git a/trytond/.flake8 b/trytond/.flake8 new file mode 100644 index 0000000000..fa6b4f66d1 --- /dev/null +++ b/trytond/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore=E123,E124,E126,E128,E741,W503 diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG new file mode 100644 index 0000000000..4d44027ad4 --- /dev/null +++ b/trytond/CHANGELOG @@ -0,0 +1,869 @@ +Version 6.0.24 - 2022-11-17 +--------------------------- +* Bug fixes (see mercurial logs for details) + +Version 6.0.23 - 2022-11-05 +--------------------------- +* Bug fixes (see mercurial logs for details) + +Version 6.0.22 - 2022-10-17 +* Bug fixes (see mercurial logs for details) + +Version 6.0.21 - 2022-08-17 +* Bug fixes (see mercurial logs for details) + +Version 6.0.20 - 2022-07-01 +* Bug fixes (see mercurial logs for details) +* Enforce certificate validation for SMTP connection (issue11564) + +Version 6.0.19 - 2022-06-03 +* Bug fixes (see mercurial logs for details) + +Version 6.0.18 - 2022-04-23 +* Bug fixes (see mercurial logs for details) + +Version 6.0.17 - 2022-04-15 +* Bug fixes (see mercurial logs for details) +* Add support for werkzeug 2.0 + +Version 6.0.16 - 2022-03-01 +* Bug fixes (see mercurial logs for details) +* Do not resolve entities by default with lxml (issue11219) +* Use defusedxml to parse XML (issue11244) + +Version 6.0.15 - 2022-02-16 +* Bug fixes (see mercurial logs for details) + +Version 6.0.14 - 2022-02-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.13 - 2022-01-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.12 - 2021-12-03 +* Bug fixes (see mercurial logs for details) + +Version 6.0.11 - 2021-11-20 +* Bug fixes (see mercurial logs for details) + +Version 6.0.10 - 2021-11-04 +* Bug fixes (see mercurial logs for details) + +Version 6.0.9 - 2021-10-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.8 - 2021-10-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.7 - 2021-09-16 +* Bug fixes (see mercurial logs for details) + +Version 6.0.6 - 2021-09-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.5 - 2021-08-05 +* Bug fixes (see mercurial logs for details) + +Version 6.0.4 - 2021-07-21 +* Bug fixes (see mercurial logs for details) + +Version 6.0.3 - 2021-07-01 +* Bug fixes (see mercurial logs for details) + +Version 6.0.2 - 2021-05-15 +* Bug fixes (see mercurial logs for details) + +Version 6.0.1 - 2021-05-07 +* Bug fixes (see mercurial logs for details) + +Version 6.0.0 - 2021-05-03 +* Bug fixes (see mercurial logs for details) +* Allow column sql types to be tested from the table handler +* Add support for database connection parameters to configuration URI +* Use immutable datastructures for Dict and MultiSelection fields +* Skip warnings for non-interactive operations +* Check rule only if _check_access is set +* Add statistics to Cache +* Add support for avatars +* Add status command +* Add document widget on form view +* Add full text and similarity search and FullText field +* Add URL type to icon +* Use instance method to get next sequence value +* Store the sequence type directly on sequence +* Allow sending email with record's attachments +* Add which records to use for actions +* Add parent to group to inherit accesses +* Add __access__ to Model +* Add route wrapper to allow null origin +* Add header grouping on report +* Protect trusted devices against brute force attack +* Make ModelView.parse_view public +* Add help for each value to selection and multiselection +* Use safe_join in SharedDataMiddlewareIndex (issue10068) +* Use menu name for window opened from menu +* Add trigonometric functions to sqlite backend +* Allow skipping user warnings globally +* Add validate option to trytond-admin +* Refresh pool of other processes +* Add clear_all method to Cache +* Support Genshi element directives in HTML editor +* Add firstline tool +* Add support for Python 3.9 +* Skip access check on ModelStorage instances +* Eager load Function field with same multiple getter +* Support other methods for button_change decorator +* Allow copying Python instances of Model +* Add all buttons to default form view +* Unify PYSON string format +* Forbid all white spaces except space in Char field + +Version 5.8.0 - 2020-11-02 +* Bug fixes (see mercurial logs for details) +* Remove support for Python 3.5 +* Support import of timedelta data +* Add symbol widget +* Add sql_pairing tool +* Add format_datetime to Report +* Allow sharing view searches +* Allow overriding digits in Lang.currency and Report.format_currency +* Add record name in report filename +* Add deletable and writable state from ir.rule to read +* Add language attribute to data tag +* Add e-mail template +* Send e-mail on behalf of user +* Register mixins to generic Report class +* Cache in memory the report template instances +* Support Genshi's MsgDirective in report +* Support PYSON comparison of date and datetime +* Add cron task to clean old queue tasks +* Add option to run cron once +* Add defaults option to route +* Add escape_wildcard in tools +* Add option to ensure emails are sent +* Allow keyword action for all models +* Add sortable_values in tools +* Remove default colors on graph and calendar +* Add model, record and records attributes on Wizard +* Check read access of wizard records +* Add cached_property in tools +* Use custom class to store record data +* Do not write to existing targets on xxx2Many add +* Add configuration check +* Add support for properties to fields.depends +* Allow combining authentication methods together +* Add context to export CSV route + +Version 5.6.0 - 2020-05-04 +* Bug fixes (see mercurial logs for details) +* Call getter function when accessing Function field on unsaved record +* Add language configuration wizard +* Allow copying attachments and notes to created records +* Add link button on form +* Support explicit delete and remove for saving and on_change xxx2Many +* Add export_data_domain to ModelStorage +* Add route to export CSV data +* Add test on target of relation fields +* Add width and height attributes on calendar view +* Add default database name configuration +* Add retry option to report convert +* Add depends fields on view_attributes +* Simplify trigger action +* Run trigger in the queue +* Do not copy Binary content with file_id +* Replace memoize with functools.lru_cache +* Add support for Python 3.8 +* Set all fields readonly for inactive record +* Enable check_access context when checking wizard access (issue9108) +* Add option to send test email with trytond-admin +* Add editable on calendar view +* Add xalign and yalign to group +* Add MultiSelection entry to Dict field +* Allow empty order clause +* Change editable tree attribute into boolean +* Send default order to clients +* Replace advisory lock by SKIP LOCKED in queue pulling +* Load WSGI middleware from configuration +* Add help text to Dict keys +* Update button_action with returned values +* Use direct access to backend classes +* Add weasyprint support +* Add slugify tool +* Add is_secure, host and http_host to url module +* Add __href__ to URLMixin +* Add validate attribute to wizard's Button +* Return 400 instead of 500 for Tryton exception +* Fill the reverse field in the One2Many setter +* Drop support for skiptest attribute on xml files +* Remove implicit field names in ModelStorage.search_read + +Version 5.4.0 - 2019-11-04 +* Bug fixes (see mercurial logs for details) +* Improve tests on depends +* Add permission groups on export +* Retry cron job on DatabaseOperationalError +* Add visual context on tree view +* Add start value to PYSON Date and DateTime +* Add MultiSelection field +* Support dot notation on PYSON Eval +* Add __slots__ +* Pass app and request to wsgi error handlers +* Add lazy_gettext +* Add format_timedelta to Report +* Add partial TO_CHAR support for date and datetime on SQLite +* Setup default logging for WSGI app +* Add format argument to report format_date function +* Change expand attribute into a factor +* Allow SQL expression as value of fields +* Allow customizing Dict keys order with a sequence field +* Add invalid domain in DomainValidationError +* Add domain inversion tools +* Add get_relation_fields to DictSchemaMixin +* Add level on Model.fields_get to fill relation_fields + +Version 5.2.0 - 2019-05-06 +* Bug fixes (see mercurial logs for details) +* Add sort and translate options to Reference field +* Do not create empty translations +* Replace dsn by params to connect to postgresql +* Simplify cron +* Add duration on Cache +* Add strip wildcards helpers +* Add list-form view +* Do not set id for on_change calls if cached +* Add cache on RPC +* Remove support for Python 3.4 +* Allow to update record when importing data +* Set context on record instantiated by Field.__set__ +* Set context when reading related fields +* Check read access on field in search order (issue8189) +* Add base64 converter to URL map +* Add HTML widget +* Support import of native numeric, date and datetime data +* Add day view on calendar +* Raise NotImplementedError when setting Function field without setter +* Add exports in view_toolbar_get +* Add resources method to ModelStorage +* Validate selection format in Dict schema +* Allow to extend Field's string and help +* Add console +* Add Model.__names__ to retrieve model and field names +* Add size attribute on image tag +* Allow to use channel to synchronize Cache +* Display ids and rule names on access error +* Move field definition from Model to Field +* Use number of verbose flag as log level +* Add coroutine concurrency option +* Add increasing delay on database operational error retry +* Allows to lock records for update +* Remove _nocache on Transaction +* Make Cache transactional +* Allow to search on key's value of Dict +* Allow to order Dict by key's value +* Do not store keys with null value in Dict +* Add cache timeout for web +* Allow the bus requests to be redirected to another host +* Add support for CORS +* Add extras modules to ModuleTestCase +* Add timeout to Report.convert +* Allow records from XML with noupdate to be deleted +* Add ir.calendar for month and day +* Rename languages: hu_HU, it_IT and pt_BR into hu, it and pt +* Define custom exceptions +* Remove _error_messages, raise_user_error and raise_user_warning +* Remove ModelStorage._constraints +* Add ir.message +* Add check on ModelSQL for positive id +* New API to read related fields +* Remove implicit fields names in ModelStorage.read +* Check read access on field in search domain (issue7766) +* Add active field on ModelAccess, ModelFieldAccess and Group +* Use write mode by default to check create and delete of resources + +Version 5.0.0 - 2018-10-01 +* Bug fixes (see mercurial logs for details) +* Allow non translatable reports +* Replace hard coded 'state' by '_transition_state' in copy +* Support dotted notation in copy default +* Allow callable in copy default +* Add bus system +* Allow to set any default configuration value from environment +* Clear existing session when password is changed +* Manage session with max_age and timeout +* Use passlib to hash and update password +* Remove unique constraint on attachment name +* New icons +* Add pyson TimeDelta +* Remove button if it is not allowed to access some of its dependant fields +* Add support for Python 3.7 +* Add transactional queue and workers +* Add __table_handler__ to ModelSQL +* Support partial index +* Allow to use SQL expression for index action +* Add cache clean timeout configuration +* Implicit conversion to boolean in PYSON statement +* Add domain to dictionary schema +* Fill main language in ir.configuration at database initialization +* Remove support for Python 2.7 +* Check Rule also after modification +* Check Rule after indirect fields +* Allow to define view id in switch client action +* Do not validate Function fields even with setter +* Simplify the creation of dependencies graph +* Make depends on methods generic to any method +* Add width/height attribute to notebook +* Allow to call set_lang with None and language instance +* Add hostname configuration to list database +* Add tree mixin +* Rename "install-dependences" into "activate-dependencies" +* Use recursive common table expression for child_of/parent_of operators +* Add sql_cast on Field + +Version 4.8.0 - 2018-04-23 +* Bug fixes (see mercurial logs for details) +* Add index method to order field set calls +* Add deactivable mixin +* Ensure active field is present on tree view +* Manage depends xml attribute on field tag +* Add expand attribute on group tag +* Add depends on Pool.register +* Add Exclude constraint +* Allow to perform unaccented searches on Char fields +* Always raise exception in table handler +* Simplify API for Session: new, remove, check and reset +* Ensure that all buttons are registered in ir.model.button +* Make trytond-admin ask for admin email +* Add option to set admin email with trytond-admin +* Add option to reset admin password with trytond-admin +* Add reset password button +* Add depends on Button +* Add ModelData.has_model cache for ModelStorage.check_xml_record +* Limit authentication attempt per IP network +* Manage 'X-Forwarded' headers from proxies +* Add '_request' attribute to Transaction context +* Delete translations on deletion only when model has translatable field +* Remove MySQL backend +* Add keyword attribute to button tag +* Allow field name on image tag +* Remove unoconv and call soffice directly +* Allow to include mixin to pool objects +* Improve validation of PYSON domain +* Use JSON canonical form for Dict value +* Remove rules on user +* Add monetary formatting to language +* Add exceptional parent language +* Add get method on ir.lang +* Convert format, currency and strftime of ir.lang into instance methods + +Version 4.6.0 - 2017-10-30 +* Bug fixes (see mercurial logs for details) +* Add support for Python 3.6 +* Remove support for Python 3.3 +* Move handling of sequences to the Database object +* Allow to add tests through entry points +* Add translated descriptor for Reference field +* Implement alter_type and alter_size for sqlite backend +* Add a maximum size for request +* Add support for single record report +* Add support for Flat OpenDocument +* Add get_email in trytond.report +* Replace plain extension by txt +* Support test database cache for remote postgresql +* Increase session randomness to 32 bytes +* Allow to specify datetime related values in XML files +* Add environment variables to wsgi script +* Add unique ids check on RPC +* Assert unique records on ModelView.button and Workflow.transition +* Add test for function field methods +* Add option install module dependencies with trytond-admin +* Add localhost_name and timeout as get_smtp_server uri parameters +* Delete missing modules not activated when updating module list +* Remove empty pages from notebook +* Allow to store Dict as JSON on the database + +Version 4.4.0 - 2017-05-01 +* Bug fixes (see mercurial logs for details) +* Sanitize path in file_open against suffix (CVE-2017-0360) +* Add constraint on user password +* Remove Property field +* Add MultiValueMixin and ValueMixin +* Use sql type in column creation +* Use generic SQL type in field and let backend determine the SQLType +* Add filter to xxx2Many fields +* Add NULLS ordering +* Add context domain on ir.action.act_window +* Allow None limit in action window +* Add has_window_functions on Database +* Allow Many2One on ModelSQL to target ModelStorage +* Manage Cache in Transaction +* Allow to register multiple exit functions on Transaction +* Return 429 status when too many login attempts +* Add set_rpc on Field +* Add has_select_for on Database +* Store custom report translation in separate module +* Add form action keyword for set/synchronize translation on report and view +* Add negative value for col attribute +* Allow to use domain_ method with Function fields +* Validate wizard definition on module tests +* Remove order constraint on register ModelSQL +* Add relate from report to translations + +Version 4.2.0 - 2016-11-28 +* Bug fixes (see mercurial logs for details) +* Add toolbar attribute for richtext widget +* Add PYSON widget +* Allow to define the text color and background color in calendar view +* Allow to override cache implementation +* Add button rule +* Allow to specify translatable languages in trytond-admin +* Add datetime_field on Reference +* Merge Spanish's into Spanish (Latin American) +* Do not check write access on model for wizard with groups +* Add user application +* Add sequence_ordered +* Remove most country specific code in languages +* Add support for derivative translations +* Remove IDENTIFIER regexp on ir.model and ir.model.field +* Enforce suffix and prefix to have id or name +* Sanitize path in file_open (CVE-2016-1242) +* Prevent read of user password hash (CVE-2016-1241) +* Add database dump cache for tests +* Remove unused tools: find_in_path, exec_command_pipe and mod10r +* Implementation of drop_column for SQLite +* Allow to pass many configuration files +* Remove translate on field name of User +* Allow to define the default mode in calendar view +* Use 'default_rec_name' context key in Model.default_get +* Add option to store Attachment in database +* Allow to store Binary field in filestore +* Add filestore module +* Allow None in Greater/Less PYSON +* Add option to set admin password with trytond-admin +* Remove super password +* Remove database management from RPC +* Remove ModelView.view_header_get +* Remove string attribute from views +* Don't check write access on model for button with groups +* Limit readonly state for xxx2Many +* Add option to update modules list with trytond-admin +* Use home directory as default path for database and web root. +* Add count option on Action Window Domains +* Remove window_name on Action Window +* Return the calling keyword in ActionKeyword.get_keywords +* Add customizable login process +* Allow to customize the substitutions used on sequence +* Allow PYSON in tree_invisible attribute + +Version 4.0.0 - 2016-05-02 +* Bug fixes (see mercurial logs for details) +* Add sendmail module to send transactional email +* Support Two-Phase Commit in Transaction +* Allow Report to generate text plain, XML, HTML and XHTML +* Add workflow graph on ir.model +* Add context model on ir.action.act_window +* Switch to WSGI API +* Limit the login size in LoginAttempt +* Remove LocalDict from tools +* Add LRUDictTransaction +* Follow PEP-0249 for Database, Transaction and Cursor +* Add Python3 support +* Make TestCase create and drop its database +* Add with_transaction decorator for tests +* Add note on resources +* Add 'where' operator for xxx2many fields +* Strip and unquote double-quote from Postgresql schema in search_path +* Move webdav into a separate module +* Don't read historized user when evaluating record rules as it could lead to + past privilege escalation. +* Only rebuild mptt tree if left or right values have their default values +* Allow nested inherited view +* Add button on cron to run once +* Check all fields when writing a sequence of records, values (CVE-2015-0861) +* Add view_ids on tree view +* Add parent_of operator +* Enforce type of inheriting view +* Use instance context in translated descriptor of Selection + +Version 3.8.0 - 2015-11-02 +* Bug fixes (see mercurial logs for details) +* Add test for all field methods +* Load po files also in 'override' subdirectory +* Add support for float and integer on Property fields +* Remove foreign-key on create_uid and write_uid +* Prevent deletion of any user +* Manage PostgreSQL schema +* Remove colors attribute on tree view +* Remove style on Report +* Add StateReport to Wizard +* Allow to override ModelSQL._table +* Allow to import backend from entry points +* Add reversed operators to PYSON expressions +* Explicity raise error when creating/writing/deleting models with table_query +* Use CURRENT_TIMESTAMP instead of now +* Use Constraint instance in ModelSQL._sql_constraints +* Only return default record if no domain supplied on ModelSingleton search +* Rule.domain_get returns a domain and Rule.query_get a SQL query +* Add target_search option to Many2One +* Add tables argument to ModelSQL.search_domain +* Rename ir.module.module* into ir.module* + +Version 3.6.0 - 2015-04-20 +* Bug fixes (see mercurial logs for details) +* Use bytes and bytearray for Binary +* Add button_change +* Add support for PyPy +* Add support for psycopg2cffi +* Add noeval on PYSONDecoder +* Add __repr__ to PYSON +* Remove safe_eval +* Add ModelView.view_attributes +* Add pyson attribute on data field tag +* Changed into JSON: + - record rule domain + - trigger condition + - 'states', 'domain', 'spell' and 'colors' view attributes + - view domain + - 'email', 'domain', 'context', 'order' and 'search_value' action fields +* Add product attribute on form view for One2Many +* Remove float_time widget +* Add TimeDelta field +* search_global yields record instead of id +* Add ModelTestCase +* Add test for missing default model access +* Report API refactorization +* Add test for access rights of menu and actions +* Allow to use the dotted notation for order parameters +* Use action_id to find report to use +* Allow custom StateView without Model +* Remove Pool.object_name_list +* Add translated descriptor for Dict field +* Clean private context keyword in RPC +* Add cache section in configuration +* Use dualmethod on ModelStorage.save +* New API for on_change: instance changes +* Add restore_history_before on ModelSQL +* Remove img_{width,height} form attributes + +Version 3.4.0 - 2014-10-20 +* Bug fixes (see mercurial logs for details) +* Use literal_eval instead of safe_eval (CVE-2014-6633) +* Prevent double underscore in safe_eval (CVE-2014-6633) +* Add pre-validation on button +* Model and Field access checked only if _check_access is set +* Add check_access to RPC +* Add check_access to Wizard and Report +* Add support for domain_ method +* Refactor configuration file and command line +* Use the context of the relation field for instanciation +* Use a configuration field for logging +* Add translated descriptor for Selection field +* Add tree_state attribute on tree view +* Allow to sync XML data +* Remove on_change calls in Model.default_get +* Add group call to on_change +* Add UnionMixin +* Allow to disable sorting of dictionary field's selection +* Add active field to views of action window +* Make global cache depends on explicit context keys +* Don't add to global cache Binary fields +* Add MatchMixin +* Add image widget to tree +* Remove context, current_date and time from record rule evaluation + +Version 3.2.0 - 2014-04-21 +* Bug fixes (see mercurial logs for details) +* Add restore_history to ModelSQL +* Add history revisions +* Add the multi selection widget +* Add index to one2many's on_change +* Remove auto-refresh on Action Window +* Add support of domain for non-relation field +* Manage microseconds in JSON-RPC and XML-RPC +* Remove Sha field +* Add password widget +* Add Len to PYSON +* Use bcrypt to hash password if possible +* Use a sequence of ids, values to set fields +* Client side actions on button and wizard +* Add depends attribute to data tag +* Add tree_invisible attribute to button in tree view +* Drop support of Python 2.6 +* Deprecate on_change, on_change_with, selection_change_with and autocomplete + field arguments +* Add fields.depends decorator +* Add run-tests +* Validate only modified and dependant fields on model write +* Improve error messages by showing the failing value +* Remove relation field actions: + - delete_all + - unlink_all + - set +* Rename relation field action unlink into remove +* Use a sequence of records, values in write +* set_context of Transaction.set_user is restricted to root +* Add a "copy" action to One2Many and Many2Many's set method +* Force UTC as timezone (migration script available on tryton-tools) +* Add relation_field for many2one + +Version 3.0.0 - 2013-10-21 +* Bug fixes (see mercurial logs for details) +* Allow customization of translation in po files +* Use python-sql +* Add convert_domain method on Fields +* Add sql_format and sql_type methods on Fields +* Allow to return a full domain for Function.searcher +* Replace static backend by dynamic get method +* Replace order_field by order_ method +* Allow field One2One in check_recursion +* Remove the default order on MPTT +* Add grouped attribute to data tag +* Store selection tree state +* Add order to Action Window +* Add factor on number fields +* Add calendar view +* Remove request + +Version 2.8.0 - 2013-04-22 +* Bug fixes (see mercurial logs for details) +* Allow to search on target of Reference field +* Remove _inherits +* Add dynamic label +* Add prefix, suffix on tree view +* Replace _constraints with validate in ModelStorage +* Add selection_change_with on Selection and Reference fields +* Add Dict fields +* Remove unique constraint on model and field access +* Use lists of values in create +* (Field, Operator, Operand) are replaced by Domain on Rule +* Add global search +* Replace view shortcut by menu favorite +* Store default language in database +* Add icon attribute on fields +* Prevent Wizard State name to start with '_' +* Add completion attribute for Many2One, Many2Many and One2Many +* Add ViewSearch +* Add domains on Action Window + +Version 2.6.0 - 2012-10-22 +* Bug fixes (see mercurial logs for details) +* Add pre-validation +* Allow to use XML file for views +* Add RPC definition +* Repace BrowseRecord by Model instance +* Replace Cache decorator by a simple LRU Cache +* Remove Cacheable +* Remove _description +* Rename _name by __name__ +* Use class in Pool +* Fix search button clause in ModelButton.get_groups (CVE-2012-2238) +* Merge all kind of buttons +* Use XML id for board action instead of id +* Add states attribute to notebook +* Allow to use tuple for Reference +* Add constant interpolation on line graph +* Add create/delete field access +* Add dynamic size limit on the One2Many, Many2Many and Char +* Replace __tryton__.py by tryton.cfg +* Allow to use Reference field in One2Many & Many2Many +* Remove hexmd5 from ModelView.fields_view_get +* Allow client to manage model access +* Add time format validation +* Remove ModelSQL.default_sequence + +Version 2.4.0 - 2012-04-23 +* Bug fixes (see mercurial logs for details) +* Don't allow rpc call on ModelStorage without ModelView (CVE-2012-0215) +* Add shared WebDAV +* Remove workflow module +* Simplify workflow engine +* Add ir.model.button for access rights +* Replace fill by expand +* Integer, Numeric and Float allow None as value +* NULL value is None and not False +* Replace user action by a list of actions +* Add group call to on_change_with +* Allow to get fuzzy translation +* Allow to customize server timezone +* Add richtext widget for WYSIWYG editor +* Add support of fields.Time +* Replace nested view by reference id +* Remove underscore to ir.translation methods +* Add default database language code +* Add extras_depend to module definition +* Store session in database +* New Wizard design +* Add pyson.Id +* Use XML-RPC struct for Decimal and Date +* Remove change_default on fields + +Version 2.2.0 - 2011-10-24 +* Bug fixes (see mercurial logs for details) +* Remove name field in ir.property +* Add wizard to show views +* Switched from .csv to .po for translations +* Search on Many2One fields include inactive records +* Change select on fields into Boolean +* Change format of search_value on ir.action.act_window +* Manage relation field access +* Support size format in context for Binary fields +* Use buffer for Binary fields +* No more store Binary fields in base64 +* Remove tabpos attribute on notebook +* Make PYSON more Pythonic +* Add readonly on Transaction +* Add has_returning on Cursor +* Remove use of python code in workflow XML +* Use SQL sequence on PostgreSQL for ir.sequence +* Allow to specify more than one interface +* Use unoconv for report format conversion +* Allow to use any Opendocument as report template +* Drop NetRPC and activate JSON-RPC by default +* Remove ir.action.wizard_size +* Rename expand and fill attributes into yexpand and yfill +* Add xalign and yalign as field attributes +* Drop support of Python 2.5 +* Remove support of Many2Many field in record XML +* Change Pool into a Singleton +* Remove support of zipped modules + +Version 2.0.0 - 2011-04-26 +* Bug fixes (see mercurial logs for details) +* Use md5 hash for indexing translation +* Merge tree and list views +* Added autocompletion on fields.Char +* Remove ir.default +* Add type, last user and last modification on ir.attachment +* Rename datas into data on ir.attachment +* Add new configuration option to prevent database listing +* Add warning to wizards +* Add server-side icons +* Add support for file link to BinaryField +* Add model field access +* Add loading attribute on fields +* Remove priority attribute on fields +* Model doesn't convert anymore ids for inherited methods +* Remove required attribute on Boolean fields +* Add One2One field +* Add AUTOINCREMENT to sqlite primary key + +Version 1.8.0 - 2010-11-01 +* Bug fixes (see mercurial logs for details) +* Add timestamp sequence +* New transaction management +* Make _timestamp numeric instead of datetime to work with XML-RPC and JSON-RPC +* Add ir.trigger to trigger ModelStorage change +* Add default value to Boolean fields at database level + +Version 1.6.0 - 2010-05-08 +* Bug fixes (see mercurial logs for details) +* Add symbol parameter to formatLang +* searcher on Function fields take only one domain clause as argument +* Use a Reference field on ir.attachment to store resource informations + Improve the ir.attachment views to be usable on the client side +* Use basic access authentication for XML-RPC +* Replace child{1,2} attributes in xml views by a unique child +* Models that uses _inherits will search in inherited parents for missing functions +* Remove ids from on_change* calls +* Improve search on translatable fields +* export_data return empty value for invisible fields +* Don't allow to use float in Numeric fields +* set/get function on Fields take a list of ids +* New interface for Function/Property fields + Take a Field as first argument instead of many separate arguments + Remove static arguments +* Fix Float and Numeric for lost of precision +* Add decimal digits validation on fields +* Improve unittests to run trytond from tests + Use sqlite as default backend + Add skiptest attribute to data tag + Add option to test_tryton to run tests from all modules +* Add MySQL backend +* Refactoring by validation with pylint +* Modify ondelete attribute of Many2One according to required attribute value +* Refactor import_data of ModelStorage +* Raise exception when search function is missing on Function fields +* Add LRU memoization, use it to cache compiled code for safe_eval. +* Add PySON to replace python string evaluation on client side +* Add JSON-RPC +* Add groups on Sequence Types and add rules on sequence based on it. +* Ignore Fields that starts with "_" +* Add comment on table and field for postgresql backend +* Remove egenix-mx-base and replace it by python-dateutil +* Add cache to safe_eval +* Rename HttpDaemon into XMLRPCDaemon +* Improve TranslateFactory to fetch all translations for a report in one query +* Handle displayname on webdav.collection +* Handle current-user-privilege-set on webdav.collection + +Version 1.4.0 - 2009-10-19 +* Bug fixes (see mercurial logs for details) +* Add datetime_field on xxx2Many to use a specific _datetime when reading the related record +* Add new tool safe_eval +* Handle sequence and history renaming when renaming table +* Add old_password to set_preferences of res.user on password change +* Allow to drop constraint, index and foreign key with custom table name +* Added column_rename on TableHandler +* Add new tool reduce_ids +* Add limit_clause function on cursor +* Fill the cursor cache at search +* Allow rpc on today of ir.date +* Use the module dependency order to apply views that inherits +* Allow to update database at the end of restore +* Add ir.model.access check get and set of ir.property +* Add ModelSingleton +* Move login test in res.user +* Rename osv into model on workflow +* Add logout method +* Move BrowseRecord cache onto the cursor except for Function fields +* Don't order search result if order value is [] +* Add reload of modules if files have changed +* Add salt to sha of password +* Add strftime to ir.lang to handle locale's format +* Add sqlite backend +* Add validate test for required and size +* Remove _sequence on ModelSQL +* Use gzip in pysocket +* Add gzip encoding for XML-RPC +* Add report name in the result of Report.execute +* Add ir.action.wizard_size to store prefered wizard size +* Add delete_all action on One2Many field +* Read, write, create, delete permission on record rules +* Add reset_default method to ir.default +* Doesn't append '%' to "like", "ilike" clauses +* Handle database dump and restore with password +* Add float_time attribute in fields view + +Version 1.2.0 - 2009-04-20 +* Bug fixes (see mercurial logs for details) +* Add delete of foreign keys with ondelete CASCADE +* Add write for foreign keys with ondelete SET NULL +* Add datetime_field on Many2One to use a specific _datetime when reading the related record +* Use _datetime in context to read record value at specific datetime +* Add _history_table on ModelSQL to historize change on records +* Allow to use related fields (many2one and reference) in read +* Use rec_name function field instead of name_get and name_search +* Use a new Pool for objects +* Move workflow in ModelWorkflow and the workflow module +* Remove Service and LocalService objects +* New netrpc/xmlrpc syntax +* copy on ModelStorage copies one2many directly on the right record +* Search on translated field will search only on translated value +* Add active field on ir.action to allow better override of reports +* Add depends attributes on Column +* Make Modified Preorder Tree Traversal respects the default order of the model +* Add replace_attributes for xpath tag +* Add email parameter on report action +* Allow to inherit views from an other model +* Add user warnings +* Allow to use a list of id for copy method +* Use one lock per database +* Improve netrpc communication speed +* Add contextual domain on inherited views +* Allow to use globals in domain and states +* Add translate attribute on Selection field +* Use explicit join in search SQL query +* Fix for host with IPv6 enable but without default IPv6 route +* Allow egg installation + +Version 1.0.0 - 2008-11-17 +* Initial release diff --git a/trytond/COPYRIGHT b/trytond/COPYRIGHT new file mode 100644 index 0000000000..87f09dbd4f --- /dev/null +++ b/trytond/COPYRIGHT @@ -0,0 +1,21 @@ +Copyright (C) 2004-2008 Tiny SPRL. +Copyright (C) 2007-2022 Cédric Krier. +Copyright (C) 2007-2013 Bertrand Chenal. +Copyright (C) 2008-2022 B2CK SPRL. +Copyright (C) 2011 Openlabs Technologies & Consulting (P) Ltd. +Copyright (C) 2011-2022 Nicolas Évrard. +Copyright (C) 2020-2021 Maxime Richez +Copyright (C) 2020-2021 SALUC SA + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/trytond/LICENSE b/trytond/LICENSE new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/trytond/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/trytond/MANIFEST.in b/trytond/MANIFEST.in new file mode 100644 index 0000000000..0bbd4068ed --- /dev/null +++ b/trytond/MANIFEST.in @@ -0,0 +1,9 @@ +include CHANGELOG +include COPYRIGHT +include LICENSE +include README.rst +include doc/* +include trytond/ir/ui/icons/LICENSE +recursive-include doc *.rst +recursive-include doc *.po +recursive-include doc *.pot diff --git a/trytond/README.rst b/trytond/README.rst new file mode 100644 index 0000000000..fcdfb91ace --- /dev/null +++ b/trytond/README.rst @@ -0,0 +1,7 @@ +trytond +======= + +The server of Tryton. +Tryton is business software, ideal for companies of any size, easy to use, +complete and 100% Open Source. +It provides modularity, scalability and security. diff --git a/trytond/bin/trytond b/trytond/bin/trytond new file mode 100755 index 0000000000..d1dddec5b9 --- /dev/null +++ b/trytond/bin/trytond @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# 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 glob +import logging +import os +import sys +import threading + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config, split_netloc + +parser = commandline.get_parser_daemon() +options = parser.parse_args() +commandline.config_log(options) +extra_files = config.update_etc(options.configfile) + +if options.coroutine: + # Monkey patching must be done before importing + from gevent import monkey + monkey.patch_all() + +# Import trytond things after it is configured +from trytond.wsgi import app +from trytond.pool import Pool +from trytond.modules import get_module_list, get_module_info + +with commandline.pidfile(options): + Pool.start() + threads = [] + for name in options.database_names: + thread = threading.Thread(target=Pool(name).init) + thread.start() + threads.append(thread) + for thread in threads: + thread.join() + hostname, port = split_netloc(config.get('web', 'listen')) + certificate = config.get('ssl', 'certificate') + try: + if config.getboolean('ssl', 'certificate'): + certificate = None + except ValueError: + pass + privatekey = config.get('ssl', 'privatekey') + try: + if config.getboolean('ssl', 'privatekey'): + privatekey = None + except ValueError: + pass + if certificate or privatekey: + from werkzeug.serving import load_ssl_context + ssl_args = dict( + ssl_context=load_ssl_context(certificate, privatekey)) + else: + ssl_args = {} + if options.dev and not options.coroutine: + for module in get_module_list(): + info = get_module_info(module) + for ext in ['xml', + 'fodt', 'odt', 'fodp', 'odp', 'fods', 'ods', 'fodg', 'odg', + 'txt', 'html', 'xhtml']: + path = os.path.join(info['directory'], '**', '*.' + ext) + extra_files.extend(glob.glob(path, recursive=True)) + + if options.coroutine: + from gevent.pywsgi import WSGIServer + logger = logging.getLogger('gevent') + WSGIServer((hostname, port), app, + log=logger, + error_log=logger, + **ssl_args).serve_forever() + else: + from werkzeug.serving import run_simple + run_simple(hostname, port, app, + threaded=True, + extra_files=extra_files, + use_reloader=options.dev, + **ssl_args) diff --git a/trytond/bin/trytond-admin b/trytond/bin/trytond-admin new file mode 100755 index 0000000000..81b27afa06 --- /dev/null +++ b/trytond/bin/trytond-admin @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# 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 sys +import os + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config + +parser = commandline.get_parser_admin() +options = parser.parse_args() +config.update_etc(options.configfile) +commandline.config_log(options) + +# Import after application is configured +import trytond.admin as admin + +admin.run(options) diff --git a/trytond/bin/trytond-console b/trytond/bin/trytond-console new file mode 100755 index 0000000000..549483e064 --- /dev/null +++ b/trytond/bin/trytond-console @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# 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 sys +import os + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config + +parser = commandline.get_parser_console() +options = parser.parse_args() +config.update_etc(options.configfile) + +# Import after application is configured +import trytond.console as console + +console.run(options) diff --git a/trytond/bin/trytond-cron b/trytond/bin/trytond-cron new file mode 100755 index 0000000000..9f777911cd --- /dev/null +++ b/trytond/bin/trytond-cron @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# 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 sys +import os + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config + +parser = commandline.get_parser_cron() +options = parser.parse_args() +config.update_etc(options.configfile) +commandline.config_log(options) + +# Import after application is configured +from trytond.pool import Pool +import trytond.cron as cron + +with commandline.pidfile(options): + Pool.start() + cron.run(options) diff --git a/trytond/bin/trytond-stat b/trytond/bin/trytond-stat new file mode 100755 index 0000000000..bc1a48772c --- /dev/null +++ b/trytond/bin/trytond-stat @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# 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 curses +import datetime as dt +import math +import os +import sys + +from collections import defaultdict + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config + +parser = commandline.get_parser_stat() +options = parser.parse_args() +config.update_etc(options.configfile) + +import trytond.status as status + + +def main(stdscr): + global reverse + stdscr.nodelay(1) + reverse = True + processes = {} + status_pad = curses.newpad(1, 1) + cache_pad = curses.newpad(1, 1) + + def refresh_status(): + now = dt.datetime.now() + height, width = stdscr.getmaxyx() + + def expired(process): + return process['expire'] > now + + def format_status(since, pid, request): + since = str(dt.timedelta(seconds=int(since))) + return f"{pid:>5} {since:>18} {request}" + status_pad.clear() + status = [format_status(*i) for i in sorted( + ((msg['since'], p['id'], msg['request']) + for p in filter(expired, processes.values()) + for msg in p['status']), + reverse=reverse)] + prow = min(len(status) + 1, height // 2) + pcol = max(max(map(len, status), default=0), width) + status_pad.resize(len(status) + 1, pcol + 1) + for i, line in enumerate(status, 1): + status_pad.addnstr(i, 0, line.ljust(pcol), pcol) + status_pad.addnstr( + 0, 0, "{pid:>5} {since:>18} {request} ({n})".format( + pid="pid", + since="TIME" + ('↑' if reverse else '↓'), + request="request", + n=len(status), + ).upper().ljust(pcol), pcol, curses.A_REVERSE) + status_pad.noutrefresh(0, 0, 0, 0, prow, width - 1) + + def ratio(cache): + if cache['hit'] or cache['miss']: + return cache['hit'] / (cache['hit'] + cache['miss']) + return 0 + + def format_cache(name, hit, miss, ratio, size): + return f"{hit:{size}d} {miss:{size}d} {ratio * 100:6.2f} {name}" + + cache_pad.clear() + cache_stats = defaultdict(lambda: defaultdict(lambda: 0)) + for p in filter(expired, processes.values()): + for cache in p['caches']: + stats = cache_stats[cache['name']] + stats['name'] = cache['name'] + stats['hit'] += cache['hit'] + stats['miss'] += cache['miss'] + for cache in cache_stats.values(): + cache['ratio'] = ratio(cache) + try: + size = math.ceil(math.log10( + max(s['hit'] + s['miss'] for s in cache_stats.values()))) + except ValueError: + size = 1 + size = max(size, 4) + caches = [format_cache(size=size, **cache) for cache in sorted( + cache_stats.values(), key=lambda c: (c['ratio'], c['miss']), + reverse=reverse)] + crow = max(len(caches) + 1, (height - prow)) + ccol = max(max(map(len, caches), default=0), width) + cache_pad.resize(crow + 1, ccol + 1) + for i, line in enumerate(caches, 1): + cache_pad.addnstr(i, 0, line.ljust(ccol), ccol) + cache_pad.addstr( + 0, 0, + "{hit:>{size}} {miss:>{size}} {ratio:>6} {name} ({n})".format( + size=size, + hit="hit", + miss="miss", + ratio="% " + ('↑' if reverse else '↓'), + name="name", + n=len(caches), + ).upper().ljust(ccol), curses.A_REVERSE) + cache_pad.noutrefresh(0, 0, prow, 0, height - 1, width - 1) + + def refresh(): + global reverse + refresh_status() + stdscr.refresh() + + key = stdscr.getch() + if key == ord('q'): + sys.exit() + elif key == ord('r'): + reverse = not reverse + refresh() + + def update(data=None): + if data: + pid = data['id'] + data['expire'] = dt.datetime.now() + dt.timedelta(seconds=10) + processes[pid] = data + refresh() + refresh() + return status.listen(config.get('database', 'path'), update) + + +if not curses.wrapper(main): + sys.stderr.write("status not supported on this platform\n") diff --git a/trytond/bin/trytond-worker b/trytond/bin/trytond-worker new file mode 100755 index 0000000000..92ff76555f --- /dev/null +++ b/trytond/bin/trytond-worker @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# 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 sys +import os + +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import trytond.commandline as commandline +from trytond.config import config + +parser = commandline.get_parser_worker() +options = parser.parse_args() +config.update_etc(options.configfile) +commandline.config_log(options) + +# Import after application is configured +from trytond.pool import Pool +import trytond.worker as worker + +with commandline.pidfile(options): + Pool.start() + worker.work(options) diff --git a/trytond/doc/index.rst b/trytond/doc/index.rst new file mode 100644 index 0000000000..ada2ab4409 --- /dev/null +++ b/trytond/doc/index.rst @@ -0,0 +1,56 @@ +.. _index: + +============= +Tryton Server +============= + +First steps +=========== + + * **Installation:** + :ref:`Installation ` | + :ref:`Configuration ` | + :ref:`Setup a database ` | + :ref:`Start the server ` + +The model layer +=============== + + * **Models:** + :ref:`Model syntax ` | + :ref:`Field types ` | + :ref:`Domain syntax ` | + :ref:`Access rights ` | + :ref:`Triggers ` + +The view layer +============== + + * **Views:** + :ref:`View types ` | + :ref:`Extension ` + + * **Reports:** + :ref:`Report definition ` + +The development process +======================= + + * **Modules** + :ref:`Module definition ` + +Contents +======== + +.. toctree:: + :maxdepth: 2 + + topics/index + ref/index + +Indices, glossary and tables +============================ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/trytond/doc/ref/bus.rst b/trytond/doc/ref/bus.rst new file mode 100644 index 0000000000..0edb1746c3 --- /dev/null +++ b/trytond/doc/ref/bus.rst @@ -0,0 +1,104 @@ +.. _ref-bus: +.. module:: trytond.bus + +=== +Bus +=== + +.. class:: Bus + +The Tryton server listens on ``POST`` requests on the routes matching +``//bus`` and replies with JSON dictionary containing: + +channel + The channel on which this message has been received. + +message + A dictionary that is the message the client must handle. The specification + of the message depends of its type. + + All messages should at least content a unique identifier in the key + ``message_id`` and their type in the key of the same name. + +Client sending their request on the route must be authenticated. The request +must submit a JSON dictionary containing: + +last_message + A value identifying the last message received by the client. This value + can be ``null``. + +channels + A list of strings denoting the channels the client is listening to. + +The :class:Bus exposes two methods that are used by the framework: ``publish`` +and ``subscribe``. + +.. classmethod:: Bus.publish(channel, message) + + Send a message to a specific channel. + + Currently implemented messages are: + + * :ref:`Notifications ` + +.. classmethod:: Bus.subscribe(database, channels[, last_message]) + + Subscribe a user client to some ``channels`` of messages. + + The ``message_id`` parameter defines the last message ID received by the + client. It defaults to None when not provided. + +The default implementation provides an helper method to construct the response: + +.. classmethod:: Bus.create_response(channel, message) + + Creates a dictionary suitable as a response from a message and a timestamp. + + ``channel`` is the channel on which the message has been received. + + ``message`` is the content of the message sent to the client. + +Please note that the implementation relies on the fact that the order of the +messages received is consistent accros different trytond instances allowing to +dispatch the request to any trytond server running. + +Notification +------------ + +Tryton provides a shortcut to send a notification with the ``notify`` method. + +.. method:: notify(title[, body[, priority[, user[, client]]]]) + + Send a text message to a user's client to be displayed using a + notification popup. The meaning of ``title``, ``body`` and ``priority`` is + defined in :ref:`bus_notification_spec`. + + If ``user`` is not set, the current + :attr:`~trytond.transaction.Transaction.user` is used. Otherwise ``user`` + is the user ID to notify. + + If ``client`` is not set then every client of the user will receive the + message. If ``client`` and ``user`` are not set, the system will send the + notification to the current user client. Otherwise the notification will + be sent to the client whose ID matches ``client``. + +.. _bus_notification_spec: + +Notification message +~~~~~~~~~~~~~~~~~~~~ + +Notification messages are composed of four parts: + +kind + The string 'notification'. + +title + A string. A one-line summary of the message. + +body + A string. The short informative message for the user. It can span multiple + lines but no markup is allowed. + +priority + An integer between 0 (low priority) to 3 (urgent). The notification priority + on the platform supporting it. diff --git a/trytond/doc/ref/cache.rst b/trytond/doc/ref/cache.rst new file mode 100644 index 0000000000..2e9e03702d --- /dev/null +++ b/trytond/doc/ref/cache.rst @@ -0,0 +1,78 @@ +.. _ref-cache: +.. module:: trytond.cache + +===== +Cache +===== + +.. class:: Cache(name[, size_limit[, duration[, context]]]) + +The class is used to cache values between server requests. The ``name`` should +be unique and it's used to identify the cache. We usually use +``.`` to make it unique. The ``size_limit`` parameter +can be used to limit the number of values cached and it has 1024 as the default +value. The ``duration`` parameter defines how long a cached value stays valid +but if it is not set the value remains valid until it is cleared. And the +``context`` parameter is used to indicate if the cache depends on the user +context and is true by default. The cache is cleaned on :class:`Transaction` +starts and resets on :class:`Transaction` commit or rollback. + +.. warning:: + As there is no deepcopy of the values cached, they must never be mutated + after being set in or retrieved from the cache. +.. + +.. attribute:: hit + +Count the number of times the cache returned a cached value. + +.. attribute:: miss + +Count the number of times the cache did not contain the key. + +.. classmethod:: stats() + +Yield statistics for each instance. + +.. method:: get(key[, default]) + +Retrieve the value of the key in the cache. If a ``default`` is specified it +will be returned when the key is missing otherwise it will return ``None``. + +.. method:: set(key, value) + +Sets the ``value`` of the ``key`` in the cache. + +.. method:: clear() + +Clears all the keys in the cache. + +.. classmethod:: clear_all() + +Clears all cache instances. + +.. classmethod:: sync(transaction) + +Synchronize cache instances using transaction. + +.. method:: sync_since(value) + +Return ``True`` if the last synchronization was done before ``value``. + +.. classmethod:: commit(transaction) + +Apply cache changes from transaction. + +.. classmethod:: rollback(transaction) + +Remove cache changes from transaction. + +.. classmethod:: drop(dbname) + +Drops all the caches for database ``dbname`` + +.. note:: + By default Tryton uses a MemoryCache, but this behaviour can be overridden + by setting a fully qualified name of an alternative class defined in the + configuration ``class`` of the ``cache`` section. +.. diff --git a/trytond/doc/ref/exceptions.rst b/trytond/doc/ref/exceptions.rst new file mode 100644 index 0000000000..40e0dd8285 --- /dev/null +++ b/trytond/doc/ref/exceptions.rst @@ -0,0 +1,94 @@ +.. _ref-exceptions: +.. module:: trytond.exceptions + +========== +Exceptions +========== + + +.. exception:: TrytonException + + The base class for all Tryton exceptions. + +.. exception:: UserError(message[, description[, domain]]) + + The base class for exceptions used to display an error message to users. + The domain may be a 2-tuple containing a :ref:`domain ` and + a dictionary of field definitions used to format the domain and append to + the description. + +.. exception:: UserWarning(name, message[, description]) + + The base class for exceptions used to display a warning message to users. + +.. exception:: LoginExceptions(name, message[, type]) + + The exception used to request ``name`` parameter for the login process. + +.. exception:: ConcurrencyException(message) + + The exception raised on concurrent modification. + +.. exception:: RateLimitException + + The exception raised when user has sent too many login requests. + +.. exception:: MissingDependenciesException(missings) + + The exception raised when modules are missing. + + +.. module:: trytond.model.exceptions + +.. exception:: AccessError + + The exception raised when trying to access a record without the rights. + +.. exception:: AccessButtonError + + The exception raised when trying to execute a button without the rights. + +.. exception:: ImportDataError + + The exception raises when importing data fails. + +.. exception:: ValidationError + + The base class for all record validation error. + +.. exception:: DomainValidationError + + The exception raised when the domain of a field is not valid. + +.. exception:: RequiredValidationError + + The exception raised when a required field is empty. + +.. exception:: SizeValidationError + + The exception raised when the size of a field is too big. + +.. exception:: DigitsValidationError + + The exception raised when the value of a field does not respect its digits. + +.. exception:: SelectionValidationError + + The exception raised when the value is not in the selection. + +.. exception:: TimeFormatValidationError + + The exception raised when the time format of a field is not respected. + +.. exception:: ForeignKeyError + + The exception raised when a foreign key is not respected. + +.. exception:: SQLConstraintError + + The exception raised when a :attr:`~trytond.model.ModelSQL._sql_constraints` is not + respected. + +.. exception:: RecursionError + + The exception raised by :class:`~trytond.model.TreeMixin.check_recursion`. diff --git a/trytond/doc/ref/filestore.rst b/trytond/doc/ref/filestore.rst new file mode 100644 index 0000000000..c9045d54bb --- /dev/null +++ b/trytond/doc/ref/filestore.rst @@ -0,0 +1,43 @@ +.. _ref-filestore: +.. module:: trytond.filestore + +========= +FileStore +========= + +.. class:: FileStore() + +The class is used to store and retrieve files from the directory defined in the +configuration ``path`` of ``database`` section. It uses a two levels of +directory composed of the 2 chars of the file hash. It is an append only +storage. + +.. method:: get(id[, prefix]) + +Retrieve the content of the file referred by the id in the prefixed directory. + +.. method:: getmany(ids[, prefix]) + +Retrieve a list of contents for the sequence of ids. + +.. method:: size(id[, prefix]) + +Return the size of the file referred by the id in the prefixed directory. + +.. method:: sizemany(ids[, prefix]) + +Return a list of sizes for the sequence of ids. + +.. method:: set(data[, prefix]) + +Store the data in the prefixed directory and return the identifiers. + +.. method:: setmany(data[, prefix]) + +Store the sequence of data and return a list of identifiers. + +.. note:: + The class can be overridden by setting a fully qualified name of a + alternative class defined in the configuration ``class`` of the ``database`` + section. +.. diff --git a/trytond/doc/ref/i18n.rst b/trytond/doc/ref/i18n.rst new file mode 100644 index 0000000000..fcaa98c680 --- /dev/null +++ b/trytond/doc/ref/i18n.rst @@ -0,0 +1,19 @@ +.. _ref-i18n: +.. module:: trytond.i18n + +==================== +Internationalization +==================== + +.. method:: gettext(message_id, [language[, \**variables]]) + + Returns the message translated into the ``language``. + The ``message_id`` is the ``XML`` id for the ``ir.message`` that is to be + translated, and the ``variables`` keyword arguments are used as a mapping to + format the string. + If ``language`` is not set, then the :attr:`Transaction.language` is used. + +.. method:: lazy_gettext(message_id, [language[, \**variables]]) + + Return a LazyString that will be translated with gettext later when + actually used. diff --git a/trytond/doc/ref/index.rst b/trytond/doc/ref/index.rst new file mode 100644 index 0000000000..b5149790bb --- /dev/null +++ b/trytond/doc/ref/index.rst @@ -0,0 +1,23 @@ +.. _ref-index: + +============= +API Reference +============= + +.. toctree:: + :maxdepth: 1 + + models/index + wizard + pyson + transaction + exceptions + tools/index + pool + rpc + i18n + sendmail + filestore + cache + bus + tests diff --git a/trytond/doc/ref/models/fields.rst b/trytond/doc/ref/models/fields.rst new file mode 100644 index 0000000000..e6ec1e44a1 --- /dev/null +++ b/trytond/doc/ref/models/fields.rst @@ -0,0 +1,1066 @@ +.. _ref-models-fields: +.. module:: trytond.model.fields + +====== +Fields +====== + +Fields define the behavior of the data on model's record. + +Field options +============= + +The following arguments are available to all field types. All are optional +except :attr:`Field.string`. + +``string`` +---------- + +.. attribute:: Field.string + +A string for the label of the field. + +``help`` +-------- + +.. attribute:: Field.help + +A multi-line help string for the field. + +``required`` +------------ + +.. attribute:: Field.required + +If ``True``, the field is not allowed to be empty. Default is ``False``. + +``readonly`` +------------ + +.. attribute:: Field.readonly + +If ``True``, the field is not editable in the client. Default is ``False``. + +.. warning:: + For relational fields, it means only the new, delete, add and remove + buttons are inactivated. The editable state of the target record must be + managed at the target model level. + + +``domain`` +---------- + +.. attribute:: Field.domain + +A :ref:`domain ` constraint that will be applied on the field +value. + +``states`` +---------- + +.. attribute:: Field.states + +A dictionary that defines dynamic states of the field and overrides the static +one. Possible keys are ``required``, ``readonly`` and ``invisible``. +The values are :class:`~trytond.pyson.PYSON` statements that will be evaluated +with the values of the record. + +``select`` +---------- + +.. attribute:: Field.select + +If true, the content of the field will be indexed. + +.. _ref-models-fields-on_change: + +``on_change`` +------------- + +.. attribute:: Field.on_change + +A set of field names. If this attribute is set, the client will call the +method ``on_change_`` of the model when the user changes the +current field value and will give the values of each fields in this list. The +method signature is:: + + on_change_() + +This method must change the value of the fields to be updated. + +.. note:: + + The on_change_ methods are running in a rollbacked transaction. +.. + +The set of field names could be filled by using the decorator :meth:`depends`. + +.. _ref-models-fields-on_change_with: + +``on_change_with`` +------------------ + +.. attribute:: Field.on_change_with + +A set of field names. Same like :attr:`on_change`, but defined the other way +around. If this attribute is set, the client will call the method +``on_change_with_`` of the model when the user changes one of the +fields defined in the list and will give the values of each fields in this +list. The method signature is:: + + on_change_with_() + +This method must return the new value of the field. + +.. note:: + + The on_change_with_ methods are running in a rollbacked transaction. + +.. + +The set of field names could be filled by using the decorator :meth:`depends`. + +``depends`` +----------- + +.. attribute:: Field.depends + +A list of field names on which the current one depends. This means that the +client will also read these fields even if they are not defined on the view. +:attr:`Field.depends` is used per example to ensure that +:class:`~trytond.pyson.PYSON` statement could be evaluated. + +``context`` +----------- + +.. attribute:: Field.context + +A dictionary which will update the current context for *relation field*. + +.. warning:: + The context could only depend on direct field of the record and without + context. +.. + +``loading`` +----------- + +.. attribute:: Field.loading + +Define how the field must be loaded: ``lazy`` or ``eager``. + +``name`` +-------- + +.. attribute:: Field.name + +The name of the field. + +Instance methods: + +.. method:: Field.convert_domain(domain, tables, Model) + + Convert the simple :ref:`domain ` clause into a SQL + expression or a new domain. :ref:`tables ` could be updated to + add new joins. + +.. method:: Field.sql_format(value) + + Convert the value to use as parameter of SQL queries. + +.. method:: Field.sql_type() + + Return the namedtuple('SQLType', 'base type') which defines the SQL type to + use for creation and casting. Or ``None`` if the field is not stored in the + database. + + sql_type is using the ``_sql_type`` attribute to compute its return value. + The backend is responsible for the computation. + + For the list of supported types by Tryton see + :ref:`backend types `. + +.. method:: Field.sql_cast(expression) + + Return the SQL expression with cast with the type of the field. + +.. method:: Field.sql_column(table) + + Return the Column instance based on table. + +.. method:: Field.set_rpc(model) + + Adds to ``model`` the default RPC instances required by the field. + +.. method:: Field.definition(model, language) + + Returns a dictionary with the definition of the field. + +.. method:: Field.definition_translations(model, language) + + Returns a list of translation sources used by :meth:`~Field.definition`. + +Default value +============= + +See :ref:`default value ` + +Searching +========= + +A class method could be defined for each field which must return a SQL +expression for the given domain instead of the default one. +The method signature is:: + + domain_(domain, tables) + +Where ``domain`` is the simple :ref:`domain ` clause and +``tables`` is a nested dictionary, see :meth:`~Field.convert_domain`. + +Ordering +======== + +A class method could be defined for each field which must return a list of SQL +expression on which to order instead of the field. +The method signature is:: + + order_(tables) + +Where ``tables`` is a nested dictionary, see :meth:`~Field.convert_domain`. + +Depends +======= + +.. method:: depends([\*fields[, methods]]) + +A decorator to define the field names on which the decorated method depends. +The ``methods`` argument can be used to duplicate the field names from other +decorated methods. This is useful if the decorated method calls another method. + +Field types +=========== + +Boolean +------- + +.. class:: Boolean(string[, \**options]) + +A true/false field. + +Integer +------- + +.. class:: Integer(string[, \**options]) + +An integer field. + +BigInteger +---------- + +.. class:: BigInteger(string[, \**options]) + +A long integer field. + +Char +---- + +.. class:: Char(string[, size[, translate[, \**options]]]) + +A single line string field. + +Search by similarity is used for the ``ilike`` operator and +:meth:`~trytond.tools.is_full_text` value if the backend supports it and a +threshold is set. +The similarity threshold is defined for the context key ``..search_similarity`` or ``search_similarity``. + +The field is ordered using the similarity with the context value from the key +``..order`` if it is set. + +:class:`Char` has some extra optional arguments: + +.. attribute:: Char.size + + The maximum length (in characters) of the field. The size is enforced at + the storage level and in the client input. + +.. attribute:: Char.translate + + If true, the value of the field is translatable. The value readed and + stored will depend on the ``language`` defined in the context. + +.. attribute:: Char.autocomplete + + A set of field names. If this attribute is set, the client will call the + method ``autocomplete_`` of the model when the user changes one + of those field value. The method signature is:: + + autocomplete_() + + This method must return a list of string that will populate the + ComboboxEntry in the client. + The set of field names could be filled by using the decorator :meth:`depends`. + +.. attribute:: Char.search_unaccented + + If this attribute is set to True, ``ilike`` searches will be performed on + unaccented strings. The default value is True. + +.. warning:: + + The database backend must supports unaccented search. + +.. attribute:: Char.search_full_text + + If this attribute is set to True, ``ilike`` searches with an + :meth:`~trytond.tools.is_full_text` value use the full text search of the + backend. + The default value is False. + + The context can be used to force the full text search behaviour. + This is done using the key ``..search_full_text``. + If True, the full text search is used no matter what the value. + If False, no full text search is peformed. + + The full text ranking value is added to the similarity if the + ``search_full_text`` is True. + +.. note:: + + The database backend must support full text search otherwise ``ilike`` is + always used. + +Text +---- + +.. class:: Text(string[, size[, translatable[, \**options]]]) + +A multi line string field. + +:class:`Text` has four extra optional arguments: + +.. attribute:: Text.size + + Same as :attr:`Char.size` + +.. attribute:: Text.translate + + Same as :attr:`Char.translate` + +.. attribute:: Text.search_unaccented + + Same as :attr:`Char.search_unaccented` + +.. attribute:: Text.search_full_text + + Same as :attr:`Char.search_full_text` + The default value is True. + +FullText +-------- + +.. class:: FullText(\**options) + +An internal field to store a list of parsed strings ordered by weights. +The field is ordered using the full text ranking with the context value from +the key `..order`` if it is set. + + +Float +----- + +.. class:: Float(string[, digits[, \**options]]) + +A floating-point number field. It will be represented in Python by a ``float`` +instance. + +:class:`Float` has one extra optional arguments: + +.. attribute:: Float.digits + + A tuple of two integers. The first integer defines the total of numbers in + the integer part. The second integer defines the total of numbers in the + decimal part. + Integers can be replaced by a :class:`~trytond.pyson.PYSON` statement. + If digits is None or any values of the tuple is ``None``, no validation on + the numbers will be done. + +Numeric +------- + +.. class:: Numeric(string[, digits[, \**options]]) + +A fixed-point number field. It will be represented in Python by a +``decimal.Decimal`` instance. + +:class:`Numeric` has one extra optional arguments: + +.. attribute:: Numeric.digits + + Same as :attr:`Float.digits` + +Date +---- + +.. class:: Date(string[, \**options]) + +A date, represented in Python by a ``datetime.date`` instance. + +DateTime +-------- + +.. class:: DateTime(string[, format, \**options]) + +A date and time, represented in Python by a ``datetime.datetime`` instance. +It is stored in `UTC`_ while displayed in the user timezone. + +.. _`UTC`: https://en.wikipedia.org/wiki/Coordinated_Universal_Time + +.. attribute:: DateTime.format + + A string format as used by strftime. This format will be used to display + the time part of the field. The default value is ``%H:%M:%S``. + The value can be replaced by a :class:`~trytond.pyson.PYSON` statement. + +Timestamp +--------- + +.. class:: Timestamp(string[, \**options]) + +A timestamp, represented in Python by a ``datetime.datetime`` instance. + +Time +---- + +.. class:: Time(string[, format, \**options]) + +A time, represented in Python by a ``datetime.time`` instance. + +.. attribute:: Time.format + + Same as :attr:`DateTime.format` + +TimeDelta +--------- + +.. class:: TimeDelta(string[, converter[, \**options]]) + +An interval, represented in Python by a ``datetime.timedelta`` instance. + +.. attribute:: TimeDelta.converter + + The name of the context key containing the time converter. + A time converter is a dictionary with the keys: ``s`` (second), ``m`` + (minute), ``h`` (hour), ``d`` (day), ``w`` (week), ``M`` (month), ``Y`` + (year) and the value in second. + +Binary +------ + +.. class:: Binary(string[, \**options]) + +A binary field. It will be represented in Python by a ``bytes`` instance. + +.. warning:: + If the context contains a key composed of the model name and field name + separated by a dot and its value is the string ``size`` then the read value + is the size instead of the content. + +:class:`Binary` has three extra optional arguments: + +.. attribute:: Binary.filename + + Name of the field that holds the data's filename. Default value + is an empty string, which means the data has no filename (in this case, the + filename is hidden, and the "Open" button is hidden when the widget is set + to "image"). + +.. attribute:: Binary.file_id + + Name of the field that holds the ``FileStore`` identifier. Default value is + ``None`` which means the data is stored in the database. The field must be + on the same table and accept ``char`` values. + +.. warning:: + Switching from database to file-store is supported transparently. But + switching from file-store to database is not supported without manually + upload to the database all the files. + +.. attribute:: Binary.store_prefix + + The prefix to use with the ``FileStore``. Default value is ``None`` which + means the database name is used. + +Selection +--------- + +.. class:: Selection(selection, string[, sort[, selection_change_with[, translate[, help_selection[, \**options]]]]]) + +A string field with limited values to choose from. + +:class:`Selection` has one extra required argument: + +.. attribute:: Selection.selection + + A list of 2-tuples that looks like this:: + + [ + ('M', 'Male'), + ('F', 'Female'), + ] + + The first element in each tuple is the actual value stored. The second + element is the human-readable name. + + It can also be the name of a class or instance method on the model, that + will return an appropriate list. The signature of the method is:: + + selection() + + .. note:: + The method is automaticly added to :attr:`trytond.model.Model._rpc` if + not manually set. + .. + +:class:`Selection` has two extra optional arguments: + +.. attribute:: Selection.sort + + If true, the choices will be sorted by human-readable value. Default value + is ``True``. + +.. attribute:: Selection.selection_change_with + + A set of field names. If this attribute is set, the client will call the + ``selection`` method of the model when the user changes on of the fields + defined in the list and will give the values of each fields in the list. + The ``selection`` method should be an instance method. + The set of field names could be filled by using the decorator :meth:`depends`. + +.. attribute:: Selection.translate_selection + + If true, the human-readable values will be translated. Default value is + ``True``. + +.. attribute:: Selection.help_selection + + A dictionary mapping the selection value with its help string. + +Instance methods: + +.. method:: Selection.translated([name]) + + Returns a descriptor for the translated value of the field. The descriptor + must be used on the same class as the field. It will use the language + defined in the context of the instance accessed. + + +MultiSelection +-------------- + +.. class:: MultiSelection(selection, string[, sort[, translate[, help_selection[, \**options]]]]) + +A list field with limited values to choose from. + +:class:`MultiSelection` has one extra required argument: + +.. attribute:: MultiSelection.selection + + Same as :attr:`Selection.selection` + +:class:`MultiSelection` has two extra optional arguments: + +.. attribute:: MultiSelection.sort + + Same as :attr:`Selection.sort` + +.. attribute:: MultiSelection.translate_selection + + Same as :attr:`Selection.translate_selection` + +.. attribute:: MultiSelection.help_selection + + Same as :attr:`Selection.help_selection` + +Instance methods: + +.. method:: MultiSelection.translated([name]) + + Same as :meth:`Selection.translated` but returns a list of translated values. + + +Reference +--------- + +.. class:: Reference(string[, selection[, sort[, selection_change_with[, translate[, help_selection[,search_order[, search_context[, \**options]]]]]]]]) + +A field that refers to a record of a model. It will be represented in Python by +a ``str`` instance like this:: + + ',' + +But a ``tuple`` can be used to search or set value. + +:class:`Reference` has three extra optional arguments: + +.. attribute:: Reference.selection + + Same as :attr:`Selection.selection` but only for model name. + +.. attribute:: Reference.sort + + Same as :attr:`Selection.sort`. + +.. attribute:: Reference.selection_change_with + + Same as :attr:`Selection.selection_change_with`. + +.. attribute:: Reference.translate_selection + + Same as :attr:`Selection.translate_selection`. + +.. attribute:: Reference.help_selection + + Same as :attr:`Selection.help_selection`. + +.. attribute:: Reference.datetime_field + + Same as :attr:`Many2One.datetime_field` + +.. attribute:: Reference.search_order + + Same as :attr:`Many2One.search_order` + +.. attribute:: Reference.search_context + + Same as :attr:`Many2One.search_context` + +Instance methods: + +.. method:: Reference.translated([name]) + + Same as :meth:`~Selection.translated` but for the translated name of the + target model. + +Many2One +-------- + +.. class:: Many2One(model_name, string[, left[, right[, ondelete[, datetime_field[, target_search[, search_order[, search_context[, \**options]]]]]]]) + +A many-to-one relation field. + +:class:`Many2One` has one extra required argument: + +.. attribute:: Many2One.model_name + + The name of the target model. + +:class:`Many2One` has some extra optional arguments: + +.. attribute:: Many2One.left + + The name of the field that stores the left value for the `Modified Preorder + Tree Traversal`_. + It only works if the :attr:`model_name` is the same then the model. + +.. warning:: The MPTT Tree will be rebuild on database update if one record + is found having left or right field value equals to the default or NULL. + +.. _`Modified Preorder Tree Traversal`: http://en.wikipedia.org/wiki/Tree_traversal + +.. attribute:: Many2One.right + + The name of the field that stores the right value. See :attr:`left`. + +.. attribute:: Many2One.ondelete + + Define the behavior of the record when the target record is deleted. + Allowed values are: + + - ``CASCADE``: it will try to delete the record. + + - ``RESTRICT``: it will prevent the deletion of the target record. + + - ``SET NULL``: it will empty the relation field. + + ``SET NULL`` is the default setting. + + .. note:: + + ``SET NULL`` will be override into ``RESTRICT`` if + :attr:`~Field.required` is true. + .. + +.. attribute:: Many2One.datetime_field + + If set, the target record will be read at the date defined by the datetime + field name of the record. + It is usually used in combination with + :attr:`trytond.model.ModelSQL._history` to request a value for a given date + and time on a historicized model. + +.. attribute:: Many2One.target_search + + Define the kind of SQL query to use when searching on related target. + Allowed values are: + + - ``subquery``: it will use a subquery based on the ids. + + - ``join``: it will add a join on the main query. + + ``join`` is the default value. + + .. note:: + + ``join`` could improve the performance if the target has a huge amount + of records. + .. + +.. attribute:: Many2One.search_order + + A :ref:`PYSON ` expression defining the default order used to + display search results in the clients. + +.. attribute:: Many2One.search_context + + A dictionary defining the default context used when searching from the + client. + + Beware that ``search_context`` will override the values from the client + ``context``. + +One2Many +-------- + +.. class:: One2Many(model_name, field, string[, add_remove[, order[, datetime_field[, size[, search_order[, search_context[, \**options]]]]]]]) + +A one-to-many relation field. It requires to have the opposite +:class:`Many2One` field or a :class:`Reference` field defined on the target +model. + +This field accepts as written value a list of tuples like this: + + - ``('create', [{: value, ...}, ...])``: it will create new + target records and link them to this one. + + - ``('write'[[, ids, ...], {: value, ...}, ...])``: it will + write values to target ids. + + - ``('delete'[, ids, ...])``: it will delete the target ids. + + - ``('add'[, ids, ...])``: it will link the target ids to this record. + + - ``('remove'[, ids, ...])``: it will unlink the target ids from this + record. + + - ``('copy', ids[, {: value, ...}])``: it will copy the target + ids to this record. Optional field names and values may be added to + override some of the fields of the copied records. + +.. note:: + + :class:`~trytond.pyson.PYSON` statement or :attr:`Field.depends` of target + records can access value of the parent record fields by prepending + ``_parent_`` to the opposite field name and followed by the dotted + notation. + +.. + +:class:`One2Many` has some extra required arguments: + +.. attribute:: One2Many.model_name + + The name of the target model. + +.. attribute:: One2Many.field + + The name of the field that handles the opposite :class:`Many2One` or + :class:`Reference`. + +:class:`One2Many` has some extra optional arguments: + +.. attribute:: One2Many.add_remove + + A :ref:`domain ` to select records to add. If set, the + client will allow to add/remove existing records instead of only + create/delete. + +.. attribute:: One2Many.filter + + A :ref:`domain ` that is not a constraint but only a + filter on the records. + +.. warning:: + + Only a static domain is allowed, it cannot contain any + :class:`~trytond.pyson.PYSON` statements. + +.. attribute:: One2Many.order + + A list of tuple defining the default order of the records like for + :attr:`trytond.model.ModelSQL._order`. + +.. attribute:: One2Many.datetime_field + + Same as :attr:`Many2One.datetime_field` + +.. attribute:: One2Many.size + + An integer or a PYSON expression denoting the maximum number of records + allowed in the relation. + +.. attribute:: One2Many.search_order + + Same as :attr:`Many2One.search_order` + +.. attribute:: One2Many.search_context + + Same as :attr:`Many2One.search_context` + +Instance methods: + +.. method:: One2Many.remove(instance, records) + + Remove the target records from the instance instead of deleting them. + +Many2Many +--------- + +.. class:: Many2Many(relation_name, origin, target, string[, order[, datetime_field[, size[, search_order[, search_context[, \**options]]]]]]) + +A many-to-many relation field. It requires to have the opposite origin +:class:`Many2One` field or a :class:`Reference` field defined on the relation +model and a :class:`Many2One` field pointing to the target. + +This field accepts as written value a list of tuples like the :class:`One2Many`. + +:class:`Many2Many` has some extra required arguments: + +.. attribute:: Many2Many.relation_name + + The name of the relation model. + +.. attribute:: Many2Many.origin + + The name of the field that has the :class:`Many2One` or :class:`Reference` + to the record. + +.. attribute:: Many2Many.target + + The name of the field that has the :class:`Many2One` to the target record. + +.. note:: + + A :class:`Many2Many` field can be used on a simple + :class:`~trytond.model.ModelView`, like in a + :class:`~trytond.wizard.Wizard`. For this, :attr:`~Many2Many.relation_name` + is set to the target model and :attr:`~Many2Many.origin` and + :attr:`~Many2Many.target` are set to ``None``. +.. + +:class:`Many2Many` has some extra optional arguments: + +.. attribute:: Many2Many.order + + Same as :attr:`One2Many.order` + +.. attribute:: Many2Many.datetime_field + + Same as :attr:`Many2One.datetime_field` + +.. attribute:: Many2Many.size + + An integer or a PYSON expression denoting the maximum number of records + allowed in the relation. + +.. attribute:: Many2Many.add_remove + + An alias to the :attr:`domain` for compatibility with the :class:`One2Many`. + +.. attribute:: Many2Many.filter + + Same as :attr:`One2Many.filter` + +.. attribute:: Many2Many.search_order + + Same as :attr:`Many2One.search_order` + +.. attribute:: Many2Many.search_context + + Same as :attr:`Many2One.search_context` + + +Instance methods: + +.. method:: Many2Many.get_relation() + + Return the relation :class:`~trytond.model.Model`. + +.. method:: Many2Many.get_target() + + Return the target :class:`~trytond.model.Model`. + +.. method:: Many2Many.delete(instance, records): + + Delete the target records from the instance instead of removing them. + +One2One +------- + +.. class:: One2One(relation_name, origin, target, string[, datetime_field[, \**options]]) + +A one-to-one relation field. + +.. warning:: + It is on the relation_name :class:`~trytond.model.Model` that the + unicity of the couple (origin, target) must be checked. +.. + +.. attribute:: One2One.datetime_field + + Same as :attr:`Many2One.datetime_field` + +.. attribute:: One2One.filter + + Same as :attr:`One2Many.filter` + +Instance methods: + +.. method:: One2One.get_relation() + + Return the relation :class:`~trytond.model.Model`. + +.. method:: One2One.get_target() + + Return the target :class:`~trytond.model.Model`. + +Function +-------- + +.. class:: Function(field, getter[, setter[, searcher]]) + +A function field can emulate any other given ``field``. + +:class:`Function` has a required argument: + +.. attribute:: Function.getter + + The name of the classmethod or instance of the + :class:`~trytond.model.Model` for getting values. + The signature of the classmethod is:: + + getter(instances, name) + + where ``name`` is the name of the field, and it must return a dictionary + with a value for each instance. + + Or the signature of the classmethod is:: + + getter(instances, names) + + where ``names`` is a list of name fields, and it must return a dictionary + containing for each names a dictionary with a value for each instance. + + The signature of the instancemethod is:: + + getter(name) + + where ``name`` is the name of the field, and it must return the value. + +:class:`Function` has some extra optional arguments: + +.. attribute:: Function.setter + + The name of the classmethod of the :class:`~trytond.model.Model` to set + the value. + The signature of the method id:: + + setter(instances, name, value) + + where ``name`` is the name of the field and ``value`` the value to set. + +.. warning:: + The modifications made to instances will not be saved automatically. + +.. attribute:: Function.searcher + + The name of the classmethod of the :class:`~trytond.model.Model` to search + on the field. + The signature of the method is:: + + searcher(name, clause) + + where ``name`` is the name of the field and ``clause`` is a + :ref:`domain clause `. + It must return a list of :ref:`domain ` clauses but the + ``operand`` can be a SQL query. + +Instance methods: + +.. method:: Function.get(ids, model, name[, values]) + + Call the :attr:`~Function.getter` classmethod where ``model`` is the + :class:`~trytond.model.Model` instance of the field, ``name`` is the name of + the field. + +.. method:: Function.set(ids, model, name, value) + + Call the :attr:`~Function.setter` classmethod where ``model`` is the + :class:`~trytond.model.Model` instance of the field, ``name`` is the name of + the field, ``value`` is the value to set. + +.. method:: Function.search(model, name, clause) + + Call the :attr:`~Function.searcher` classmethod where ``model`` is the + :class:`~trytond.model.Model` instance of the field, ``name`` is the name of + the field, ``clause`` is a clause of :ref:`domain `. + +MultiValue +---------- + +.. class:: MultiValue(field) + +A multivalue field that is like a :class:`Function` field but with predefined +:attr:`~Function.getter` and :attr:`~Function.setter` that use the +:class:`~trytond.model.MultiValueMixin` for stored values. + +.. warning:: + The :meth:`~trytond.model.MultiValueMixin.get_multivalue` and + :meth:`~trytond.model.MultiValueMixin.set_multivalue` should be prefered + over the descriptors of the field. +.. + +.. warning:: + The :ref:`default ` method of the field must + accept pattern as keyword argument. +.. + +Dict +---- + +.. class:: Dict(schema_model[, \**options]) + +A dictionary field with predefined keys. + +.. note:: + It is possible to store the dict as JSON in the database if the backend + supports by manually altering the column type to JSON on the database. + +:class:`Dict` has one extra required argument: + +.. attribute:: Dict.schema_model + + The name of the :class:`DictSchemaMixin` model that stores the definition + of keys. + +.. attribute:: Dict.search_unaccented + + Same as :attr:`Char.search_unaccented` but when searching on key's value. + +Instance methods: + +.. method:: Dict.translated([name[, type_]]) + + Returns a descriptor for the translated ``values`` or ``keys`` of the field + following ``type_``. The descriptor must be used on the same class as the + field. Default ``type_`` is ``values``. diff --git a/trytond/doc/ref/models/index.rst b/trytond/doc/ref/models/index.rst new file mode 100644 index 0000000000..c2198aeb9d --- /dev/null +++ b/trytond/doc/ref/models/index.rst @@ -0,0 +1,13 @@ +.. _ref-models-index: + +====== +Models +====== + +Model API reference. + +.. toctree:: + :maxdepth: 1 + + models + fields diff --git a/trytond/doc/ref/models/models.rst b/trytond/doc/ref/models/models.rst new file mode 100644 index 0000000000..25fe8958a4 --- /dev/null +++ b/trytond/doc/ref/models/models.rst @@ -0,0 +1,954 @@ +.. _ref-models: +.. module:: trytond.model + +===== +Model +===== + +.. class:: Model([id[, \**kwargs]]) + +This is the base class that every kind of :ref:`model ` +inherits. It defines common attributes of all models. + +Class attributes are: + +.. attribute:: Model.__name__ + + It contains the a unique name to reference the model throughout the + platform. + +.. attribute:: Model.__access__ + + A set that contains the names of relation field for which the access rights + are also checked for this model. + +.. attribute:: Model.__rpc__ + + It contains a dictionary with method name as key and an instance of + :class:`trytond.rpc.RPC` as value. + +.. attribute:: Model._rec_name + + It contains the name of the field used as name of records. The default + value is 'name'. + +.. attribute:: Model.id + + The definition of the field ``id`` of records. + +.. attribute:: Model.__queue__ + + It returns a queue caller for the model. The called method will be pushed + into the queue. + +.. attribute:: Model._fields + + It contains a dictionary with the field name as key and its + :class:`~trytond.model.field` instance as value. + +.. attribute:: Model._record + + It stores the record class to store internaly the values of the instances. + +.. attribute:: Model._defaults + + It contains a dictionary with the field name as key and its default method + as value. + +Class methods: + +.. classmethod:: Model.__setup__() + + Setup the class before adding into the :class:`trytond.pool.Pool`. + +.. classmethod:: Model.__post_setup__() + + Setup the class after added into the :class:`trytond.pool.Pool`. + +.. classmethod:: Model.__register__(module_name) + + Registers the model in ``ir.model`` and ``ir.model.field``. + +.. classmethod:: Model.default_get(fields_names[, with_rec_name]) + + Returns a dictionary with the default values for each field in + ``fields_names``. Default values are defined by the returned value of each + instance method with the pattern ``default_`field_name`()``. + ``with_rec_name`` allow to add ``rec_name`` value for each many2one field. + The ``default_rec_name`` key in the context can be used to define the value + of the :attr:`Model._rec_name` field. + +.. classmethod:: Model.fields_get([fields_names[, level]]) + + Return the definition of each field on the model. + The ``level`` defines the number of relations to include in the relation + field definition. + +.. classmethod:: Model.__names__([field]) + + Returns a dictionary with the name of the ``model`` and the ``field``. + It is a convenience-method used to format messages which should include + those names. + +Instance methods: + +.. method:: Model.on_change(fieldnames) + + Returns the list of changes by calling ``on_change`` method of each field. + +.. method:: Model.on_change_with(fieldnames) + + Returns the new values of all fields by calling ``on_change_with`` method of + each field. + +.. method:: Model.pre_validate() + + This method is called by the client to validate the instance. + +========= +ModelView +========= + +.. class:: ModelView + +It adds requirements to display a view of the model in the client. + +Class attributes: + +.. attribute:: ModelView._buttons + + It contains a dictionary with button name as key and the states dictionary + for the button. + The states possible keys are ``invisible``, ``readonly`` and ``icon`` which + have a :class:`~trytond.pyson.PYSON` statement as value and ``depends`` + which has a list of field names on which the button states depend. + The keys will be set as default attributes on the buttons for the views + that show them. + +Static methods: + +.. staticmethod:: ModelView.button + + Decorate button method to check group access and rule. + +.. staticmethod:: ModelView.button_action(action) + + Same as :meth:`ModelView.button` but return the action id of the XML ``id`` + action or the action value updated by the returned value of the method. + +.. staticmethod:: ModelView.button_change([\*fields[, methods]]) + + Same as :meth:`ModelView.button` but for button that change values of the + fields on client side (similar to :ref:`on_change + `). + The `methods` argument can be used to duplicate the field names from other + decorated methods. This is useful if the decorated method calls another + method. + + .. warning:: + Only on instance methods. + +Class methods: + +.. classmethod:: ModelView.fields_view_get([view_id[, view_type[, level]]]) + + Return a view definition used by the client. The definition is:: + + { + 'model': model name, + 'type': view type, + 'view_id': view id, + 'arch': XML description, + 'fields': { + field name: { + ... + }, + }, + 'field_childs': field for tree, + } + +.. classmethod:: ModelView.view_toolbar_get() + + Returns the model specific actions and exports in a dictionary with keys: + - ``print``: a list of available reports + - ``action``: a list of available actions + - ``relate``: a list of available relations + - ``exports``: a list of available exports + +.. classmethod:: ModelView.view_attributes() + + Returns a list of XPath, attribute, value and an optional depends list. + Each element from the XPath will get the attribute set with the JSON + encoded value. If the depends list is set its fields are added to the + view if the xpath matches at least one element. + + +.. classmethod:: ModelView.parse_view(tree, type[, field_children[, level[, view_depends]]]) + + Return the sanitized XML and the corresponding fields definition. + + +.. note:: + + This method is public mainly to allow modification the existing XML of the + view by code. + +============ +ModelStorage +============ + +.. class:: ModelStorage + +It adds storage capability. + +Class attributes are: + +.. attribute:: ModelStorage.create_uid + + The definition of the :class:`trytond.model.fields.Many2One` field + :attr:`create_uid` of records. It contains the :attr:`id` of the user who + creates the record. + +.. attribute:: ModelStorage.create_date + + The definition of the :class:`trytond.model.fields.DateTime` field + :attr:`create_date` of records. It contains the datetime of the creation of + the record. + +.. attribute:: ModelStorage.write_uid + + The definition of the :class:`trytond.model.fields.Many2One` field + :attr:`write_uid` of the records. + It contains the :attr:`id` of the last user who writes on the record. + +.. attribute:: ModelStorage.write_date + + The definition of the :class:`trytond.model.fields.DateTime` field + :attr:`write_date` of the records. It contains the datetime of the last + write on the record. + +.. attribute:: ModelStorage.rec_name + + The definition of the :class:`trytond.model.fields.Function` field + :attr:`rec_name`. It is used in the client to display the records with a + single string. + +Static methods: + +.. staticmethod:: ModelStorage.default_create_uid() + + Return the default value for :attr:`create_uid`. + +.. staticmethod:: ModelStorage.default_create_date() + + Return the default value for :attr:`create_date`. + +Class methods: + +.. classmethod:: ModelStorage.create(vlist) + + Create records. ``vlist`` is list of dictionaries with fields names as key + and created values as value and return the list of new instances. + +.. classmethod:: ModelStorage.trigger_create(records) + + Trigger create actions. It will call actions defined in ``ir.trigger`` if + ``on_create`` is set and ``condition`` is true. + +.. classmethod:: ModelStorage.read(ids, fields_names) + + Return a list of dictionary for the record ids. The dictionary is composed + of the fields as key and their values. + ``fields_names`` can contain dereferenced fields from related models. + Their values will be returned under the referencing field suffixed by a + ``.``. The number of *dots* in the name is not limited. + The order of the returned list is not guaranteed. + +.. classmethod:: ModelStorage.write(records, values, [[records, values], ...]) + + Write ``values`` on the list of records. ``values`` is a dictionary with + fields names as key and writen values as value. + +.. classmethod:: ModelStorage.trigger_write_get_eligibles(records) + + Return eligible records for write actions by triggers. This dictionary + is to pass to :meth:`~ModelStorage.trigger_write`. + +.. classmethod:: ModelStorage.trigger_write(eligibles) + + Trigger write actions. It will call actions defined in ``ir.trigger`` if + ``on_write`` is set and ``condition`` was false before + :meth:`~ModelStorage.write` and true after. + +.. classmethod:: ModelStorage.index_set_field(name) + + Return the index sort order of the field set calls. + +.. classmethod:: ModelStorage.delete(records) + + Delete records. + +.. classmethod:: ModelStorage.trigger_delete(records) + + Trigger delete actions. It will call actions defined in ``ir.trigger`` if + ``on_delete`` is set and ``condition`` is true. + +.. classmethod:: ModelStorage.copy(records[, default]) + + Duplicate the records. ``default`` is a dictionary of default value per + field name for the created records. + + The values of ``default`` may be also callable that take a dictionary + containing the fields and values of the record copied and return of the + value. + + The keys of ``default`` may use the dotted notation for the + :class:`fields.One2Many` to define the default to pass to its ``copy`` + operation. + + New records are returned following the input order. + +.. classmethod:: ModelStorage.search(domain[, offset[, limit[, order[, count]]]]) + + Return a list of records that match the :ref:`domain `. + + If ``offset`` or ``limit`` are set, the result starts at the offset and + has the length of the limit. + + The ``order`` is a list of tuples defining the order of the result: + + [ ('field name', 'ASC'), ('other field name', 'DESC'), ... ] + + The first element of the tuple is a field name of the model and the second + is the sort ordering as ``ASC`` for ascending, ``DESC`` for descending or + empty for a default order. This second element may contain 'NULLS FIRST' or + 'NULLS LAST' to sort null values before or after non-null values. If + neither is specified the default behavior of the backend is used. + + In case the field used is a :class:`fields.Many2One`, it is also possible + to use the dotted notation to sort on a specific field from the target + record. Or for a :class:`fields.Dict` field, the dotted notation is used to + sort on the key's value. + + If ``count`` is set to ``True``, then the result is the number of records. + +.. classmethod:: ModelStorage.search_count(domain) + + Return the number of records that match the :ref:`domain `. + +.. classmethod:: ModelStorage.search_read(domain[, offset[, limit[, order[, fields_names]]]]) + + Call :meth:`search` and :meth:`read` at once. + Useful for the client to reduce the number of calls. + +.. classmethod:: ModelStorage.search_rec_name(name, clause) + + Searcher for the :class:`trytond.model.fields.Function` field + :attr:`rec_name`. + +.. classmethod:: ModelStorage.search_global(cls, text) + + Yield tuples (record, name, icon) for records matching text. + It is used for the global search. + +.. classmethod:: ModelStorage.browse(ids) + + Return a list of record instance for the ``ids``. + +.. classmethod:: ModelStorage.export_data(records, fields_names) + + Return a list of list of values for each ``records``. + The list of values follows ``fields_names``. + Relational fields are defined with ``/`` at any depth. + Descriptor on fields are available by appending ``.`` and the name of the + method on the field that returns the descriptor. + +.. classmethod:: ModelStorage.export_data_domain(domain, fields_names[, offset[, limit[, order]]]) + + Call :meth:`search` and :meth`export_data` together. + Useful for the client to reduce the number of calls and the data transfered. + +.. classmethod:: ModelStorage.import_data(fields_names, data) + + Create or update records for all values in ``data``. + The field names of values must be defined in ``fields_names``. + It returns the number of imported records. + +.. classmethod:: ModelStorage.check_xml_record(records, values) + + Verify if the records are originating from XML data. It is used to prevent + modification of data coming from XML files. This method must be overiden to + change this behavior. + +.. classmethod:: ModelStorage.validate(records) + + Validate the integrity of records after creation and modification. This + method must be overridden to add validation and must raise an exception if + validation fails. + +Dual methods: + +.. classmethod:: ModelStorage.save(records) + + Save the modification made on the records. + +Instance methods: + +.. method:: ModelStorage.resources() + + Return a dictionary with the number of attachments (``attachment_count``), + notes (``note_count``) and unread note (``note_unread``). + +.. method:: ModelStorage.get_rec_name(name) + + Getter for the :class:`trytond.model.fields.Function` field + :attr:`rec_name`. + +======== +ModelSQL +======== + +.. class:: ModelSQL + +It implements :class:`ModelStorage` for an SQL database. + +Class attributes are: + +.. attribute:: ModelSQL._table + + The name of the database table which is mapped to the class. + If not set, the value of :attr:`Model._name` is used with dots converted to + underscores. + +.. attribute:: ModelSQL._order + + The default ``order`` parameter of :meth:`ModelStorage.search` method. + +.. attribute:: ModelSQL._order_name + + The name of the field (or an SQL statement) on which the records must be + sorted when sorting on a field refering to the model. If not set, + :attr:`ModelStorage._rec_name` will be used. + +.. attribute:: ModelSQL._history + + If true, all changes on records will be stored in a history table. + +.. attribute:: ModelSQL._sql_constraints + + A list of SQL constraints that are added on the table: + + [ ('constraint name', constraint, 'xml id'), ... ] + + - ``constraint name`` is the name of the SQL constraint in the database + + - constraint is an instance of :class:`Constraint` + + - message id for :meth:`trytond.i18n.gettext` + +Class methods: + +.. classmethod:: ModelSQL.__table__() + + Return a SQL Table instance for the Model. + +.. classmethod:: ModelSQL.__table_history__() + + Return a SQL Table instance for the history of Model. + +.. classmethod:: ModelSQL.__table_handler__([module_name[, history]]) + + Return a TableHandler for the Model. + +.. classmethod:: ModelSQL.table_query() + + Could be defined to use a custom SQL query instead of a table of the + database. It should return a SQL FromItem. + + .. warning:: + By default all CRUD operation will raise an error on models + implementing this method so the create, write and delete methods may + also been overriden if needed. + .. + +.. classmethod:: ModelSQL.history_revisions(ids) + + Return a sorted list of all revisions for ids. The list is composed of + the date, id and username of the revision. + +.. classmethod:: ModelSQL.restore_history(ids, datetime) + + Restore the record ids from history at the specified date time. + Restoring a record will still generate an entry in the history table. + + .. warning:: + No access rights are verified and the records are not validated. + .. + +.. classmethod:: ModelSQL.restore_history_before(ids, datetime) + + Restore the record ids from history before the specified date time. + Restoring a record will still generate an entry in the history table. + + .. warning:: + No access rights are verified and the records are not validated. + .. + +.. classmethod:: ModelSQL.search(domain[, offset[, limit[, order[, count[, query]]]]]) + + Same as :meth:`ModelStorage.search` with the additional ``query`` argument. + If ``query`` is set to ``True``, the the result is the SQL query. + +.. classmethod:: ModelSQL.search_domain(domain[, active_test[, tables]]) + + Convert a :ref:`domain ` into a SQL expression by returning + the updated tables dictionary and a SQL expression. + +.. _ref-tables: + + Where ``tables`` is a nested dictionary containing the existing joins:: + + { + None: (, None), + 'party': { + None: (
, ), + 'addresses': { + None: (
, ), + }, + }, + } + +Dual methods: + +.. classmethod:: ModelSQL.lock(records) + + Take a lock for update on the records. + +Constraint +========== + +.. class:: Constraint(table) + +It represents a SQL constraint on a table of the database and it follows the +API of the python-sql expression. + +Instance attributes: + +.. attribute:: Constraint.table + + The SQL Table on which the constraint is defined. + +Check +----- + +.. class:: Check(table, expression) + +It represents a check :class:`Constraint` which enforce the validity of the +expression. + +Instance attributes: + +.. attribute:: Check.expression + + The SQL expression to check. + +Unique +------ + +.. class:: Unique(table, \*columns) + +It represents a unique :class:`Constraint` which enforce the uniqeness of the +group of columns with respect to all the rows in the table. + +Instance attributes: + +.. attribute:: Unique.columns + + The tuple of SQL Column instances. + +.. attribute:: Unique.operators + + The tuple of ``Equal`` operators. + +Exclude +------- + +.. class:: Exclude(table[, (expression, operator), ...[, where]]) + +It represents an exclude :class:`Constraint` which guarantees that if any two +rows are compared on the specified expression using the specified operator not +all of these comparisons will return ``TRUE``. + +Instance attributes: + +.. attribute:: Exclude.excludes + + The tuple of expression and operator. + +.. attribute:: Exclude.columns + + The tuple of expressions. + +.. attribute:: Exclude.operators + + The tuple of operators. + +.. attribute:: Exclude.where + + The clause for which the exclusion applies. + +======== +Workflow +======== + +.. class:: Workflow + +A Mix-in class to handle transition check. + +Class attribute: + +.. attribute:: Workflow._transition_state + + The name of the field that will be used to check state transition. + +.. attribute:: Workflow._transitions + + A set containing tuples of from and to state. + +Static methods: + +.. staticmethod:: Workflow.transition(state) + + Decorate method to filter ids for which the transition is valid and finally + to update the state of the filtered ids. + +============== +ModelSingleton +============== + +.. class:: ModelSingleton + +Modify :class:`ModelStorage` into a singleton_. +This means that there will be only one record of this model. +It is commonly used to store configuration value. + +.. _singleton: http://en.wikipedia.org/wiki/Singleton_pattern + +Class methods: + +.. classmethod:: ModelSingleton.get_singleton() + + Return the instance of the unique record if there is one. + +=============== +DictSchemaMixin +=============== + +.. class:: DictSchemaMixin + +A mixin_ for the schema of :class:`trytond.model.fields.Dict` field. + +Class attributes are: + +.. attribute:: DictSchemaMixin.name + + The definition of the :class:`trytond.model.fields.Char` field for the name + of the key. + +.. attribute:: DictSchemaMixin.string + + The definition of the :class:`trytond.model.fields.Char` field for the + string of the key. + +.. attribute:: DictSchemaMixin.help + + The definition of the :class:`trytond.model.fields.Char` field used as the + help text for the key. + +.. attribute:: DictSchemaMixin.type\_ + + The definition of the :class:`trytond.model.fields.Selection` field for the + type of the key. The available types are: + + * boolean + * integer + * char + * float + * numeric + * date + * datetime + * selection + +.. attribute:: DictSchemaMixin.digits + + The definition of the :class:`trytond.model.fields.Integer` field for the + digits number when the type is ``float`` or ``numeric``. + +.. attribute:: DictSchemaMixin.domain + + A :ref:`domain ` constraint on the dictionary key that will + be enforced only on the client side. + + The key must be referenced by its name in the left operator of the domain. + The :ref:`PYSON ` evaluation context used to compute the domain + is the dictionary value. Likewise the domain is tested using the dictionary + value. + +.. attribute:: DictSchemaMixin.selection + + The definition of the :class:`trytond.model.fields.Text` field to store the + couple of key and label when the type is ``selection``. + The format is a key/label separated by ":" per line. + +.. attribute:: DictSchemaMixin.selection_sorted + + If the :attr:`selection` must be sorted on label by the client. + +.. attribute:: DictSchemaMixin.selection_json + + The definition of the :class:`trytond.model.fields.Function` field to + return the JSON_ version of the :attr:`selection`. + +Static methods: + +.. staticmethod:: DictSchemaMixin.default_digits() + + Return the default value for :attr:`digits`. + +Class methods: + +.. classmethod:: DictSchemaMixin.get_keys(records) + + Return the definition of the keys for the records. + +.. classmethod:: DictSchemaMixin.get_relation_fields() + + Return a dictionary with the field definition of all the keys like the + result of :meth:`Model.fields_get`. + + It is possible to disable this method (returns an empty dictionary) by + setting in the ``dict`` section of the configuration, the + :attr:`Model.__name__` to ``False``. + +Instance methods: + +.. method:: DictSchemaMixin.get_selection_json(name) + + Getter for the :attr:`selection_json`. + +========== +MatchMixin +========== + +.. class:: MatchMixin + +A mixin_ to add to a :class:`Model` a match method on pattern. +The pattern is a dictionary with field name as key and the value to compare. +The record matches the pattern if for all dictionary entries, the value of the +record is equal or not defined. + +Instance methods: + +.. method:: MatchMixin.match(pattern[, match_none]) + + Return if the instance match the pattern. If ``match_none`` is set ``None`` + value of the instance will be compared. + +========== +UnionMixin +========== + +.. class:: UnionMixin + +A mixin_ to create a :class:`ModelSQL` which is the UNION_ of some +:class:`ModelSQL`'s. The ids of each models are sharded to be unique. + +Static methods: + +.. staticmethod:: UnionMixin.union_models() + + Return the list of :class:`ModelSQL`'s names + +Class methods: + +.. classmethod:: UnionMixin.union_shard(column, model) + + Return a SQL expression that shards the column containing record id of + model name. + +.. classmethod:: UnionMixin.union_unshard(record_id) + + Return the original instance of the record for the sharded id. + +.. classmethod:: UnionMixin.union_column(name, field, table, Model) + + Return the SQL column that corresponds to the field on the union model. + +.. classmethod:: UnionMixin.union_columns(model) + + Return the SQL table and columns to use for the UNION for the model name. + +=========== +SymbolMixin +=========== + +.. class:: SymbolMixin + +A mixin_ to manage the display of symbols on the client side. + +Instance methods: + +.. method:: SymbolMixin.get_symbol(sign, [symbol]) + + Return a symbol and its position. + The position indicates whether the symbol should appear before (0) or after + (1) the value. If no symbol parameter is supplied then the mixin uses the + value of attribute named ``symbol``. + +================ +sequence_ordered +================ + +.. method:: sequence_ordered([field_name, [field_label, [order]]]) + +Retuns a mixin_ class which defines the order of a :class:`ModelSQL` with an +:class:`trytond.model.fields.Integer` field. field_name indicates the name of +the field to be created and its default values is ``sequence``. field_label +defines the label which will be used by the field and defaults to ``Sequence``. +Order specifies the order direction and defaults to ``ASC NULLS FIRST``. + +=============== +MultiValueMixin +=============== + +.. class:: MultiValueMixin + +A mixin_ for :class:`Model` to help having +:class:`trytond.model.fields.MultiValue` fields with multi-values on a +:class:`ValueMixin`. The values are stored by creating one record per pattern. +The patterns are the same as those on :class:`MatchMixin`. + +Class methods: + +.. classmethod:: MultiValueMixin.multivalue_model(field) + + Return the :class:`ValueMixin` on which the values are stored for the + field name. The default is class name suffixed by the field name. + +.. classmethod:: MultiValueMixin.setter_multivalue(records, name, value, \*\*pattern) + + The setter method for the :class:`trytond.model.fields.Function` fields. + +Instance methods: + +.. method:: MultiValueMixin.multivalue_records(field) + + Return the list of all :class:`ValueMixin` records linked to the instance. + By default, it returns the value of the first found + :class:`trytond.model.fields.One2Many` linked to the multivalue model or + all the records of this one. + +.. method:: MultiValueMixin.multivalue_record(field, \*\*pattern) + + Return a new record of :class:`ValueMixin` linked to the instance. + +.. method:: MultiValueMixin.get_multivalue(name, \*\*pattern) + + Return the value of the field ``name`` for the pattern. + +.. method:: MultiValueMixin.set_multivalue(name, value[, save], \*\*pattern) + + Store the value of the field ``name`` for the pattern. + If ``save`` is true, it will be stored in the database, otherwise the + modified :class:`ValueMixin` records are returned unsaved. ``save`` is true + by default. + +.. warning:: + To customize the pattern, both methods must be override the same way. +.. + +========== +ValueMixin +========== + +.. class:: ValueMixin + +A mixin_ to store the values of :class:`MultiValueMixin`. + +================ +DeactivableMixin +================ + +.. class:: DeactivableMixin + +A mixin_ to add soft deletion to the model. It renders all the fields as +read-only when the record is inactive. + +Class attributes are: + +.. attribute:: DictSchemaMixin.active + + The definition of the :class:`trytond.model.fields.Boolean` field to + store soft deletion state. False values will be consideres as soft + deletion. + +.. _mixin: http://en.wikipedia.org/wiki/Mixin +.. _JSON: http://en.wikipedia.org/wiki/Json +.. _UNION: http://en.wikipedia.org/wiki/Union_(SQL)#UNION_operator + +==== +tree +==== + +.. method:: tree([parent[, name[, separator]]]) + +Returns a mixin_ class :class:`TreeMixin`. ``parent`` indicates the name of the +field that defines the parent of the tree and its default value is ``parent``. +``name`` indicates the name of the field that defines the name of the record and +its default value is ``name``. If ``separator`` is set, the +:meth:`ModelStorage.get_rec_name` constructs the name by concatenating each +parent names using it as separator and :meth:`ModelStorage.search_rec_name` is +adapted to search across the tree. + + +.. class:: TreeMixin + +.. classmethod:: TreeMixin.check_recursion(records) + + Helper method that checks if there is no recursion in the tree defined by + :meth:`tree`. + +============ +avatar_mixin +============ + +.. method:: avatar_mixin([size[, default]]) + +Returns a mixin_ :class:`AvatarMixin`. ``size`` defines the size of the avatar +image and its default value is ``64``. ``default`` indicates the name of the +field to use for generating a default avatar, if it's not set then no default +avatar is generated. + +.. class:: AvatarMixin + +.. attribute:: AvatarMixin.avatars + + The :class:`trytond.model.fields.One2Many` field used to store the + ``ir.avatar`` records. + +.. attribute:: AvatarMixin.avatar + + The :class:`trytond.model.fields.Binary` field that contains the avatar. + +.. attribute:: AvatarMixin.avatar_url + + The :class:`trytond.model.fields.Char` field that containts the URL for the + avatar. + +.. attribute:: AvatarMixin.has_avatar + + Indicates whether the record has an avatar. + +.. classmethod:: AvatarMixin.generate_avatar(records, field) + + Generate a default avatar for each record using the field. diff --git a/trytond/doc/ref/pool.rst b/trytond/doc/ref/pool.rst new file mode 100644 index 0000000000..33c82970aa --- /dev/null +++ b/trytond/doc/ref/pool.rst @@ -0,0 +1,81 @@ +.. _ref-pool: +.. module:: trytond.pool + +==== +Pool +==== + +.. class:: Pool([database_name]) + +The Pool store the instances of models, wizards and reports per database. + +Static methods: + +.. staticmethod:: Pool.register(klass, ..., type_, module[, depends]) + + Register the classes of type (``model``, ``report`` or ``wizard``) for the + module. + If ``depends`` is set, the registration happens only if all the modules are + activated. + +.. staticmethod:: Pool.register_mixin(mixin, classinfo, module) + + Register the mixin for the module. The mixin will be included to all + subclasses of classinfo. + +Class methods: + +.. classmethod:: Pool.start() + + Start the pool by registering all Tryton modules found. + +.. classmethod:: Pool.stop(database_name) + + Stop the pool by removing instances for the database. + +.. classmethod:: Pool.database_list() + + List all started database. + +Instance methods: + +.. method:: Pool.get(name[, type]) + + Return the named instance of type from the pool. + +.. method:: Pool.iterobject([type]) + + Return an interator over instances names. + +.. method:: Pool.fill(module, modules) + + Fill the pool with the registered class from the module and for the + activated modules and return a list of classes for each type in a + dictionary. + +.. method:: Pool.setup([classes]) + + Call all setup methods of the classes provided or for all the registered + classes. + +.. method:: Pool.setup_mixin(modules) + + Include all the mixin registered for the modules to the corresponding + registered classes. + +======== +PoolMeta +======== + +.. class:: PoolMeta + +The PoolMeta is a metaclass helper to setup __name__ on class to be registered +in the Pool. + +======== +PoolBase +======== + +.. class:: PoolBase + +The base class of registered class that will be setup. diff --git a/trytond/doc/ref/pyson.rst b/trytond/doc/ref/pyson.rst new file mode 100644 index 0000000000..505fec3ec2 --- /dev/null +++ b/trytond/doc/ref/pyson.rst @@ -0,0 +1,283 @@ +.. _ref-pyson: +.. module:: trytond.pyson + +===== +PYSON +===== + + +PYSON is the PYthon Statement and Object Notation. + +There is also a more :ref:`practical introduction into +PYSON statements `. + +.. class:: PYSON + +Base class of any PYSON statement. It is never used directly. + +Instance methods: + +.. method:: PYSON.pyson() + +Method that returns the internal dictionary representation of the +statement. + +.. method:: PYSON.types() + +Method that returns a set of all possible types which the statement +can become when evaluated. + +.. classmethod:: PYSON.eval(dct, context) + +Method which returns the evaluation of the statement given in +``dct`` within the ``context``. ``dct`` contains a +dictionary which is the internal representation of a PYSON +statement. ``context`` contains a dictionary with contextual +values. + + +Encoder and Decoder +=================== + +.. class:: PYSONEncoder() + +Encoder for PYSON statements into string representations. + +Instance method: + + .. method:: PYSONEncoder.encode(object) + + Returns a string representation of a given PYSON statement. + ``object`` contains a PYSON statement. + +.. class:: PYSONDecoder([context[, noeval]]) + +Decoder for string into the evaluated or not PYSON statement. + +Instance method: + + .. method:: PYSONDecoder.decode(object) + + Returns a PYSON statement evaluated or not of a given string. + ``object`` contains a string. + +Statements +========== + +The following statements can be used in :class:`PYSON`. + +.. class:: Eval(value[, default]) + +An :class:`Eval()` object represents the PYSON ``Eval()`` +statement for evaluations. When evaluated, it returns the +value of the statement named by ``value``, if defined in the +evaluation context, otherwise the ``default`` value (empty +string by default). + +.. note:: + + The default value determines the type of the statement. +.. + +.. note:: + + If the ``value`` includes dots the value will be dereferenced. For + example:: + + Eval('_parent_sale.number') + + The ``number`` value of the ``_parent_sale`` key of the evaluation context + will be returned. +.. + +.. class:: Not(value) + +A :class:`Not` object represents the PYSON ``Not()`` +statement for logical negations. When evaluated, returns +the boolean negation of the value of the statement named by +``value``, if defined in the evaluation context. Returns an +instance of itself. + +.. class:: Bool(value) + +A :class:`Bool` object represents the PYSON ``Bool()`` +statement for boolean evaluations. Returns the boolean +representation of the value of the statement named by +``value``. + +.. class:: And(\*statements) + +An :class:`And` object represents the PYSON ``And()`` +statement for logical *and* operations. Returns the result of +the logical conjunction of two or more values named by the +statements in the ``statements`` tuple. + +.. class:: Or(\*statements) + +An :class:`Or` object represents the PYSON ``Or()`` +statement for logical *or* operations. Returns the result of +the logical disjunction of two or more values named by the +statements in the ``statements`` tuple. + +.. class:: Equal(statement1, statement2) + +An :class:`Equal` object represents the PYSON ``Equal()`` +statement for equation comparisons. Returns true when a value of +a statement named by ``statement1`` and the value of a statement +named by ``statement2`` are equal, otherwise returns false. + +.. class:: Greater(statement1, statement2[, equal]) + +A :class:`Greater` object represents the PYSON ``Greater()`` +statement for *greater-than* comparisons. Returns true when the value +of the statement named by ``statement1`` is strictly greater than the +value of the statement named by ``statement2``, otherwise +returns false. Is the value of the variable named by ``equal`` is +true, then returns also true when both values of statements named by +``statement1`` and ``statement2`` are equal. In this case +:class:`Greater` works as a *greater-than or equal* operator. + +.. note:: ``None`` value is replaced by ``0`` for the comparison. + +.. class:: Less(statement1, statement2[, equal]) + +A :class:`Less` object represents the PYSON ``Less()`` +statement for *less-than* comparisons. Returns true when the value +of the statement named by ``statement1`` is strictly less than the +value of the statement named by ``statement2``, otherwise +returns false. Is the value of the variable named ``equal`` is true, +then returns also true when both values of the statements named by +``statement1`` and ``statement2`` are equal. In this case +:class:`Less` works as a *less-than or equal* operator. + +.. note:: ``None`` value is replaced by ``0`` for the comparison. + +.. class:: If(condition, then_statement, else_statement) + +An :class:`If` object represents the PYSON ``If()`` +statement for conditional flow control operations. Returns the +value of the statement named by ``then_statement`` when the value +of the statement named by ``condition`` evaluates true. +Otherwise returns the value of the statement named by +``else_statement``. + +.. class:: Get(obj, key[, default]) + +A :class:`Get` object represents the PYSON ``Get()`` +statement for dictionary look-up operations and evaluation. +Look up and returns the value of a key named by ``key`` in an +object named by ``obj`` if defined. +Otherwise returns the value of the variable named by ``default``. + +.. class:: In(key, obj) + +An :class:`In` object represents the PYSON ``In()`` +statement for look-up dictionary or integer objects. Returns true when +a list (or dictionary) object named by ``obj`` contains the value of +the variable (or key) named by ``key``. Otherwise returns false. + +.. class:: Date([year[, month[, day[, delta_years[, delta_month[, delta_days[, start]]]]]]]) + +A :class:`Date` object represents the PYSON ``Date()`` statement for date +related conversions and basic calculations. +Returns a date object which represents the values of arguments named by the +*variables* explained below. +Missing values of arguments named by ``year`` or ``month`` or ``day`` take +their defaults from ``start`` or the actual date. When values of arguments +named by ``delta_*`` are given, they are added to the values of the appropriate +arguments in a date and time preserving manner. + +Arguments: + +``year`` + Contains a PYSON statement of type int or long. + +``month`` + Contains a PYSON statement of type int or long. + +``day`` + Contains a PYSON statement of type int or long. + +``delta_years`` + Contains a PYSON statement of type int or long. + +``delta_month`` + Contains a PYSON statement of type int or long. + +``delta_days`` + Contains a PYSON statement of type int or long. + +``start`` + Contains a PYSON statement of type date. + +.. class:: DateTime([year[, month[, day[, hour[, minute[, second[, microsecond[, delta_years[, delta_months[, delta_days[, delta_hours[, delta_minutes[, delta_seconds[, delta_microseconds[, start]]]]]]]]]]]]]]]) + +A :class:`DateTime` object represents the PYSON ``Date()`` statement for date +and time related conversions and calculations. +Returns a date time object which represents the values of variables named by +the *arguments* explained below. +Missing values of arguments named by ``year``, ``month``, ``day``, ``hour``, +``minute``, ``second``, ``microseconds`` take their defaults from ``start`` or +the actual date and time in `UTC`_. +When values of arguments named by ``delta_*`` are given, these are added to +the appropriate attributes in a date and time preserving manner. + +.. _`UTC`: https://en.wikipedia.org/wiki/Coordinated_Universal_Time + +Arguments: + +``year`` + Contains a PYSON statement of type int or long. + +``month`` + Contains a PYSON statement of type int or long. + +``day`` + Contains a PYSON statement of type int or long. + +``hour`` + Contains a PYSON statement of type int or long. + +``minute`` + Contains a PYSON statement of type int or long. + +``second`` + Contains a PYSON statement of type int or long. + +``microsecond`` + Contains a PYSON statement of type int or long. + +``delta_years`` + Contains a PYSON statement of type int or long. + +``delta_month`` + Contains a PYSON statement of type int or long. + +``delta_days`` + Contains a PYSON statement of type int or long. + +``delta_hours`` + Contains a PYSON statement of type int or long. + +``delta_minutes`` + Contains a PYSON statement of type int or long. + +``delta_seconds`` + Contains a PYSON statement of type int or long. + +``delta_microseconds`` + Contains a PYSON statement of type int or long. + +``start`` + Contains a PYSON statement of type datetime. + +.. class:: Len(value) + +A :class:`Len` object represents the PYSON ``Len()`` statement for length of a +dictionary, list or string. Returns the number of items in ``value``. + +.. class:: Id(module, fs_id) + +An :class:`Id` object represents the PYSON ``Id()`` statement for filesystem id +evaluations. When converted into the internal dictionary, it returns the +database id stored in ``ir.model.data``. diff --git a/trytond/doc/ref/rpc.rst b/trytond/doc/ref/rpc.rst new file mode 100644 index 0000000000..93fde018fc --- /dev/null +++ b/trytond/doc/ref/rpc.rst @@ -0,0 +1,59 @@ +.. _ref-rpc: +.. module:: trytond.rpc + +=== +RPC +=== + +.. class:: RPC([readonly[, instantiate[, result[, check_access[, unique[, fresh_session[, cache]]]]]]]) + +RPC is an object to define the behavior of Remote Procedure Call. + +Instance attributes are: + +.. attribute:: RPC.readonly + + The transaction mode + +.. attribute:: RPC.instantiate + + The position or the slice of the argument to be instanciated + +.. attribute:: RPC.result + + The function to transform the result + +.. attribute:: RPC.check_access + + Set ``_check_access`` in the context to activate the access right on model + and field. Default is ``True``. + +.. attribute:: RPC.unique + + If set, it ensures the instantiated records are unique. Default is ``True``. + +.. attribute:: RPC.fresh_session + + If set, it requires a fresh session. Default is ``False``. + +.. attribute:: RPC.cache + + A :class:`RPCCache` instance to compute the cache duration for the answer. + +======== +RPCCache +======== + +.. class:: RPCCache([days[, seconds]) + +Instance attributes are: + +.. attribute:: RPC.duration + + A timdelta instance. + +Instance methods are: + +.. method:: RCP.headers + + Returns a dictionary of the headers. diff --git a/trytond/doc/ref/sendmail.rst b/trytond/doc/ref/sendmail.rst new file mode 100644 index 0000000000..13755ed29b --- /dev/null +++ b/trytond/doc/ref/sendmail.rst @@ -0,0 +1,49 @@ +.. _ref-sendmail: +.. module:: trytond.sendmail + +======== +Sendmail +======== + +.. method:: sendmail_transactional(from_addr, to_addrs, msg[, transaction[, datamanager]]) + +Send email message only if the current transaction is successfully committed. +The required arguments are an `RFC 822`_ from-address string, a list of `RFC +822`_ to-address strings (a bare string will be treated as a list with 1 +address), and an email message. +The caller may pass a :class:`Transaction` instance to join otherwise the +current one will be joined. A specific data manager can be specified otherwise +the default :class:`SMTPDataManager` will be used for sending email. + +.. warning:: + + An SMTP failure will be only logged without raising any exception. + +.. method:: sendmail(from_addr, to_addrs, msg[, server]) + +Send email message like :meth:`sendmail_transactional` but directly without +caring about the transaction. +The caller may pass a server instance from `smtplib`_. + +.. method:: get_smtp_server([uri[, strict]]) + +Return a SMTP instance from `smtplib`_ using the ``uri`` or the one defined in +the ``email`` section of the :ref:`configuration `. +If strict is ``True``, an exception is raised if it is not possible to connect +to the server. + + +.. class:: SMTPDataManager([uri[, strict]]) + +A :class:`SMTPDataManager` implements a data manager which send queued email at +commit. An option optional ``uri`` can be passed to configure the SMTP +connection. +If strict is ``True``, the data manager prevents the transaction if it fails to +send the emails. + +.. method:: SMTPDataManager.put(from_addr, to_addrs, msg) + + Queue the email message to send. + +.. _`RFC 822`: https://tools.ietf.org/html/rfc822.html +.. _`smtplib`: https://docs.python.org/2/library/smtplib.html diff --git a/trytond/doc/ref/tests.rst b/trytond/doc/ref/tests.rst new file mode 100644 index 0000000000..c1c5fd719e --- /dev/null +++ b/trytond/doc/ref/tests.rst @@ -0,0 +1,104 @@ +.. _ref-test: +.. module:: trytond.tests.test_tryton + +==== +Test +==== + +.. attribute:: DB_NAME + + The name of the database to use for testing. Its value is taken from the + environment variable of the same name. + +.. attribute:: USER + + The user id used to test the transactions + +.. attribute:: CONTEXT + + The context used to test the transactions + +.. function:: activate_module(name) + +Activates the module ``name`` for the tested database. + +In case database does not exist and the ``DB_CACHE`` environment variable is +set then Tryton will restore a backup found in the directory pointed by +``DB_CACHE``. +Otherwise it will proceed to the creation of the database and the activation of +the module. + +============== +ModuleTestCase +============== + +.. class:: ModuleTestCase() + +A subclass of `unittest.TestCase`_ that tests a Tryton module. Some tests are +included to ensure that the module works properly. + +This class creates a temporary database with the module activated in +`setUpClass`_ and drops it in the ``tearDownClass`` method. + +.. attribute:: ModuleTestCase.module + + Name of the tested module. + +.. _`unittest.TestCase`: https://docs.python.org/library/unittest.html#test-cases +.. _setUpClass: https://docs.python.org/library/unittest.html#unittest.TestCase.setUpClass +.. _tearDownClass: https://docs.python.org/library/unittest.html#unittest.TestCase.tearDownClass + +======= +Helpers +======= + +.. function:: with_transaction(user=1, context=None) + +This function returns a decorator to run a test case inside a transaction. The +transaction is rolled back and the cache cleared at the end of the test. + +doctest helpers +--------------- + +.. function:: doctest_setup + +A function that prepares the run of the `doctest`_ by creating a database and +dropping it beforehand if necessary. This function should be used as the +``setUp`` parameter + +.. deprecated:: + The ``doctest_setup`` function should not be used anymore to set up + ``DocFileSuite``. New modules should use :func:`activate_modules` instead. + +.. _doctest: https://docs.python.org/library/doctest.html + +.. function:: doctest_teardown() + +A function that cleans up after the run of the doctest by dropping the +database. It should be used as ``tearDown`` parameter when creating a +``DocFileSuite``. + +.. attribute:: doctest_checker + + A specialized doctest checker to ensure the Python 2/3 compatibility + +.. function:: suite() + +A function returning a subclass of ``unittest.TestSuite`` that will drop the +database if it does not exist prior to the run of the tests. + +.. module:: trytond.tests.tools + +=========== +Tests tools +=========== + +.. function:: activate_modules(modules) + +This function is used in proteus doctests to activate a list of ``modules`` in +the scenario. + +.. function:: set_user(user, config) + +This function will set the user of the ``config`` proteus connection to +``user``. diff --git a/trytond/doc/ref/tools/index.rst b/trytond/doc/ref/tools/index.rst new file mode 100644 index 0000000000..cbf985e9a0 --- /dev/null +++ b/trytond/doc/ref/tools/index.rst @@ -0,0 +1,13 @@ +.. _ref-tools-index: + +===== +Tools +===== + +Tools API reference. + +.. toctree:: + :maxdepth: 1 + + misc + singleton diff --git a/trytond/doc/ref/tools/misc.rst b/trytond/doc/ref/tools/misc.rst new file mode 100644 index 0000000000..ad08770b0d --- /dev/null +++ b/trytond/doc/ref/tools/misc.rst @@ -0,0 +1,28 @@ +.. _ref-tools: +.. module:: trytond.tools + +============= +Miscellaneous +============= + +.. method:: resolve(name) + +Resolve a dotted name to a global object. + +.. method:: unescape_wildcard(string[, wildcards[, escape]]) + +Return the string without the wild card escapes. + +.. method:: is_full_text(value[, escape]) + +Determine if the value can be used as full text search. +This is the case when the value starts and ends with a ``%`` or does not +contain any wild cards. + +.. method:: sql_pairing(x, y) + +Return an SQL expression that pairs SQL integers x and y. + +.. method:: firstline(text) + +Return first non-empty line of a text field. diff --git a/trytond/doc/ref/tools/singleton.rst b/trytond/doc/ref/tools/singleton.rst new file mode 100644 index 0000000000..12693fa2b1 --- /dev/null +++ b/trytond/doc/ref/tools/singleton.rst @@ -0,0 +1,14 @@ +.. _ref-tools-singleton: +.. module:: trytond.tools.singleton + + +========= +Singleton +========= + + +.. class:: Singleton + +A class to use as metaclass to create a `singleton`_ object. + +.. _`singleton`: http://en.wikipedia.org/wiki/Singleton_pattern diff --git a/trytond/doc/ref/transaction.rst b/trytond/doc/ref/transaction.rst new file mode 100644 index 0000000000..1a234e56cf --- /dev/null +++ b/trytond/doc/ref/transaction.rst @@ -0,0 +1,125 @@ +.. _ref-transaction: +.. module:: trytond.transaction + +=========== +Transaction +=========== + +.. class:: Transaction + +This class represents a Tryton transaction that contains thread-local +parameters of a database connection. The Transaction instances are +`context manager`_ that will commit or rollback the database transaction. In +the event of an exception the transaction is rolled back, otherwise it is +commited. + +.. attribute:: Transaction.database + + The database. + +.. attribute:: Transaction.readonly + +.. attribute:: Transaction.connection + + The database connection as defined by the `PEP-0249`_. + +.. attribute:: Transaction.user + + The id of the user. + +.. attribute:: Transaction.context + +.. attribute:: Transaction.create_records + +.. attribute:: Transaction.delete_records + +.. attribute:: Transaction.delete + +.. attribute:: Transaction.trigger_records + +.. attribute:: Transaction.timestamp + +.. attribute:: Transaction.started_at + + The monotonic timestamp when the transaction started. + +.. attribute:: Transaction.language + + The language code defines in the context. + +.. attribute:: Transaction.counter + + Count the number of modification made in this transaction. + +.. staticmethod:: Transaction.monotonic_time + + Return a monotonic time used to populate :attr:~Transaction.started_at. + +.. method:: Transaction.start(database_name, user[, readonly[, context[, close[, autocommit]]]]) + + Start a new transaction and return a `context manager`_. The non-readonly + transaction will be committed when exiting the ``with`` statement without + exception. The other cases will be rollbacked. + +.. method:: Transaction.stop([commit]) + + Stop the transaction. If commit is True, the transaction will be committed + otherwise it will be rollbacked. + The `context manager`_ returned by :meth:`Transaction.start` should be used + instead of calling this method. + +.. method:: Transaction.set_context(context, \**kwargs) + + Update the transaction context and return a `context manager`_. The context + will be restored when exiting the ``with`` statement. + +.. method:: Transaction.reset_context() + + Clear the transaction context and return a `context manager`_. The context + will be restored when exiting the ``with`` statement. + +.. method:: Transaction.set_user(user[, set_context]) + + Modify the user of the transaction and return a `context manager`_. + ``set_context`` will put the previous user id in the context to simulate the + record rules. The user will be restored when exiting the ``with`` statement. + +.. method:: Transaction.set_current_transaction(transaction) + + Add a specific ``transaction`` on the top of the transaction stack. A + transaction is commited or rollbacked only when its last reference is + popped from the stack. + +.. method:: Transaction.new_transaction([autocommit[, readonly]]) + + Create a new transaction with the same database, user and context as the + original transaction and adds it to the stack of transactions. + +.. method:: Transaction.commit() + + Commit the transaction and all data managers associated. + +.. method:: Transaction.rollback() + + Rollback the transaction and all data managers associated. + +.. method:: Transaction.join(datamanager) + + Register in the transaction a data manager conforming to the `Two-Phase + Commit protocol`_. More information on how to implement such data manager + is available at the `Zope documentation`_. + + This method returns the registered datamanager. It could be a different yet + equivalent (in term of python equality) datamanager than the one passed to the + method. + +.. method:: Transaction.atexit(func, \*args, \*\*kwargs) + + Register a function to be executed upon normal transaction termination. + The function can not use the current transaction because it will be already + committed or rollbacked. + +.. _`context manager`: http://docs.python.org/reference/datamodel.html#context-managers +.. _`PEP-0249`: https://www.python.org/dev/peps/pep-0249/ +.. _`Two-Phase Commit protocol`: https://en.wikipedia.org/wiki/Two-phase_commit_protocol +.. _`Zope documentation`: http://zodb.readthedocs.org/en/latest/transactions.html#the-two-phase-commit-protocol-in-practice diff --git a/trytond/doc/ref/wizard.rst b/trytond/doc/ref/wizard.rst new file mode 100644 index 0000000000..c252815a5d --- /dev/null +++ b/trytond/doc/ref/wizard.rst @@ -0,0 +1,233 @@ +.. _ref-wizard: +.. module:: trytond.wizard + +====== +Wizard +====== + +A wizard is a `finite state machine`_. + +There is also a more :ref:`practical introduction into wizards +`. + +.. _`finite state machine`: http://en.wikipedia.org/wiki/Finite-state_machine + +.. class:: Wizard(session_id) + + This is the base for any wizard. It contains the engine for the finite + state machine. A wizard must have some :class:`State` instance attributes + that the engine will use. + +Class attributes are: + +.. attribute:: Wizard.__name__ + + It contains the unique name to reference the wizard throughout the + platform. + +.. attribute:: Wizard.start_state + + It contains the name of the starting state. + +.. attribute:: Wizard.end_state + + It contains the name of the ending state. + If an instance method with this name exists on the wizard, it will be + called on deletion of the wizard and it may return one of the :ref:`client + side action keywords `. + +.. attribute:: Wizard.__rpc__ + + Same as :attr:`trytond.model.Model.__rpc__`. + +.. attribute:: Wizard.states + + It contains a dictionary with state name as key and :class:`State` as + value. + +.. attribute:: model + + It contains the :class:`~trytond.model.Model` class on which the wizard is + executed. + +.. attribute:: record + + It contains the :class:`~trytond.model.Model` instance on which the wizard + is executed. + +.. attribute:: records + + It contains the list of :class:`~trytond.model.Model` instances on which + the wizard is executed. + +Class methods are: + +.. classmethod:: Wizard.__setup__() + + Setup the class before adding into the :class:`trytond.pool.Pool`. + +.. classmethod:: Wizard.__post_setup__() + + Setup the class after added into the :class:`trytond.pool.Pool`. + +.. classmethod:: Wizard.__register__(module_name) + + Register the wizard. + +.. classmethod:: Wizard.create() + + Create a session for the wizard and returns a tuple containing the session + id, the starting and ending state. + +.. classmethod:: Wizard.delete(session_id) + + Delete the session. + +.. classmethod:: Wizard.execute(session_id, data, state_name) + + Execute the wizard for the state. + ``session_id`` is a session id. + ``data`` is a dictionary with the session data to update. + ``active_id``, ``active_ids``, ``active_model`` and ``action_id`` must be + set in the context according to the records on which the wizard is run. + +===== +State +===== + +.. class:: State() + + This is the base for any wizard state. + +Instance attributes are: + +.. attribute:: State.name + + The name of the state. + +========= +StateView +========= + +.. class:: StateView(model_name, view, buttons) + + A :class:`StateView` is a state that will display a form in the client. + The form is defined by the :class:`~trytond.model.ModelView` with the name + ``model_name``, the ``XML`` id in ``view`` and the ``buttons``. + The default value of the view can be set with a method on wizard having the + same name as the state but starting with ``default_``. + +Instance attributes are: + +.. attribute:: StateView.model_name + + The name of the :class:`~trytond.model.ModelView`. + +.. attribute:: StateView.view + + The ``XML`` id of the form view. + +.. attribute:: StateView.buttons + + The list of :class:`Button` instances to display on the form. + +Instance methods are: + +.. method:: StateView.get_view(wizard, state_name) + + Returns the view definition like + :meth:`~trytond.model.ModelView.fields_view_get`. + + * wizard is a :class:`Wizard` instance + * state_name is the name of the :class:`StateView` instance + +.. method:: StateView.get_defaults(wizard, state_name, fields) + + Return default values for the fields. + + * wizard is a :class:`Wizard` instance + * state_name is the name of the :class:`State` + * fields is the list of field names + +.. method:: StateView.get_buttons(wizard, state_name) + + Returns button definitions of the wizard. + + * wizard is a :class:`Wizard` instance + * state_name is the name of the :class:`StateView` instance + +=============== +StateTransition +=============== + +.. class:: StateTransition() + + A :class:`StateTransition` brings the wizard to the ``state`` returned by + the method having the same name as the state but starting with + ``transition_``. + +=========== +StateAction +=========== + +.. class:: StateAction(action_id) + + A :class:`StateAction` is a :class:`StateTransition` which let the client + launch an ``ir.action``. This action definition can be customized with a + method on wizard having the same name as the state but starting with + ``do_``. + +Instance attributes are: + +.. attribute:: StateAction.action_id + + The ``XML`` id of the ``ir.action``. + +Instance methods are: + +.. method:: StateAction.get_action() + + Returns the ``ir.action`` definition. + +=========== +StateReport +=========== + +.. class:: StateReport(report_name) + + A :class:`StateReport` is a :class:`StateAction` which find the report + action by name instead of ``XML`` id. + +====== +Button +====== + +.. class:: Button(string, state[, icon[, default[, validate]]]) + + A :class:`Button` is a single object containing the definition of a wizard + button. + +Instance attributes are: + +.. attribute:: Button.string + + The label display on the button. + +.. attribute:: Button.state + + The next state to reach if button is clicked. + +.. attribute:: Button.icon + + The name of the icon to display on the button. + +.. attribute:: Button.default + + A boolean to set it as default on the form. + +.. attribute:: Button.validate + + A boolean or None. + If True, validation of the form will occur, if False it won't. If the value + is None the validation will occur only if the state of the button is not the + wizard ending state. diff --git a/trytond/doc/topics/access_rights.rst b/trytond/doc/topics/access_rights.rst new file mode 100644 index 0000000000..201a385c64 --- /dev/null +++ b/trytond/doc/topics/access_rights.rst @@ -0,0 +1,108 @@ +.. _topics-access_rights: + +============= +Access Rights +============= + +There are 5 levels of access rights: `Model`_ , `Actions`_, `Field`_, `Button`_ +and `Record Rule`_. They are based on the user's group membership. +If any of those levels are violated, an error is raised. + +The access rights are checked if the :attr:`Transaction.context +` has the key ``_check_access`` set to +``True`` (set by default by :attr:`RPC.check_access +`) and if the +:attr:`~trytond.transaction.Transaction.user` is not ``root``. + +Model +===== + +They are defined by records of ``ir.model.access`` which define for each couple +of model and group, the ``read``, ``write``, ``create`` and ``delete`` +permission. The permissions are related to the +:class:`~trytond.model.ModelStorage` methods with the same name and on +:meth:`~trytond.model.ModelStorage.search` using the ``read`` permission. + +If any group the user belongs to has the checked permission activated, then the +user is granted this permission. + +If there is no record for the model, then access is granted to all users. + +.. note:: + Relation fields for which the user has no read access are automatically + removed from the :ref:`views `. + +Actions +======= + +Each ``ir.action`` has a ``groups`` field which contains a list of user groups +that are allowed to see and launch it. + +There is a special case for :ref:`wizard ` for which the read +access on the model is also checked and also the write access if there is no +groups linked. + +Field +===== + +They are defined by records of ``ir.model.field.access`` and work like those +for `Model`_ but are applied to :ref:`fields `. + +.. note:: + Fields for which the user has no read access are automatically removed from + the :ref:`views `. + +Button +====== + +For each button of a model the records of ``ir.model.button`` define the list of +groups that are allowed to call it. The user only needs to belong to one of the +groups to be granted the permission to use it. + +If no group is defined for a button, the ``write`` permission to the model is +checked instead. + +The ``read`` permission to the model is always enforced. + +.. note:: + Buttons for which the user has no access are marked readonly. + +Button Rule +----------- + +The ``ir.model.button`` can contain a list of rules which define how many +different users must click on the button. Each rule, for which the condition +is met, must be passed to actually trigger the action. The counter can be reset +when another defined button is clicked. + +Record Rule +=========== + +The record rules are conditions that records must meet for the user to be +granted permission to use them. +They are defined by records of ``ir.rule.group`` which contains: + + - a model on which it applies + - the permissions granted + - a set of user groups to which the rule applies + - a global flag to always enforce + - a default flag to add to all users + - a list of ``ir.rule`` with a :ref:`domain ` to select the + records to which the rule applies. + +A rule group matches a record if the record is validated by at least one of the +domains. +The access is granted to a record: + + - if the user belongs to a group which has at least one matching rule group + that has the permission, + + - or if there is a default matching rule group with the permission, + + - or if there is a global matching rule group with the permission. + +Otherwise the access is denied if there is any matching rule group. + +.. note:: + Records for which the user has no ``read`` access are filtered out from the + :meth:`~trytond.model.ModelStorage.search` result. diff --git a/trytond/doc/topics/actions.rst b/trytond/doc/topics/actions.rst new file mode 100644 index 0000000000..0e4143cba7 --- /dev/null +++ b/trytond/doc/topics/actions.rst @@ -0,0 +1,71 @@ +.. _topics-actions: + +======= +Actions +======= + +Actions are used to describe specific behaviors in the client. + +There are four types of actions: + + * Report + + * Window + + * Wizard + + * URL + + +Keyword +------- + +Keywords define where to display the action in the client. + +There are five places: + + * Open tree (``tree_open``) + + * Print form (``form_print``) + + * Action form (``form_action``) + + * Form relate (``form_relate``) + + * Open Graph (``graph_open``) + +Report +====== + +.. TODO + +Window +====== + +The window action describe how to create a new tab in the client. + +View +---- + +.. TODO + +Domain +------ + +The window action could have a list of domains which could be activated on the +view. The boolean field count indicates if the client must display the number +of records for this domain. + +.. warning:: + The counting option must be activated only on domains which have not too + much records otherwise it may overload the database. + +Wizard +====== + +.. TODO + +URL +=== + +.. TODO diff --git a/trytond/doc/topics/backend_types.rst b/trytond/doc/topics/backend_types.rst new file mode 100644 index 0000000000..067cfd510b --- /dev/null +++ b/trytond/doc/topics/backend_types.rst @@ -0,0 +1,68 @@ +.. _topics-backend_types: + +Backend Types supported +======================= + +This table give a comprehensive list of the SQL Types that are expected to be +supported by the database backends. If the type is not supported then the +backend will have to emulate the behavior described here. + +The columns are in the following order: + +* The SQL type [#]_ representing the field +* The python type expected on input +* The python type received on output + +.. [#] Corresponding to the `SQL 92`_ standard or to a `PostgreSQL type`_. +.. _`SQL 92`: http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt +.. _`PostgreSQL type`: https://www.postgresql.org/docs/current/static/datatype.html + +``None`` will represent the ``NULL`` value and vice versa, it can be used as +input or output for any SQL type. + ++----------------------+----------------------+----------------------+ +| SQL Type | Python input type | Python output type | ++======================+======================+======================+ +| ``BOOL`` | bool | bool | ++----------------------+----------------------+----------------------+ +| ``INTEGER`` | int | int | ++----------------------+----------------------+----------------------+ +| ``BIGINT`` | int / long | int / long | +| | [#pyver_int]_ | [#pyver_int]_ | ++----------------------+----------------------+----------------------+ +| ``FLOAT`` | float / int / long | float | +| | [#pyver_int]_ | | ++----------------------+----------------------+----------------------+ +| ``NUMERIC`` | decimal.Decimal_ | decimal.Decimal_ | ++----------------------+----------------------+----------------------+ +| ``VARCHAR`` / | str / unicode | str / unicode | +| ``VARCHAR(length)`` | [#pyver_str]_ | [#pyver_str]_ | ++----------------------+----------------------+----------------------+ +| ``TEXT`` | str / unicode | str / unicode | +| | [#pyver_str]_ | [#pyver_str]_ | ++----------------------+----------------------+----------------------+ +| ``TIMESTAMP`` | datetime.datetime_ | datetime.datetime_ | ++----------------------+----------------------+----------------------+ +| ``DATETIME`` | datetime.datetime_ | datetime.datetime_ | +| | without microseconds | without microseconds | +| | [#utc_tz]_ | [#utc_tz]_ | ++----------------------+----------------------+----------------------+ +| ``DATE`` | datetime.date_ | datetime.date_ | ++----------------------+----------------------+----------------------+ +| ``TIME`` | datetime.time_ | datetime.time_ | ++----------------------+----------------------+----------------------+ +| ``INTERVAL`` | datetime.timedelta_ | datetime.timedelta_ | ++----------------------+----------------------+----------------------+ +| ``BLOB`` | bytes | bytes | ++----------------------+----------------------+----------------------+ + +.. [#pyver_int] in python 2 integers over *sys.maxint* are represented by the + ``long`` type +.. [#pyver_str] str when using python 3 ; unicode when using python 2 +.. [#utc_tz] Datetime objects are not localized to any timezone + +.. _datetime.date: https://docs.python.org/library/datetime.html#date-objects +.. _datetime.datetime: https://docs.python.org/library/datetime.html#datetime-objects +.. _datetime.time: https://docs.python.org/library/datetime.html#time-objects +.. _datetime.timedelta: https://docs.python.org/library/datetime.html#timedelta-objects +.. _decimal.Decimal: https://docs.python.org/library/decimal.html#decimal-objects diff --git a/trytond/doc/topics/bus.rst b/trytond/doc/topics/bus.rst new file mode 100644 index 0000000000..86c48f5f58 --- /dev/null +++ b/trytond/doc/topics/bus.rst @@ -0,0 +1,28 @@ +.. _topics-notification: + +===================== +Sending notifications +===================== + +Tryton embeds a bus system allowing the system to send text messages to clients +logged in the system. + +It allows the server to warn quickly the client user about some events using +the :meth:`trytond.bus.notify` function. Sending the notifications is done in a +transactional way and will occur at then end of the transaction. + +For example, we warn the user of low stock level when selecting a product:: + + from trytond.bus import notify + + class SaleLine: + __name__ = 'sale.line' + + def on_change_product(self): + super().on_change_product() + + # compute the product current stock + stock = … + + if stock < 0: + notify('Not enough stock', priority=3) diff --git a/trytond/doc/topics/configuration.rst b/trytond/doc/topics/configuration.rst new file mode 100644 index 0000000000..41a87a8acc --- /dev/null +++ b/trytond/doc/topics/configuration.rst @@ -0,0 +1,563 @@ +.. _topics-configuration: + +============================= +Configuration file for Tryton +============================= + +The configuration file controls some aspects of the behavior of Tryton. +The file uses a simple ini-file format. It consists of sections, led by a +``[section]`` header and followed by ``name = value`` entries: + +.. highlight:: ini + +:: + + [database] + uri = postgresql://user:password@localhost/ + path = /var/lib/trytond + +For more information see ConfigParser_. + +.. _ConfigParser: http://docs.python.org/2/library/configparser.html + +The default value of any option can be changed using environment variables +with names using this syntax: ``TRYTOND_
__``. + +Sections +======== + +This section describes the different main sections that may appear in a Tryton +configuration file, the purpose of each section, its possible keys, and their +possible values. +Some modules could request the usage of other sections for which the guideline +asks them to be named like their module. + +web +--- + +Defines the behavior of the web interface. + +listen +~~~~~~ + +Defines the couple of host (or IP address) and port number separated by a colon +to listen on. + +Default ``localhost:8000`` + +hostname +~~~~~~~~ + +Defines the hostname to use when generating a URL when there is no request +context available, for example during a cron job. + +root +~~~~ + +Defines the root path served by ``GET`` requests. + +Default: Under the ``www`` directory of user's home running ``trytond``. + +num_proxies +~~~~~~~~~~~ + +The number of proxy servers in front of ``trytond``. + +Default: 0 + +cache_timeout +~~~~~~~~~~~~~ + +The cache timeout in seconds. + +Default: 12h + +cors +~~~~ + +The list (one per line) of origins allowed for `Cross-Origin Resource sharing +`_. + +avatar_base +~~~~~~~~~~~ + +The base URL without a path for avatar URL. + +Default: ``''`` + +.. note:: It can be used to setup a CDN. + + +avatar_timeout +~~~~~~~~~~~~~~ + +The time in seconds that the avatar can be stored in cache. + +Default: 7 days + +database +-------- + +Defines how the database is managed. + +uri +~~~ + +Contains the URI to connect to the SQL database. The URI follows the RFC-3986_. +The typical form is: + + database://username:password@host:port/?param1=value1¶m2=value2 + +The parameters are database dependent, check the database documentation for a +list of valid parameters. + +Default: The value of the environment variable ``TRYTOND_DATABASE_URI`` or +``sqlite://`` if not set. + +The available databases are: + +PostgreSQL +********** + +``psycopg2`` supports two type of connections: + + - TCP/IP connection: ``postgresql://user:password@localhost:5432/`` + - Unix domain connection: ``postgresql://username:password@/`` + +Please refer to `psycopg2 for the complete specification of the URI +`_. + +A list of parameters supported by PostgreSQL can be found in the +`documentation `_. + +SQLite +****** + +The URI is defined as ``sqlite://`` + +If the name of the database is ``:memory:``, the parameter ``mode`` will be set +to ``memory`` thus using a pure in-memory database. + +The recognized query parameters can be found in SQLite's +`documentation `_. + +path +~~~~ + +The directory where Tryton stores files and so the user running ``trytond`` +must have write access on this directory. + +Default: The ``db`` folder under the user home directory running ``trytond``. + +list +~~~~ + +A boolean value to list available databases. + +Default: ``True`` + +retry +~~~~~ + +The number of retries when a database operational error occurs during a request. + +Default: ``5`` + +language +~~~~~~~~ + +The main language of the database that will be used for storage in the main +table for translations. + +Default: ``en`` + +avatar_filestore +~~~~~~~~~~~~~~~~ + +This configuration value indicates whether the avatars should be stored in the +:py:mod:`trytond.filestore` (``True``) or the database (``False``). + +Default: ``False`` + +avatar_prefix +~~~~~~~~~~~~~ + +The prefix to use with the :ref:`FileStore ` to store avatars. + +Default: ``None`` + +default_name +~~~~~~~~~~~~ + +The name of the database to use for operations without a database name. +Default: ``template1`` for PostgreSQL, ``:memory:`` for SQLite. + +request +------- + +max_size +~~~~~~~~ + +The maximum size in bytes of unauthenticated request (zero means no limit). + +Default: 2MB + +max_size_authenticated +~~~~~~~~~~~~~~~~~~~~~~ + +The maximum size in bytes of an authenticated request (zero means no limit). + +Default: 2GB + + +cache +----- + +Defines size of various cache. + +model +~~~~~ + +The number of different model kept in the cache per transaction. + +Default: ``200`` + +record +~~~~~~ + +The number of record loaded kept in the cache of the list. +It can be changed locally using the ``_record_cache_size`` key in +:attr:`Transaction.context`. + +Default: ``2000`` + +field +~~~~~ + +The number of field to load with an ``eager`` :attr:`Field.loading`. + +Default: ``100`` + +clean_timeout +~~~~~~~~~~~~~ + +The minimum number of seconds between two cleanings of the cache. +If the value is 0, the notification between processes will be done using +channels if the back-end supports them. + +Default: ``300`` + +queue +----- + +worker +~~~~~~ + +Activate asynchronous processing of the tasks. Otherwise they are performed at +the end of the requests. + +Default: ``False`` + +clean_days +~~~~~~~~~~ + +The number of days after which processed tasks are removed. + +Default: ``30`` + +table +----- + +This section allows to override the default generated table name for a +:class:`ModelSQL`. The main goal is to bypass limitation on the name length of +the database backend. +For example:: + + [table] + account.invoice.line = acc_inv_line + account.invoice.tax = acc_inv_tax + +ssl +--- + +Activates SSL_ on the web interface. + +.. note:: It is recommended to delegate the SSL support to a proxy. + +privatekey +~~~~~~~~~~ + +The path to the private key. + +certificate +~~~~~~~~~~~ + +The path to the certificate. + +.. tip:: + Set only one of ``privatekey`` or ``certificate`` to ``true`` if the SSL is + delegated. + +email +----- + +.. note:: Email settings can be tested with the ``trytond-admin`` command + +uri +~~~ + +The SMTP-URL_ to connect to the SMTP server which is extended to support SSL_ +and STARTTLS_. +The available protocols are: + + - ``smtp``: simple SMTP + - ``smtp+tls``: SMTP with STARTTLS + - ``smtps``: SMTP with SSL + +The uri accepts the following additional parameters: + +* ``local_hostname``: used as FQDN of the local host in the HELO/EHLO commands, + if omited it will use the value of ``socket.getfqdn()``. +* ``timeout``: A number of seconds used as timeout for blocking operations. A + ``socket.timeout`` will be raised when exceeded. If omited the default timeout + will be used. + + +Default: ``smtp://localhost:25`` + +from +~~~~ + +Defines the default ``From`` address (using RFC-822_) for emails sent by Tryton. + +For example:: + + from: "Company Inc" + +session +------- + +authentications +~~~~~~~~~~~~~~~ + +A comma separated list of the authentication methods to try when attempting to +verify a user's identity. Each method is tried in turn, following the order of +the list, until one succeeds. In order to allow `multi-factor authentication`_, +individual methods can be combined together using a plus (``+``) symbol. + +Example:: + + authentications = password+sms,ldap + +By default, Tryton only supports the ``password`` method. This method compares +the password entered by the user against a stored hash of the user's password. +Other modules can define additional authentication methods, please refer to +their documentation for more information. + +Default: ``password`` + +max_age +~~~~~~~ + +The time in seconds that a session stay valid. + +Default: ``2592000`` (30 days) + +timeout +~~~~~~~ + +The time in seconds without activity before the session is no more fresh. + +Default: ``300`` (5 minutes) + +max_attempt +~~~~~~~~~~~ + +The maximum authentication attempt before the server answers unconditionally +``Too Many Requests`` for any other attempts. The counting is done on all +attempts over a period of ``timeout``. + +Default: ``5`` + +max_attempt_ip_network +~~~~~~~~~~~~~~~~~~~~~~ + +The maximum authentication attempt from the same network before the server +answers unconditionally ``Too Many Requests`` for any other attempts. The +counting is done on all attempts over a period of ``timeout``. + +Default: ``300`` + +ip_network_4 +~~~~~~~~~~~~ + +The network prefix to apply on IPv4 address for counting the authentication +attempts. + +Default: ``32`` + +ip_network_6 +~~~~~~~~~~~~ + +The network prefix to apply on IPv6 address for counting the authentication +attempts. + +Default: ``56`` + +password +-------- + +length +~~~~~~ + +The minimal length required for the user password. + +Default: ``8`` + +forbidden +~~~~~~~~~ + +The path to a file containing one forbidden password per line. + +entropy +~~~~~~~ + +The ratio of non repeated characters for the user password. + +Default: ``0.75`` + +reset_timeout +~~~~~~~~~~~~~ + +The time in seconds until the reset password expires. + +Default: ``86400`` (24h) + +passlib +~~~~~~~ + +The path to the `INI file to load as CryptContext +`_. +If not path is set, Tryton will use the schemes ``bcrypt`` or ``pbkdf2_sha512``. + +Default: ``None`` + +attachment +---------- + +Defines how to store the attachments + +filestore +~~~~~~~~~ + +A boolean value to store attachment in the :ref:`FileStore `. + +Default: ``True`` + +store_prefix +~~~~~~~~~~~~ + +The prefix to use with the ``FileStore``. + +Default: ``None`` + +bus +--- + +allow_subscribe +~~~~~~~~~~~~~~~ + +A boolean value to allow clients to subscribe to bus channels. + +Default: ``False`` + +url_host +~~~~~~~~ + +If set redirects bus requests to the host URL. + +long_polling_timeout +~~~~~~~~~~~~~~~~~~~~ + +The time in seconds to keep the connection to the client opened when using long +polling for bus messages + +Default: ``300`` + +cache_timeout +~~~~~~~~~~~~~ + +The number of seconds a message should be kept by the queue before being +discarded. + +Default: ``300`` + +select_timeout +~~~~~~~~~~~~~~ + +The timeout duration of the select call when listening on a channel. + +Default: ``5`` + +html +---- + +src +~~~ + +The URL pointing to `TinyMCE `_ editor. + +Default: ``https://cloud.tinymce.com/stable/tinymce.min.js`` + +plugins +~~~~~~~ + +The space separated list of TinyMCE plugins to load. +It can be overridden for specific models and fields using the names: +``plugins--`` or ``plugins-``. + +Default: `` + +css +~~~ + +The JSON list of CSS files to load. +It can be overridden for specific models and fields using the names: +``css--`` or ``css-``. + +Default: ``[]`` + +class +~~~~~ + +The class to add on the body. +It can be overridden for specific models and fields using the names: +``class--`` or ``class-``. + +Default: ``''`` + +wsgi middleware +--------------- + +The section lists the `WSGI middleware`_ class to load. +Each middleware can be configured with a section named ``wsgi `` +containing ``args`` and ``kwargs`` options. + +Example:: + + [wsgi middleware] + ie = werkzeug.contrib.fixers.InternetExplorerFix + + [wsgi ie] + kwargs={'fix_attach': False} + + +.. _JSON-RPC: http://en.wikipedia.org/wiki/JSON-RPC +.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC +.. _RFC-3986: http://tools.ietf.org/html/rfc3986 +.. _SMTP-URL: http://tools.ietf.org/html/draft-earhart-url-smtp-00 +.. _RFC-822: https://tools.ietf.org/html/rfc822 +.. _SSL: http://en.wikipedia.org/wiki/Secure_Sockets_Layer +.. _STARTTLS: http://en.wikipedia.org/wiki/STARTTLS +.. _WSGI middleware: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface#Specification_overview +.. _`multi-factor authentication`: https://en.wikipedia.org/wiki/Multi-factor_authentication diff --git a/trytond/doc/topics/cron.rst b/trytond/doc/topics/cron.rst new file mode 100644 index 0000000000..e7b65acaf9 --- /dev/null +++ b/trytond/doc/topics/cron.rst @@ -0,0 +1,47 @@ +.. _topics-cron: + +================= +Scheduled Actions +================= + +Tryton provides a scheduler (aka cron) which can execute methods of +:ref:`models ` periodically at set intervals. + +The planning is managed by ``ir.cron`` records which store the method to call +and the interval of time between calls. The method must be a class method of a +:class:`~trytond.model.Model` which can be called without any parameters. + +To register a new method with the scheduler, you must extend the ``ir.cron`` +model and append the new method to the +:attr:`~trytond.model.fields.Selection.selection` attribute of the ``method`` +field in :meth:`~trytond.model.Model.__setup__()`. The name of the selection +must be the model name and the method name joined together with a ``|`` between +them. + +Example: + +.. highlight:: python + +:: + + from trytond.model import Model + from trytond.pool import PoolMeta + + class Cron(metaclass=PoolMeta): + __name__ = 'ir.cron' + + @classmethod + def __setup__(cls): + super().__setup__() + cls.method.selection.append( + ('my_model|my_method', "Run my method"), + ) + + + class MyModel(Model): + "My Model" + __name__ = 'my_model' + + @classmethod + def my_method(cls): + pass diff --git a/trytond/doc/topics/domain.rst b/trytond/doc/topics/domain.rst new file mode 100644 index 0000000000..096a3ddbad --- /dev/null +++ b/trytond/doc/topics/domain.rst @@ -0,0 +1,264 @@ +.. _topics-domain: + + +====== +Domain +====== + +Domains_ represent a set of records. A domain is a list of none or +more clauses. A clause is a condition, which returns true or false. +A record belongs to a domain, when the final result of the list of +clauses returns true. + +.. _Domains: http://en.wikipedia.org/wiki/Data_domain + + +Syntax +====== + +The definition of a simple domain with one clause is represented +by this pattern:: + + domain = [(, , )] + +```` + Is the name of a :mod:`~trytond.model.fields` or a + :ref:`pyson ` statement, that evaluates to a + string. + + A field of type :class:`~trytond.model.fields.Many2One` or + :class:`~trytond.model.fields.Many2Many` or + :class:`~trytond.model.fields.One2Many` or + :class:`~trytond.model.fields.One2One` or + :class:`~trytond.model.fields.Reference` can be dereferenced to related + models. This is illustrated by the following example:: + + domain = [('country.name', '=', 'Japan')] + + The number of *dots* in a clause is not limited. + +.. warning:: + For :class:`trytond.model.fields.Reference`, an extra ending clause is + needed to define the target model to join, for example:: + + domain = [('origin.party.name', '=', 'John Doe', 'sale.sale')] +.. + + A field of type :class:`~trytond.model.fields.Dict` can be searched by key + also by using one *dot*. For example:: + + domain = [('attributes.color', '=', 'yellow')] + +.. warning:: + Order comparison of ``date`` and ``datetime`` types is not supported. + +``operator`` + Is an operator out of `Domain Operators`_ or a + :ref:`pyson ` statement, that evaluates to + a domain operator string. + +``operand`` + Is an operand or a :ref:`pyson ` statement. The + type of operand depends on the kind of . + +The definition of an empty domain is:: + + domain = [] + +An empty domain without clauses will always return all *active* +records. A record is active, when its appropriate +:class:`~trytond.model.Model` contains a +:class:`~trytond.model.fields.Boolean` field with name ``active``, +and set to true. When the appropriate :class:`~trytond.model.Model` +does not contain a :class:`~trytond.model.fields.Boolean` field with +name ``active`` all records are returned. + +A domain can be setup as a combination of clauses, like shown in +this pattern:: + + domain = [ + ('field name1', 'operator1', 'operand1'), + ('field name2', 'operator2', 'operand2'), + ('field name3', 'operator3', 'operand3'),] + +The single clauses are implicitly combined with a logical +AND_ operation. + + +In the domain syntax it is possible to provide explicitly the +combination operation of the clauses. These operations can be AND_ +or OR_. This is illustrated by the following pattern:: + + domain = [ 'OR', [ + ('field name1', 'operator1', 'operand1'), + ('field name2', 'operator2', 'operand2'), + ], [ + ('field name3', 'operator3', 'operand3'), + ],] + +.. _AND: http://en.wikipedia.org/wiki/Logical_and +.. _OR: http://en.wikipedia.org/wiki/Logical_or + + +Here the domain is evaluated like this: ``((clause1 AND clause2) +OR clause3)``. Please note that the ``AND`` operation is implicit +assumed when no operator is given. While the ``OR`` operation must +be given explicitly. The former pattern is equivalent to the +following completely explicit domain definition:: + + domain = [ 'OR', + [ 'AND', [ + ('field name1', 'operator1', 'operand1'), + ], [ + ('field name2', 'operator2', 'operand2'), + ], + ], [ + ('field name3', 'operator3', 'operand3'), + ],] + +Obviously the use of the implicit ``AND`` operation makes the code +more readable. + + +Domain Operators +================ + +The following operators are allowed in the domain syntax. +````, ```` and ```` are dereferenced +to their values. The description of each operator follows this +pattern, unless otherwise noted:: + + (, , ) + +``=`` +----- + + Is a parity operator. Returns true when ```` + equals to ````. + +``!=`` +------ + + Is an imparity operator. It is the negation of the `=`_ operator. + +``like`` +-------- + + Is a pattern matching operator. Returns true when ```` + is contained in the pattern represented by ````. + + In ```` an underscore (``_``) matches any single + character, a percent sign (``%``) matches any string with zero + or more characters. To use ``_`` or ``%`` as literal, use the + backslash ``\`` to escape them. All matching is case sensitive. + +``not like`` +------------ + + Is a pattern matching operator. It is the negation of the `like`_ + operator. + +``ilike`` +--------- + + Is a pattern matching operator. The same use as `like`_ operator, + but matching is case insensitive. + +``not ilike`` +------------- + + Is a pattern matching operator. The negation of the `ilike`_ operator. + +``in`` +------ + + Is a list member operator. Returns true when ```` is + in ```` list. + +``not in`` +---------- + + Is a list non-member operator. The negation of the `in`_ operator. + +``<`` +----- + + Is a *less than* operator. Returns true for type string of + ```` when ```` is alphabetically + sorted before ````. + + Returns true for type number of ```` when + ```` is less than ````. + +``>`` +----- + + Is a *greater than* operator. Returns true for type string of + ```` when ```` is alphabetically + sorted after ````. + + Returns true for type number of ```` when + ```` is greater ````. + +``<=`` +------ + + Is a *less than or equal* operator. Returns the same as using the + `<`_ operator, but also returns true when ```` is + equal to ````. + +``>=`` +------ + + Is a *greater than or equal* operator. Returns the same as using + the `>`_ operator, but also returns true when ```` + is equal to ````. + +``child_of`` +------------ + + Is a parent child comparison operator. Returns true for records that are + a child of ````. ```` is a list of ``ids`` and ```` must be a :class:`~trytond.model.fields.many2one` or a + :class:`~trytond.model.fields.many2many`. + In case ```` is not linked to itself, the clause pattern + extends to:: + + (, ['child_of'|'not_child_of'], , ) + + Where ```` is the name of the field constituting the + :class:`~trytond.model.fields.many2one` on the target model. + +``not child_of`` +---------------- + + Is a parent child comparison operator. It is the negation of the + `child_of`_ operator. + +``parent_of`` +------------- + + Is a parent child comparison operator. It is the same as `child_of`_ + operator but if ```` is a parent of ````. + +``not parent_of`` +----------------- + + Is a parent child comparison operator. It is the negation of this + `parent_of`_ operator. + +``where`` +--------- + + Is a :class:`trytond.model.fields.One2Many` / + :class:`trytond.model.fields.Many2Many` domain operator. It returns true + for every row of the target model that match the domain specified as + ````. + +``not where`` +------------- + + Is a :class:`trytond.model.fields.One2Many` / + :class:`trytond.model.fields.Many2Many` domain operator. It returns true + for every row of the target model that does not match the domain specified + as ````. diff --git a/trytond/doc/topics/index.rst b/trytond/doc/topics/index.rst new file mode 100644 index 0000000000..661c48ec4b --- /dev/null +++ b/trytond/doc/topics/index.rst @@ -0,0 +1,38 @@ +.. _topics-index: + +============= +Using trytond +============= + +Introduction to all the key parts of trytond: + +.. toctree:: + :maxdepth: 1 + + install + configuration + setup_database + logs + start_server + models/index + backend_types + models/fields_default_value + models/fields_on_change + domain + pyson + access_rights + user_errors_warnings + triggers + actions + views/index + views/extension + wizard + reports/index + rpc + task_queue + cron + user_application + bus + modules/index + translation + testing diff --git a/trytond/doc/topics/install.rst b/trytond/doc/topics/install.rst new file mode 100644 index 0000000000..e9f8bd6c04 --- /dev/null +++ b/trytond/doc/topics/install.rst @@ -0,0 +1,44 @@ +.. _topics-install: + +====================== +How to install Tryton +====================== + +Install Tryton +============== + +There are three options to install Tryton: + + * Install the version provided by your operating system distribution. This + is the quickest and recommended option for those who has operating system + that distributes Tryton. + + * Install the published package. + You first need to have `pip ` installed. + Then to install ``trytond`` run: + + .. code-block:: sh + + python -m pip install trytond + + You can also install for example the ``sale`` module with: + + .. code-block:: sh + + python -m pip install trytond_sale + + * Without installation, you need to make sure you have all the dependencies + installed and then run: + + .. code-block:: sh + + python bin/trytond + + You can register modules by linking them into the ``trytond/modules`` + folder. + +.. warning:: + Note that you may need administrator/root privileges for this step, as + this command will by default attempt to install trytond to the Python + site-packages directory on your system. +.. diff --git a/trytond/doc/topics/logs.rst b/trytond/doc/topics/logs.rst new file mode 100644 index 0000000000..ef912451b2 --- /dev/null +++ b/trytond/doc/topics/logs.rst @@ -0,0 +1,50 @@ +.. _topics-logs: + +===================== +Logging configuration +===================== + +Without any configuration, trytond writes ERROR messages to standard output. +For each verbose flag set, the log level decreases. + +Logs can be configured using a `configparser-format`_ file. The filename can +be specified using trytond ``logconf`` parameter. + +.. _`configparser-format`: https://docs.python.org/2/library/logging.config.html#configuration-file-format + +Example +======= + +This example allows to write INFO messages on standard output and on a disk log +file rotated every day. + +.. highlight:: ini + +:: + + [formatters] + keys=simple + + [handlers] + keys=rotate,console + + [loggers] + keys=root + + [formatter_simple] + format=[%(asctime)s] %(levelname)s:%(name)s:%(message)s + datefmt=%a %b %d %H:%M:%S %Y + + [handler_rotate] + class=handlers.TimedRotatingFileHandler + args=('/tmp/tryton.log', 'D', 1, 30) + formatter=simple + + [handler_console] + class=StreamHandler + formatter=simple + args=(sys.stdout,) + + [logger_root] + level=INFO + handlers=rotate,console diff --git a/trytond/doc/topics/models/fields_default_value.rst b/trytond/doc/topics/models/fields_default_value.rst new file mode 100644 index 0000000000..71f5246abe --- /dev/null +++ b/trytond/doc/topics/models/fields_default_value.rst @@ -0,0 +1,31 @@ +.. _topics-fields_default_value: + +======================= +Default value of fields +======================= + +When a record is created, each field, which doesn't have a value specified, +is set with the default value if exists. + +The following class method:: + + Model.default_() + +Return the default value for ``field name``. + +This example defines an ``Item`` model which has a default ``since``:: + + import datetime + + from trytond.model import ModelView, ModelSQL, fields + + class Item(ModelSQL, ModelView): + "Item" + __name__ = 'item' + since = fields.Date('since') + + @classmethod + def default_since(cls): + return datetime.date.today() + +See also method ``Model.default_get``: :attr:`~trytond.model.Model.default_get` diff --git a/trytond/doc/topics/models/fields_on_change.rst b/trytond/doc/topics/models/fields_on_change.rst new file mode 100644 index 0000000000..6a77f7cd90 --- /dev/null +++ b/trytond/doc/topics/models/fields_on_change.rst @@ -0,0 +1,28 @@ +.. _topcis-fields_on_change: + +=================== +on_change of fields +=================== + +Tryton allows developers to define methods that can be called once a field's +value has changed by the user this is the :ref:`ref-models-fields-on_change` +method. The method has the following name:: + + Model.on_change_ + +This is an instance method, an instance of ``Model`` will be created by using +the values from the form's fields specified by the ``on_change`` list defined +on the field. Any change made on the instance will be pushed back to the +client-side record. + +There is also a way to define a method that must update a field whenever any +field from a predefined list is modified. This list is defined by the +:ref:`ref-models-fields-on_change_with` attribute of the field. The method +that will be called has the following name:: + + Model.on_change_with_ + +Just like for the classic ``on_change``, an instance of ``Model`` will be +created by using the values entered in the form's fields specified by the +``on_change_with`` attribute. The method must return the new value of the field +to push back to the client-side record. diff --git a/trytond/doc/topics/models/index.rst b/trytond/doc/topics/models/index.rst new file mode 100644 index 0000000000..a643acab2c --- /dev/null +++ b/trytond/doc/topics/models/index.rst @@ -0,0 +1,45 @@ +.. _topics-models: + +====== +Models +====== + +A model represents a single business logic or concept. It contains fields and +defines the behaviors of the record. Most of the time, each model stores +records in a single database table. + +The basics: + + * Each model is a Python class that subclasses one of + :class:`trytond.model.model.Model`. + + * :ref:`Fields ` are defined as model attributes. + + * Tryton generates the table definitions + + * Tryton provides an API following the `active record pattern`_ to access the records. + +.. _active record pattern: http://en.wikipedia.org/wiki/Active_record + +Example +======= + +This example defines a ``Party`` model which has a ``name`` and a ``code`` +fields:: + + from trytond.model import ModelView, ModelSQL, fields + + class Party(ModelSQL, ModelView): + "Party" + __name__ = "party.party" + name = fields.Char('Name') + code = fields.Char('Code') + +The class must be registered in the :ref:`Pool ` by the ``register()`` +method of the :ref:`module `. +Model classes are essentially data mappers to records and Model instances are +records. + +Model attributes define meta-information of the model. They are class +attributes starting with an underscore. Some model properties are instance +attributes allowing to update them at other places in the framework. diff --git a/trytond/doc/topics/modules/index.rst b/trytond/doc/topics/modules/index.rst new file mode 100644 index 0000000000..c34a1c511b --- /dev/null +++ b/trytond/doc/topics/modules/index.rst @@ -0,0 +1,187 @@ +.. _topics-modules: + +======= +Modules +======= + +The modules of Tryton extend the functionality of the platform. The server +comes by default with only a basic functionality included in these modules: +``ir``, ``res``. + +Module Structure +================ + +A module is a directory in trytond/modules which contains at least two files: + + * ``__init__.py``: a Tryton module must be a Python module. + + * ``tryton.cfg``: a Configuration file that describes the Tryton module. + +``__init__.py`` file +-------------------- + +It is the Python ``__init__.py`` to define a module. It must contains a method +named ``register()`` that must register to the pool all the objects of the +module. + + +``tryton.cfg`` file +------------------- + +It is a configuration file using the format of `ConfigParser`_ that must +contain ``tryton`` section with this following name: + + * ``version``: The version number of the module. + + * ``depends``: A one per line list of modules on which this module depends. + + * ``extras_depend``: A one per line list of modules on which this module + *may* depend. + + * ``xml``: The one per line list of the XML files of the module. They will + be loaded in the given order when the module is activated or updated. + +Here is an example:: + + [tryton] + version=0.0.1 + depends: + ir + res + country + xml: + party.xml + category.xml + address.xml + contact_mechanism.xml + +Python Files +============ + +The Python files define the models for the modules. + +XML Files +========= + +The XML files define data that will be inserted into the database. + +There is an rnc for those files stored in ``trytond/tryton.rnc``. + +The following snippet gives a first idea of what an xml file looks: + +.. highlight:: xml + +:: + + + + + + Party Administration + + + + + + + + + + party.party + tree + + + + + + + + + + + ]]> + + + + + +Here is the list of the tags: + + * ``tryton``: The main tag of the xml + + * ``data``: Define a set of data inside the file. It can have the + attributes: + + * ``noupdate`` to prevent the framework to update the records, + * ``depends`` to import data only if all modules in the comma separated + module list value are activated, + * ``grouped`` to create records at the end with a grouped call. + * ``language`` to import data only if the language is translatable. + + * ``record``: Create a record of the model defined by the attribute + ``model`` in the database. The ``id`` attribute can be used to refer to + the record later in any xml file. + + * ``field``: Set the value of the field with the name defined by the + attribute ``name``. + + Here is the list of attributes: + + * ``search``: Only for relation field. It contains a domain which is + used to search for the value to use. The first value found will be + used. + + * ``ref``: Only for relation field. It contains an xml id of the + relation to use as value. It must be prefixed by the module name with + an ending dot, if the record is defined in an other module. + + * ``eval``: Python code to evaluate and use result as value. + The following expressions are available: + + * ``time``: The python time_ module + * ``version``: The current Tryton version + * ``ref``: A function that converts an XML id into a database id. + * ``Decimal``: The python Decimal_ object + * ``datetime``: The python datetime_ module + + * ``pyson``: convert the evaluated value into :ref:`PYSON ` + string. + + * ``depends``: set value only if all modules in the comma separated + module list value are activated. + + .. note:: + Field content is considered as a string. So for fields that require + other types, it is required to use the ``eval`` attribute. + .. + + + * ``menuitem``: Shortcut to create ir.ui.menu records. + + Here is the list of attributes: + + * ``id``: The id of the menu. + + * ``name``: The name of the menu. + + * ``icon``: The icon of the menu. + + * ``sequence``: The sequence value used to order the menu entries. + + * ``parent``: The xml id of the parent menu. + + * ``action``: The xml id of the action linked to the menu. + + * ``groups``: A list of xml id of group, that have access to the menu, + separated by commas. + + * ``active``: A boolean telling if the menu is active or not. + + +.. _ConfigParser: http://docs.python.org/library/configparser.html +.. _time: http://docs.python.org/library/time.html +.. _Decimal: https://docs.python.org/library/decimal.html +.. _datetime: https://docs.python.org/library/datetime.html diff --git a/trytond/doc/topics/pyson.rst b/trytond/doc/topics/pyson.rst new file mode 100644 index 0000000000..a193caa1ee --- /dev/null +++ b/trytond/doc/topics/pyson.rst @@ -0,0 +1,104 @@ +.. _topics-pyson: + +===== +PYSON +===== + +PYSON is the PYthon Statement and Object Notation. It is a +lightweight `domain specific language`_ for the general representation +of statements. PYSON is used to encode statements which can be +evaluated in different programming languages, serving for the +communication between trytond and any third party software. +A PYSON parser can easily be implemented in other programming languages. +So third party softwares do not need to depend on Python to be able to +fully communicate with the Tryton server. + +PYSON is a `deterministic algorithm`_ which will always succeed to evaluate +statements. There is a default behavior for unknown values. It is statically +typed and checked on instantiation. + +There is also a :ref:`reference documentation of the API `. + +.. _`domain specific language`: http://en.wikipedia.org/wiki/Domain-specific_language +.. _`deterministic algorithm`: http://en.wikipedia.org/wiki/Deterministic_algorithm + +Syntax +====== + +The syntax of a PYSON statement follows this pattern:: + + Statement(argument1[, argument2[, ...]]) + +where arguments can be another statement or a value. The evaluation +direction is inside out, deepest first. + + +PYSON Examples +============== + +Given the PYSON statement:: + + Eval('active_id', -1) + +``Eval()`` checks the evaluation context for the variable ``active_id`` +and returns its value or ``-1`` if not defined. A similar expression +in Python looks like this:: + + 'active_id' in locals() and active_id or -1 + +Given the PYSON statement:: + + Not(Bool(Eval('active'))) + +``Eval()`` checks the evaluation context for a variable ``active`` and +returns its value to ``Bool()`` or ``''`` if not defined. ``Bool()`` +returns the corresponding boolean value of the former result to ``Not()``. +``Not()`` returns the boolean negation of the previous result. A similar +expression in Python looks like this:: + + 'active' in locals() and active == False + +Given the PYSON statement:: + + Or(Not(Equal(Eval('state'), 'draft')), Bool(Eval('lines'))) + +In this example are the results of two partial expressions +``Not(Equal(Eval('state'), 'draft'))`` and ``Bool(Eval('lines'))`` +evaluated by a logical *OR* operator. The first expression part is +evaluated as follow: When the value of ``Eval('state')`` is equal to +the string ``'draft'`` then return true, else false. ``Not()`` negates +the former result. A similar expression in Python looks like this:: + + 'states' in locals() and 'lines' in locals() \ + and state != 'draft' or bool(lines) + +Given the PYSON statement:: + + If(In('company', Eval('context', {})), '=', '!=') + +In this example the result is determined by an `if-then-else`_ condition. +``In('company', Eval('context', {}))`` is evaluated like this: When +the key ``'company'`` is in the dictionary ``context``, returns +true, otherwise false. ``If()`` evaluates the former result and returns +the string ``'='`` if the result is true, otherwise returns the +string ``'!='``. A similar expression in Python looks like this:: + + 'context' in locals() and isinstance(context, dict) \ + and 'company' in context and '=' or '!=' + +.. _if-then-else: http://en.wikipedia.org/wiki/Conditional_statement#If-Then.28-Else.29 + +Given the PYSON statement:: + + Get(Eval('context', {}), 'company', 0)) + +``Eval()`` checks the evaluation context for a variable ``context`` if +defined, return the variable ``context``, otherwise return an empty +dictionary ``{}``. ``Get()`` checks the former resulting dictionary +and returns the value of the key ``'company'``, otherwise it returns +the number ``0``. A similar expression in Python looks like this:: + + 'context' in locals() and context.get('company', 0) or 0 + + + diff --git a/trytond/doc/topics/reports/index.rst b/trytond/doc/topics/reports/index.rst new file mode 100644 index 0000000000..ab594ff673 --- /dev/null +++ b/trytond/doc/topics/reports/index.rst @@ -0,0 +1,250 @@ +.. _topics-reports: + +======= +Reports +======= + +Tryton can generate dynamic reports in many formats from templates. The reports +are generated in one step as follows: a report template in a special file +format, explained later, is interpolated with dynamic data and placed into a +document of the same file format. Tryton's ability to generate documents in +this way allows documents to be generated for any editor that supports the Open +Document Format which can be converted to third party formats, such as PDF. +`LibreOffice`_ must be installed on the server host for format conversion. + +.. _LibreOffice: https://www.libreoffice.org/ + +Report Templates +================ + +Report templates are files with a format supported by relatorio, that contain +snippets of the Genshi templating language. + +Here is an example of the text that would be placed in an open document text +document, ``*.odt``, that displays the full name and the address lines of the +first address of each party. The genshi code is placed in the template using +``Functions->Placeholder->Text`` Fields. These are specific to ODT files. + +Report API +========== + +Python API +---------- + +.. TODO + +XML Description +--------------- + +When defining an ``ir.action.report`` the following attributes are available: + + * ``name``: The name of the report. + + * ``report_name``: The name of the report model, for example + my_module.my_report. This is the name you would use with ``Pool().get`` + + * ``model``: If this report is of an existing model this is its name. + For example my_module.my_model. Custom reports that aren't of a specific + model will need to leave this blank. + + * ``report``: The path to the template file starting with the module, for + example my_module/my_report.odt. + + * ``template_extension``: The template format. + + * ``single``: ``True`` if the template works only for one record. If such + report is called with more than one record, a zip file containing all the + reports will be generated. + + +Report Usage +============ + +Using genshi and open office reports +------------------------------------ + +Setting up an ODT file +^^^^^^^^^^^^^^^^^^^^^^ + +If you are creating a report from scratch you should perform the following +steps: + + - Remove user data + + * "File > Properties..." + + * Uncheck "Apply user data" + + * Click on "Reset" + + - Select Style and Formatting + + * Press F11 or "Format > Style and Formatting" + + * Click on the drop down at the right top + + * Select "Load Styles" + + * Click on "From File..." + + * Select a existing report (``company/header_A4.odt``) + + - Set some parameters + + * Set the zoom to 100% (View>Zoom) + + * Set the document in read-only mode (``File>Properties>Security``) + (Decreases the time it takes to open the document.) + + - Usage + + * Use Liberation fonts (Only necessary if being officially included in + Tryton) + + * Try to use styles in report templates so that they can be extended. + +Using Genshi in an ODT file +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The genshi code is placed in the template using Functions->Placeholder->Text +Fields. These are specific to ``*.odt`` files and can be found in the open +office menu at Insert -> Fields -> Other and then Functions -> Placeholder -> +Text. Type genshi code into the Placeholder field. There are alternatives for +embedding genshi that are supported by relatorio but their use is not +encouraged within Tryton. + +Also note that relatorio only supports a subset of genshi. The directives that +are supported by relatorio can be found here: `Quick Example`_ . + +See genshi's documentation for more information: `Genshi XML Templates`_ + +Examples +^^^^^^^^ + +The modules company, account_invoice and stock all contain helpful examples. + +Also see relatorio's site for some examples: + + - `Quick Example`_ + + - `In Depth Introduction`_ + + - `Example Documents`_ + + +Accessing models from within the report +--------------------------------------- + +By default instances of the models the report is for are passed in to the +report via a list of objects called ``records`` (or ``record`` if ``single`` is +``True``). These records behave just as they would within trytond itself. You +can access any of the models relations as well. For example within the invoice +report each object is an invoice and you can access the name of the party of +the invoice via ``invoice.party.name``. Additional objects can be passed to a +report. This is discussed below in ``Passing custom data to a report``. + +Within Tryton the underlying model the report can be found by following the +Menu to ``Administration > UI > Actions > Report``. Furthermore in tryton the +fields for that model can be found by following the menu to ``Administration > +Model > Model``. Model relation fields can be accessed to any depth, for +example, one could access ``invoice.party.addresses`` to get a list of addresses +for the party of an invoice. + +Creating a simple report template for a model from client +--------------------------------------------------------- + +Once you have created a report template it has to be uploaded to trytond. This +can be done by creating a new record in the +``Administration > UI > Actions > Report`` menu. Just make sure to include the +template file in the content field. + +In order to make the report printable from a model create a "Print form" +keyword related to the model where the report should be available. + +Creating a simple report template for a model in XML +---------------------------------------------------- + +Less work has to be done if you just want a simple report representation of a +model. There are just 2 steps. First, create a report template file in a +format supported by relatorio. Second, describe your report in XML making sure +to define the correct ``report_name`` and ``model``. + +Replacing existing Tryton reports +--------------------------------- + +To replace an existing report you must deactivate the old report and activate +the new report. + +For example to deactivate the sale report: + +.. highlight:: xml + +:: + + + + + +Then you must activate the new sale report that exists in your new module: + +.. highlight:: xml + +:: + + + Sale + sale.sale + sale.sale + my_module/sale.odt + odt + + +And create the keyword for the new report: + +.. highlight:: xml + +:: + + + form_print + sale.sale,-1 + + + +Passing custom data to a report +------------------------------- + +In this example ``Report.get_context`` is overridden and an employee +object is set into context. Now the invoice report will be able to access the +employee object. + +.. highlight:: python + +:: + + from trytond.report import Report + from tryton.pool import Pool + + class InvoiceReport(Report): + __name__ = 'account.invoice' + + @classmethod + def get_context(cls, records, header, data): + pool = Pool() + Employee = pool.get('company.employee') + + context = super().get_context(records, header, data) + employee_id = Transaction().context.get('employee') + employee = Employee(employee_id) if employee_id else None + context['employee'] = employee + + return context + + Pool.register(InvoiceReport, type_='report') + +.. _Genshi XML Templates: http://genshi.edgewall.org/wiki/Documentation/0.5.x/xml-templates.html + +.. _Quick Example: https://relatorio.readthedocs.io/en/latest/quickexample.html + +.. _In Depth Introduction: https://relatorio.readthedocs.io/en/latest/indepthexample.html + +.. _Example Documents: http://hg.tryton.org/relatorio/file/default/examples diff --git a/trytond/doc/topics/rpc.rst b/trytond/doc/topics/rpc.rst new file mode 100644 index 0000000000..1ad8420cc7 --- /dev/null +++ b/trytond/doc/topics/rpc.rst @@ -0,0 +1,50 @@ +.. _topics-rpc: + +===================== +Remote Procedure Call +===================== + +There are two protocols supported by trytond: `JSON-RPC`_ (Version 1.0) and +`XML-RPC`_. +The URL of the calls must end with the database name with a trailing '/'. + +The available methods are: + +common.db.login +--------------- + +It takes as parameters: the user name and a dictionary of login parameters. +It returns in case of success the user ID and the session. +If the parameters are not valid to authenticate the user, it returns nothing. +Otherwise if it misses a key in the parameters, it raises a ``LoginException`` +exception with the missing key name, type and the message to ask to the +user. + +common.db.logout +---------------- + +It takes no parameters and it invalidate the current session. + +.. TODO - other methods + +.. _`JSON-RPC`: https://en.wikipedia.org/wiki/JSON-RPC +.. _`XML-RPC`: https://en.wikipedia.org/wiki/XML-RPC + +Authorization +============= + +Most of the calls require authorization, there are two methods: + +Basic +----- + +It follows the `Basic access authentication`_. + +.. _`Basic access authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication + +Session +------- + +The authorization field is constructed by the username, the user ID and the +session combined with a single colon and encoded in Base64. +The session is retrieved by calling the method ``common.db.login``. diff --git a/trytond/doc/topics/setup_database.rst b/trytond/doc/topics/setup_database.rst new file mode 100644 index 0000000000..dfe14406c2 --- /dev/null +++ b/trytond/doc/topics/setup_database.rst @@ -0,0 +1,53 @@ +.. _topics-setup-database: + +======================= +How to setup a database +======================= + +The database section of the :ref:`configuration ` must be +set before starting. + +Create a database +================= + +Depending of the database backend choosen, you must create a database (see the +documentation of the choosen backend). The user running ``trytond`` must be +granted the priviledge to create tables. For backend that has the option, the +encoding of the database must be set to ``UTF-8``. + +Initialize a database +===================== + +A database can be initialized using this command line:: + + trytond-admin -c -d --all + +At the end of the process, ``trytond-admin`` will ask to set the password for +the ``admin`` user. + +Update a database +================= + +To upgrade to a new series, the command line is:: + + trytond-admin -c -d --all + +.. warning:: + Prior to upgrade see if there is no manual action to take on the `migration + topic`_. + +.. _`migration topic`: https://discuss.tryton.org/c/migration + +To activate a new language on an existing database, the command line is:: + + trytond-admin -c -d --all -l + +Once activated, the language appears in the user preferences. + +When installing new modules, the list of modules must be updated with:: + + trytond-admin -c -d --update-modules-list + +Once updated, the new modules can be activated from the client or activated with:: + + trytond-admin -c -d -u --activate-dependencies diff --git a/trytond/doc/topics/start_server.rst b/trytond/doc/topics/start_server.rst new file mode 100644 index 0000000000..b692d0c454 --- /dev/null +++ b/trytond/doc/topics/start_server.rst @@ -0,0 +1,76 @@ +.. _topics-start-server: + +======================= +How to start the server +======================= + +Web service +=========== + +You can start the default web server bundled in Tryton with this command line:: + + trytond -c + +The server will wait for client connections on the interface defined in the +``web`` section of the :ref:`configuration `. + +.. note:: When using multiple config files the order is importart as last + entered files will override the items of first files + +WSGI server +----------- + +If you prefer to run Tryton inside your own WSGI server instead of the simple +server of Werkzeug, you can use the application ``trytond.application.app``. +Following environment variables can be set: + + * ``TRYTOND_CONFIG``: Point to :ref:`configuration ` + file. + * ``TRYTOND_LOGGING_CONFIG``: Point to :ref:`logging ` file. + * ``TRYTOND_LOGGING_LEVEL``: An integer to set the default `logging level`_ + (default: ``ERROR``). + * ``TRYTOND_COROUTINE``: Use coroutine for concurrency. + * ``TRYTOND_DATABASE_NAMES``: A list of database names in CSV format, using + python default dialect. + +.. warning:: You must manage to serve the static files from the web root. + +.. _`logging level`: https://docs.python.org/library/logging.html#logging-levels + +Coroutine server +---------------- + +The Werkzeug server uses thread for concurrency. This is not optimal for the +long-polling request on the :ref:`bus ` as each client consumes +permanently one thread. +You can start the server with coroutine using the option ``--coroutine``. + +Cron service +============ + +If you want to run some :ref:`scheduled actions `, you must also +run the cron server with this command line:: + + trytond-cron -c -d + +The server will wake up every minutes and preform the scheduled actions defined +in the ``database``. +You can also launch the command every few minutes from a scheduler with the +option ``--once``. + +Worker service +============== + +If you want to use a pool of workers to run :ref:`asynchronously some tasks +`, you must activate the worker in the ``queue`` section of +the :ref:`configuration ` and run the worker manager with +this command line:: + + trytond-worker -c -d + +The manager will dispatch tasks from the queue to a pool of worker processes. + +Services options +================ + +You will find more options for those services by using ``--help`` arguments. diff --git a/trytond/doc/topics/task_queue.rst b/trytond/doc/topics/task_queue.rst new file mode 100644 index 0000000000..8d78f3d416 --- /dev/null +++ b/trytond/doc/topics/task_queue.rst @@ -0,0 +1,59 @@ +.. _topics-task-queue: + +========== +Task Queue +========== + +Tryton provides a way to run asynchronously some tasks. You must activate the +worker in the ``queue`` section of the :ref:`configuration +` and :ref:`run the worker manager +` otherwise the tasks will be run at the end of the +transaction. + +A task is the parameters that defines how to call a method from a +:class:`trytond.model.Model`. This include the +:attr:`trytond.transaction.Transaction.context`, the +:attr:`trytond.transaction.Transaction.user` and the arguments. +The first argument of the method must be an instance or a list of instances of +:class:`trytond.model.Model`. This other arguments must be JSON-ifiable. + +A task is pushed into the ``queue`` by calling the desired method on the +:attr:`trytond.model.Model.__queue__`. This stores in the queue all the current +parameters of the call and it will be execute by a worker or at the end of the +transaction if no worker is configured. +The following :attr:`trytond.transaction.Transaction.context` keys are used as +parameters for the queue: + + - ``queue_name``: The name of the queue. Default value is ``default``. + + - ``queue_scheduled_at``: A ``datetime.timedelta`` to add to current time + to define when the task should be started. Default value is ``None`` which + means directly. + + - ``queue_expected_at``: A ``datetime.timedelta`` to add to current time to + define when the task should be finished. Default value is ``None`` which + means as soon as possible. + +.. warning:: + + There is no access right verification during the execution of the task. + +Example: + +.. highlight:: python + +:: + + from trytond.model import Model + + class MyModel(Model): + "My Model" + __name__ = 'my_model' + + @classmethod + def launch(cls, records): + for record in records: + cls.__queue__.process(record, 42) + + def process(self, value): + self.value = value diff --git a/trytond/doc/topics/testing.rst b/trytond/doc/topics/testing.rst new file mode 100644 index 0000000000..4a281c3be5 --- /dev/null +++ b/trytond/doc/topics/testing.rst @@ -0,0 +1,105 @@ +.. _topics-testing: + +======= +Testing +======= + +Tryton supports both functional and unit tests. + +Testing your module +~~~~~~~~~~~~~~~~~~~ + +Functional tests +---------------- + +Functional tests are written as doctests_ using proteus. + +Unit tests +---------- + +Tryton provides the :class:`ModuleTestCase` class that bundles a set of tests +that are useful for every module. + +Unit tests in ``ModuleTestCase`` can be decorated with +:func:`trytond.tests.test_tryton.with_transaction` to run the test in a +transaction. + +To use it in your own module you just have to inherit from +:class:`ModuleTestCase` and set the class attribute +:attr:`module ` to the name of your module. + +.. highlight:: python + +:: + + from trytond.tests.test_tryton import ModuleTestCase, with_transaction + + class MyModuleTestCase(ModuleTestCase): + "My Module Test Case" + module = 'my_module' + + @with_transaction() + def test_method(self): + "Test method" + self.assertTrue(True) + + +Tests from this modules are found by the function +``trytond.modules.my_module.tests.suite`` which must return a +``unittest.TestSuite`` containing all the module's tests. This function is +called by the Tryton test runner script to gather all the tests. + +A typical ``suite()`` function thus looks like this: + +.. highlight:: python + +:: + + def suite(): + suite = trytond.tests.test_tryton.suite() + suite.addTests(unittest.TestLoader().loadTestsFromTestCase( + MyModuleTestCase)) + suite.addTests(doctest.DocFileSuite('scenario_my_module.rst', + tearDown=doctest_teardown, encoding='utf-8', + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, + checker=doctest_checker)) + return suite + +.. _doctests: https://docs.python.org/library/doctest.html +.. _unittest: https://docs.python.org/library/unittest.html + +Running your module's tests +--------------------------- + +Tryton provides the script ``trytond/tests/run-tests.py``, just invoke it like +that:: + + run-tests.py -m my_module + +Testing trytond +~~~~~~~~~~~~~~~ + +Extending tests +--------------- + +Python modules extending tryton core can define additional tests that should be +added to the existing ones. + +Those modules must create an entry point ``trytond.tests``. Any file in the +module path specified by this entry point starting with ``test_`` and ending by +``.py`` will be imported. Each of those file must define a ``suite()`` function +that returns a ``unittest.TestSuite`` that will be included in the trytond test +suite. If the module from the entry point defines a ``register`` function it +will be called when registering the test-specific models in the +:class:`trytond.pool.Pool`. + +Running trytond tests +--------------------- + +You should use the script ``trytond/tests/run-tests.py`` by invoking it like +that:: + + run-tests.py [-c configuration] + +You can use a different configuration file to check trytond against different +backend. diff --git a/trytond/doc/topics/translation.rst b/trytond/doc/topics/translation.rst new file mode 100644 index 0000000000..dcd01c1730 --- /dev/null +++ b/trytond/doc/topics/translation.rst @@ -0,0 +1,68 @@ +.. _topics-translation: + +=========== +Translation +=========== + +The translation of the user interface is provided module-wise. +Translations are stored in the ``locale/`` directory of a module, each language +in a `PO-file `_. The +official language files are named after the +`POSIX locale `_ +standard, e.g. de_DE.po, es.po, es_AR.po, es_EC.po... + +The names of custom language files must match the code of the language in the +Model ir.lang. + +If a language is set ``translatable``, the translations will be loaded into +the database on each trytond module update. + +Tryton supports derivative translations. This means that if the translation of +a term is missing in one language, it will search on the parent languages. +Also when activate a children language, you must also activate all parents. + +Translation Wizards +=================== + +Set Translations +---------------- + +The wizard adds new translations to the base language ``en``. + +Clean Translations +------------------ + +The wizard deletes obsolete translations from the database. + +Synchronize Translations +------------------------ + +The wizard updates the translations of the selected language based on the +translations of the base language ``en``. It will also remove duplicate +translations with its direct parent. + +Export Translations +------------------- + +The wizard requires to select a language and a module and will export the +translations for this selection into a PO-file. + + +Override translations +===================== + +Translations of a module can be overridden by another module. This can be done +by putting a PO file into the ``locale/override`` directory of the module that +shall contain the translations to override. + +To override the translation of another module the ``msgctxt`` string must have +the following content: + +type:name:module.xml_id + + * ``type``: Value of the field type of ir.translation. + * ``name``: Value of the field name of ir.translation. + * ``module``: Value of the field module ir.translation. + * ``xml_id``: The XML id that is stored in ir.model.data as fs_id + +The xml_id part is optional and can be omitted if it is None. diff --git a/trytond/doc/topics/triggers.rst b/trytond/doc/topics/triggers.rst new file mode 100644 index 0000000000..b895705707 --- /dev/null +++ b/trytond/doc/topics/triggers.rst @@ -0,0 +1,28 @@ +.. _topics-triggers: + +======== +Triggers +======== + +Triggers allow to define methods of :class:`trytond.model.model.Model` that are +called at the end of the transaction when one of those events happen to a +record: + +* On Creation +* On Modification +* On Deletions +* On Time: When a condition changes over time. + +The method signature is:: + + (cls, records, trigger) + +Where ``records`` is the list of records that triggered the event and +``trigger`` is the ``ir.trigger`` instance which is triggered. + +Triggers are defined by records of ``ir.trigger``. Each record must define a +pyson condition which will be evaluated when the event occurs. Only those +records for which the condition is evaluated to true will be processed by the +trigger with the exception of modification triggers which will only process the +records for which the condition is evaluated to false before and evaluated to +true after the modification. diff --git a/trytond/doc/topics/user_application.rst b/trytond/doc/topics/user_application.rst new file mode 100644 index 0000000000..25b1b6445f --- /dev/null +++ b/trytond/doc/topics/user_application.rst @@ -0,0 +1,76 @@ +.. _topics-user_application: + +================ +User Application +================ + +Tryton provides a way to connect URL rules to an callable endpoint using the +decorator method ``route`` of the ``trytond.application.app`` instance. This +allows you to define a custom API based on HTTP that can be used to create a +specific user application. + +The decorator takes as first parameter a string which follow the `Rule +Format`_ of Werkzeug and as second parameter sequence of HTTP methods. + +Example:: + + from trytond.application import app + + @app.route('/hello', methods=['GET']) + def hello(request): + return 'Hello world' + +.. _Rule Format: http://werkzeug.pocoo.org/docs/latest/routing/#rule-format + +The following converter is added by Tryton: + + - ``base64``: This converter accepts any Base64_ string and transforms it + into its corresponding bytes value. + +.. _Base64: https://en.wikipedia.org/wiki/Base64 + +Tryton also provides some wrappers in ``trytond.protocols.wrappers`` to ease the +creation of such route. + + - ``set_max_request_size(size)``: change the default limit of the request to + the size in bytes. + + - ``allow_null_origin``: allow requests which have their ``Origin`` set to + ``null``. + + - ``with_pool``: which takes the first parameter as database name and + replace it by the corresponding instance of the :ref:`Pool `. + + - ``with_transaction([readonly])``: which starts a :class:`Transaction` + using the :ref:`Pool ` from ``with_pool``. If ``readonly`` is + not set, the transaction will not be readonly for ``POST``, ``PUT``, + ``DELETE`` and ``PATCH`` methods and readonly for all others. + + - ``user_application(name[, json])``: which set the + :attr:`Transaction.user` from the ``Authorization`` header using the + type ``bearer`` and a valid key for the named user application. + +User Application Key +==================== + +Tryton also provides a easy way to manage access to user application using +keys per named application. +A key is created with a ``POST`` request on the ``URL`` +``//user/application/`` which returns the key. The request must +contain as data a json object with the keys: + + - ``user``: the user login + + - ``application``: the name of the application + +After the creation, the key must be validated by the user from the preferences +of a Tryton client. + +A key can be deleted with a ``DELETE`` request on the same ``URL``. The request +must contain as data a json object with the keys: + + - ``user``: the user login + + - ``key``: the key to delete + + - ``application``: the name of the application of the key diff --git a/trytond/doc/topics/user_errors_warnings.rst b/trytond/doc/topics/user_errors_warnings.rst new file mode 100644 index 0000000000..72980cb56d --- /dev/null +++ b/trytond/doc/topics/user_errors_warnings.rst @@ -0,0 +1,69 @@ +.. _topics-user_errors_warnings: + +======================== +User Errors and Warnings +======================== + +When processing a request, you can stop the flow by raising an exception that +will be displayed to the user as an error message or a warning. They are +respectively :exc:`~trytond.exceptions.UserError` and +:exc:`~trytond.exceptions.UserWarning`. + +User Errors +=========== + +An error displays a message and optionally a description to the user. + +Example: + +.. highlight:: python + +:: + + from trytond.exceptions import UserError + from trytond.model import Model + + class MyModel(Model): + "My Model" + __name__ = 'my_model' + + def process(self): + if check_failed: + raise UserError("You cannot process.", "because…") + +.. note:: + They are often used in combination with :meth:`~trytond.i18n.gettext` to + translate the messages. + +User Warnings +============= + +A warning displays a confirmation message with optionally a description to the +user. The user can decide to continue so the request is processed again without +stopping at the warning. Otherwise the user can cancel its request. +The warning instance is identified by a name which allows to skip it the next +time it is checked, that's why they often contain data like the id of a record. + +Example: + +.. highlight:: python + +:: + + from trytond.exceptions import UserWarning + from trytond.model import Model + from trytond.pool import Pool + + class MyModel(Model): + "My Model" + __name__ = 'my_model' + + def process(self): + Warning = Pool().get('res.user.warning') + warning_name = 'mywarning,%s' % self + if Warning.check(warning_name): + raise UserWarning(warning_name, "Process cannot be canceled.") + +.. note:: + If there is no user interaction the warnings can be skipped by setting the + ``_skip_warnings`` key of the context to ``True``. diff --git a/trytond/doc/topics/views/extension.rst b/trytond/doc/topics/views/extension.rst new file mode 100644 index 0000000000..49d4e13cad --- /dev/null +++ b/trytond/doc/topics/views/extension.rst @@ -0,0 +1,51 @@ +.. _topics-extension: + +============== +Extending View +============== + +Extending a view means, that the original view will be modified by a set of +rules which are defined with XML. + +For this purpose, the extension engine uses xpath expressions. + +The view is defined with the field ``inherit`` of the ir.ui.view. + +If the field ``domain`` (a Python string representation of a :ref:`domain +`) is not set or evaluated to True, the inheritance will be +proceeded. + +XML Description +=============== + +data +---- + +Each view must start with this tag. + +xpath +----- + + * ``expr``: the xpath expression to find a node in the inherited view. + + * ``position``: Define the position in relation to the node found. It can + be ``before``, ``after``, ``replace``, ``inside`` or + ``replace_attributes`` which will change the attributes. + +Example +======= + +.. highlight:: xml + +:: + + + + + diff --git a/trytond/doc/topics/views/index.rst b/trytond/doc/topics/views/index.rst new file mode 100644 index 0000000000..ce6de0acc3 --- /dev/null +++ b/trytond/doc/topics/views/index.rst @@ -0,0 +1,916 @@ +.. _topics-views: + +===== +Views +===== + +The views are used to display records of an :class:`ModelView +` to the user. + +In Tryton, :class:`ModelView ` can have several views. +An ``action`` opens a window and defines which view to show. + +The views are built from XML that is stored in the ``view`` directory of the +module or in the databases thanks to the model ir.ui.view. + +So generally, they are defined in xml files with this kind of xml where name is +the name of the XML file in the ``view`` directory: + +.. highlight:: xml + +:: + + + model name + type name + + + view_name + + + +There are different types of views: + + * Form + + * Tree + + * List-Forms + + * Graph + + * Board + + * Calendar + +Form view +========= + +The RNG describing the xml of a form view is stored in trytond/ir/ui/form.rng. +There is also a RNC in trytond/ir/ui/form.rnc. + +A form view is used to display one record of an object. + +Elements of the view are put on the screen following the rules: + + * Elements are placed on the screen from left to right, from top to bottom, + according to the order of the xml. + + * The screen composed of a table with a fixed number of columns and enough + rows to handle all elements. + + * Elements take one or more columns when they are put in the table. If + there are not enough free columns on the current row, the elements are put + at the beginning of the next row. + + +XML description +--------------- + +List of attributes shared by many form elements: + + .. _common-attributes-id: + + * ``id``: A unique identifier for the tag if there is no name attribute. + + .. _common-attributes-yexpand: + + * ``yexpand``: A boolean to specify if the label should expand to take up + any extra vertical space. + + .. _common-attributes-yfill: + + * ``yfill``: A boolean to specify if the label should fill the vertical + space allocated to it in the table cell. + + .. _common-attributes-yalign: + + * ``yalign``: The vertical alignment, from 0.0 to 1.0. + + .. _common-attributes-xexpand: + + * ``xexpand``: The same as yexpand but for horizontal space. + + .. _common-attributes-xfill: + + * ``xfill``: The same as yfill but for horizontal space. + + .. _common-attributes-xalign: + + * ``xalign``: The horizontal alignment, from 0.0 to 1.0. + + .. _common-attributes-colspan: + + * ``colspan``: The number of columns the widget must take in the table. + + .. _common-attributes-col: + + * ``col``: The number of columns the container must have. + + A negative value (or zero) will remove the constraint on the number of + columns. + + The default value is 4. + + .. _common-attributes-states: + + * ``states``: A string of :ref:`PYSON statement ` that will + be evaluated with the values of the current record. + + It must return a dictionary where keys can be: + + * ``invisible``: If true, the widget will be hidden. + + * ``required``: If true, the field will be required. + + * ``readonly``: If true, the field will be readonly. + + * ``icon``: Only for button, it must return the icon name to use or + False. + + * ``pre_validate``: Only for button, it contains a domain to apply + on the record before calling the button. + + * ``depends``: Only for button, it must return the list of field on + which the button depends. + + .. _common-attributes-help: + + * ``help``: The string that will be displayed when the cursor hovers over + the widget. + + .. _common-attributes-pre_validate: + + * ``pre_validate``: A boolean only for fields + :class:`trytond.model.fields.One2Many` to specify if the client must + pre-validate the records using + :meth:`trytond.model.Model.pre_validate`. + + .. _common-attributes-completion: + + * ``completion``: A boolean only for fields + :class:`trytond.model.fields.Many2One`, + :class:`trytond.model.fields.Many2Many` and + :class:`trytond.model.fields.One2Many` to specifiy if the client must + auto-complete the field. The default value is True. + + .. _common-attributes-factor: + + * ``factor``: A factor to apply on fields + :class:`trytond.model.fields.Integer`, + :class:`trytond.model.fields.Float` and + :class:`trytond.model.fields.Numeric` to display on the widget. The + default value is 1. + + .. _common-attributes-symbol: + + * ``symbol``: Only on numerical fields, the name of field which provides + the symbol to display. + + .. _common-attributes-help_field: + + * ``help_field``: The name of Dict field mapping the Selection value with + its help string. + +form +^^^^ + +Each form view must start with this tag. + + .. _form-attributes-on_write: + + * ``on_write``: The name of a method on the Model of the view that will be + called when a record is saved. The method must return a list of record + ids that the client must reload if they are already loaded. The function + must have this syntax: + + ``on_write(self, ids)`` + + .. note:: + The method must be registered in :attr:`trytond.model.Model.__rpc__`. + .. + + * ``col``: see in common-attributes-col_. + + * ``cursor``: The name of the field that must have the cursor by default. + +label +^^^^^ + +Display static string. + + * ``string``: The string that will be displayed in the label. + + * ``name``: The name of the field whose description will be used for + string. Except if ``string`` is set, it will use this value and the value + of the field if ``string`` is empty. + + * ``id``: see common-attributes-id_. + + * ``yexpand``: see in common-attributes-yexpand_. + + * ``yfill``: see in common-attributes-yfill_. + + * ``yalign``: see in common-attributes-yalign_. + + * ``xexpand``: see in common-attributes-xexpand_. + + * ``xfill``: see in common-attributes-xfill_. + + * ``xalign``: see in common-attributes-xalign_. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + + * ``help``: see in common-attributes-help_. + + * Requires that either ``id`` or ``name`` is defined. + +field +^^^^^ + +Display a field of the object with the value of the current record. + + * ``name``: The name of the field. + + * ``string``: The string that will be displayed for the widget. + + * ``widget``: The widget that must be used instead of the default one. + + * ``help``: The string that will be displayed when the cursor stays over the + widget. + + * ``width``: The minimum width the widget should request, or -1 to unset. + + * ``height``: The minimum height the widget should request, or -1 to unset. + + * ``readonly``: Boolean to set the field readonly. + + * ``mode``: Only for One2Many fields: it is a comma separated list, that + specifies the order of the view used to display the relation. (Example: + ``tree,form``) + + * ``view_ids``: A comma separated list that specifies the view ids used to + display the relation. For Many2One and Many2Many, the order should always + be tree then form. + + * ``product``: Only for One2Many fields, a comma separated list of target + field name used to create records from the cartesian product. + + * ``completion``: Only for Many2One fields, it is a boolean to set the + completion of the field. + + * ``invisible``: The field will not be displayed, but it will fill cells in + the table. + + * ``filename_visible``: Only for Binary fields, boolean that enables the + display of the filename. + + * ``toolbar``: Only for Rich Text widget, boolean that enables the + display of the Rich Text toolbar. The default value is 1. + + * ``yexpand``: see in common-attributes-yexpand_. + + * ``yfill``: see in common-attributes-yfill_. + + * ``xexpand``: see in common-attributes-xexpand_. + + * ``xfill``: see in common-attributes-xfill_. + + * ``colspan``: see in common-attributes-colspan_. + + * ``help``: see in common-attributes-help_. + + * ``pre_validate``: see in common-attributes-pre_validate_. + + * ``completion``: see in common-attributes-completion_. + + * ``factor``: see in common-attributes-factor_. + + * ``spell``: Only for Text widgets, a :ref:`PYSON statement ` + that will be evaluated to the language code for which spell checking must + be done. + + * ``symbol``: see in common-attributes-symbol_. + + * ``help_field``: see in common-attributes-help_field_. + +.. _form-image: + +image +^^^^^ + +Display an image. + + * ``type``: the type of image source. Available values are ``icon`` or + ``url``. The default value is ``icon``. + + * ``name``: The image name or the field name which contains the image name. + For the ``icon`` type it must be the name of a record of ``ir.ui.icon``. + For the ``url`` type it must be the URL. It can be relative to the server. + + * ``url_size``: the name of the size parameter to add to the URL. + + * ``yexpand``: see in common-attributes-yexpand_. + + * ``yfill``: see in common-attributes-yfill_. + + * ``colspan``: see in common-attributes-colspan_. + + * ``size``: the size of the image in pixels. The default value is 48. + + * ``states``: see in common-attributes-states_. + + * ``help``: see in common-attributes-help_. + + +separator +^^^^^^^^^ + +Display a horizontal separator. + + * ``string``: The string that will be displayed above the separator. + + * ``name``: The name of the field from which the description will be used + for string. + + * ``id``: see in common-attributes-id_. + + * ``yexpand``: see in common-attributes-yexpand_. + + * ``yfill``: see in common-attributes-yfill_. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + + * ``help``: see in common-attributes-help_. + + * Requires that either ``id`` or ``name`` is defined. + +newline +^^^^^^^ + +Force to use a new row. + + +.. _form-button: + +button +^^^^^^ + +Display a button. + + * ``name``: The name of the function that will be called. The function must + have this syntax: + + ``button(cls, records)`` + + The function may return an ``ir.action`` id or one of those client side + action keywords: + +.. _topics-views-client-actions: + + * ``new``: to create a new record + * ``delete``: to delete the selected records + * ``remove``: to remove the record if it has a parent + * ``copy``: to copy the selected records + * ``next``: to go to the next record + * ``previous``: to go to the previous record + * ``close``: to close the current tab + * ``switch []``: to switch the view + * ``reload``: to reload the current tab + * ``reload context``: to reload user context + * ``reload menu``: to reload menu + + * ``icon`` + + * ``confirm``: A text that will be displayed in a confirmation popup when + the button is clicked. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + + * ``help``: see in common-attributes-help_. + + * ``keyword``: specify where will the button be displayed in the client + toolbar. The valid values are the keywords starting with ``form_`` from + :ref:`Actions ` without the ``form_`` part. + + +.. warning:: + The button should be registered on ``ir.model.button`` where the default + value of the ``string``, ``confirm`` and ``help`` attributes can be can be + defined. + +.. _form-link: + +link +^^^^ + +Display an ``ir.action.act_window`` as a button with a counter or one counter +per tab. When clicked it opens the window. + + * ``name``: The XML id of ``ir.action.act_window``. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + + * ``icon``: The name of the icon to display. + + * ``empty``: If set to ``hide`` the button is not displayed if the counter + is zero. The default is ``show``. + +notebook +^^^^^^^^ + +It adds a notebook widget which can contain page tags. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + +page +^^^^ + +Define a new tab inside a notebook. + + * ``string``: The string that will be displayed in the tab. + + * ``angle``: The angle in degrees between the baseline of the label and the + horizontal, measured counterclockwise. + + * ``col``: see in common-attributes-col_. + + * ``id``: see in common-attributes-id_. + + * ``states``: see in common-attributes-states_. + + * Requires that either ``id`` or ``name`` is defined. + +group +^^^^^ + +Create a sub-table in a cell. + + * ``string``: If set a frame will be drawn around the field with a label + containing the string. Otherwise, the frame will be invisible. + + * ``rowspan``: The number of rows the group spans in the table. + + * ``col``: see in common-attributes-col_. + + * ``expandable``: If this attribute is present the content of the group + will be expandable by the user to reveal its content. A value of "1" + means that the group will start expanded, a value of "0" means + that the group will start unexpanded. There is no default value. + + * ``homogeneous``: If True all the tables cells are the same size. + + * ``id``: see in common-attributes-id_. + + * ``yexpand``: see in common-attributes-yexpand_. + + * ``yfill``: see in common-attributes-yfill_. + + * ``yalign``: see in common-attributes-yalign_. + + * ``xexpand``: see in common-attributes-xexpand_. + + * ``xfill``: see in common-attributes-xfill_. + + * ``xalign``: see in common-attributes-xalign_. + + * ``colspan``: see in common-attributes-colspan_. + + * ``states``: see in common-attributes-states_. + + * Requires that either ``id`` or ``name`` is defined. + +hpaned, vpaned +^^^^^^^^^^^^^^ + + * ``position``: The pixel position of divider, a negative value means that + the position is unset. + + * ``id``: see in common-attributes-id_. + + * ``colspan``: see in common-attributes-colspan_. The default + for panes is 4 columns. + +child +^^^^^ + +Contains the childs of a hpaned or vpaned. + +.. _example_form_view: + +Example +------- + +.. highlight:: xml + +:: + +
+