Merge new root

--HG--
branch : 6.0
This commit is contained in:
Cédric Krier committed 2022-12-16 14:46:14 +01:00
commit 346ecd2293
757 files changed
+243710

No files matched your search

+291
View File
@@ -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
+36
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
[flake8]
ignore=E123,E124,E126,E128,E741,W503
+507
View File
@@ -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
+26
View File
@@ -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/>.
+674
View File
@@ -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>.
+17
View File
@@ -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
+7
View File
@@ -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.
+54
View File
@@ -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()
+17
View File
@@ -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"
+107
View File
@@ -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
+40
View File
@@ -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"
+166
View File
@@ -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
+18
View File
@@ -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
+25
View File
@@ -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.
+600
View File
@@ -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.
+17
View File
@@ -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"
BIN
View File
Binary file not shown.
+17
View File
@@ -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"
+17
View File
@@ -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"
+15
View File
@@ -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
+10
View File
@@ -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
+17
View File
@@ -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"
+149
View File
@@ -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'),
)])
+19
View File
@@ -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
+184
View File
@@ -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
+164
View File
@@ -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
)
+17
View File
@@ -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"
+17
View File
@@ -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"
+6
View File
@@ -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}
+30
View File
@@ -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;
+45
View File
@@ -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)
+5
View File
@@ -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]
+188
View File
@@ -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
+98
View File
@@ -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))
+91
View File
@@ -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()
+33
View File
@@ -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]
+62
View File
@@ -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
+183
View File
@@ -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)
+15
View File
@@ -0,0 +1,15 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from 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)
+61
View File
@@ -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)
+28
View File
@@ -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)
File diff suppressed because it is too large. Load diff
+74
View File
@@ -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)
+650
View File
@@ -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()
+462
View File
@@ -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)
+877
View File
@@ -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
+9
View File
@@ -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)
+60
View File
@@ -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()
+83
View File
@@ -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)
+464
View File
@@ -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()
+91
View File
@@ -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()
+145
View File
@@ -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()
+346
View File
@@ -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()
+230
View File
@@ -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
+112
View File
@@ -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)
+36
View File
@@ -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)
+7
View File
@@ -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('_', '__')
+10
View File
@@ -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)
+176
View File
@@ -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'))
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+202
View File
@@ -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

Loaded 100 of 757 files, more files were not shown because too many files have changed in this diff. Show more