Merge new root
--HG-- branch : 6.0
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
[flake8]
|
||||
ignore=E123,E124,E126,E128,E741,W503
|
||||
@@ -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
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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" ""
|
||||
" <svg" "* " 100
|
||||
" <!DOCTYPE svg" "* " 100
|
||||
|
||||
"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-tga.so"
|
||||
"tga" 4 "gdk-pixbuf" "Targa" "LGPL"
|
||||
"image/x-tga" ""
|
||||
"tga" "targa" ""
|
||||
" \001\001" "x " 100
|
||||
" \001\t" "x " 100
|
||||
" \002" "xz " 99
|
||||
" \003" "xz " 100
|
||||
" \n" "xz " 100
|
||||
" \v" "xz " 100
|
||||
|
||||
"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-tiff.so"
|
||||
"tiff" 5 "gdk-pixbuf" "TIFF" "LGPL"
|
||||
"image/tiff" ""
|
||||
"tiff" "tif" ""
|
||||
"MM *" " z " 100
|
||||
"II* " " z" 100
|
||||
"II* \020 CR\002 " " z zzz z" 0
|
||||
|
||||
"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-xbm.so"
|
||||
"xbm" 4 "gdk-pixbuf" "XBM" "LGPL"
|
||||
"image/x-xbitmap" ""
|
||||
"xbm" ""
|
||||
"#define " "" 100
|
||||
"/*" "" 50
|
||||
|
||||
"@executable_path/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-xpm.so"
|
||||
"xpm" 4 "gdk-pixbuf" "XPM" "LGPL"
|
||||
"image/x-xpixmap" ""
|
||||
"xpm" ""
|
||||
"/* XPM */" "" 100
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# GTK+ Input Method Modules file
|
||||
# Automatically generated file, do not edit
|
||||
# Created by gtk-query-immodules-3.0 from gtk+-3.22.26
|
||||
#
|
||||
# ModulesPath = @executable_path/lib/gtk-3.0/3.0.0/x86_64-apple-darwin16.7.0/immodules:@executable_path/lib/gtk-3.0/3.0.0/immodules:@executable_path/lib/gtk-3.0/x86_64-apple-darwin16.7.0/immodules:@executable_path/lib/gtk-3.0/immodules
|
||||
#
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-am-et.so"
|
||||
"am_et" "Amharic (EZ+)" "gtk30" "@executable_path/share/locale" "am"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-cedilla.so"
|
||||
"cedilla" "Cedilla" "gtk30" "@executable_path/share/locale" "az:ca:co:fr:gv:oc:pt:sq:tr:wa"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-cyrillic-translit.so"
|
||||
"cyrillic_translit" "Cyrillic (Transliterated)" "gtk30" "@executable_path/share/locale" ""
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-inuktitut.so"
|
||||
"inuktitut" "Inuktitut (Transliterated)" "gtk30" "@executable_path/share/locale" "iu"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-ipa.so"
|
||||
"ipa" "IPA" "gtk30" "@executable_path/share/locale" ""
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-multipress.so"
|
||||
"multipress" "Multipress" "gtk30" "" ""
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-quartz.so"
|
||||
"quartz" "Mac OS X Quartz" "gtk30" "@executable_path/share/locale" "ja:ko:zh:*"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-thai.so"
|
||||
"thai" "Thai-Lao" "gtk30" "@executable_path/share/locale" "lo:th"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-ti-er.so"
|
||||
"ti_er" "Tigrigna-Eritrean (EZ+)" "gtk30" "@executable_path/share/locale" "ti"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-ti-et.so"
|
||||
"ti_et" "Tigrigna-Ethiopian (EZ+)" "gtk30" "@executable_path/share/locale" "ti"
|
||||
|
||||
"@executable_path/lib/gtk-3.0/3.0.0/immodules/im-viqr.so"
|
||||
"viqr" "Vietnamese (VIQR)" "gtk30" "@executable_path/share/locale" "vi"
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
Glossary
|
||||
########
|
||||
|
||||
.. glossary::
|
||||
:sorted:
|
||||
|
||||
Actions
|
||||
An *action* is a function which is triggered by a user intervention.
|
||||
*Actions* are called from activating menu items or pushing buttons.
|
||||
Actions often provide :term:`wizards`.
|
||||
|
||||
Board
|
||||
The *board* is a type of :term:`views` able to handle other views. This
|
||||
view type is not documented or not used for now.
|
||||
|
||||
Character Encoding
|
||||
See [WP-ENCOD]_
|
||||
|
||||
CSV
|
||||
File format for Comma Separated Values. See [WP-CSV]_
|
||||
|
||||
Data
|
||||
*Data* means information content produced by users.
|
||||
|
||||
|
||||
Dialog
|
||||
A *dialog* is a :term:`popup` window, which overlays other windows and
|
||||
request user interaction. *Dialogs* are used to set up special
|
||||
:term:`actions`.
|
||||
|
||||
Fields
|
||||
*Fields* are attributes of a *data object*. *Fields* are
|
||||
represented as table fields in relational databases.
|
||||
|
||||
Form View
|
||||
The *form* is a mode of :term:`views`, which displays single
|
||||
:term:`records` of data.
|
||||
|
||||
Form
|
||||
The *form* is the general type of :term:`views` used in Tryton. The
|
||||
*form* provides several modes for presenting :term:`data`:
|
||||
|
||||
* :term:`Form View`
|
||||
* :term:`Tree View`
|
||||
* :term:`Graph View`
|
||||
|
||||
Graph View
|
||||
*Graph view* is a mode of :term:`views` to show sets of data in a
|
||||
diagram. *Graph views* can be pie-charts or bar-charts.
|
||||
|
||||
Main Frame
|
||||
The *main frame* is a huge part arranged in the center of the
|
||||
:term:`Tryton client`. *Using the Tryton client* means mainly using the
|
||||
*main frame* part. It contains :term:`tabs` to organize and to show
|
||||
different :term:`views`.
|
||||
|
||||
Model
|
||||
A *model* describes how data is represented and accessed. Models
|
||||
formally define records and relationships for a certain domain
|
||||
of interest.
|
||||
|
||||
Modules
|
||||
*Modules* are enclosed file packages for the :term:`Tryton server`. A
|
||||
*Module* defines the :term:`Model`, the presentation of the
|
||||
information (:term:`views`), functions, :term:`actions` and default
|
||||
presets. Additionally *modules* may provide standardized data like ISO
|
||||
names for countries. *Modules* in Tryton are build up generically. That
|
||||
is, they are constructed as simple as possible to provide the
|
||||
desired functionality.
|
||||
|
||||
Plugins
|
||||
A *plugin* is an add-on module for the :term:`Tryton client`.
|
||||
|
||||
Popup
|
||||
A small window which pops up the main window.
|
||||
|
||||
Records
|
||||
A *record* is a singular dataset in a :term:`Model`. *Records* are
|
||||
represented as lines or *records* in a relational database table.
|
||||
|
||||
Tabs
|
||||
*Tabs* are :term:`widgets` to arrange different contents side by side.
|
||||
They are used to switch quickly between different domains of interest.
|
||||
Tryton uses *tabs* in two layer:
|
||||
|
||||
* A tabbed :term:`Main Frame`.
|
||||
* Tabs inside :term:`Views`.
|
||||
|
||||
The main frame consists of *tabs* that embed the main menu and all views
|
||||
to an appropriate :term:`model`. The other type of *tabs* is used
|
||||
inside of :term:`views` to split them into visual domains of the same
|
||||
model. These *tabs* are used for structuring contents of one model to
|
||||
different sub headings.
|
||||
|
||||
Three-Tiers
|
||||
A *three-tiers* application framework like Tryton, is build up of three
|
||||
different software components:
|
||||
|
||||
1. The storage or data tier.
|
||||
2. The logic or application tier.
|
||||
3. The presentation tier.
|
||||
|
||||
The storage tier in the Tryton framework is provided by the PostgreSQL
|
||||
database engine. The application logic tier is provided by
|
||||
:term:`Tryton server` and its :term:`modules`. The presentation tier is
|
||||
mainly provided by the :term:`Tryton client`. In a *three tiers*
|
||||
framework, the presentation tier (client) never connects directly to the
|
||||
storage tier. All communication is controlled by the application tier.
|
||||
|
||||
Tree View
|
||||
*Tree view* is a mode of :term:`views` showing sets of :term:`data`.
|
||||
*Tree views* can be flat lists or tables as well as tree-like nested
|
||||
lists.
|
||||
|
||||
Tryton Server
|
||||
The *Tryton server* is the application or logic tier in the
|
||||
:term:`three-tiers` application platform *Tryton*. The *Tryton server*
|
||||
connects the underlying application logic of the different
|
||||
:term:`modules` with corresponding database records. The
|
||||
*Tryton server* provides different interfaces to present the
|
||||
generated information:
|
||||
|
||||
* :term:`Tryton client`: (graphical user interface GUI)
|
||||
* XMLRPC see [WP-XMLRPC]_
|
||||
* WebDAV see [WP-WebDAV]_
|
||||
* OpenOffice
|
||||
|
||||
Tryton Client
|
||||
The *Tryton client* application is the graphical user interface (GUI)
|
||||
of the :term:`Tryton server`.
|
||||
|
||||
Views
|
||||
A *view* is the visual presentation of :term:`data`.
|
||||
*Views* resides inside :term:`tabs` in the :term:`main frame` of the
|
||||
:term:`Tryton client`. There are two general types of *views* in Tryton:
|
||||
|
||||
1. :term:`Form`
|
||||
2. :term:`Board`
|
||||
|
||||
Each of the view types has different modes to show data. *Views*
|
||||
are built of several :term:`widgets` and provide often additional
|
||||
:term:`actions`. It is also possible to present the same data in
|
||||
different view modes alternately.
|
||||
|
||||
Widgets
|
||||
A *Widget* is a visual element of a graphical user interface (GUI). Some
|
||||
*Widgets* solely show information, others allow manipulation from user
|
||||
side. Example *Widgets* are buttons, check-boxes, entry-boxes, selection
|
||||
lists, tables, lists, trees, ...
|
||||
|
||||
Wizards
|
||||
*Wizards* define stateful sequences of interaction to proceed
|
||||
complex :term:`actions`. A *wizard* divides the complexity of some actions
|
||||
into several user guided steps.
|
||||
|
||||
References
|
||||
**********
|
||||
|
||||
.. [WP-XMLRPC] http://en.wikipedia.org/wiki/Xmlrpc
|
||||
|
||||
.. [WP-WebDAV] http://en.wikipedia.org/wiki/Webdav
|
||||
|
||||
.. [WP-CSV] http://en.wikipedia.org/wiki/Comma-separated_values
|
||||
.. [WP-ENCOD] http://en.wikipedia.org/wiki/Character_encoding
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
=====================
|
||||
Tryton Desktop Client
|
||||
=====================
|
||||
|
||||
Tryton is a Graphical User Interface to Tryton based on GTK__ and Python__.
|
||||
|
||||
__ http://www.gtk.org
|
||||
__ http://www.python.org
|
||||
|
||||
Contents
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
usage
|
||||
glossary
|
||||
@@ -0,0 +1,25 @@
|
||||
Installing tryton
|
||||
=================
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Once you've downloaded and unpacked a tryton source release, enter the
|
||||
directory where the archive was unpacked, and run:
|
||||
|
||||
``python setup.py install``
|
||||
|
||||
Note that you may need administrator/root privileges for this step, as
|
||||
this command will by default attempt to install tryton to the Python
|
||||
site-packages directory on your system.
|
||||
|
||||
For advanced options, please refer to the easy_install__ and/or the
|
||||
distutils__ documentation:
|
||||
|
||||
__ http://setuptools.readthedocs.io/en/latest/easy_install.html
|
||||
|
||||
__ http://docs.python.org/inst/inst.html
|
||||
|
||||
To use without installation, run ``bin/tryton`` from where the archive was
|
||||
unpacked.
|
||||
|
||||
@@ -0,0 +1,600 @@
|
||||
|
||||
:tocdepth: 2
|
||||
|
||||
Client Usage
|
||||
############
|
||||
This document is the reference about the concepts of the graphical user
|
||||
interface (also known as *Tryton client* ) of the Tryton application framework.
|
||||
|
||||
|
||||
Name
|
||||
****
|
||||
tryton - Graphical user client of the Tryton application framework
|
||||
|
||||
|
||||
Synopsis
|
||||
********
|
||||
|
||||
::
|
||||
|
||||
tryton [options] [url]
|
||||
|
||||
On startup the login dialog is displayed.
|
||||
|
||||
Options
|
||||
*******
|
||||
|
||||
--version Show program version number and exit
|
||||
|
||||
-h, --help Show help message and exit
|
||||
|
||||
-c FILE, --config=FILE Specify alternate `configuration file`_
|
||||
|
||||
-d, --dev Enable development mode, which deactivates
|
||||
client side caching
|
||||
|
||||
-v, --verbose Enable basic debugging
|
||||
|
||||
-l LOG_LEVEL, --log-level=LOG_LEVEL Specify the log level: DEBUG, INFO,
|
||||
WARNING, ERROR, CRITICAL
|
||||
|
||||
-u LOGIN, --user=LOGIN Specify the login user
|
||||
|
||||
-s SERVER, --server=SERVER Specify the server hostname:port
|
||||
|
||||
URL
|
||||
***
|
||||
|
||||
When an url is passed, the client will try to find already running client that
|
||||
could handle it and send to this one to open the url. If it doesn't find one
|
||||
then it will start the GUI and open the url itself.
|
||||
|
||||
The url schemes are:
|
||||
|
||||
`tryton://<hostname>[:<port>]/<database>/model/<model name>[/<id>][;parameters]`
|
||||
|
||||
`tryton://<hostname>[:<port>]/<database>/wizard/<wizard name>[;parameters]`
|
||||
|
||||
`tryton://<hostname>[:<port>]/<database>/report/<report name>[;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. <b>text</b>
|
||||
* Italic: Tag `i` is used, i.e. <i>text</i>
|
||||
* Underline: Tag `u` is used, i.e. <u>text</u>
|
||||
* Font family: It is a attribute `font-family` for `span` tag, i.e.
|
||||
<span font-family="Arial">text</span>
|
||||
* Font size: It is a attribute `size` for `span` tag, i.e. <span size="12">
|
||||
text</span>
|
||||
* 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: <p align='center'>some text</p>
|
||||
* Background color: It is a attribute `background` for `span` tag, i.e.
|
||||
<span background='#7f7f7f'>text</span>
|
||||
* Foreground color: It is a attribute `foreground` for `span` tag, i.e.
|
||||
<span foreground='#00f'>text</span>
|
||||
|
||||
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-<name>`: 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.
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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'),
|
||||
)])
|
||||
@@ -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
|
||||
@@ -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=<VERSION>'"
|
||||
!endif
|
||||
;Check series
|
||||
!ifndef SERIES
|
||||
!error "Missing SERIES! Specify if with '/DSERIES=<SERIES>'"
|
||||
!endif
|
||||
;Check bits
|
||||
!ifndef BITS
|
||||
!error "Missing BITS! Specify it with '/DBITS=<BITS>'"
|
||||
!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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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, _('<i>Search...</i>'))
|
||||
if create:
|
||||
completion.insert_action_markup(1, _('<i>Create...</i>'))
|
||||
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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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 '<markup>%s</markup>' % text
|
||||
|
||||
|
||||
def _strip_markup(text):
|
||||
return text[len('<markup>'):-len('</markup>')]
|
||||
|
||||
|
||||
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 += '<div align="%s">' % align
|
||||
else:
|
||||
text += '<div>'
|
||||
|
||||
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 += '</div>'
|
||||
|
||||
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('</%s>' % 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 = '''<b>Bold</b>
|
||||
<i>Italic</i>
|
||||
<u>Underline</u>
|
||||
<div><br/></div>
|
||||
<div align="center">Center</div>
|
||||
<div><font face="sans" size="6">Sans6<font color="#ff0000">red</font></font></div>
|
||||
<div align="center"> <b> <i><u>Title</u></i> </b></div>''' # 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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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('_', '__')
|
||||
@@ -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)
|
||||
@@ -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'))
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 185 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 382 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<path d="M7.41,8.59L12,13.17l4.59-4.58L18,10l-6,6l-6-6L7.41,8.59z"/>
|
||||
<path fill="none" d="M0,0h24v24H0V0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<path d="M15.41,16.59L10.83,12l4.58-4.59L14,6l-6,6l6,6L15.41,16.59z"/>
|
||||
<path fill="none" d="M0,0h24v24H0V0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<path d="M8.59,16.59L13.17,12L8.59,7.41L10,6l6,6l-6,6L8.59,16.59z"/>
|
||||
<path fill="none" d="M0,0h24v24H0V0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 196 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 402 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z"/>
|
||||
<path d="M17.51 3.87L15.73 2.1 5.84 12l9.9 9.9 1.77-1.77L9.38 12l8.13-8.13z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 209 B |