Compare commits
583 Commits
Author | SHA1 | Date |
---|---|---|
El Mau | 34a981a2dd | |
El Mau | 66549e271b | |
El Mau | 884b10bd79 | |
El Mau | fd050159ba | |
El Mau | 8199667188 | |
El Mau | 05d3f617eb | |
El Mau | 0ec4f0c2c9 | |
El Mau | 29bfb42abc | |
El Mau | 7f129b84b2 | |
El Mau | 13a3c106c6 | |
El Mau | 0c7abf634b | |
El Mau | f0c4423fc8 | |
El Mau | d925587a98 | |
El Mau | 510ebdd1e5 | |
El Mau | d2e92098da | |
El Mau | c1ce206835 | |
El Mau | 1efa280920 | |
El Mau | 6fd8f9e2ea | |
El Mau | e3a44e6400 | |
El Mau | 390ec60fdd | |
El Mau | 957160755a | |
El Mau | 070ee31a1b | |
El Mau | 2620159408 | |
El Mau | 4d9bc4a172 | |
El Mau | 17ed56e9d7 | |
El Mau | 5057ac57ae | |
El Mau | 410a7080da | |
El Mau | edd8d560bd | |
El Mau | bd1699d082 | |
El Mau | 6d4023bb17 | |
Mauricio | beda45690f | |
Mauricio | 8feba167af | |
Mauricio | 4dbb797d99 | |
Mauricio | cd424b71dc | |
Mauricio | ea13d11319 | |
Mauricio | d9c863820a | |
Categulario | 6815d6c64c | |
Mauricio | 402cb7d70c | |
Mauricio | a5f1114774 | |
Mauricio | 2588a5f0b8 | |
Mauricio | eeae157772 | |
Mauricio | 7cad4093d4 | |
Mauricio | 7caf6423e1 | |
Mauricio | 8a3aea41eb | |
Mauricio | bd49356d3f | |
Mauricio | 3b54ef0a36 | |
Mauricio | 497a4be392 | |
Mauricio | 7fc6264322 | |
Mauricio | c5d0e97212 | |
Mauricio | 5bbad9ef58 | |
Mauricio | 8e7aa23d1b | |
Mauricio | f3d8b1dae5 | |
Mauricio | 2c684ae6bd | |
el Mau | febea3f289 | |
el Mau | 5facb96662 | |
el Mau | 6411c2e51e | |
el Mau | eeb128fda6 | |
el Mau | a078e27703 | |
el Mau | 28ef53043f | |
el Mau | e956c243f7 | |
el Mau | 1b33413de2 | |
el Mau | 64541a84b9 | |
el Mau | 834c1faae6 | |
el Mau | affe57bfcd | |
el Mau | 4dc54ade1e | |
el Mau | e72739a5fe | |
el Mau | dd7b9c1680 | |
el Mau | 9823bd967f | |
el Mau | aee467d98f | |
el Mau | 64917527f7 | |
el Mau | 5f56179ebb | |
el Mau | 955a0348f8 | |
el Mau | 0826b0cea2 | |
el Mau | 8b0f9fbc64 | |
el Mau | f3d827364b | |
el Mau | 50cbb3bae8 | |
el Mau | a7c8d80219 | |
el Mau | ca9d7a02e3 | |
el Mau | 31fcb63e51 | |
el Mau | bd7a31ea8a | |
el Mau | 6a6b6ca395 | |
el Mau | 3148c5ba82 | |
el Mau | 5f62ad7750 | |
el Mau | ff44162622 | |
el Mau | a37b3a5c23 | |
el Mau | 4fdea6173b | |
el Mau | 968c2744ec | |
el Mau | c9d85ca2e7 | |
el Mau | 93e64eafd8 | |
el Mau | 1984103ff5 | |
el Mau | e4ba06c0ff | |
el Mau | 38bb267e55 | |
el Mau | 602467c8e9 | |
el Mau | 4020237f68 | |
el Mau | 933c9820e6 | |
el Mau | 51daf0ad5e | |
el Mau | 709c524830 | |
el Mau | 434e15bb5b | |
el Mau | ae5949c529 | |
el Mau | af5fa3c812 | |
el Mau | 6d399fcb39 | |
el Mau | 8003fc42b9 | |
el Mau | 7a0fb2b243 | |
el Mau | 83bebde169 | |
el Mau | 76d5f51d78 | |
el Mau | 5f641c632e | |
el Mau | 85eb7cd58c | |
el Mau | 1c11a1b013 | |
el Mau | 701bb68478 | |
el Mau | f13491d983 | |
el Mau | 4020365616 | |
el Mau | a90f218c76 | |
el Mau | 37995befd8 | |
el Mau | 746c827492 | |
el Mau | 3cea54c07c | |
el Mau | 661eff1dc3 | |
el Mau | cd11456ec3 | |
el Mau | 2526a8f5aa | |
el Mau | 0a13004419 | |
el Mau | 296874a9b1 | |
el Mau | 34b1af1319 | |
el Mau | 5fbf80fb92 | |
el Mau | b56fee5f73 | |
el Mau | c9ca45b60d | |
el Mau | 86e7f50621 | |
el Mau | 2cef108856 | |
el Mau | 260ba62b2e | |
el Mau | 76d15e482b | |
el Mau | 8e2446e8f5 | |
el Mau | 482a0385cd | |
el Mau | 29e02e649d | |
el Mau | 5aece843bf | |
el Mau | d47e059d93 | |
el Mau | 89f79de9b6 | |
el Mau | fbaa85f13a | |
el Mau | bed6039fff | |
el Mau | acbaecbfc5 | |
el Mau | 8dddb7b293 | |
el Mau | 24777f691e | |
el Mau | 1d270aa478 | |
el Mau | 8872af8c50 | |
el Mau | d57ded161c | |
el Mau | 0c8d6d3cea | |
el Mau | f14c1f74d8 | |
el Mau | 210f50a7d7 | |
el Mau | 5418b23200 | |
el Mau | a13c7c0a1d | |
el Mau | a6276a33fb | |
el Mau | e3bd1e8871 | |
el Mau | 2ddb3d6b75 | |
Mauricio Baeza | 4cda9f7f21 | |
Mauricio Baeza | 7c184c9513 | |
Mauricio Baeza | aea8aeaecf | |
Mauricio Baeza | e08e4e038f | |
Mauricio Baeza | 496dcf7162 | |
Mauricio Baeza | f8cd99084a | |
Mauricio Baeza | 1e824bd841 | |
Mauricio Baeza | bc0cd9f3e1 | |
Mauricio Baeza | ae259e461b | |
Mauricio Baeza | 1a507d2eeb | |
Mauricio Baeza | f766f92618 | |
Mauricio Baeza | ad56356a6f | |
El Mau | c6cf33c4c4 | |
El Mau | dbc3910297 | |
El Mau | 6c78a282fe | |
El Mau | f8c2c5e2d6 | |
El Mau | 7d40e79f3c | |
El Mau | a36034a476 | |
El Mau | de2ab5cedd | |
El Mau | b60e2d6a37 | |
El Mau | d9021cca4a | |
El Mau | 7bbef31936 | |
El Mau | 74e7f12088 | |
El Mau | 20c0757f04 | |
El Mau | 3a5fbc609b | |
El Mau | 5e888d2337 | |
El Mau | d97fd4867a | |
El Mau | e58b8f90af | |
El Mau | 50d4a3f4b7 | |
El Mau | 1978edfaf0 | |
El Mau | 7ca87d1811 | |
El Mau | a7bc6d6f6c | |
El Mau | 1991c68b3b | |
El Mau | bebcd29710 | |
El Mau | de856f28ab | |
El Mau | e726cda085 | |
El Mau | eee386a97b | |
El Mau | ae21403c59 | |
El Mau | 110d41cf6e | |
El Mau | bd6fb65a1b | |
El Mau | b68d79d0ca | |
El Mau | 670aca3886 | |
El Mau | 68c0203039 | |
El Mau | b2cdd0efbe | |
El Mau | 51af15f311 | |
El Mau | edf35aa5ce | |
El Mau | b19d087d10 | |
El Mau | 64ef75fbf5 | |
El Mau | d6a8c1e3fa | |
El Mau | a7080bf954 | |
El Mau | 3ddf99075e | |
El Mau | bd6ba5d4d0 | |
El Mau | 6a18934b19 | |
El Mau | 61548c9135 | |
El Mau | fe3f743e32 | |
El Mau | c83e6981ae | |
El Mau | 39b9af125c | |
El Mau | dd606be46b | |
El Mau | 34df8882b3 | |
El Mau | e992103c57 | |
El Mau | 8f74d24b14 | |
El Mau | 480ec57d1e | |
El Mau | a38247727c | |
El Mau | 702aa264d8 | |
El Mau | 0422eece40 | |
El Mau | 12774af7ac | |
El Mau | b16eabd773 | |
El Mau | 1bab1cb23c | |
El Mau | 15a05e38f7 | |
El Mau | a0a8e0ce62 | |
El Mau | 825e23e369 | |
El Mau | 121831a139 | |
El Mau | c09a5749da | |
El Mau | 01ccbcabd5 | |
El Mau | cc0eee1443 | |
El Mau | 7a88c4b97f | |
El Mau | 0e7db2b711 | |
El Mau | 65851182c3 | |
El Mau | 2de98b9b92 | |
El Mau | 5a24bc159d | |
El Mau | f84721716c | |
El Mau | 8b91532b82 | |
El Mau | 0a98338fad | |
El Mau | 7bcf6d6e3c | |
El Mau | dbc8717575 | |
El Mau | 0adcd7f30f | |
El Mau | d237b33020 | |
El Mau | 5b997f7858 | |
El Mau | a609c0c6f5 | |
El Mau | d70cae64b7 | |
El Mau | 9daf07693a | |
El Mau | e40acd28ba | |
El Mau | 88dd9ca04b | |
El Mau | d2f879c224 | |
El Mau | 4922010caf | |
El Mau | ff8bc31f50 | |
El Mau | e795e87461 | |
El Mau | d2c361e174 | |
El Mau | 6b0ca817a3 | |
El Mau | 84fd8f57df | |
El Mau | f105f2a0fa | |
El Mau | ea145c630b | |
El Mau | 88bb6d9411 | |
El Mau | 0498217b65 | |
El Mau | 581e8bbdc5 | |
El Mau | c79e8492d3 | |
El Mau | f9df0d34d0 | |
El Mau | f06022ac69 | |
El Mau | 074cdc475f | |
El Mau | 0f041ed975 | |
El Mau | 6de30c3417 | |
El Mau | 8bbd2ba62b | |
El Mau | 00c58d9b41 | |
El Mau | a3d291c893 | |
El Mau | 86bc032413 | |
El Mau | a91feb15d7 | |
El Mau | 3ffc54f966 | |
El Mau | b3663dfcc6 | |
El Mau | 3cc11430d7 | |
El Mau | 4ae5197ced | |
El Mau | bf601dfcef | |
El Mau | 481594839d | |
El Mau | 7b2467f99a | |
El Mau | 2f22ad4cc8 | |
El Mau | 46d6329754 | |
El Mau | 2a2689a61b | |
El Mau | 76663fdd67 | |
El Mau | 54ab86a089 | |
El Mau | 7fe5251153 | |
El Mau | fe4829c6b8 | |
El Mau | bb90b3f6d7 | |
El Mau | f4779dbb99 | |
El Mau | 47ec5ad360 | |
El Mau | 80957afb84 | |
El Mau | b9af99aad6 | |
El Mau | 43e932e445 | |
El Mau | 8a151d42ad | |
El Mau | db77f6972d | |
El Mau | ef2314880f | |
El Mau | 1a1cb1173e | |
El Mau | fe26744a8f | |
El Mau | 69402e03bb | |
El Mau | 4a84654117 | |
El Mau | 81428c4f8e | |
El Mau | 28ef107949 | |
El Mau | e372f064b4 | |
El Mau | 46efb3693c | |
El Mau | d2bacc3b6b | |
El Mau | 264090f2a6 | |
El Mau | 425cd53df1 | |
El Mau | b7e8bb3cff | |
El Mau | 1df477e0b1 | |
El Mau | 9b09fbbcd1 | |
El Mau | f793cd2240 | |
El Mau | 54f2a574a3 | |
El Mau | 219f7c6712 | |
El Mau | 342f64a18b | |
El Mau | e880256cde | |
El Mau | 3a1cb8d142 | |
El Mau | 42a840f85c | |
Mauricio Baeza | 2e5f4ac27e | |
Mauricio Baeza | 98f091a347 | |
Mauricio Baeza | 82ec2faac5 | |
Mauricio Baeza | 11c0cc4899 | |
Mauricio Baeza | 1c9c829296 | |
Mauricio Baeza | 03f26b65e4 | |
Mauricio Baeza | 00f2b2b126 | |
Mauricio Baeza | c77438216d | |
Mauricio Baeza | b02b2b0e63 | |
Mauricio Baeza | 75788474ba | |
Mauricio Baeza | 866e8c40b9 | |
Mauricio Baeza | c64de2bbb9 | |
Mauricio Baeza | cd35884999 | |
Mauricio Baeza | c4e29e47fa | |
Mauricio Baeza | 00cbe4d557 | |
Mauricio Baeza | a95a6842f9 | |
Mauricio Baeza | 30a4a276dd | |
Mauricio Baeza | 416bc65174 | |
Mauricio Baeza | 3dd88f14d6 | |
Mauricio Baeza | fb24346601 | |
Mauricio Baeza | f792b79655 | |
Mauricio Baeza | 2055a904f6 | |
Mauricio Baeza | 7a1fd8fcaf | |
Mauricio Baeza | cd22a5a24d | |
Mauricio Baeza | 25e86cd28c | |
Mauricio Baeza | 4aad1a94eb | |
Mauricio Baeza | 229be3b415 | |
Mauricio Baeza | bc53171969 | |
Mauricio Baeza | 61a8937eb3 | |
Mauricio Baeza | be75abeb54 | |
Mauricio Baeza | 2c314a3891 | |
Mauricio Baeza | 7fc1d42381 | |
Mauricio Baeza | 0957cf673f | |
Mauricio Baeza | f99adaf109 | |
Mauricio Baeza | 91cfbb0508 | |
Mauricio Baeza | 4f18e1e01f | |
Mauricio Baeza | 7e7b08b1c0 | |
Mauricio Baeza | 4006322f30 | |
Mauricio Baeza | 935483f7ee | |
Mauricio Baeza | 80b766783d | |
Mauricio Baeza | 8822687308 | |
Mauricio Baeza | 0512e0f6dd | |
Mauricio Baeza | 93f0d09f4d | |
Mauricio Baeza | 870d95cb5a | |
Mauricio Baeza | 9fa35d5592 | |
Mauricio Baeza | 2bfe86973a | |
Mauricio Baeza | 896be35e3b | |
Mauricio Baeza | f2af10c2df | |
Mauricio Baeza | 8f7aaac563 | |
Mauricio Baeza | 8a615144a6 | |
Mauricio Baeza | 227204abec | |
Mauricio Baeza | 637c69b9c7 | |
Mauricio Baeza | 4f3bdbda95 | |
Mauricio Baeza | 34c467d26e | |
Mauricio Baeza | d61672dac4 | |
Mauricio Baeza | 1aa6540ac1 | |
Mauricio Baeza | 1b87591f62 | |
Mauricio Baeza | 986020a4a3 | |
Mauricio Baeza | 3514944f5d | |
Mauricio Baeza | 9be913c47d | |
Mauricio Baeza | b9d87f0343 | |
Mauricio Baeza | 585632882d | |
Mauricio Baeza | 920ff62e98 | |
Mauricio Baeza | dc61d3b010 | |
Mauricio Baeza | 62a0a82699 | |
Mauricio Baeza | 702ac88b38 | |
Mauricio Baeza | 535cc9d527 | |
Mauricio Baeza | cf35cd08cd | |
Mauricio Baeza | e9eeab8c2c | |
Mauricio Baeza | a518278b55 | |
Mauricio Baeza | 0442019ea5 | |
Mauricio Baeza | fa94e36e7f | |
Mauricio Baeza | 348be949ac | |
Mauricio Baeza | 565f36be3a | |
Mauricio Baeza | 7062151e79 | |
Mauricio Baeza | 5f1edc76ca | |
Mauricio Baeza | 5729a1f83e | |
Mauricio Baeza | 1a9087b13a | |
Mauricio Baeza | a2670cb92c | |
Mauricio Baeza | a8667a5d4f | |
Mauricio Baeza | 952dcba7ae | |
Mauricio Baeza | 676ff29b57 | |
Mauricio Baeza | 12a24c1117 | |
Mauricio Baeza | e59941db34 | |
Mauricio Baeza | 0e881eb17a | |
Mauricio Baeza | fa01e6cf01 | |
Mauricio Baeza | 51e2464141 | |
Mauricio Baeza | 3e43cc3b46 | |
Mauricio Baeza | b5bdcbbeb3 | |
Mauricio Baeza | 4f71dd0de3 | |
Mauricio Baeza | 720c9f0cb1 | |
Mauricio Baeza | 66d740e8d1 | |
Mauricio Baeza | 892906c46f | |
Mauricio Baeza | aa68225571 | |
Mauricio Baeza | 4f40bc3bfa | |
Mauricio Baeza | b03fbebe09 | |
Mauricio Baeza | 3cdbe14287 | |
Mauricio Baeza | 0bbf0b438f | |
Mauricio Baeza | c98cdd7fff | |
Mauricio Baeza | f0b5fa5ae9 | |
Mauricio Baeza | 537cecbb0b | |
Mauricio Baeza | 7f44f7f26d | |
Mauricio Baeza | c1e1c5ad7c | |
Mauricio Baeza | 3c574425eb | |
Mauricio Baeza | 5b8cd0ecb2 | |
Mauricio Baeza | 05a51741b6 | |
Mauricio Baeza | b3e2671359 | |
Mauricio Baeza | 5c047ee9d5 | |
Mauricio Baeza | b680d728a7 | |
Mauricio Baeza | 431422361b | |
Mauricio Baeza | 85e58c2728 | |
Mauricio Baeza | 739ac08205 | |
Mauricio Baeza | 64adba1588 | |
Mauricio Baeza | de06f706a6 | |
Mauricio Baeza | 6785dfebf7 | |
Mauricio Baeza | aaccece897 | |
Mauricio Baeza | 94f5df0723 | |
Mauricio Baeza | f813fb4dd8 | |
Mauricio Baeza | 49d5838822 | |
Mauricio Baeza | 51b4386b24 | |
Mauricio Baeza | 38f3c125cd | |
Mauricio Baeza | e678083eea | |
Mauricio Baeza | b612991476 | |
Mauricio Baeza | 32679fced3 | |
Mauricio Baeza | 8e4bf56846 | |
Mauricio Baeza | 0084197906 | |
Mauricio Baeza | ace4ef21a7 | |
Mauricio Baeza | 17fa99e45b | |
Mauricio Baeza | e547ce0113 | |
Mauricio Baeza | 858fd829aa | |
Mauricio Baeza | 788a1723b4 | |
Mauricio Baeza | 4b9df44dac | |
Mauricio Baeza | cca598310b | |
Mauricio Baeza | 39071be43b | |
Mauricio Baeza | e633f8e2bf | |
Mauricio Baeza | 21fd903943 | |
Mauricio Baeza | fec1c8523a | |
Mauricio Baeza | 0a04ec6c26 | |
Mauricio Baeza | a7945dba58 | |
Mauricio Baeza | 348a7f6ecb | |
Mauricio Baeza | cf189b08fa | |
Mauricio Baeza | 9423aeef6c | |
Mauricio Baeza | 90eec635ce | |
Mauricio Baeza | 8f15961d20 | |
Mauricio Baeza | 8a8f05384b | |
Mauricio Baeza | a54ad8d760 | |
Mauricio Baeza | 0389c0734f | |
Mauricio Baeza | cb04910a84 | |
Mauricio Baeza | 38c9c676af | |
Mauricio Baeza | e0d1f40a11 | |
Mauricio Baeza | d8ecae2c8f | |
Mauricio Baeza | 85c5a37798 | |
Mauricio Baeza | 56e52782f4 | |
Mauricio Baeza | aae856bb74 | |
Mauricio Baeza | 95399798f8 | |
Mauricio Baeza | 75e4f2e1c0 | |
Mauricio Baeza | f02b6782be | |
Mauricio Baeza | bd7508fe6b | |
Mauricio Baeza | 1f3e51861d | |
Mauricio Baeza | 3a51a50c8e | |
Mauricio Baeza | 1c6f9cd5f7 | |
Mauricio Baeza | b6c5c72df8 | |
Mauricio Baeza | 8d815bf570 | |
Mauricio Baeza | afced92aff | |
Mauricio Baeza | 443b916056 | |
Mauricio Baeza | 1d18d97b90 | |
Mauricio Baeza | 43808efc91 | |
Mauricio Baeza | 3ef195ed48 | |
Mauricio Baeza | 980a7aafdb | |
Mauricio Baeza | 1e95b180e8 | |
Mauricio Baeza | 267a96ebbc | |
Mauricio Baeza | 5e8d4d5fd9 | |
Mauricio Baeza | 8a293110ab | |
Mauricio Baeza | 4fcd7e18c0 | |
Mauricio Baeza | ed23a3531c | |
Mauricio Baeza | 82af00fedf | |
Mauricio Baeza | 763f3c21fb | |
Mauricio Baeza | 6b4eb36795 | |
Mauricio Baeza | 2877a68b92 | |
Mauricio Baeza | cca836bb6a | |
Mauricio Baeza | 26e4ce3d13 | |
Mauricio Baeza | 3166723cdc | |
Mauricio Baeza | a202f12a17 | |
Mauricio Baeza | bf3ed47fac | |
Mauricio Baeza | b2f3cdd66e | |
Mauricio Baeza | a17345c0fd | |
Mauricio Baeza | 7d7ca4eeba | |
Mauricio Baeza | d509bfb8be | |
Mauricio Baeza | 0a006f4508 | |
Mauricio Baeza | 71bd536cb1 | |
Mauricio Baeza | 068c57c018 | |
Mauricio Baeza | e94cda3577 | |
Mauricio Baeza | f102cb7c01 | |
Mauricio Baeza | 92e6a317ad | |
Mauricio Baeza | 77b35f8322 | |
Mauricio Baeza | 3f0daec6ad | |
Mauricio Baeza | a641024a86 | |
Mauricio Baeza | f9a73dfd66 | |
Mauricio Baeza | 2cc6e7046a | |
Mauricio Baeza | fe09860922 | |
Mauricio Baeza | aeb045e76a | |
Mauricio Baeza | 1c55fbd7c7 | |
Mauricio Baeza | e1b7e525df | |
Mauricio Baeza | 6f3d9cc3fa | |
Mauricio Baeza | cff2294601 | |
Mauricio Baeza | ce0e960534 | |
Mauricio Baeza | af6b13fa67 | |
Mauricio Baeza | df901090a2 | |
Mauricio Baeza | a87c78fc6b | |
Mauricio Baeza | abbb4cc608 | |
Mauricio Baeza | a67d174782 | |
Mauricio Baeza | 63ae4a5127 | |
Mauricio Baeza | 4574ff744f | |
Mauricio Baeza | 163e0f50c9 | |
Mauricio Baeza | 692477f41a | |
Mauricio Baeza | ae351b6148 | |
Mauricio Baeza | 519a61e132 | |
Mauricio Baeza | d7260a8a95 | |
Mauricio Baeza | 485dc4154e | |
Mauricio Baeza | 4bff2703a4 | |
Mauricio Baeza | 2ee2b8be43 | |
Mauricio Baeza | 6cef8fe669 | |
Mauricio Baeza | aad48639ee | |
Mauricio Baeza | 27a29d331c | |
Mauricio Baeza | 7a709f056c | |
Mauricio Baeza | 49f1c24df5 | |
Mauricio Baeza | 4a38410e8f | |
Mauricio Baeza | 2cacb786b6 | |
Mauricio Baeza | 7f33252fa1 | |
Mauricio Baeza | 79cea5b092 | |
Mauricio Baeza | 9ea396b133 | |
Mauricio Baeza | 4c2ea774b4 | |
Mauricio Baeza | da41bb272e | |
Mauricio Baeza | 8d7b42e368 | |
Mauricio Baeza | e292ba88b8 | |
Mauricio Baeza | 7e73541c96 | |
Mauricio Baeza | a0e1f83c25 | |
Mauricio Baeza | 8a4021ce49 | |
Mauricio Baeza | a4997be78d | |
Mauricio Baeza | 95d3345418 | |
Mauricio Baeza | f761ab7a5a | |
Mauricio Baeza | 5cdbb0aaa7 | |
Mauricio Baeza | c9f9ea526f | |
Mauricio Baeza | 06170cb054 | |
Mauricio Baeza | 43684b7fe9 | |
Mauricio Baeza | d48122608b | |
Mauricio Baeza | 3d300146f1 | |
Mauricio Baeza | 5ed281fd9d | |
Mauricio Baeza | dbf3a342bf | |
Mauricio Baeza | 43e643cccd | |
Mauricio Baeza | 454de7bf1e | |
Mauricio Baeza | 997dfe90ba | |
Mauricio Baeza | 812e510660 | |
Mauricio Baeza | 538a9f39ce | |
Mauricio Baeza | cf0c4b8622 | |
Mauricio Baeza | 7864e236db | |
Mauricio Baeza | 00bdd2b1a2 | |
Mauricio Baeza | 0d52e9b570 | |
Mauricio Baeza | 7dc65a2a2b | |
Mauricio Baeza | ff0ee73a73 | |
Mauricio Baeza | 2d045af037 | |
Mauricio Baeza | 4660e1d4e0 | |
Mauricio Baeza | 68ed8ee0db | |
Mauricio Baeza | 47ed1cb72e | |
Mauricio Baeza | dc8aeaebba | |
Mauricio Baeza | d2dc80a0c9 | |
Mauricio Baeza | e83281bdee | |
Mauricio Baeza | 0bddc189b4 | |
Mauricio Baeza | c2c8a10dec | |
Mauricio Baeza | 1dddbac0f2 | |
Mauricio Baeza | bbaf709e0e | |
Mauricio Baeza | c42bc817b7 | |
Mauricio Baeza | ba91d28fbd |
|
@ -2,6 +2,7 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
Pipfile*
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
@ -14,9 +15,17 @@ source/media
|
|||
# Sphinx documentation
|
||||
*.xlsx
|
||||
|
||||
docs/bk/
|
||||
bk/
|
||||
source/docs/
|
||||
site/
|
||||
vedev/
|
||||
|
||||
# Virtualenv
|
||||
.env/
|
||||
virtual/
|
||||
env
|
||||
|
||||
docs/build
|
||||
cache/
|
||||
credenciales.conf
|
||||
*.sqlite
|
||||
|
@ -24,4 +33,6 @@ credenciales.conf
|
|||
*.service
|
||||
*.orig
|
||||
rfc.db
|
||||
Dockerfile
|
||||
chuletas/
|
||||
|
||||
|
|
567
CHANGELOG.md
|
@ -1,3 +1,570 @@
|
|||
v 2.3.2 [10-Abr-2024]
|
||||
- Fix: En las mercancias en la Carta Porte al generar el PDF.
|
||||
|
||||
|
||||
v 2.3.1 [02-Abr-2024]
|
||||
- Fix: En la cantidad de la mercancia en la Carta Porte.
|
||||
|
||||
|
||||
v 2.3.0 [01-Abr-2024]
|
||||
- Mejora: Soporte para complemento Carta Porte 3.0
|
||||
- **IMPORTANTE**: Aunque no lo uses, esto afecta al JS de facturación, por
|
||||
lo que tienes que forzar el refresco (CTRL+F5) si tienes algún problema.
|
||||
|
||||
|
||||
v 2.2.0 [24-Ene-2024]
|
||||
- Mejora: Soporte para complemento Comercio Exterior 2.0
|
||||
- **IMPORTANTE**: Aunque no lo uses, esto afecta al JS de facturación, por
|
||||
lo que tienes que forzar el refresco (CTRL+F5) si tienes algún problema.
|
||||
|
||||
|
||||
v 2.1.0 [26-Dic-2023]
|
||||
- Mejora: Se agrega filtro por día en facturas.
|
||||
|
||||
|
||||
v 2.0.9 [20-Dic-2023]
|
||||
- Fix: Issue 98 y 107
|
||||
|
||||
|
||||
v 2.0.8 [30-Oct-2023]
|
||||
---------------------
|
||||
- Fix: Permitir generar CFDI de egreso para facturas globales sin datos globales.
|
||||
- Fix: Permitir cambiar la zona horaria para cuando se usa en servidor.
|
||||
|
||||
Es necesario hacer una migración:
|
||||
```
|
||||
cd /opt/empresa-libre
|
||||
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
|
||||
v 2.0.7 [06-Ju1-2023]
|
||||
---------------------
|
||||
- Fix: En tasa de retención de un Resico a una Persona Moral.
|
||||
|
||||
|
||||
v 2.0.6 [14-Jun-2023]
|
||||
---------------------
|
||||
- Fix: Al generar complementos de pago con facturas con retención de impuestos.
|
||||
|
||||
|
||||
v 2.0.5 [05-Jun-2023]
|
||||
---------------------
|
||||
- Fix: Al generar complementos de pago con facturas con varios impuestos.
|
||||
- Fix: ticket #103, hay que usar {receptor.correo_facturas} y {receptor.telefonos}
|
||||
|
||||
|
||||
v 2.0.2 [01-Abr-2023]
|
||||
---------------------
|
||||
- Fix: Obtener la clave del sat al facturar por lote.
|
||||
|
||||
|
||||
v 2.0.1 [29-Mar-2023]
|
||||
---------------------
|
||||
- Fix ticket #97
|
||||
|
||||
Es necesario hacer una migración:
|
||||
```
|
||||
cd /opt/empresa-libre
|
||||
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
|
||||
v 2.0.0 [08-Ene-2023]
|
||||
----------------------
|
||||
- Liberamos para todos la versión CFDI 4.0
|
||||
- **IMPORTANTE** NO intentes timbrar si **antes** no has validado en nuestro demo que puedes timbrar tus CFDIs habituales.
|
||||
|
||||
* IMPORTANTE:
|
||||
|
||||
Es necesario hacer una migración:
|
||||
|
||||
```
|
||||
cd /opt/empresa-libre
|
||||
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
Y reiniciar todo. IMPORTANTE: dependiendo desde que versión actualices, tal vez debas de hacer algún prodecimiento extra. Siempre revisa este historial.
|
||||
|
||||
|
||||
v 2.0.0 [31-Mar-2022]
|
||||
----------------------
|
||||
- Primera versión de timbrado con CFDI4
|
||||
- **IMPORTANTE** NO intentes timbrar si **antes** no has validado en nuestro demo que puedes timbrar tus CFDIs habituales.
|
||||
|
||||
|
||||
v 1.47.0 [28-Mar-2022]
|
||||
----------------------
|
||||
- Mejora: Soporte basico para complemento Comercio Exterior.
|
||||
|
||||
|
||||
v 1.46.5 [10-Mar-2022]
|
||||
----------------------
|
||||
- Error: Al timbrar nómina con Comercio Digital
|
||||
|
||||
|
||||
v 1.46.4 [18-Feb-2022]
|
||||
----------------------
|
||||
- Error: Issue #54
|
||||
|
||||
|
||||
v 1.46.3 [15-Feb-2022]
|
||||
----------------------
|
||||
- Error: Issue #53
|
||||
|
||||
|
||||
v 1.46.2 [31-Ene-2022]
|
||||
----------------------
|
||||
- Error: Al generar Carta Porte sin remolque.
|
||||
- Error: Al cancelar con Finkok.
|
||||
|
||||
|
||||
v 1.46.1 [29-Ene-2022]
|
||||
----------------------
|
||||
- Error: Issue #49
|
||||
- Mejora: Agregar validación para distancia en origen de Carta Porte.
|
||||
|
||||
|
||||
v 1.46.0 [27-Ene-2022]
|
||||
----------------------
|
||||
- Mejora: Issue #45
|
||||
- Mejora: Agregar tipos de persimos SCT para Carta Porte
|
||||
- Mejora: Buscar estado y municipio por CP en Carta Porte
|
||||
|
||||
|
||||
v 1.45.4 [25-Ene-2022]
|
||||
----------------------
|
||||
- Error: Al timbrar carta porte.
|
||||
- Error: Al cancelar con Comercio Digital.
|
||||
- Error: Issue #42
|
||||
- Mejora: Issue #44
|
||||
|
||||
* IMPORTANTE: Es necesario subir de nuevo tus certificados de sello, **solo** si timbras con Comercio Digital.
|
||||
|
||||
|
||||
v 1.45.3 [23-Ene-2022]
|
||||
----------------------
|
||||
- Error: El enviar por correo CFDI de pago. Ticket #40
|
||||
|
||||
|
||||
v 1.45.2 [21-Ene-2022]
|
||||
----------------------
|
||||
- Error: Al cancelar un CFDI
|
||||
|
||||
|
||||
v 1.45.1 [20-Ene-2022]
|
||||
----------------------
|
||||
- Error: Al enviar correos con la nueva configuración
|
||||
|
||||
|
||||
* IMPORTANTE: Revisa tu configuración de correo para verificar que todo funcione.
|
||||
|
||||
|
||||
v 1.45.0 [20-Ene-2022]
|
||||
----------------------
|
||||
- Importar Carta Porte desde archivo JSON
|
||||
|
||||
|
||||
v 1.44.2 [19-Ene-2022]
|
||||
----------------------
|
||||
- Agregar opción STARTTLS que requieren algunos servidores de correo
|
||||
|
||||
* IMPORTANTE: Revisa tu configuración de correo para verificar si tienes que usar esta opción.
|
||||
|
||||
|
||||
v 1.44.1 [19-Ene-2022]
|
||||
----------------------
|
||||
- Correciones en generación de Carta Porte v2.0
|
||||
- Plantilla para representación impresa de Carta Porte v2.0
|
||||
|
||||
|
||||
v 1.44.0 [10-Ene-2022]
|
||||
----------------------
|
||||
- Soporte para Carta Porte v2 con CFDI 3.3
|
||||
- Soporte para el nuevo esquema de cancelación del SAT
|
||||
|
||||
* IMPORTANTE:
|
||||
|
||||
Es necesario hacer una migración:
|
||||
|
||||
```
|
||||
cd /opt/empresa-libre
|
||||
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
|
||||
v 1.43.0 [12-Dic-2021]
|
||||
----------------------
|
||||
- Soporte para entradas de almacen.
|
||||
- Soporte para multi almacen.
|
||||
- Soporte para regenerar un ticket.
|
||||
- Soporte para movimientos entre almacenes
|
||||
- Consulta en SAT para estatus de nómina
|
||||
|
||||
* IMPORTANTE:
|
||||
|
||||
Instalar nuevo requerimiento:
|
||||
|
||||
```
|
||||
pip install segno
|
||||
```
|
||||
|
||||
Es necesario hacer una migración:
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
v 1.42.2 [14-Sep-2021]
|
||||
----------------------
|
||||
- Los productos pueden no llevar ningún impuesto.
|
||||
|
||||
|
||||
v 1.42.1 [01-Jun-2021]
|
||||
----------------------
|
||||
- Error - Ticket #5
|
||||
|
||||
|
||||
v 1.42.0 [24-May-2021]
|
||||
----------------------
|
||||
- Opción para que solo un admin pueda cancelar.
|
||||
|
||||
|
||||
v 1.41.4 [13-Abr-2021]
|
||||
----------------------
|
||||
- Error - Ticket #4
|
||||
|
||||
|
||||
v 1.41.3 [12-Feb-2021]
|
||||
----------------------
|
||||
- Error - Ticket #3
|
||||
|
||||
|
||||
v 1.41.2 [12-Feb-2021]
|
||||
----------------------
|
||||
- Error - Ticket #2
|
||||
|
||||
|
||||
v 1.41.1 [11-Feb-2021]
|
||||
----------------------
|
||||
- Error - Ticket #1
|
||||
|
||||
|
||||
v 1.41.0 [10-Feb-2021]
|
||||
----------------------
|
||||
- Importar clientes desde archivo CSV
|
||||
|
||||
|
||||
v 1.40.1 [09-Feb-2021]
|
||||
----------------------
|
||||
- Fix #422
|
||||
|
||||
|
||||
v 1.40.0 [05-ene-2021]
|
||||
----------------------
|
||||
- Error: Al parsear XML en Python 3.9+
|
||||
- Mejora: Agregar versión de Empresa Libre a plantilla.
|
||||
- Mejora: Sellado en memoria
|
||||
- Mejora: Se agrega un segundo PAC y se refactoriza el timbrado.
|
||||
|
||||
* **IMPORTANTE**
|
||||
|
||||
Es necesario seguir una serie de pasos **obligatorios** para migrar a esta
|
||||
versión, **no continues hasta seguir paso a paso** estas instrucciones.
|
||||
**Antes** de comenzar ten a la mano tus certificados de sello para timbrar, es
|
||||
necesario subirlos de nuevo. **NO actualices si no tienes tus certificados**
|
||||
con su respectiva contraseña, te quedarás sin poder timbrar.
|
||||
|
||||
Estas instrucciones solamente aplican para la maquina virtual con Ubuntu Server,
|
||||
la ultima y única versión sobre la que se dará soporte.
|
||||
|
||||
1. Entra a la parte administrativa y toma de tus credenciales de timbrado en el
|
||||
menú "Emisor" ficha "Otros Datos", usuario y token de timbrado.
|
||||
1. Agregar nuevos requerimientos `sudo apt install pkgconf libxml2-dev libxmlsec1-dev libxmlsec1-openssl`
|
||||
1. Agregar nuevo requerimiento `pip install --user xmlsec`
|
||||
1. Entrar a la carpeta del sistema: `/opt/empresa-libre`
|
||||
1. Actualizar `git pull origin master`
|
||||
1. Entrar a `source/app/controllers/pacs/comerciodigital` y copiar `conf.py.example` a `conf.py`
|
||||
1. Entrar a `source/app/controllers/pacs/finkok` y copiar `conf.py.example` a `conf.py`
|
||||
1. Agregar la variable `TOKEN = ''` al archivo `source/app/conf.py` mira el archivo de ejemplo en el mismo directorio.
|
||||
1. Reiniciar el servicio: `sudo systemctl restart empresalibre`
|
||||
1. Si no ves los cambios descritos a continuación, fuerza el refresco de tu navegador, generalmente con **CTRL+F5**
|
||||
1. Sube de nuevo tus certificados en el menú "Emisor" ficha "Certificado".
|
||||
1. Ve al menú "Opciones", ficha "Otros".
|
||||
1. Selecciona tu PAC, si tu usuario es un correo electrónico, invariablemente
|
||||
debes seleccionar Finkok. Si tienes duda ponte en contacto con nosotros.
|
||||
1. Establece las credenciales del punto 1.
|
||||
1. Guarda los datos.
|
||||
|
||||
|
||||
v 1.39.1 [17-sep-2020]
|
||||
----------------------
|
||||
- Error: Esquema para complemento IEDU
|
||||
|
||||
|
||||
v 1.39.0 [25-ago-2020]
|
||||
----------------------
|
||||
- Mejora: Consulta de las facturas de pago en el SAT
|
||||
- Mejora: Mostrar totales en nómina
|
||||
- Mejora: Mostrar totales por cantidad al facturar
|
||||
- Mejora: Validar líneas con importe cero antes de facturar
|
||||
- Error: Validaciones de namespace en CFDI de nómina por parte del PAC
|
||||
|
||||
|
||||
v 1.38.1 [30-mar-2020]
|
||||
----------------------
|
||||
- Error: En nómina al timbrar asimilados
|
||||
|
||||
|
||||
v 1.38.0 [08-mar-2020]
|
||||
----------------------
|
||||
- Mejora: Factura global por ticket o nota
|
||||
- Error: Al generar algunos PDFs
|
||||
|
||||
|
||||
v 1.37.0 [02-mar-2020]
|
||||
----------------------
|
||||
- Mejora: Soporte para complemento Leyendas Fiscales
|
||||
|
||||
* IMPORTANTE:
|
||||
|
||||
Es necesario hacer una migración, y agregar los campos necesarios para mostrar
|
||||
las leyendas, mira la carpeta pública con la plantilla de ejemplo.
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m -r RFC
|
||||
```
|
||||
|
||||
|
||||
v 1.36.0 [25-feb-2020]
|
||||
----------------------
|
||||
- Mejora: Permitir ver a usuarios todos los documentos
|
||||
|
||||
|
||||
v 1.35.0 [24-feb-2020]
|
||||
----------------------
|
||||
- Mejora: Filtrado de documentos por sucursal
|
||||
|
||||
|
||||
v 1.34.1 [09-feb-2020]
|
||||
----------------------
|
||||
- Error: Al timbrar nómina de asimilados
|
||||
|
||||
|
||||
v 1.34.0 [29-ene-2020]
|
||||
----------------------
|
||||
- Error: Al timbrar nómina
|
||||
|
||||
|
||||
v 1.33.2 [27-ene-2020]
|
||||
----------------------
|
||||
- Se actualizan métodos de pago en catálogos del SAT
|
||||
|
||||
* IMPORTANTE:
|
||||
Es necesario actualizar los catálogos del SAT
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -us
|
||||
```
|
||||
|
||||
|
||||
v 1.33.0 [22-ene-2020]
|
||||
----------------------
|
||||
- Mejora: Cambio del mensaje para cuando se intenta dar de alta un cliente ya existente.
|
||||
- Mejora: Solo los admins pueden ver la nómina.
|
||||
- Se agrega un segundo PAC
|
||||
- Se actualizan los catálogos del SAT
|
||||
|
||||
* IMPORTANTE:
|
||||
Es necesario actualizar los catálogos del SAT
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -us
|
||||
```
|
||||
|
||||
|
||||
v 1.32.0 [05-ene-2020]
|
||||
----------------------
|
||||
- Mejora: Recuperar facturas no aceptadas para cancelación por el receptor
|
||||
|
||||
|
||||
v 1.31.2 [28-oct-2019]
|
||||
----------------------
|
||||
- Error: Al generar PDF con tags en las series
|
||||
|
||||
|
||||
v 1.31.1 [28-ago-2019]
|
||||
----------------------
|
||||
- Error: Al agregar nuevo impuesto #369
|
||||
|
||||
|
||||
v 1.31.0 [23-abr-2019]
|
||||
----------------------
|
||||
- Error: Validar cantidad o valor unitario cero en tickets
|
||||
- Mejora: Envío de nómina por correo al empleado
|
||||
|
||||
|
||||
v 1.30.0 [24-mar-2019]
|
||||
----------------------
|
||||
- Mejora: Se actualizan los catálogos de Nómina
|
||||
|
||||
* IMPORTANTE:
|
||||
Es necesario realizar una migración, después de actualizar.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
v 1.29.0 [08-mar-2019]
|
||||
----------------------
|
||||
- Error: Al mostrar detalle en facturas importadas #343
|
||||
- Mejora: Editar la descripción de un movimiento bancario.
|
||||
|
||||
|
||||
v 1.28.2 [04-mar-2019]
|
||||
----------------------
|
||||
- Error: Al enviar facturas de pago
|
||||
|
||||
|
||||
v 1.28.1 [04-mar-2019]
|
||||
----------------------
|
||||
- Error: Al buscar facturas por fechas
|
||||
|
||||
|
||||
v 1.28.0 [17-feb-2019]
|
||||
----------------------
|
||||
- Mejora: Manejo de empaques para mensajeria
|
||||
- Mejora: Usar concepto personalizado en deducciones de nómina 004 Otros
|
||||
- Mejora: Búsqueda en notas
|
||||
- Mejora: Soporte para el complemento de Divisas
|
||||
- Mejora: Descarga de nómina en lote
|
||||
|
||||
* IMPORTANTE:
|
||||
Es necesario realizar una migración, después de actualizar.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
Es necesario agregar un nuevo requerimiento.
|
||||
|
||||
```
|
||||
sudo pip install cryptography
|
||||
```
|
||||
|
||||
**IMPORTANTE** Si envias tus facturas por correo directamente, es necesario
|
||||
volver a capturar la contraseña de tu servidor de correo y guardar los datos
|
||||
nuevamente.
|
||||
|
||||
|
||||
v 1.27.1 [23-ene-2019]
|
||||
----------------------
|
||||
- Error: Al cancelar nómina
|
||||
|
||||
|
||||
v 1.27.0 [17-ene-2019]
|
||||
----------------------
|
||||
- Mejora: Permitir capturar folio manualmente
|
||||
|
||||
|
||||
v 1.26.1 [16-ene-2019]
|
||||
----------------------
|
||||
- Error: Guardar logos de emisor
|
||||
|
||||
|
||||
v 1.26.0 [15-ene-2019]
|
||||
----------------------
|
||||
- Mejora: Generar PDF desde plantilla JSON
|
||||
|
||||
|
||||
v 1.25.0 [07-nov-2018]
|
||||
----------------------
|
||||
- Fix: Al importar días de pago en nómina
|
||||
- Fix: Al importar descripción en factura en lote
|
||||
|
||||
|
||||
v 1.24.0 [06-nov-2018]
|
||||
----------------------
|
||||
- Fix: Consulta estatus SAT
|
||||
|
||||
|
||||
v 1.23.0 [30-oct-2018]
|
||||
----------------------
|
||||
- Mejora: Permitir importar CFDI 3.2
|
||||
|
||||
|
||||
v 1.22.0 [25-oct-2018]
|
||||
----------------------
|
||||
- Mejora: Permitir cambiar descripción al facturar en lote
|
||||
|
||||
|
||||
v 1.21.2 [23-oct-2018]
|
||||
----------------------
|
||||
- Error unicode
|
||||
|
||||
|
||||
v 1.21.1 [14-oct-2018]
|
||||
----------------------
|
||||
- Error #307
|
||||
|
|
|
@ -13,9 +13,3 @@
|
|||
* Propon nuevas funcionalidades
|
||||
* Difunde Empresa Libre
|
||||
|
||||
|
||||
### Prioridades
|
||||
|
||||
1. [X] Darle continuidad a la facturación de los clientes
|
||||
2. Cubrir todas las funcionalidades que cubre ahora Factura Libre
|
||||
3. Agregar nuevas funcionalidades
|
||||
|
|
41
README.md
|
@ -10,26 +10,51 @@ Este proyecto está en continuo desarrollo, contratar un esquema de soporte,
|
|||
nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para
|
||||
contratar: administracion ARROBA empresalibre.net
|
||||
|
||||
#### Ahora también puede aportar con Bitcoin Cash (BCH):
|
||||
#### Ahora también puede aportar con criptomonedas:
|
||||
|
||||
`pq763fj7kxxf2wtf360lfsy5ydw84yz72q76hanhxq`
|
||||
G1: `A5DdXxCKPw3QKWVdDVs7CzkNugNUW1sHu5zDJFWxCU2h`
|
||||
BCH: `qztd3l00xle5tffdqvh2snvadkuau2ml0uqm4n875d`
|
||||
|
||||
|
||||
### Requerimientos:
|
||||
## Requerimientos:
|
||||
|
||||
* Servidor web, recomendado Nginx
|
||||
* uwsgi
|
||||
* python3
|
||||
* python 3.8
|
||||
* xsltproc
|
||||
* openssl
|
||||
* xmlsec
|
||||
|
||||
* Ubuntu 20.04
|
||||
|
||||
```
|
||||
apt install pkg-config libxml2-dev libxmlsec1-dev libxmlsec1-openssl xsltproc
|
||||
```
|
||||
|
||||
Debería de funcionar con cualquier combinación servidor-wsgi que soporte
|
||||
aplicaciones Python.
|
||||
|
||||
El sistema tiene soporte para tres bases de datos: SQLite, MySQL y PostgreSQL
|
||||
(recomendado), debes de instalar el servidor de la base de datos y sus drivers
|
||||
respectivos, excepto SQLite que es nativo en Python.
|
||||
El sistema tiene soporte solo para PostgreSQL, debes de instalar el servidor de
|
||||
la base de datos y su driver respectivo.
|
||||
|
||||
## Configuración para desarrollo local
|
||||
|
||||
* Crea un entorno virtual con python 3.8 y actívalo. Por ejemplo con virtualenv:
|
||||
|
||||
virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
* Instala las dependencias
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
* Copia y ajusta algunos archivos necesarios.
|
||||
- `source/app/conf.py`
|
||||
- `source/app/controllers/pacs/comerciodigital/conf.py.example`
|
||||
- `source/app/controllers/pacs/finkok/conf.py.example`
|
||||
* Finalmente ejecutamos la aplicación. Para esto vamos a necesitar un servidor
|
||||
wsgi como uwsgi:
|
||||
|
||||
pip install uwsgi
|
||||
cd source/app
|
||||
uwsgi main_debug.ini
|
||||
* Ahora puedes ver la aplicación en http://localhost:8000
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[ ] Permitir más de un remolque en la Carta Porte
|
||||
[ ] Campos de captura de Comercio Exterior
|
||||
[ ] Representación impresa de Comercio Exterior
|
|
@ -1,81 +0,0 @@
|
|||
## Administración del sistema
|
||||
|
||||
<BR>
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Se requiere un usuario con derechos de <b>administración</b> para usar esta área
|
||||
</div>
|
||||
|
||||
<BR><BR>
|
||||
|
||||
Para acceder al área administrativa, presiona el icono de los engranes en la
|
||||
esquina superior derecha. Si no lo ves, tu usuario no tiene derechos administrativos.
|
||||
|
||||
![Acceso al admin](img/02/admin_001.png)
|
||||
|
||||
### Agregar cuenta bancaria
|
||||
|
||||
Para agregar una cuenta bancaria, primero hay que seleccionar el banco o bancos
|
||||
a usar, esto se hace en ***Catalogos SAT***, pestaña ***Bancos***. Por default
|
||||
veras primero los bancos ya seleccionados. Marca la casilla de verificación de
|
||||
cada banco que deses tener disponible al agregar las cuentas bancarias. Usa la
|
||||
barra de desplazamiento de la tabla o la rueda de tu ratón para ver más bancos.
|
||||
Cada banco que selecciones, también estará disponible en la sección ***Clientes
|
||||
y Proveedores***.
|
||||
|
||||
<BR>
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Si vas a agregar los datos bancarios en las ***Facturas de pago***, es
|
||||
necesario capturar el RFC de cada banco seleccionado.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
Para editar el RFC, solo da clic en la celda correspondiente y captura
|
||||
correctamente el RFC del banco, para guardar presionas ***Enter***. El sistema
|
||||
solo válida que sea un RFC válido, por lo que **asegurate de que sea el RFC
|
||||
correcto para cada banco**.
|
||||
|
||||
![Seleccionar bancos](img/02/admin_002.png)
|
||||
|
||||
Ahora, desde la sección ***Emisor***, pestaña ***Cuentas de Banco***, captura
|
||||
los siguiente campos, todos son requeridos:
|
||||
|
||||
* **Nombre**: Un nombre para identificar la cuenta, puedes usar cualquier texto
|
||||
* **Banco**: Solo veras los bancos previamente seleccionados
|
||||
* **Cuenta**: El número de cuenta
|
||||
* **CLABE**: La clave interbancaria, se valida que este correcta.
|
||||
* **Moneda**: Solo veras las monedas seleccionadas del Catalogo del SAT
|
||||
* **Fecha de apertura**: La fecha de apertura de la cuenta
|
||||
* **Saldo inicial**: Saldo inicial **a partir de donde se empezará a llevar el control de movimientos**
|
||||
* **Fecha saldo inicial**: La fecha del saldo inicial. **No puedes capturar movimientos anteriores a esta fecha**
|
||||
|
||||
![Agregar cuenta bancaria](img/02/admin_003.png)
|
||||
|
||||
Usa el botón de comando **Agregar cuenta** para agregarla al sistema. Si todos
|
||||
los campos son correctos, deberá agregarse al listado inferior.
|
||||
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Solo puedes eliminar cuentas bancarias que no tengan movimientos capturados.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
### Complementos
|
||||
|
||||
Los complementos, son caracteristicas especiales dictadas por el SAT que se
|
||||
pueden agregar a cada CFDI generado.
|
||||
|
||||
<BR>
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Consulta con tu departamente contable los requerimientos y caracteristicas de cada uno.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
Todos se encuentran en la sección ***Opciones***, pestaña ***Otros***. Ve a la
|
||||
subsección ***Complementos*** y activa solo los que requieras.
|
||||
|
||||
![Agregar cuenta bancaria](img/02/admin_004.png)
|
||||
|
||||
* **INE**: Para CFDI emitidos a partidos politicos
|
||||
* **EDU**: Solo para escuelas incorporadas a la SEP
|
||||
* **Pagos**: Para generar facturas de pago.
|
||||
* La serie predeterminada es **FP**, puedes personalizar la que quieras, capturas y presionas **ENTER** para guardar el valor.
|
||||
* El folio predeterminado es **1**, puedes personalizar el que quieras, capturas y presionas **ENTER** para guardar el valor.
|
|
@ -1,309 +0,0 @@
|
|||
## Bancos
|
||||
|
||||
|
||||
<BR>
|
||||
|
||||
Esta herramienta nos permite llevar el control de ingresos y egresos de una o
|
||||
más cuentas bancarias. También nos permite generar el CFDI (factura) de pago.
|
||||
|
||||
Primero, es necesario [dar de alta una cuenta bancaria][1]
|
||||
|
||||
![Bancos Principal](img/03/bancos_001.png)
|
||||
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Asegurate siempre de agregar solo movimientos conciliados con tu estado de
|
||||
cuenta bancario y de, en la medida de lo posible, agregarlos en orden.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
### Fecha y saldo inicial
|
||||
|
||||
Estos valores estarán determinados por los que hayas capturado al [dar de alta
|
||||
la cuenta bancaria][1] en el área administrativa. Mientras una cuenta no tenga
|
||||
movimientos, aparte del saldo inicial, es posible eliminarla desde
|
||||
administración.
|
||||
|
||||
![Saldo inicial](img/03/bancos_002.png)
|
||||
|
||||
|
||||
<BR>
|
||||
### Agregar depósito sin facturas relacionadas
|
||||
|
||||
Con el botón de comando ***+ Depósito***, agregamos un nuevo depósito. ![+](img/03/bancos_003.png)
|
||||
|
||||
Todos los campos con asterisco rojo son requeridos.
|
||||
|
||||
* **Fecha**: La fecha en que fue realizado el depósito.
|
||||
* **Hora**: La hora en que fue realizado el depósito.
|
||||
* **Referencia**: Referencia del depósito.
|
||||
* **Forma de pago**: Selecciona de la lista la forma de pago.
|
||||
* **Importe**: Captura el importe del depósito.
|
||||
* **Descripción**: El detalle del concepto de este depósito.
|
||||
|
||||
![Agregar depósito](img/03/bancos_004.png)
|
||||
|
||||
Guarda con el botón de comando ***+ Guardar Depósito***. Se te preguntara para
|
||||
confirmar la acción. **Asegurate que todos los datos sean correctos**.
|
||||
|
||||
![Confirmar](img/03/bancos_005.png)
|
||||
|
||||
Si todo esta bien, el nuevo movimiento será agregado a la cuenta y actualizado
|
||||
el saldo de la misma.
|
||||
|
||||
![Nuevo depósito](img/03/bancos_006.png)
|
||||
|
||||
<div class="alert-box warning"><span>TIP: </span>
|
||||
La hora de los depósitos se usa al generar una Factura de Pago. El SAT, en
|
||||
caso de no saber la hora, permite usar el medío día para todos (12:00:00).
|
||||
<B>Resiste la "comodidad" de usar esto y asegurate de usar una hora diferente
|
||||
para cada movimiento.</B>
|
||||
</div>
|
||||
|
||||
|
||||
<BR>
|
||||
### Agregar retiro
|
||||
|
||||
Con el botón de comando ***- Retiro***, agregamos un nuevo retiro.
|
||||
|
||||
Todos los campos con asterisco rojo son requeridos.
|
||||
|
||||
* **Fecha**: La fecha en que fue realizado el retiro.
|
||||
* **Hora**: La hora en que fue realizado el retiro.
|
||||
* **Referencia**: Referencia del retiro.
|
||||
* **Forma de pago**: Selecciona de la lista la forma de pago.
|
||||
* **Importe**: Captura el importe del retiro.
|
||||
* **Descripción**: El detalle del concepto de este retiro.
|
||||
|
||||
![Agregar retiro](img/03/bancos_007.png)
|
||||
|
||||
Guarda con el botón de comando ***- Guardar Retiro***. Se te preguntara para
|
||||
confirmar la acción. **Asegurate que todos los datos sean correctos**.
|
||||
|
||||
![Confirmar retiro](img/03/bancos_008.png)
|
||||
|
||||
Si todo esta bien, el nuevo movimiento será agregado a la cuenta y actualizado
|
||||
el saldo de la misma.
|
||||
|
||||
![Nuevo retiro](img/03/bancos_009.png)
|
||||
|
||||
<div class="alert-box warning"><span>TIP: </span>
|
||||
Recuerda: <B>Resiste la "comodidad" de usar la misma hora para todos los
|
||||
movimientos.</B>
|
||||
</div>
|
||||
|
||||
|
||||
<BR>
|
||||
### Pagar factura sin relacionar a un depósito
|
||||
|
||||
Usa el botón de comando ***+ Depósito***. En la tabla ***Facturas por pagar***,
|
||||
veras las facturas pendientes de pago. Selecciona la correcta y da clic en el
|
||||
botón de comando ***Solo marcar pagada***.
|
||||
|
||||
![Nuevo retiro](img/03/bancos_010.png)
|
||||
|
||||
* No captures ningún dato del formulario, solo selecciona la factura a marcar como pagada.
|
||||
* **IMPORTANTE**: Solo marca facturas pagadas que **estes 100% seguro que NO requeriras generarle Factura de pago** y que no esten relacionadas con ningún depósito.
|
||||
|
||||
Al dar clic en el botón de comando ***Solo marcar pagada***, debes confirmar la
|
||||
acción.
|
||||
|
||||
![Confirmar pago](img/03/bancos_011.png)
|
||||
|
||||
Si todo esta bien, la factura será retirada de la tabla ***Facturas por pagar***,
|
||||
y recibiras una notificación de confirmación.
|
||||
|
||||
![Confirmar pago](img/03/bancos_012.png)
|
||||
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Marca <B>solo como pagada</b> una factura, al no estar relacionada con
|
||||
ningún depósito, no afecta al saldo de la cuenta bancaria, pero <b>si a la
|
||||
cuenta del cliente.</b>
|
||||
</div>
|
||||
|
||||
|
||||
<BR>
|
||||
### Agregar depósito relacionando facturas
|
||||
|
||||
Con el botón de comando ***+ Depósito***, agregamos un nuevo depósito.
|
||||
|
||||
Todos los campos con asterisco rojo son requeridos.
|
||||
|
||||
* **Fecha**: La fecha en que fue realizado el depósito.
|
||||
* **Hora**: La hora en que fue realizado el depósito. **Usa diferente hora para cada movimiento**
|
||||
* **Referencia**: Referencia del depósito.
|
||||
* **Forma de pago**: **Es importante selecciones el valor correcto, si vas a generar Factura de Pago.**
|
||||
* **Importe**: **NO captures este valor**
|
||||
* **Descripción**: **NO captures este valor**
|
||||
|
||||
![Agregar depósito](img/03/bancos_013.png)
|
||||
|
||||
Para relacionar las facturas pagadas en este depósito, ubica las facturas
|
||||
correctas y arrastralas a la tabla ***Facturas a pagar en este depósito***,
|
||||
también puede usar un doble clic para esto. Puedes agregar una o más facturas,
|
||||
conforme las vas agregando, el sistema sumara sus respectivos importes y
|
||||
actualizará la descripción del depósito. La descripción puedes editarla
|
||||
libremente, pero **no cambies el importe del depósito manualmente**.
|
||||
|
||||
![Agregar depósito](img/03/bancos_014.png)
|
||||
|
||||
Guarda con el botón de comando ***+ Guardar Depósito***. Se te preguntara para
|
||||
confirmar la acción. **Asegurate que todos los datos sean correctos** y de que
|
||||
la cantidad y las facturas relacionadas son correctas.
|
||||
|
||||
![Confirmar](img/03/bancos_015.png)
|
||||
|
||||
Si todo esta bien, el nuevo movimiento será agregado a la cuenta y actualizado
|
||||
el saldo de la misma.
|
||||
|
||||
![Nuevo depósito](img/03/bancos_016.png)
|
||||
|
||||
<div class="alert-box warning"><span>REITERAMOS: </span>
|
||||
La hora de los depósitos se usa al generar una Factura de Pago. El SAT, en
|
||||
caso de no saber la hora, permite usar el medío día para todos (12:00:00).
|
||||
<B>Resiste la "comodidad" de usar esto y asegurate de usar una hora diferente
|
||||
para cada movimiento.</B>
|
||||
</div>
|
||||
|
||||
|
||||
<BR>
|
||||
### Cancelar un movimiento.
|
||||
|
||||
En cualquier momento puedes cancelar un movimiento con el botón de comando
|
||||
***Cancelar***. Es mucho mejor te asegures de capturar movimientos solo
|
||||
confirmados y conciliados, en vez de estar eliminando movimientos. Cuando se
|
||||
elimina un movimiento, no importa si es retiro o depósito, el saldo de la cuenta
|
||||
se actualiza a partir de la fecha del movimiento eliminado y hasta el más
|
||||
reciente.
|
||||
|
||||
* Si es un depósito y tiene facturas relacionadas, las mismas estarán de nuevo
|
||||
marcadas como **Por pagar** y disponibles para relacionarse con otro depósito.
|
||||
* Puedes eliminar un depósito aún si este tiene generada una Factura de Pago. La
|
||||
misma quedará ***huerfana***, pero disponible desde el listado de Facturas de
|
||||
Pago elaboradas.
|
||||
|
||||
![Cancelar movimiento](img/03/bancos_017.png)
|
||||
|
||||
|
||||
<BR>
|
||||
### Generar Factura de Pago
|
||||
|
||||
Para usar esta herramienta, debe estar activado el uso del [complemento de pago][2]
|
||||
dentro de administración. Si no ves en ***Bancos*** el botón de comando ***Generar
|
||||
Factura de Pago***, solicita a un administrador que lo active.
|
||||
|
||||
![Factura de pago](img/03/bancos_018.png)
|
||||
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Asegurate de revisar que todos tus movimientos este correctamente capturados
|
||||
y las facturas correctas relacionadas con cada depósito, antes de usar esta
|
||||
herramienta.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
|
||||
* Solo puedes generar ***Facturas de pago*** de movimientos de depósito que tengan
|
||||
facturas relacionadas.
|
||||
* Selecciona el depósito correcto y presiona el botón de comando ***Generar Factura de Pago***.
|
||||
|
||||
En la siguiente pantalla, **verifica una vez más que todos los datos con correctos**.
|
||||
En este momento puedes cerrar e incluso cancelar el movimiento para hacer cualquier
|
||||
corrección. Una vez generada la ***Factura de Pago***, no podrás hacer ningún
|
||||
cambio. Todos los datos mostrados son de solo lectura, tanto los datos del
|
||||
depósito como las facturas relacionadas para que, reiteramos, verifiques que
|
||||
todos los datos son correctos.
|
||||
|
||||
![Factura de pago](img/03/bancos_019.png)
|
||||
|
||||
**En cuanto estes 100% seguro de que todos los datos son correctos**, usa el
|
||||
botón de comando ***Timbrar***, para enviar a timbrar la ***Factura de Pago***.
|
||||
Se te pedirá una confirmación de esta acción.
|
||||
|
||||
![Factura de pago](img/03/bancos_020.png)
|
||||
|
||||
Si todo esta bien, el sistema: guardará, generará y timbrará la nueva ***Factura
|
||||
de Pago***.
|
||||
|
||||
![Factura de pago](img/03/bancos_021.png)
|
||||
|
||||
Con un clic en el icono ***PDF***, puedes generar el PDF respectivo de esta
|
||||
***Factura de Pago***. Como con las plantillas de las facturas, puedes
|
||||
personalizar completamente esta plantilla.
|
||||
|
||||
![Factura de pago](img/03/bancos_022.png)
|
||||
|
||||
<BR>
|
||||
### Facturas de pago con facturas en otras monedas
|
||||
|
||||
Cuando el sistema detecta que se están usando más de una moneda, automáticamente
|
||||
te mostrará las columnas del ***Total*** y la ***Moneda*** respectiva.
|
||||
|
||||
![Factura de pago](img/03/bancos_023.png)
|
||||
|
||||
Al relacionar la factura, observa que tomará el valor original en la moneda del
|
||||
documento.
|
||||
|
||||
![Factura de pago](img/03/bancos_024.png)
|
||||
|
||||
Los siguientes pasos **son importantes y en este orden**, los campos que debes de modificar en la
|
||||
tabla ***Facturas a pagar en este depósito*** son:
|
||||
|
||||
* **Este pago**: Si el valor pagado es el total de la factura, no lo modifiques,
|
||||
si es parcial, captura el valor pagado **en la moneda del documento**, en este
|
||||
ejemplo, en UDS. Este valor se usa para el estado de cuenta de la factura.
|
||||
* **Este Pago MXM**: Captura el valor pagado en moneda nacional de este documento.
|
||||
En este ejemplo, sería el valor **real** del depósito. Este valor se usará para
|
||||
la ***Factura de pago***, para el estado de cuenta y para el saldo del cliente.
|
||||
* **T.C.**: Al capturar correctamente los dos valores anteriores, el sistema te
|
||||
calculará el tipo de cambio (***T.C***) usado. Este valor se usa para la ***Factura de pago***.
|
||||
|
||||
Ahora, nuestros datos deben verse así:
|
||||
|
||||
![Factura de pago](img/03/bancos_025.png)
|
||||
|
||||
Guarda con el botón de comando ***+ Guardar Depósito***. Se te preguntara para
|
||||
confirmar la acción. **Asegurate que todos los datos sean correctos** y de que
|
||||
la cantidad y las facturas relacionadas son correctas.
|
||||
|
||||
![Factura de pago](img/03/bancos_026.png)
|
||||
|
||||
Si todo esta bien, el nuevo movimiento será agregado a la cuenta y actualizado
|
||||
el saldo de la misma.
|
||||
|
||||
Ahora si, puedes generar la ***Factura de Pago*** de este depósito.
|
||||
|
||||
![Factura de pago](img/03/bancos_027.png)
|
||||
|
||||
|
||||
<BR>
|
||||
### Cuenta de banco y facturas relacionadas en otras monedas
|
||||
|
||||
Cuando la cuenta de banco no este en M.N (MXN) y los documentos relacionados
|
||||
esten en la misma moneda de la cuenta de banco, el procedimiento para generar
|
||||
la factura de pago es:
|
||||
|
||||
Los campos que debes de modificar en la tabla ***Facturas a pagar en este depósito*** son:
|
||||
|
||||
* **Este pago**: Si el valor pagado es el total de la factura, no lo modifiques, si es parcial, captura el valor pagado **en la moneda del documento**, en este ejemplo, en UDS. Este valor se usa para el estado de cuenta de la factura.
|
||||
* **Este Pago USD**: Captura el valor pagado en la moneda de la cuenta bancaria. En este ejemplo, sería el valor **real** del depósito. Este valor se usará para la ***Factura de pago***.
|
||||
* **T.C.**: Siempre debe ser 1.00 al ser la misma moneda de la cuente bancaria y del documento.
|
||||
|
||||
Ahora, el tipo de cambio del movimiento, **si debes de capturarlo**, es el tipo de
|
||||
cambio de la moneda de la cuenta en relación con la M.N (MXN). Este valor se usará
|
||||
para la ***Factura de pago***.
|
||||
|
||||
![Factura de pago](img/03/bancos_028.png)
|
||||
|
||||
Guarda con el botón de comando ***+ Guardar Depósito***. Se te preguntara para
|
||||
confirmar la acción. **Asegurate que todos los datos sean correctos** y de que
|
||||
la cantidad y las facturas relacionadas son correctas.
|
||||
|
||||
Si todo esta bien, el nuevo movimiento será agregado a la cuenta y actualizado
|
||||
el saldo de la misma.
|
||||
|
||||
Ahora si, puedes generar la ***Factura de Pago*** de este depósito.
|
||||
|
||||
![Factura de pago](img/03/bancos_029.png)
|
||||
|
||||
|
||||
[1]: /administracion/#agregar-cuenta-bancaria
|
||||
[2]: /administracion/#complementos
|
|
@ -1,30 +0,0 @@
|
|||
.alert-box {
|
||||
color: #555;
|
||||
border-radius: 10px;
|
||||
font-family: Tahoma,Geneva,Arial,sans-serif;
|
||||
font-size: 15px;
|
||||
padding: 10px 10px 10px 36px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.alert-box span {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.error {
|
||||
background:#ffecec url('../img/error.png') no-repeat 10px 50%;
|
||||
border:1px solid #f5aca6;
|
||||
}
|
||||
.success {
|
||||
background:#e9ffd9 url('../img/success.png') no-repeat 10px 50%;
|
||||
border:1px solid #a6ca8a;
|
||||
}
|
||||
.warning {
|
||||
background:#fff8c4 url('../img/warning.png') no-repeat 10px 50%;
|
||||
border:1px solid #f2c779;
|
||||
}
|
||||
.notice {
|
||||
background:#e3f7fc url('../img/notice.png') no-repeat 10px 50%;
|
||||
border:1px solid #8ed9f6;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
## Guía de Usuario
|
||||
|
||||
|
||||
<BR>
|
||||
|
||||
* Cliente y Proveedores
|
||||
* Productos y Servicios
|
||||
* [Bancos](bancos.md)
|
||||
* Facturas
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 476 B |
Before Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 483 B |
|
@ -1,36 +0,0 @@
|
|||
## Bienvenido a la documentación de Empresa Libre
|
||||
|
||||
<div style="text-align: right">
|
||||
<BR><B>
|
||||
En cada relación comercial,<BR>
|
||||
existe una relación humana.</B>
|
||||
<BR>
|
||||
<BR>
|
||||
<BR>
|
||||
</div>
|
||||
|
||||
**Empresa Libre** es un sistema de facturación electrónica especificamente
|
||||
diseñado para la legislación mexicana. **Empresa Libre** es totalmente
|
||||
[software libre][1].
|
||||
|
||||
### Contenido
|
||||
|
||||
1. [Instalación y configuración](instalacion.md)
|
||||
1. [Administración del sistema](administracion.md)
|
||||
1. [Guía de usuario](guiadeusuario.md)
|
||||
1. [Bancos](bancos.md)
|
||||
1. [Preguntas más frecuentes](preguntas.md)
|
||||
|
||||
|
||||
### Como ayudar
|
||||
|
||||
* Usa **Empresa Libre**
|
||||
* Reportando errores
|
||||
* Escribe documentación
|
||||
* Graba videos de uso
|
||||
* Propon mejoras
|
||||
* Conviertete en **patrocinador**
|
||||
|
||||
|
||||
|
||||
[1]: https://www.gnu.org/philosophy/free-sw.es.html
|
|
@ -1,250 +0,0 @@
|
|||
## Instalación de Empresa Libre
|
||||
|
||||
|
||||
<BR>
|
||||
### Actualización
|
||||
|
||||
<BR>
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Es <b>muy importante</b> y es <b>tu responsabilidad</b> mantener siempre actualizado y al día el sistema.
|
||||
</div>
|
||||
|
||||
<BR><BR>
|
||||
Mantener al día el sistema te permite tener siempre los ultimos cambios realizados
|
||||
en el, recibir las correcciones de errores y las mejoras que vamos incorporando.
|
||||
|
||||
Siempre consulta el [historial de cambios](/notas) para saber si hay algún proceso
|
||||
extra que seguir al actualizar. Es muy importante que revises cada nota desde
|
||||
tu versión actual y hasta la más reciente.
|
||||
|
||||
<BR>
|
||||
Al iniciar el sistema, debes de ver la versión actual del mismo. Si no la ves,
|
||||
es que tienes una versión demasiado desactualizada, es importante actualices a
|
||||
la brevedad.
|
||||
|
||||
![Versión del sistema](img/01/install_001.png)
|
||||
|
||||
<BR>
|
||||
<div class="alert-box notice"><span>TIP: </span>
|
||||
Para todas las instrucciones siguientes, se asume que son para nuestra
|
||||
maquina virtual (MV), las rutas pueden cambiar si has personalizado tu
|
||||
propia MV o estas ejecutando Empresa Libre en un servidor propio.
|
||||
</div>
|
||||
|
||||
<BR>
|
||||
Ya dentro del sistema, el proceso para actualizar es:
|
||||
|
||||
1. Entra a la carpeta del proyecto.
|
||||
|
||||
┌─[empresalibre-][empresa]->{~}
|
||||
└──> cd proyectos/empresa-libre
|
||||
┌─[empresalibre-][empresa]->{~/proyectos/empresa-libre}
|
||||
└──>
|
||||
|
||||
1. Actualiza la rama `master` del repositorio. El resultado variará, dependiendo desde la versión que estes actualizando, entre más vieja sea, veras más archivos modificados.
|
||||
|
||||
└──> git pull origin master
|
||||
remote: Enumerating objects: 197, done.
|
||||
remote: Counting objects: 100% (197/197), done.
|
||||
remote: Compressing objects: 100% (93/93), done.
|
||||
remote: Total 197 (delta 135), reused 146 (delta 99)
|
||||
Recibiendo objetos: 100% (197/197), 131.71 KiB | 956.00 KiB/s, listo.
|
||||
Resolviendo deltas: 100% (135/135), listo.
|
||||
Desde https://gitlab.com/mauriciobaeza/empresa-libre
|
||||
* branch master -> FETCH_HEAD
|
||||
f0ab924..18b1880 master -> origin/master
|
||||
Actualizando f0ab924..18b1880
|
||||
Fast-forward
|
||||
CHANGELOG.md | 44 ++++++++
|
||||
VERSION | 2 +-
|
||||
source/app/controllers/main.py | 3 +-
|
||||
source/app/controllers/util.py | 35 +++++-
|
||||
source/app/models/db.py | 23 +++-
|
||||
source/app/models/main.py | 278 ++++++++++++++++++++++++++++++++++++++--------
|
||||
source/app/settings.py | 3 +-
|
||||
source/static/css/app.css | 7 ++
|
||||
source/static/js/controller/admin.js | 44 +++++++-
|
||||
source/static/js/controller/bancos.js | 89 +++++++++++++--
|
||||
source/static/js/controller/partners.js | 163 ++++++++++++++++++---------
|
||||
source/static/js/controller/util.js | 1 +
|
||||
source/static/js/ui/admin.js | 18 ++-
|
||||
source/static/js/ui/bancos.js | 9 +-
|
||||
source/static/js/ui/main.js | 5 +-
|
||||
source/static/js/ui/partners.js | 28 ++++-
|
||||
source/templates/base.html | 2 +-
|
||||
17 files changed, 626 insertions(+), 128 deletions(-)
|
||||
|
||||
1. Si las [notas de lanzamiento](/notas) te indican que debes de hacer algún otro proceso, generalmente será que migres la base de datos.
|
||||
1. ¿Cuando debo migrar la base de datos?
|
||||
1. Si te lo indica la **ultima** nota de lanzamiento
|
||||
1. Si te lo indica alguna nota intermedia desde tu versión actual y hasta la ultima.
|
||||
1. Si **no ves** la versión de **Empresa Libre** al inicio del sistema
|
||||
1. Si **no** estas seguro desde que versión estas actualizando.
|
||||
* Para migrar entra a la siguiente carpeta
|
||||
|
||||
└──> cd source/app/models/
|
||||
┌─[empresalibre-][empresa]->{~/proyectos/empresa-libre/source/app/models}
|
||||
└──>
|
||||
|
||||
* **IMPORTANTE:** siempre saca un respaldo de tu base de datos **antes de migrar**
|
||||
|
||||
└──> python main.py -bk
|
||||
[21-Sep-2018 23:34:08] INFO: API: Generando backup de: LAN7008173R5
|
||||
[21-Sep-2018 23:34:09] INFO: API: Backup generado de LAN7008173R5
|
||||
[21-Sep-2018 23:34:09] INFO: API: Sin datos para sincronización particular de lan7008173r5.bk
|
||||
|
||||
* Si en vez del mensaje anterior, ves un mensaje de error como el siguiente. Mira en [errores más comúnes][1] para arreglarlo primero. Una vez resuelto, vuelve a ejecutar el comando anterior. **Verifica siempre estar en la carpeta correcta**.
|
||||
|
||||
File "/usr/lib/python3.7/site-packages/requests/adapters.py", line 513, in send
|
||||
raise ConnectionError(e, request=request)
|
||||
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='seafile.empresalibre.net', port=443): Max retries exceeded with url: /api2/auth-token/ (Caused by NewConnectionError('<urllib3.connection.VerifiedHTTPSConnection object at 0x7f2f031a4550>: Failed to establish a new connection: [Errno -2] Name or service not known'))
|
||||
|
||||
* **IMPORTANTE:** Si **no sabes** desde que versión estas actualizando, o no has actualizado desde la **versión 1.2.0 del 18 de diciembre del 2017**. Entonces, primero usa el siguente comando.
|
||||
|
||||
└──> python main.py -bd
|
||||
Introduce el RFC: TU_RFC
|
||||
|
||||
* Ahora si, si ya **no obtienes ningún error** y estas al día, puedes migrar tu base de datos con el comando.
|
||||
|
||||
└──> python main.py -m -r TU_RFC
|
||||
|
||||
* Por supuesto, reemplaza **TU_RFC** por el RFC del emisor a actualizar.
|
||||
|
||||
└──> python main.py -m -r LAN7008173R5
|
||||
[21-Sep-2018 23:41:55] INFO: API: Conectado a la BD...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Creando tablas nuevas...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Tablas creadas correctamente...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Iniciando migración de tablas...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Tablas migradas correctamente...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Importando datos...
|
||||
[21-Sep-2018 23:41:55] INFO: API: Importando tabla: Categorias
|
||||
[21-Sep-2018 23:41:55] INFO: API: Importando tabla: SATImpuestos
|
||||
[21-Sep-2018 23:41:55] INFO: API: Importando tabla: SATUnidades
|
||||
[21-Sep-2018 23:41:55] INFO: API: Importando tabla: SATNivelesEducativos
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoRelacion
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATMonedas
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATFormaPago
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATRegimenes
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATBancos
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATUsoCfdi
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATEstados
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATOrigenRecurso
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATPeriodicidadPago
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATRiesgoPuesto
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoContrato
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoDeduccion
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoHoras
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoIncapacidad
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoJornada
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoNomina
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoOtroPago
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoPercepcion
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importando tabla: SATTipoRegimen
|
||||
[21-Sep-2018 23:41:56] INFO: API: Importación terminada...
|
||||
|
||||
* **IMPORTANTE:** Si tienes varios emisores en el sistema, **debes de migrarlos todos**.
|
||||
|
||||
1. Por ultimo reinicia el sistema.
|
||||
|
||||
└──> sudo systemctl restart empresalibre
|
||||
[sudo] password for empresa:
|
||||
|
||||
* El proceso anterior debe ser instantaneo, si se tarda más de un minuto, mira en [errores más comúnes][2].
|
||||
* También debe funcionar que reinicies la maquina virtual o servidor donde tengas el sistema.
|
||||
|
||||
1. Cualquier otro problema, usa el [sistema de tickets del proyecto][3]
|
||||
|
||||
|
||||
<BR>
|
||||
### Errores más comúnes
|
||||
|
||||
<BR>
|
||||
<div class="alert-box error"><span>PRECAUCIÓN: </span>
|
||||
La modificación incorrecta de cualquier archivo del sistema, puede provocar
|
||||
que deje de funcionar. Asegurate de seguir todos los pasos correctamente.
|
||||
Si no estas seguro, es mejor contrates el soporte para que un técnico
|
||||
especializado realice el procedimiento.
|
||||
</div>
|
||||
<BR>
|
||||
|
||||
#### Al respaldar la base de datos, muestra el error
|
||||
|
||||
File "/usr/lib/python3.7/site-packages/requests/adapters.py", line 513, in send
|
||||
raise ConnectionError(e, request=request)
|
||||
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='seafile.empresalibre.net', port=443): Max retries exceeded with url: /api2/auth-token/ (Caused by NewConnectionError('<urllib3.connection.VerifiedHTTPSConnection object at 0x7f2f031a4550>: Failed to establish a new connection: [Errno -2] Name or service not known'))
|
||||
|
||||
* Entra a la carpeta
|
||||
|
||||
└──> cd proyectos/empresa-libre/source/app/
|
||||
┌─[empresalibre-][empresa]->{~/proyectos/empresa-libre/source/app}
|
||||
└──>
|
||||
|
||||
* Siempre saca una copia de seguridad.
|
||||
|
||||
cp conf.py ~/conf.py
|
||||
|
||||
* Edita el archivo
|
||||
|
||||
nano conf.py
|
||||
|
||||
* Debe de verse exactamente como el siguiente ejemplo, agrega lo que haga falta.
|
||||
|
||||
#!/usr/bin/env python
|
||||
|
||||
DEBUG = False
|
||||
MV = True
|
||||
|
||||
#~ Establece una ruta accesible para el servidor web
|
||||
LOG_PATH = '/home/empresa/log/empresa-libre.log'
|
||||
|
||||
SEAFILE_SERVER = {
|
||||
'URL': 'https://seafile.empresalibre.net/api2/',
|
||||
'USER': '',
|
||||
'PASS': '',
|
||||
'REPO': '',
|
||||
}
|
||||
SEAFILE_SERVER = {}
|
||||
|
||||
* Para guardar los cambios presionas: `CTRL+O`
|
||||
* Para salir presionas: `CTRL+X`
|
||||
|
||||
<BR>
|
||||
#### Al reinciar el sistema, tarda más de un minuto en terminar.
|
||||
|
||||
<BR>
|
||||
<div class="alert-box warning"><span>CUIDADO: </span>
|
||||
Se muestra el contenido del archivo de la maquina virtual, puede ser diferente
|
||||
si lo usas en tu propio servidor o VPS.
|
||||
</div>
|
||||
<BR>
|
||||
|
||||
* Edita el archivo
|
||||
|
||||
sudo nano /etc/systemd/system/empresalibre.service
|
||||
|
||||
* Asegurate de que este **exactamente** como en:
|
||||
|
||||
[Unit]
|
||||
Description=uWSGI instance to serve Empresa Libre
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/uwsgi /home/empresa/empresa-libre/app/main.ini
|
||||
KillSignal=SIGQUIT
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
* Para guardar los cambios presionas: `CTRL+O`
|
||||
* Para salir presionas: `CTRL+X`
|
||||
* Recarga los cambios.
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
* Asegurate de que ahora reinicia al instante.
|
||||
|
||||
sudo systemctl restart empresalibre
|
||||
|
||||
|
||||
[1]: #al-respaldar-la-base-de-datos-muestra-el-error
|
||||
[2]: #al-reinciar-el-sistema-tarda-mas-de-un-minuto-en-terminar
|
||||
[3]: https://gitlab.com/mauriciobaeza/empresa-libre/issues
|
|
@ -1,680 +0,0 @@
|
|||
## License
|
||||
|
||||
|
||||
```
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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
|
||||
<https://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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
```
|
|
@ -1,240 +0,0 @@
|
|||
## Notas de lanzamiento
|
||||
|
||||
Siempre debe verificar en esta sección, el proceso que debes seguir con cada
|
||||
actualización del sistema. Recuerda; **es muy importante mantener tu sistema
|
||||
siempre actualizado.** Solo se da soporte sobre la ultima versión de **Empresa
|
||||
Libre**.
|
||||
|
||||
|
||||
### 1.21.1 [14-oct-2018]
|
||||
- Error [#307](https://gitlab.com/mauriciobaeza/empresa-libre/issues/307)
|
||||
|
||||
|
||||
### 1.21.0 [12-oct-2018]
|
||||
- Error [#287](https://gitlab.com/mauriciobaeza/empresa-libre/issues/287)
|
||||
- Mejora: Complemento de pago con datos de cuentas
|
||||
|
||||
* IMPORTANTE: Es necesario realizar una migración, despues de actualizar.
|
||||
|
||||
|
||||
### 1.20.0 [08-oct-2018]
|
||||
- Error [#295](https://gitlab.com/mauriciobaeza/empresa-libre/issues/295)
|
||||
- Mejora - Cuentas de banco para clientes
|
||||
|
||||
* IMPORTANTE: Es necesario realizar una migración, despues de actualizar.
|
||||
|
||||
|
||||
### 1.19.1 [03-oct-2018]
|
||||
- Error [#291](https://gitlab.com/mauriciobaeza/empresa-libre/issues/291)
|
||||
- Error al generar PDF de factura de pago con relacionados sin serie
|
||||
- Error al relacionar facturas versión 3.2
|
||||
|
||||
|
||||
### 1.19.0 [28-sep-2018]
|
||||
- Mejora [#280](https://gitlab.com/mauriciobaeza/empresa-libre/issues/280)
|
||||
- Mejora [#288](https://gitlab.com/mauriciobaeza/empresa-libre/issues/288)
|
||||
- Error [#290](https://gitlab.com/mauriciobaeza/empresa-libre/issues/290)
|
||||
|
||||
* IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.18.0 [27-sep-2018]
|
||||
- Fix [#282](https://gitlab.com/mauriciobaeza/empresa-libre/issues/282) Factura de pago en otras monedas
|
||||
|
||||
|
||||
### 1.17.0 [25-sep-2018]
|
||||
- Fix - Al generar factura de pago con documentos relacionados en otras monedas
|
||||
- Fix - Al generar factura de pago sin serie en documentos relacionados
|
||||
- Fix [#278](https://gitlab.com/mauriciobaeza/empresa-libre/issues/278)
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.16.1 [18-sep-2018]
|
||||
- Error [#268](https://gitlab.com/mauriciobaeza/empresa-libre/issues/268)
|
||||
- IMPORTANTE: Actualizar si usas cuatro decimales en impuestos
|
||||
|
||||
|
||||
### 1.16.0 [16-sep-2018]
|
||||
- Se puede editar el saldo de un cliente
|
||||
- Se muestra la cantidad de facturas de pago en los movimientos
|
||||
|
||||
|
||||
### 1.15.0 [12-sep-2018]
|
||||
- Se pueden mover las facturas con doble clic en los movimientos de banco.
|
||||
- Fix - Al sumar las facturas en los depósitos
|
||||
- Fix - Al importar los pedimentos en facturas por lotes
|
||||
- Fix - Al guardar los datos del emisor
|
||||
|
||||
|
||||
### 1.14.0 [10-sep-2018]
|
||||
- Personalizar plantilla para factura de pago
|
||||
- Fix - Mostrar serie y folio capturado para factura de pago
|
||||
- Fix - Agregar nueva cuenta de banco
|
||||
|
||||
|
||||
### 1.13.0 [10-sep-2018]
|
||||
- Cancelar factura de pago
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.12.0 [31-ago-2018]
|
||||
- Soporte para facturas (complemento) de pago.
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bk
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.11.1 [21-ago-2018]
|
||||
- Fix - Quitar columna en tabla facturaspagos.
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.11.0 [25-jul-2018]
|
||||
- Se cambia la forma de consultar los folios restantes. Es indispensable actualizar a esta versión para ver tus timbres restantes.
|
||||
|
||||
|
||||
### 1.10.0 [10-jul-2018]
|
||||
- Ahora se pueden manejar precios con cuatro decimales.
|
||||
|
||||
|
||||
### 1.9.3 [08-jul-2018]
|
||||
- Fix: Al refacturar conceptos con descuento
|
||||
|
||||
|
||||
### 1.9.2 [05-jul-2018]
|
||||
- Fix: Al generar el reporte de facturas en PDF
|
||||
|
||||
|
||||
### 1.9.1 [25-jun-2018]
|
||||
- Fix: Al mostrar el título de la aplicación - Se agrega el registro de acción al borrar una factura
|
||||
|
||||
|
||||
### 1.9.0 [18-jun-2018]
|
||||
- Se agrega la vista del detalle de facturas
|
||||
- Fix: Al timbrar nómina
|
||||
|
||||
|
||||
### 1.8.1 [14-jun-2018]
|
||||
- Fix: Se agrega una barra de desplazamiento al buscar productos o clientes
|
||||
- Se cambia el servidor de consulta de timbres
|
||||
|
||||
|
||||
### 1.8.0 [03-jun-2018]
|
||||
- Se permiten 4 decimales en Tipo de cambio
|
||||
- Se agrega el campo {total_cantidades} al generar el PDF
|
||||
- Se agrega opción para generar respaldos de la BD en MV
|
||||
- Fix: Al generar con complemento EDU
|
||||
|
||||
|
||||
### 1.7.0 [23-may-2018]
|
||||
- Se agrega soporte para truncar impuestos locales, para las estulticias de los "ingenieros" de las dependencias de gobierno
|
||||
|
||||
|
||||
### 1.6.1 [09-abr-2018]
|
||||
- Fix: Nómina con separación
|
||||
|
||||
|
||||
### 1.6.0 [18-feb-2018]
|
||||
- Facturacion a extranjeros
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.5.0 [30-ene-2018]
|
||||
- Timbrado de Nómina
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.4.0 [01-ene-2018]
|
||||
- Impresión de tickets
|
||||
|
||||
|
||||
### 1.3.0 [27-Dic-2017]
|
||||
- Punto de venta
|
||||
- IMPORTANTE: Es necesario realizar una migración, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -m
|
||||
```
|
||||
|
||||
|
||||
### 1.2.0 [18-Dic-2017]
|
||||
- IMPORTANTE: Es necesario actualizar la base de datos, despues de actualizar la rama principal.
|
||||
|
||||
```
|
||||
git pull origin master
|
||||
|
||||
cd source/app/models
|
||||
|
||||
python main.py -bd
|
||||
```
|
||||
|
||||
|
||||
### 0.1.0 [26-Oct-2017]
|
||||
- Generar y timbrar con CFDI 3.3
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
## Preguntas más frecuentes
|
||||
|
||||
<div style="text-align: right">
|
||||
<BR><B><FONT SIZE=+1>Software libre, no gratis</FONT></B>
|
||||
<BR>
|
||||
<BR>
|
||||
<BR>
|
||||
</div>
|
||||
|
||||
|
||||
<BR>
|
||||
### ¿Empresa Libre es gratis?
|
||||
|
||||
No, **Empresa Libre** no es gratis, es libre y es muy importante tener clara esta
|
||||
diferencia. **Empresa Libre** es [software libre][1], lo que quiere decir que
|
||||
puedes usarlo, copiarlo, mejorarlo y compartirlo con quien quieras. **Empresa
|
||||
Libre** se sostiene de la venta de los folios para el timbrado y del soporte
|
||||
técnico derivado de su uso, por eso, por favor, **no pidas soporte técnico gratis**.
|
||||
|
||||
|
||||
<BR>
|
||||
### Si es libre... ¿Por qué cobran el alta inicial?
|
||||
|
||||
Recuerda, **libre es diferente de gratis**, y el alta inicial de **Empresa Libre**,
|
||||
es el costo inicial de 100 timbres fiscales, lo que estas pagando son timbres
|
||||
fiscales, no el software. Nunca, reiteramos, nunca hemos cobrado el software.
|
||||
|
||||
|
||||
<BR>
|
||||
### Si el SAT dice que los PACs deben de dar timbrado gratuito... ¿Por que lo cobran Ustedes?
|
||||
|
||||
Efectivamente, por ley todos los PACs deben de tener una aplicación gratuita
|
||||
para el timbrado. Nosotros **NO somos PAC**. Hacemos el timbrado por medio de
|
||||
Finkok, el mejor PAC de México, puedes [timbrar gratuitamente en su página][2].
|
||||
|
||||
|
||||
<BR>
|
||||
### ¿Al contratar el servicio se incluye el soporte técnico?
|
||||
|
||||
No, no se incluye ningún tipo de soporte técnico. Dado que **Empresa Libre** es
|
||||
[software libre][1] y se ofrece sin **ningún costo para todos**, el soporte técnico
|
||||
es parte de los ingresos indispensables para poder seguir desarrollando y
|
||||
manteniendo el sistema. Esto incluye cualquier estulticia que se le ocurra al SAT.
|
||||
Por eso, por favor, **no pidas soporte técnico gratis**.
|
||||
|
||||
|
||||
<BR>
|
||||
### ¿Qué alternativas tienen de soporte?
|
||||
|
||||
El ingreso por soporte técnico es una importante fuente de ingreso para sufragar
|
||||
todas [nuestras actividades altruistas][3]. Cada vez que contratas nuestros
|
||||
servicios, **estas ayudando a otros**.
|
||||
|
||||
1. **Soporte gratuito** en el [sistema de tickets del proyecto][4], un soporte
|
||||
que hemos dado por años de forma profesional, además, otros entusiastas usuarios
|
||||
del sistema, también pueden ayudarte con cualquier duda que tengas.
|
||||
2. **Soporte de pago**, puedes contratar el servicio puntual por hora para un
|
||||
problema en específico o contratar una póliza de soporte mensual. Ponte en
|
||||
contacto con nosotros para tener más detalles.
|
||||
3. Todos nuestros [donantes y patrocinadores][5] tienen todo el soporte técnico
|
||||
que requieran para el sitema. Todas las [ONGs][6] que apoyamos, también tienen
|
||||
todo el soporte técnico que requieran.
|
||||
|
||||
|
||||
<BR>
|
||||
### ¿Puedo probar el sistema antes de contratar el servicio?
|
||||
|
||||
Claro, de hecho, **es un requisito indispensable** para contratar, los datos de
|
||||
acceso al sistema de pruebas son:
|
||||
|
||||
* **URL**: https://demo.empresalibre.net/
|
||||
* **RFC**: LAN7008173R5
|
||||
* **Usuario**: admin
|
||||
* **Contraseña**: salgueiro3.3
|
||||
|
||||
Toma en cuenta que este sitio esta en constante actualización y desarrollo, los
|
||||
datos generados se limpian de forma regular.
|
||||
|
||||
|
||||
<BR>
|
||||
### ¿Dondé descargar los archivos necesarios?
|
||||
|
||||
Todos los archivos necesarios para probar localmente **Empresa Libre** en tu
|
||||
equipo, los puedes descargar desde nuestra carpeta compartida, donde también
|
||||
puedes descargar todas las plantillas necesarias para el sistema
|
||||
|
||||
[Carpeta pública de Empresa Libre][7]
|
||||
|
||||
|
||||
<BR>
|
||||
### Después de actualizar, no veo los cambios esperados en pantalla
|
||||
|
||||
Un alto porcentaje de estos problemas, se arregla **forzando** el refresco de
|
||||
tu pantalla, en la mayoría de los navegadores con la combinación de teclas
|
||||
**CTRL+F5**. En raras ocasiones, es preciso limpiar el cache de tu navegador.
|
||||
|
||||
|
||||
|
||||
[1]: https://www.gnu.org/philosophy/free-sw.es.html
|
||||
[2]: https://www.finkok.com/
|
||||
[3]: http://universolibre.org/hacemos/
|
||||
[4]: https://gitlab.com/mauriciobaeza/empresa-libre/issues
|
||||
[5]:
|
||||
[6]: https://universolibre.org/hacemos/
|
||||
[7]: https://doc.elmau.net/d/dbb11c9186684457beb6/
|
|
@ -1,17 +0,0 @@
|
|||
site_name: Empresa Libre
|
||||
#~ repo_url: https://gitlab.com/mauriciobaeza/empresa-libre
|
||||
nav:
|
||||
- Inicio: index.md
|
||||
- Contenido:
|
||||
- Instalación y configuración: instalacion.md
|
||||
- Administracón del sistema: administracion.md
|
||||
- Guía de Usuario: guiadeusuario.md
|
||||
- Preguntas más frecuentes: preguntas.md
|
||||
- Acerca de :
|
||||
- Un poco de historia: historia.md
|
||||
- Notas de lanzamiento: notas.md
|
||||
- Como ayudar: ayudar.md
|
||||
- Licencia: licencia.md
|
||||
extra_css:
|
||||
- css/main.css
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
falcon
|
||||
falcon==1.4.1
|
||||
falcon-multipart
|
||||
Beaker
|
||||
Mako
|
||||
peewee==2.10.2
|
||||
Click
|
||||
logbook
|
||||
bcrypt
|
||||
python-dateutil
|
||||
zeep
|
||||
chardet
|
||||
pyqrcode
|
||||
pypng
|
||||
reportlab
|
||||
psycopg2-binary
|
||||
cryptography==3.4.8
|
||||
xmlsec
|
||||
segno
|
||||
|
||||
# python-escpos
|
||||
|
|
|
@ -13,13 +13,9 @@ TITLE_APP = 'Empresa Libre'
|
|||
#~ Establece una ruta accesible para el servidor web
|
||||
LOG_PATH = '/var/log/empresalibre/empresa-libre.log'
|
||||
|
||||
# ~ Establece los valores para sincronizar los backups de la base de datos
|
||||
# ~ por ejemplo
|
||||
# ~ SEAFILE_SERVER = {
|
||||
# ~ 'URL': 'https://tu_servidor_seafile',
|
||||
# ~ 'USER': 'tu_usuario',
|
||||
# ~ 'PASS': 'tu_contraseña',
|
||||
# ~ 'REPO': 'id_repo',
|
||||
# ~ }
|
||||
|
||||
SEAFILE_SERVER = {}
|
||||
# ~ Establece un token personalizado para encriptar las claves
|
||||
# ~ from secrets import token_hex
|
||||
# ~ token_hex(32)
|
||||
# ~ IMPORTANTE: Cada vez que cambies este valor, debes de subir de nuevo
|
||||
# ~ tus certificados
|
||||
TOKEN = ''
|
||||
|
|
|
@ -20,20 +20,28 @@ import datetime
|
|||
from xml.etree import ElementTree as ET
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
from dateutil import parser
|
||||
from logbook import Logger
|
||||
|
||||
|
||||
log = Logger('XML')
|
||||
CFDI_ACTUAL = 'cfdi33'
|
||||
CFDI_ACTUAL = 'cfdi40'
|
||||
NOMINA_ACTUAL = 'nomina12'
|
||||
PUBLIC = 'PUBLICO EN GENERAL'
|
||||
CFDI_EGRESO = 'E'
|
||||
|
||||
DEFAULT = {
|
||||
'exportacion': '01',
|
||||
}
|
||||
|
||||
|
||||
SAT = {
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
'cfdi32': {
|
||||
'version': '3.2',
|
||||
'cfdi40': {
|
||||
'version': '4.0',
|
||||
'prefix': 'cfdi',
|
||||
'xmlns': 'http://www.sat.gob.mx/cfd/3',
|
||||
'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd',
|
||||
'xmlns': 'http://www.sat.gob.mx/cfd/4',
|
||||
'schema': 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd',
|
||||
},
|
||||
'cfdi33': {
|
||||
'version': '3.3',
|
||||
|
@ -41,17 +49,23 @@ SAT = {
|
|||
'xmlns': 'http://www.sat.gob.mx/cfd/3',
|
||||
'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd',
|
||||
},
|
||||
'cfdi32': {
|
||||
'version': '3.2',
|
||||
'prefix': 'cfdi',
|
||||
'xmlns': 'http://www.sat.gob.mx/cfd/3',
|
||||
'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd',
|
||||
},
|
||||
'nomina11': {
|
||||
'version': '1.1',
|
||||
'prefix': 'nomina',
|
||||
'xmlns': 'http://www.sat.gob.mx/nomina',
|
||||
'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd',
|
||||
'schema': ' http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd',
|
||||
},
|
||||
'nomina': {
|
||||
'version': '1.2',
|
||||
'prefix': 'nomina12',
|
||||
'xmlns': 'http://www.sat.gob.mx/nomina12',
|
||||
'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
|
||||
'schema': ' http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
|
||||
},
|
||||
'locales': {
|
||||
'version': '1.0',
|
||||
|
@ -76,14 +90,38 @@ SAT = {
|
|||
'version': '1.0',
|
||||
'prefix': 'iedu',
|
||||
'xmlns': 'http://www.sat.gob.mx/iedu',
|
||||
'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/ine/iedu.xsd',
|
||||
'schema': ' http://www.sat.gob.mx/iedu http://www.sat.gob.mx/sitio_internet/cfd/iedu/iedu.xsd',
|
||||
},
|
||||
'pagos': {
|
||||
'version': '1.0',
|
||||
'prefix': 'pago10',
|
||||
'xmlns': 'http://www.sat.gob.mx/Pagos',
|
||||
'schema': ' http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd',
|
||||
'version': '2.0',
|
||||
'prefix': 'pago20',
|
||||
'xmlns': 'http://www.sat.gob.mx/Pagos20',
|
||||
'schema': ' http://www.sat.gob.mx/Pagos20 http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd',
|
||||
},
|
||||
'divisas': {
|
||||
'version': '1.0',
|
||||
'prefix': 'divisas',
|
||||
'xmlns': 'http://www.sat.gob.mx/divisas',
|
||||
'schema': ' http://www.sat.gob.mx/divisas http://www.sat.gob.mx/sitio_internet/cfd/divisas/divisas.xsd',
|
||||
},
|
||||
'leyendas': {
|
||||
'version': '1.0',
|
||||
'prefix': 'leyendasFisc',
|
||||
'xmlns': 'http://www.sat.gob.mx/leyendasFiscales',
|
||||
'schema': ' http://www.sat.gob.mx/leyendasFiscales http://www.sat.gob.mx/sitio_internet/cfd/leyendasFiscales/leyendasFisc.xsd',
|
||||
},
|
||||
'cartaporte': {
|
||||
'version': '3.0',
|
||||
'prefix': 'cartaporte30',
|
||||
'xmlns': 'http://www.sat.gob.mx/CartaPorte30',
|
||||
'schema': ' http://www.sat.gob.mx/CartaPorte30 http://www.sat.gob.mx/sitio_internet/cfd/CartaPorte/CartaPorte30.xsd',
|
||||
},
|
||||
'comercioe': {
|
||||
'version': '2.0',
|
||||
'prefix': 'cce20',
|
||||
'xmlns': 'http://www.sat.gob.mx/ComercioExterior20',
|
||||
'schema': ' http://www.sat.gob.mx/ComercioExterior20 http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior20/ComercioExterior20.xsd',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -101,6 +139,12 @@ class CFDI(object):
|
|||
self._edu = False
|
||||
self._pagos = False
|
||||
self._is_nomina = False
|
||||
self._leyendas = False
|
||||
self._carta_porte = False
|
||||
self._comercio_exterior = False
|
||||
self._divisas = ''
|
||||
self._tipo_de_comprobante = ''
|
||||
self._exportacion = DEFAULT['exportacion']
|
||||
self.error = ''
|
||||
|
||||
def _now(self):
|
||||
|
@ -111,6 +155,8 @@ class CFDI(object):
|
|||
return ''
|
||||
|
||||
self._comprobante(datos['comprobante'])
|
||||
if 'global' in datos:
|
||||
self._informacion_global(datos['global'])
|
||||
self._relacionados(datos['relacionados'])
|
||||
self._emisor(datos['emisor'])
|
||||
self._receptor(datos['receptor'])
|
||||
|
@ -122,13 +168,14 @@ class CFDI(object):
|
|||
|
||||
if 'nomina' in datos:
|
||||
self._nomina(datos['nomina'])
|
||||
#~ if 'complementos' in datos:
|
||||
#~ self._complementos(datos['complementos'])
|
||||
|
||||
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
|
||||
xml = self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
|
||||
|
||||
def add_sello(self, sello):
|
||||
return xml
|
||||
|
||||
def add_sello(self, sello, cert_txt):
|
||||
self._cfdi.attrib['Sello'] = sello
|
||||
self._cfdi.attrib['Certificado'] = cert_txt
|
||||
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
|
||||
|
||||
def _to_pretty_xml(self, source):
|
||||
|
@ -151,6 +198,13 @@ class CFDI(object):
|
|||
if 'ine' in datos['complementos']:
|
||||
self._ine = True
|
||||
self._pagos = bool(datos['complementos'].get('pagos', False))
|
||||
self._leyendas = bool(datos['complementos'].get('leyendas', False))
|
||||
self._carta_porte = bool(datos['complementos'].get('cartaporte', False))
|
||||
self._comercio_exterior = bool(datos['complementos'].get('comercioe', False))
|
||||
if self._comercio_exterior:
|
||||
self._exportacion = datos['complementos']['comercioe'].pop('Exportacion')
|
||||
|
||||
self._divisas = datos['comprobante'].pop('divisas', '')
|
||||
|
||||
if 'nomina' in datos:
|
||||
self._is_nomina = True
|
||||
|
@ -190,6 +244,12 @@ class CFDI(object):
|
|||
attributes[name] = SAT['edu']['xmlns']
|
||||
schema_edu = SAT['edu']['schema']
|
||||
|
||||
schema_divisas = ''
|
||||
if self._divisas:
|
||||
name = 'xmlns:{}'.format(SAT['divisas']['prefix'])
|
||||
attributes[name] = SAT['divisas']['xmlns']
|
||||
schema_divisas = SAT['divisas']['schema']
|
||||
|
||||
schema_nomina = ''
|
||||
if self._is_nomina:
|
||||
name = 'xmlns:{}'.format(SAT['nomina']['prefix'])
|
||||
|
@ -202,9 +262,28 @@ class CFDI(object):
|
|||
attributes[name] = SAT['pagos']['xmlns']
|
||||
schema_pagos = SAT['pagos']['schema']
|
||||
|
||||
schema_leyendas = ''
|
||||
if self._leyendas:
|
||||
name = 'xmlns:{}'.format(SAT['leyendas']['prefix'])
|
||||
attributes[name] = SAT['leyendas']['xmlns']
|
||||
schema_leyendas = SAT['leyendas']['schema']
|
||||
|
||||
schema_carta_porte = ''
|
||||
if self._carta_porte:
|
||||
name = 'xmlns:{}'.format(SAT['cartaporte']['prefix'])
|
||||
attributes[name] = SAT['cartaporte']['xmlns']
|
||||
schema_carta_porte = SAT['cartaporte']['schema']
|
||||
|
||||
schema_comercioe = ''
|
||||
if self._comercio_exterior:
|
||||
name = 'xmlns:{}'.format(SAT['comercioe']['prefix'])
|
||||
attributes[name] = SAT['comercioe']['xmlns']
|
||||
schema_carta_porte = SAT['comercioe']['schema']
|
||||
|
||||
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
|
||||
schema_locales + schema_donativo + schema_ine + schema_edu + \
|
||||
schema_nomina + schema_pagos
|
||||
schema_divisas + schema_nomina + schema_pagos + schema_leyendas + \
|
||||
schema_carta_porte + schema_comercioe
|
||||
attributes.update(datos)
|
||||
|
||||
if not 'Version' in attributes:
|
||||
|
@ -212,9 +291,23 @@ class CFDI(object):
|
|||
if not 'Fecha' in attributes:
|
||||
attributes['Fecha'] = self._now()
|
||||
|
||||
# ~ cfdi4
|
||||
if not 'Exportacion' in attributes:
|
||||
attributes['Exportacion'] = self._exportacion
|
||||
|
||||
self._tipo_de_comprobante = attributes['TipoDeComprobante']
|
||||
|
||||
self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes)
|
||||
return
|
||||
|
||||
def _informacion_global(self, datos):
|
||||
if not datos:
|
||||
return
|
||||
|
||||
node_name = '{}:InformacionGlobal'.format(self._pre)
|
||||
node = ET.SubElement(self._cfdi, node_name, datos)
|
||||
return
|
||||
|
||||
def _relacionados(self, datos):
|
||||
if not datos or not datos['tipo'] or not datos['cfdis']:
|
||||
return
|
||||
|
@ -234,6 +327,10 @@ class CFDI(object):
|
|||
return
|
||||
|
||||
def _receptor(self, datos):
|
||||
receptor_name = datos['Nombre'].upper()
|
||||
if receptor_name == PUBLIC and self._tipo_de_comprobante == CFDI_EGRESO:
|
||||
receptor_name = datos['Nombre']
|
||||
datos['Nombre'] = receptor_name
|
||||
node_name = '{}:Receptor'.format(self._pre)
|
||||
emisor = ET.SubElement(self._cfdi, node_name, datos)
|
||||
return
|
||||
|
@ -242,7 +339,8 @@ class CFDI(object):
|
|||
from xml.sax.saxutils import escape, unescape
|
||||
|
||||
conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre))
|
||||
for row in reversed(datos):
|
||||
# ~ for row in reversed(datos):
|
||||
for row in datos:
|
||||
complemento = {}
|
||||
if 'complemento' in row:
|
||||
complemento = row.pop('complemento')
|
||||
|
@ -302,6 +400,10 @@ class CFDI(object):
|
|||
for field in fields:
|
||||
if field in datos:
|
||||
attributes[field] = datos[field]
|
||||
|
||||
if not attributes:
|
||||
return
|
||||
|
||||
node_name = '{}:Impuestos'.format(self._pre)
|
||||
impuestos = ET.SubElement(self._cfdi, node_name, attributes)
|
||||
|
||||
|
@ -418,6 +520,47 @@ class CFDI(object):
|
|||
|
||||
return
|
||||
|
||||
def _complemento_comercio_exterior(self, datos):
|
||||
prefix = SAT['comercioe']['prefix']
|
||||
|
||||
emisor = datos.pop('emisor')
|
||||
propietarios = datos.pop('propietarios', {})
|
||||
receptor = datos.pop('receptor')
|
||||
destinatario = datos.pop('destinatario', {})
|
||||
mercancias = datos.pop('mercancias')
|
||||
|
||||
attr = {'Version': SAT['comercioe']['version']}
|
||||
attr.update(datos)
|
||||
ce = ET.SubElement(
|
||||
self._complemento, f'{prefix}:ComercioExterior', attr)
|
||||
|
||||
attributes = {}
|
||||
if 'Curp' in emisor:
|
||||
attributes = {'Curp': emisor.pop('Curp')}
|
||||
node = ET.SubElement(ce, '{}:Emisor'.format(prefix), attributes)
|
||||
ET.SubElement(node, '{}:Domicilio'.format(prefix), emisor)
|
||||
|
||||
attributes = {}
|
||||
if 'NumRegIdTrib' in receptor:
|
||||
attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
|
||||
node = ET.SubElement(ce, '{}:Receptor'.format(prefix), attributes)
|
||||
ET.SubElement(node, '{}:Domicilio'.format(prefix), receptor)
|
||||
|
||||
node = ET.SubElement(ce, '{}:Mercancias'.format(prefix))
|
||||
fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie')
|
||||
for row in mercancias:
|
||||
detalle = {}
|
||||
for f in fields:
|
||||
if f in row and row[f]:
|
||||
detalle[f] = row[f]
|
||||
row.pop(f)
|
||||
concepto = ET.SubElement(node, '{}:Mercancia'.format(prefix), row)
|
||||
if detalle:
|
||||
ET.SubElement(
|
||||
concepto, '{}:DescripcionesEspecificas'.format(prefix), detalle)
|
||||
|
||||
return
|
||||
|
||||
def _complementos(self, datos):
|
||||
if not datos:
|
||||
return
|
||||
|
@ -426,73 +569,126 @@ class CFDI(object):
|
|||
self._complemento = ET.SubElement(
|
||||
self._cfdi, '{}:Complemento'.format(self._pre))
|
||||
|
||||
if self._carta_porte:
|
||||
datos = datos['cartaporte']
|
||||
ubicaciones = datos.pop('ubicaciones')
|
||||
mercancias = datos.pop('mercancias', ())
|
||||
tiposfigura = datos.pop('tiposfigura', ())
|
||||
|
||||
autotransporte = datos.pop('autotransporte', {})
|
||||
identificacion = autotransporte.pop('identificacion')
|
||||
seguros = autotransporte.pop('seguros')
|
||||
remolques = autotransporte.pop('remolques')
|
||||
|
||||
atributos = {'Version': SAT['cartaporte']['version']}
|
||||
atributos.update(datos)
|
||||
|
||||
prefix = SAT['cartaporte']['prefix']
|
||||
node_carta = ET.SubElement(self._complemento, f'{prefix}:CartaPorte', atributos)
|
||||
|
||||
node = ET.SubElement(node_carta, f'{prefix}:Ubicaciones')
|
||||
for ubicacion in ubicaciones:
|
||||
domicilio = ubicacion.pop('domicilio', {})
|
||||
dt = parser.parse(ubicacion['FechaHoraSalidaLlegada'])
|
||||
ubicacion['FechaHoraSalidaLlegada'] = dt.isoformat()[:19]
|
||||
sub_node = ET.SubElement(node, f'{prefix}:Ubicacion', ubicacion)
|
||||
if domicilio:
|
||||
ET.SubElement(sub_node, f'{prefix}:Domicilio', domicilio)
|
||||
|
||||
attr = mercancias
|
||||
mercancias = attr.pop('mercancias')
|
||||
|
||||
node = ET.SubElement(node_carta, f'{prefix}:Mercancias', attr)
|
||||
for mercancia in mercancias:
|
||||
ET.SubElement(node, f'{prefix}:Mercancia', mercancia)
|
||||
|
||||
sub_node = ET.SubElement(node, f'{prefix}:Autotransporte', autotransporte)
|
||||
ET.SubElement(sub_node, f'{prefix}:IdentificacionVehicular', identificacion)
|
||||
ET.SubElement(sub_node, f'{prefix}:Seguros', seguros)
|
||||
if remolques:
|
||||
node_remolques = ET.SubElement(sub_node, f'{prefix}:Remolques')
|
||||
for remolque in remolques:
|
||||
ET.SubElement(node_remolques, f'{prefix}:Remolque', remolque)
|
||||
|
||||
if tiposfigura:
|
||||
sub_node = ET.SubElement(node_carta, f'{prefix}:FiguraTransporte')
|
||||
for figura in tiposfigura:
|
||||
ET.SubElement(sub_node, f'{prefix}:TiposFigura', figura)
|
||||
|
||||
if self._divisas:
|
||||
atributos = {
|
||||
'version': SAT['divisas']['version'],
|
||||
'tipoOperacion': self._divisas,
|
||||
}
|
||||
ET.SubElement(self._complemento, 'divisas:Divisas', atributos)
|
||||
|
||||
if 'ine' in datos:
|
||||
atributos = {'Version': SAT['ine']['version']}
|
||||
ine_key_entidad = datos['ine'].pop('ClaveEntidad', '')
|
||||
ine_ambito = datos['ine'].pop('Ambito', '')
|
||||
if ine_key_entidad:
|
||||
ine_id_conta = datos['ine'].pop('IdContabilidad', '')
|
||||
atributos.update(datos['ine'])
|
||||
ET.SubElement(self._complemento, 'ine:INE', atributos)
|
||||
node_ine = ET.SubElement(self._complemento, 'ine:INE', atributos)
|
||||
if ine_key_entidad:
|
||||
attr = {'ClaveEntidad': ine_key_entidad}
|
||||
if ine_ambito:
|
||||
attr['Ambito'] = ine_ambito
|
||||
node_entidad = ET.SubElement(node_ine, 'ine:Entidad', attr)
|
||||
attr = {'IdContabilidad': ine_id_conta}
|
||||
ET.SubElement(node_entidad, 'ine:Contabilidad', attr)
|
||||
|
||||
if 'pagos' in datos:
|
||||
datos = datos.pop('pagos')
|
||||
totales = datos.pop('totales')
|
||||
relacionados = datos.pop('relacionados')
|
||||
taxes_pay = datos.pop('taxes_pay')
|
||||
pre = SAT['pagos']['prefix']
|
||||
|
||||
attributes = {'Version': SAT['pagos']['version']}
|
||||
pagos = ET.SubElement(
|
||||
self._complemento, '{}:Pagos'.format(pre), attributes)
|
||||
|
||||
ET.SubElement(pagos, '{}:Totales'.format(pre), totales)
|
||||
|
||||
node_pago = ET.SubElement(pagos, '{}:Pago'.format(pre), datos)
|
||||
for row in relacionados:
|
||||
ET.SubElement(node_pago, '{}:DoctoRelacionado'.format(pre), row)
|
||||
taxes = row.pop('taxes')
|
||||
node = ET.SubElement(node_pago, f'{pre}:DoctoRelacionado', row)
|
||||
nodex_tax = None
|
||||
if taxes['traslados'] or taxes['retenciones']:
|
||||
node_tax = ET.SubElement(node, f'{pre}:ImpuestosDR')
|
||||
if taxes['retenciones']:
|
||||
node = ET.SubElement(node_tax, f'{pre}:RetencionesDR')
|
||||
for tax in taxes['retenciones']:
|
||||
ET.SubElement(node, f'{pre}:RetencionDR', tax)
|
||||
if taxes['traslados']:
|
||||
node = ET.SubElement(node_tax, f'{pre}:TrasladosDR')
|
||||
for tax in taxes['traslados']:
|
||||
ET.SubElement(node, f'{pre}:TrasladoDR', tax)
|
||||
|
||||
if 'ce' in datos:
|
||||
pre = 'cce11'
|
||||
datos = datos.pop('ce')
|
||||
emisor = datos.pop('emisor')
|
||||
propietario = datos.pop('propietario')
|
||||
receptor = datos.pop('receptor')
|
||||
destinatario = datos.pop('destinatario')
|
||||
conceptos = datos.pop('conceptos')
|
||||
if taxes_pay['traslados'] or taxes_pay['retenciones']:
|
||||
node_tax = ET.SubElement(node_pago, f'{pre}:ImpuestosP')
|
||||
if taxes_pay['retenciones']:
|
||||
node = ET.SubElement(node_tax, f'{pre}:RetencionesP')
|
||||
for key, importe in taxes_pay['retenciones'].items():
|
||||
attr = {'ImpuestoP': key, 'ImporteP': importe}
|
||||
ET.SubElement(node, f'{pre}:RetencionP', attr)
|
||||
if taxes_pay['traslados']:
|
||||
node = ET.SubElement(node_tax, f'{pre}:TrasladosP')
|
||||
for key, tax in taxes_pay['traslados'].items():
|
||||
ET.SubElement(node, f'{pre}:TrasladoP', tax)
|
||||
|
||||
attributes = {}
|
||||
attributes['xmlns:{}'.format(pre)] = \
|
||||
'http://www.sat.gob.mx/ComercioExterior11'
|
||||
attributes['xsi:schemaLocation'] = \
|
||||
'http://www.sat.gob.mx/ComercioExterior11 ' \
|
||||
'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd'
|
||||
attributes.update(datos)
|
||||
ce = ET.SubElement(
|
||||
complemento, '{}:ComercioExterior'.format(pre), attributes)
|
||||
if 'leyendas' in datos:
|
||||
pre = SAT['leyendas']['prefix']
|
||||
attributes = {'version': SAT['leyendas']['version']}
|
||||
node_leyend = ET.SubElement(
|
||||
self._complemento, '{}:LeyendasFiscales'.format(pre), attributes)
|
||||
for leyend in datos['leyendas']:
|
||||
ET.SubElement(node_leyend, '{}:Leyenda'.format(pre), leyend)
|
||||
|
||||
attributes = {}
|
||||
if 'Curp' in emisor:
|
||||
attributes = {'Curp': emisor.pop('Curp')}
|
||||
node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes)
|
||||
ET.SubElement(node, '{}:Domicilio'.format(pre), emisor)
|
||||
if self._comercio_exterior:
|
||||
datos = datos.pop('comercioe')
|
||||
self._complemento_comercio_exterior(datos)
|
||||
|
||||
if propietario:
|
||||
ET.SubElement(ce, '{}:Propietario'.format(pre), propietario)
|
||||
|
||||
attributes = {}
|
||||
if 'NumRegIdTrib' in receptor:
|
||||
attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
|
||||
node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes)
|
||||
ET.SubElement(node, '{}:Domicilio'.format(pre), receptor)
|
||||
|
||||
attributes = {}
|
||||
if 'NumRegIdTrib' in destinatario:
|
||||
attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')}
|
||||
if 'Nombre' in destinatario:
|
||||
attributes.update({'Nombre': destinatario.pop('Nombre')})
|
||||
node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes)
|
||||
ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario)
|
||||
|
||||
node = ET.SubElement(ce, '{}:Mercancias'.format(pre))
|
||||
fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie')
|
||||
for row in conceptos:
|
||||
detalle = {}
|
||||
for f in fields:
|
||||
if f in row:
|
||||
detalle[f] = row.pop(f)
|
||||
concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row)
|
||||
if detalle:
|
||||
ET.SubElement(
|
||||
concepto, '{}:DescripcionesEspecificas'.format(pre), detalle)
|
||||
return
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
||||
#~ Ecodex
|
||||
ID_INTEGRADOR = ''
|
||||
|
||||
#~ Finkok
|
||||
FINKOK= {
|
||||
'USER': '',
|
||||
'PASS': '',
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
|
||||
from .conf import DEBUG, ID_INTEGRADOR, FINKOK
|
||||
|
||||
DEBUG = DEBUG
|
||||
TIMEOUT = 10
|
||||
|
||||
#~ PACs que han proporcionado un entorno de pruebas libre y abierto
|
||||
#~ ecodex, finkok
|
||||
PAC = 'finkok'
|
||||
|
||||
|
||||
def ecodex(debug):
|
||||
NEW_SERVER = True
|
||||
auth = {'ID': ID_INTEGRADOR}
|
||||
if debug:
|
||||
#~ No cambies este ID de pruebas
|
||||
auth = {'ID': '2b3a8764-d586-4543-9b7e-82834443f219'}
|
||||
|
||||
base_url = 'https://servicios.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
|
||||
if NEW_SERVER:
|
||||
base_url = 'https://serviciosnominas.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
|
||||
base_api = 'https://api.ecodex.com.mx/{}'
|
||||
if debug:
|
||||
base_url = 'https://wsdev.ecodex.com.mx:2045/Servicio{}.svc?wsdl'
|
||||
base_api = 'https://pruebasapi.ecodex.com.mx/{}'
|
||||
url = {
|
||||
'seguridad': base_url.format('Seguridad'),
|
||||
'clients': base_url.format('Clientes'),
|
||||
'timbra': base_url.format('Timbrado'),
|
||||
'token': base_api.format('token?version=2'),
|
||||
'docs': base_api.format('api/documentos'),
|
||||
'hash': base_api.format('api/Documentos/{}'),
|
||||
'codes': {
|
||||
'HASH': 'DUPLICIDAD EN HASH',
|
||||
}
|
||||
}
|
||||
return auth, url
|
||||
|
||||
|
||||
#~ IMPORTANTE: Si quieres hacer pruebas, con tu propio correo de usuario y
|
||||
#~ contraseña, ponte en contacto con Finkok para que te asignen tus datos de
|
||||
#~ acceso, consulta su documentación para ver las diferentes opciones de acceso.
|
||||
#~ Si solo estas haciendo pruebas de timbrado y ancelación, con estos datos debería
|
||||
#~ ser suficiente.
|
||||
def finkok(debug):
|
||||
USER = FINKOK['USER']
|
||||
PASS = FINKOK['PASS']
|
||||
TOKEN = ''
|
||||
auth = {
|
||||
'DEBUG': debug,
|
||||
'USER': '',
|
||||
'PASS': TOKEN or PASS,
|
||||
'RESELLER': {'USER': USER, 'PASS': PASS}
|
||||
}
|
||||
if debug:
|
||||
USER = 'pruebas-finkok@correolibre.net'
|
||||
PASS = ''
|
||||
TOKEN = '5c9a88da105bff9a8c430cb713f6d35269f51674bdc5963c1501b7316366'
|
||||
auth = {
|
||||
'DEBUG': debug,
|
||||
'USER': USER,
|
||||
'PASS': TOKEN or PASS,
|
||||
'RESELLER': {
|
||||
'USER': '',
|
||||
'PASS': ''
|
||||
}
|
||||
}
|
||||
|
||||
base_url = 'https://facturacion.finkok.com/servicios/soap/{}.wsdl'
|
||||
if debug:
|
||||
base_url = 'http://demo-facturacion.finkok.com/servicios/soap/{}.wsdl'
|
||||
url = {
|
||||
'timbra': base_url.format('stamp'),
|
||||
'quick_stamp': False,
|
||||
'cancel': base_url.format('cancel'),
|
||||
'client': base_url.format('registration'),
|
||||
'util': base_url.format('utilities'),
|
||||
'codes': {
|
||||
'200': 'Comprobante timbrado satisfactoriamente',
|
||||
'307': 'Comprobante timbrado previamente',
|
||||
'205': 'No Encontrado',
|
||||
}
|
||||
}
|
||||
return auth, url
|
||||
|
||||
|
||||
AUTH, URL = globals()[PAC](DEBUG)
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import smtplib
|
||||
import ssl
|
||||
import collections
|
||||
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from collections import OrderedDict
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.base import MIMEBase
|
||||
|
@ -29,6 +32,9 @@ from reportlab.platypus import Paragraph, Table, TableStyle, Spacer
|
|||
from reportlab.pdfgen import canvas
|
||||
|
||||
|
||||
CANCEL = False
|
||||
|
||||
|
||||
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
|
||||
class CaseInsensitiveDict(collections.MutableMapping):
|
||||
"""A case-insensitive ``dict``-like object.
|
||||
|
@ -263,9 +269,7 @@ class SendMail(object):
|
|||
|
||||
def _login(self):
|
||||
try:
|
||||
if self._config['ssl'] and (
|
||||
'gmail' in self._config['servidor'] or
|
||||
'outlook' in self._config['servidor']):
|
||||
if self._config['ssl'] and self._config['starttls']:
|
||||
self._server = smtplib.SMTP(
|
||||
self._config['servidor'],
|
||||
self._config['puerto'], timeout=10)
|
||||
|
@ -287,7 +291,7 @@ class SendMail(object):
|
|||
if '535' in str(e):
|
||||
self._error = 'Nombre de usuario o contraseña inválidos'
|
||||
return False
|
||||
print (e)
|
||||
# ~ print (e)
|
||||
if '534' in str(e) and 'gmail' in self._config['servidor']:
|
||||
self._error = 'Necesitas activar el acceso a otras ' \
|
||||
'aplicaciones en tu cuenta de GMail'
|
||||
|
@ -340,6 +344,7 @@ class SendMail(object):
|
|||
|
||||
class NumberedCanvas(canvas.Canvas):
|
||||
X = 20.59 * cm
|
||||
XC = 21.6 * cm / 2 + 1.5 * cm
|
||||
Y = 1.5 * cm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -357,19 +362,33 @@ class NumberedCanvas(canvas.Canvas):
|
|||
for state in self._saved_page_states:
|
||||
self.__dict__.update(state)
|
||||
self.draw_page_number(page_count)
|
||||
self.draw_cancel()
|
||||
canvas.Canvas.showPage(self)
|
||||
canvas.Canvas.save(self)
|
||||
return
|
||||
|
||||
def draw_page_number(self, page_count):
|
||||
self.setFont('Helvetica', 8)
|
||||
self.setFillColor(colors.darkred)
|
||||
text = 'Página {} de {}'.format(self._pageNumber, page_count)
|
||||
# ~ self.setFillColor(colors.darkred)
|
||||
text = f'Página {self._pageNumber} de {page_count}'
|
||||
self.drawRightString(self.X, self.Y, text)
|
||||
text = 'Factura elaborada con software libre: www.empresalibre.net'
|
||||
|
||||
text = 'Este documento es una representación impresa de un CFDI'
|
||||
self.drawCentredString(self.XC, self.Y, text)
|
||||
|
||||
text = 'Factura elaborada con software libre'
|
||||
self.drawString(1.5 * cm, 1.5 * cm, text)
|
||||
return
|
||||
|
||||
def draw_cancel(self):
|
||||
global CANCEL
|
||||
if CANCEL:
|
||||
self.rotate(45)
|
||||
self.setFont('Helvetica', 100)
|
||||
self.setFillColor(colors.red)
|
||||
text = 'Cancelada'
|
||||
self.drawCentredString(20 * cm, 3 * cm, text)
|
||||
return
|
||||
|
||||
class TemplateInvoice(BaseDocTemplate):
|
||||
|
||||
|
@ -418,6 +437,9 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
def _emisor(self, styles, data):
|
||||
logo_path = data.pop('logo', '')
|
||||
logo_style = styles.pop('logo', {})
|
||||
logo_path2 = data.pop('logo2', '')
|
||||
logo_style2 = styles.pop('logo2', {})
|
||||
sucursales = styles.pop('sucursales', {})
|
||||
|
||||
for k, v in styles.items():
|
||||
self._set_text(styles[k], data.get(k, ''))
|
||||
|
@ -428,6 +450,28 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
for k in keys:
|
||||
rect[k] = rect[k] * cm
|
||||
self.canv.drawImage(logo_path, **rect)
|
||||
|
||||
if logo_path2 and logo_style2:
|
||||
rect = logo_style2['rectangulo']
|
||||
keys = ('x', 'y', 'width', 'height')
|
||||
for k in keys:
|
||||
rect[k] = rect[k] * cm
|
||||
self.canv.drawImage(logo_path2, **rect)
|
||||
|
||||
if sucursales:
|
||||
for k, sucursal in sucursales.items():
|
||||
values = sucursal.pop('textos')
|
||||
x = sucursal['rectangulo']['x']
|
||||
y = sucursal['rectangulo']['y']
|
||||
w = sucursal['rectangulo']['width']
|
||||
h = sucursal['rectangulo']['height']
|
||||
for v in values:
|
||||
self._set_text(sucursal, v)
|
||||
y -= h
|
||||
sucursal['rectangulo']['x'] = x
|
||||
sucursal['rectangulo']['y'] = y
|
||||
sucursal['rectangulo']['width'] = w
|
||||
sucursal['rectangulo']['height'] = h
|
||||
return
|
||||
|
||||
def _receptor(self, styles, data):
|
||||
|
@ -449,6 +493,7 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
|
||||
def _comprobante1(self, styles, data):
|
||||
title = styles.pop('titulo', {})
|
||||
self.canv.setTitle(f"Factura {data.get('seriefolio', '')}")
|
||||
|
||||
for k, v in styles.items():
|
||||
self._set_text(styles[k], data.get(k, ''))
|
||||
|
@ -480,6 +525,12 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
fields = ('valorunitario', 'importe')
|
||||
if field in fields:
|
||||
return self._currency(value)
|
||||
|
||||
if field == 'descripcion':
|
||||
html = '<font color="black" size=7>{}</font>'
|
||||
style_bt = getSampleStyleSheet()['BodyText']
|
||||
return Paragraph(html.format(value), style_bt)
|
||||
|
||||
return value
|
||||
|
||||
def _conceptos(self, conceptos):
|
||||
|
@ -515,8 +566,8 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white),
|
||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 8),
|
||||
('BACKGROUND', (1, 0), (-1, -1), colors.linen),
|
||||
('TEXTCOLOR', (1, 0), (-1, -1), colors.darkred),
|
||||
('BACKGROUND', (1, 0), (-1, -1), colors.lightgrey),
|
||||
('TEXTCOLOR', (1, 0), (-1, -1), colors.black),
|
||||
('FACE', (1, 0), (-1, -1), 'Helvetica-Bold'),
|
||||
]
|
||||
table = Table(rows, colWidths=widths, spaceBefore=0.25*cm)
|
||||
|
@ -524,7 +575,7 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
return table
|
||||
|
||||
def _comprobante2(self, styles, data):
|
||||
leyenda = styles.pop('leyenda', {})
|
||||
leyendas = styles.pop('leyendas', {})
|
||||
|
||||
ls = []
|
||||
for k, v in styles.items():
|
||||
|
@ -532,17 +583,27 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
if 'spaceBefore' in v['estilo']:
|
||||
v['estilo']['spaceBefore'] = v['estilo']['spaceBefore'] * cm
|
||||
ps = ParagraphStyle(**v['estilo'])
|
||||
p = Paragraph(data[k], ps)
|
||||
p = Paragraph(escape(data[k]), ps)
|
||||
ls.append(p)
|
||||
elif k=='formametodopago':
|
||||
ps = ParagraphStyle(**v['estilo'])
|
||||
v = f"{data['formadepago']} - {data['metododepago']}"
|
||||
p = Paragraph(v, ps)
|
||||
ls.append(p)
|
||||
elif k=='monedatipocambio':
|
||||
ps = ParagraphStyle(**v['estilo'])
|
||||
v = f"{data['moneda']} - {data['tipocambio']}"
|
||||
p = Paragraph(v, ps)
|
||||
ls.append(p)
|
||||
|
||||
cbb = Image(data['path_cbb'])
|
||||
cbb = Image(data['cbb'])
|
||||
cbb.drawHeight = 4 * cm
|
||||
cbb.drawWidth = 4 * cm
|
||||
|
||||
style_bt = getSampleStyleSheet()['BodyText']
|
||||
style_bt.leading = 8
|
||||
html_t = '<b><font size=6>{}</font></b>'
|
||||
html = '<font color="darkred" size=5>{}</font>'
|
||||
html = '<font color="black" size=5>{}</font>'
|
||||
msg = 'Cadena original del complemento de certificación digital del SAT'
|
||||
rows = [
|
||||
(cbb, Paragraph(html_t.format('Sello Digital del CFDI'), style_bt)),
|
||||
|
@ -558,13 +619,13 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
('FONTSIZE', (0, 0), (-1, -1), 6),
|
||||
('SPAN', (0, 0), (0, -1)),
|
||||
('FACE', (1, 0), (1, 0), 'Helvetica-Bold'),
|
||||
('BACKGROUND', (1, 1), (1, 1), colors.linen),
|
||||
('BACKGROUND', (1, 1), (1, 1), colors.lightgrey),
|
||||
('TEXTCOLOR', (1, 1), (1, 1), colors.darkred),
|
||||
('FACE', (1, 2), (1, 2), 'Helvetica-Bold'),
|
||||
('BACKGROUND', (1, 3), (1, 3), colors.linen),
|
||||
('BACKGROUND', (1, 3), (1, 3), colors.lightgrey),
|
||||
('TEXTCOLOR', (1, 3), (1, 3), colors.darkred),
|
||||
('FACE', (1, 4), (1, 4), 'Helvetica-Bold'),
|
||||
('BACKGROUND', (1, 5), (1, 5), colors.linen),
|
||||
('BACKGROUND', (1, 5), (1, 5), colors.lightgrey),
|
||||
('TEXTCOLOR', (1, 5), (1, 5), colors.darkred),
|
||||
('ALIGN', (0, 0), (0, 0), 'CENTER'),
|
||||
('VALIGN', (0, 0), (0, 0), 'MIDDLE'),
|
||||
|
@ -573,14 +634,14 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
table.setStyle(TableStyle(table_styles))
|
||||
ls.append(table)
|
||||
|
||||
if leyenda:
|
||||
if 'spaceBefore' in leyenda['estilo']:
|
||||
leyenda['estilo']['spaceBefore'] = \
|
||||
leyenda['estilo']['spaceBefore'] * cm
|
||||
msg = 'Este documento es una representación impresa de un CFDI'
|
||||
ps = ParagraphStyle(**leyenda['estilo'])
|
||||
p = Paragraph(msg, ps)
|
||||
ls.append(p)
|
||||
if leyendas:
|
||||
if 'spaceBefore' in leyendas['estilo']:
|
||||
leyendas['estilo']['spaceBefore'] = \
|
||||
leyendas['estilo']['spaceBefore'] * cm
|
||||
for t in leyendas['textos']:
|
||||
ps = ParagraphStyle(**leyendas['estilo'])
|
||||
p = Paragraph(t, ps)
|
||||
ls.append(p)
|
||||
|
||||
return ls
|
||||
|
||||
|
@ -596,8 +657,10 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
return self._data
|
||||
@data.setter
|
||||
def data(self, values):
|
||||
#~ print (values)
|
||||
global CANCEL
|
||||
# ~ print (values)
|
||||
self._data = values
|
||||
CANCEL = self._data['cancelada']
|
||||
|
||||
rows = self._conceptos(self._data['conceptos'])
|
||||
widths = [2 * cm, 9 * cm, 1.5 * cm, 2 * cm, 2 * cm, 3 * cm]
|
||||
|
@ -607,13 +670,13 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
||||
('FACE', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 7),
|
||||
('VALIGN', (0, 1), (-1, -1), 'TOP'),
|
||||
('ALIGN', (0, 1), (0, -1), 'CENTER'),
|
||||
('ALIGN', (2, 1), (2, -1), 'CENTER'),
|
||||
('ALIGN', (3, 1), (5, -1), 'RIGHT'),
|
||||
('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.darkred),
|
||||
('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.grey),
|
||||
('LINEBEFORE', (0, 1), (-1, -1), 0.05 * cm, colors.white),
|
||||
]
|
||||
table_conceptos = Table(rows, colWidths=widths, repeatRows=1)
|
||||
|
@ -623,7 +686,7 @@ class TemplateInvoice(BaseDocTemplate):
|
|||
comprobante = self._comprobante2(
|
||||
self._custom_styles['comprobante'], self.data['comprobante'])
|
||||
|
||||
self._rows = [Spacer(0, 6*cm), table_conceptos, totales] + comprobante
|
||||
self._rows = [Spacer(0, 5.7 * cm), table_conceptos, totales] + comprobante
|
||||
|
||||
def render(self):
|
||||
frame = Frame(self.leftMargin, self.bottomMargin,
|
||||
|
@ -1054,4 +1117,4 @@ class PrintTicket(object):
|
|||
self._t(self.LEYENDA)
|
||||
self._set('center', 'A', 'B')
|
||||
self._t('empresalibre.net')
|
||||
return
|
||||
return
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import falcon
|
||||
from middleware import get_template
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
class AppEmpresas(object):
|
||||
|
@ -47,6 +48,7 @@ class AppLogin(object):
|
|||
session.invalidate()
|
||||
values = req.params
|
||||
values['rfc'] = values['rfc'].upper()
|
||||
values['ip'] = req.remote_addr
|
||||
result, user = self._db.authenticate(values)
|
||||
if result['login']:
|
||||
session.save()
|
||||
|
@ -110,6 +112,14 @@ class AppValues(object):
|
|||
def on_get(self, req, resp, table):
|
||||
values = req.params
|
||||
session = req.env['beaker.session']
|
||||
|
||||
if table == 'product':
|
||||
original = values['name']
|
||||
try:
|
||||
values['name'] = unquote(req.query_string.split('=')[1])
|
||||
except Exception as e:
|
||||
values['name'] = original
|
||||
|
||||
if req.path in ('/values/titlelogin', '/values/empresas'):
|
||||
req.context['result'] = self._db.get_values(table, values, session)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
@ -278,7 +288,12 @@ class AppProducts(object):
|
|||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.get_products(values)
|
||||
user = req.env['beaker.session']['userobj']
|
||||
|
||||
if 'opt' in values:
|
||||
req.context['result'] = self._db.products_get(values, user)
|
||||
else:
|
||||
req.context['result'] = self._db.get_products(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
|
@ -301,7 +316,8 @@ class AppInvoices(object):
|
|||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.get_invoices(values)
|
||||
session = req.env['beaker.session']
|
||||
req.context['result'] = self._db.get_invoices(values, session['userobj'])
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
|
@ -310,6 +326,12 @@ class AppInvoices(object):
|
|||
req.context['result'] = self._db.invoice(values, session['userobj'])
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_put(self, req, resp):
|
||||
values = req.params
|
||||
session = req.env['beaker.session']
|
||||
req.context['result'] = self._db.invoice_put(values, session['userobj'])
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_delete(self, req, resp):
|
||||
values = req.params
|
||||
session = req.env['beaker.session']
|
||||
|
@ -349,7 +371,8 @@ class AppTickets(object):
|
|||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.get_tickets(values)
|
||||
session = req.env['beaker.session']
|
||||
req.context['result'] = self._db.get_tickets(values, session['userobj'])
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
|
@ -485,12 +508,24 @@ class AppNomina(object):
|
|||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.get_nomina(values)
|
||||
user = req.env['beaker.session']['userobj']
|
||||
if 'opt' in values:
|
||||
req.context['result'] = self._db.nomina_get(values, user)
|
||||
else:
|
||||
by = values.get('by', '')
|
||||
req.context['result'] = self._db.get_nomina(values)
|
||||
if 'download' in by:
|
||||
name = req.context['result']['name']
|
||||
req.context['blob'] = req.context['result']['data']
|
||||
resp.content_type = 'application/octet-stream'
|
||||
resp.append_header(
|
||||
'Content-Disposition', f'attachment; filename={name}')
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.nomina(values)
|
||||
session = req.env['beaker.session']
|
||||
req.context['result'] = self._db.nomina(values, session['userobj'])
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_delete(self, req, resp):
|
||||
|
@ -507,12 +542,16 @@ class AppDocumentos(object):
|
|||
self._db = db
|
||||
|
||||
def on_get(self, req, resp, type_doc, id_doc):
|
||||
# ~ print('TD', type_doc)
|
||||
session = req.env['beaker.session']
|
||||
req.context['result'], file_name, content_type = \
|
||||
self._db.get_doc(type_doc, id_doc, session['rfc'])
|
||||
if not type_doc in ('pdf', 'pre', 'tpdf', 'pdfpago'):
|
||||
if not type_doc in ('pdf', 'pre', 'tpdf', 'pdfpago', 'html', 'nompdf'):
|
||||
resp.append_header('Content-Disposition',
|
||||
'attachment; filename={}'.format(file_name))
|
||||
if type_doc in ('pdf', 'nompdf', 'pdfpago'):
|
||||
resp.append_header('Content-Disposition',
|
||||
'inline; filename={}'.format(file_name))
|
||||
resp.content_type = content_type
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
@ -577,6 +616,29 @@ class AppSATFormaPago(object):
|
|||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppSATLeyendaFiscales(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.sat_leyendas_fiscales_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.sat_leyendas_fiscales_post(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_delete(self, req, resp):
|
||||
values = req.params
|
||||
if self._db.sat_leyendas_fiscales_delete(values['id']):
|
||||
resp.status = falcon.HTTP_200
|
||||
else:
|
||||
resp.status = falcon.HTTP_204
|
||||
|
||||
|
||||
class AppSociosCuentasBanco(object):
|
||||
|
||||
def __init__(self, db):
|
||||
|
@ -592,3 +654,174 @@ class AppSociosCuentasBanco(object):
|
|||
req.context['result'] = self._db.partners_accounts_bank(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppCert(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.cert_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.cert_post(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppSucursales(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.sucursales_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.sucursales_post(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppPartnerProducts(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.partner_products_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.partner_products_post(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppInventoryEntries(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.inventory_entries_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.inventory_entries_post(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppWareHouse(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
req.context['result'] = self._db.warehouse_get(values)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.warehouse_post(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppWareHouseProduct(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.warehouseproduct_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.warehouseproduct_post(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppTicketsDetails(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.ticketsdetails_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppUsers(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.users_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.users_post(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppSATUnidadesPeso(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.sat_unidades_peso_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
def on_post(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.sat_unidades_peso_post(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppSATRegimenes(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.sat_regimenes_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class AppSociosRegimenes(object):
|
||||
|
||||
def __init__(self, db):
|
||||
self._db = db
|
||||
|
||||
def on_get(self, req, resp):
|
||||
values = req.params
|
||||
user = req.env['beaker.session']['userobj']
|
||||
req.context['result'] = self._db.socios_regimenes_get(values, user)
|
||||
resp.status = falcon.HTTP_200
|
||||
|
|
|
@ -1,735 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
#~ import re
|
||||
#~ from xml.etree import ElementTree as ET
|
||||
#~ from requests import Request, Session, exceptions
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import requests
|
||||
import time
|
||||
from lxml import etree
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.sax.saxutils import escape, unescape
|
||||
from uuid import UUID
|
||||
|
||||
from logbook import Logger
|
||||
from zeep import Client
|
||||
from zeep.plugins import HistoryPlugin
|
||||
from zeep.cache import SqliteCache
|
||||
from zeep.transports import Transport
|
||||
from zeep.exceptions import Fault, TransportError
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from configpac import DEBUG, TIMEOUT, AUTH, URL
|
||||
else:
|
||||
from .configpac import DEBUG, TIMEOUT, AUTH, URL
|
||||
|
||||
|
||||
log = Logger('PAC')
|
||||
#~ node = client.create_message(client.service, SERVICE, **args)
|
||||
#~ print(etree.tostring(node, pretty_print=True).decode())
|
||||
|
||||
|
||||
class Ecodex(object):
|
||||
|
||||
def __init__(self, auth, url):
|
||||
self.auth = auth
|
||||
self.url = url
|
||||
self.codes = self.url['codes']
|
||||
self.error = ''
|
||||
self.message = ''
|
||||
self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
|
||||
self._plugins = None
|
||||
self._history = None
|
||||
if DEBUG:
|
||||
self._history = HistoryPlugin()
|
||||
self._plugins = [self._history]
|
||||
|
||||
def _get_token(self, rfc):
|
||||
client = Client(self.url['seguridad'],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.ObtenerToken(rfc, self._get_epoch())
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
s = '{}|{}'.format(self.auth['ID'], result.Token)
|
||||
return hashlib.sha1(s.encode()).hexdigest()
|
||||
|
||||
def _get_token_rest(self, rfc):
|
||||
data = {
|
||||
'rfc': rfc,
|
||||
'grant_type': 'authorization_token',
|
||||
}
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'}
|
||||
result = requests.post(URL['token'], data=data, headers=headers)
|
||||
data = result.json()
|
||||
s = '{}|{}'.format(AUTH['ID'], data['service_token'])
|
||||
return hashlib.sha1(s.encode()).hexdigest(), data['access_token']
|
||||
|
||||
def _validate_xml(self, xml):
|
||||
NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
|
||||
if os.path.isfile(xml):
|
||||
tree = etree.parse(xml).getroot()
|
||||
else:
|
||||
tree = etree.fromstring(xml.encode())
|
||||
|
||||
fecha = tree.get('Fecha')
|
||||
rfc = tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
|
||||
data = {
|
||||
'ComprobanteXML': etree.tostring(tree).decode(),
|
||||
'RFC': rfc,
|
||||
'Token': self._get_token(rfc),
|
||||
'TransaccionID': self._get_epoch(fecha),
|
||||
}
|
||||
return data
|
||||
|
||||
def _get_by_hash(self, sh, rfc):
|
||||
token, access_token = self._get_token_rest(rfc)
|
||||
url = URL['hash'].format(sh)
|
||||
headers = {
|
||||
'Authorization': 'Bearer {}'.format(access_token),
|
||||
'X-Auth-Token': token,
|
||||
}
|
||||
result = requests.get(url, headers=headers)
|
||||
if result.status_code == 200:
|
||||
print (result.json())
|
||||
return
|
||||
|
||||
def timbra_xml(self, xml):
|
||||
data = self._validate_xml(xml)
|
||||
client = Client(self.url['timbra'],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.TimbraXML(**data)
|
||||
except Fault as e:
|
||||
error = str(e)
|
||||
if self.codes['HASH'] in error:
|
||||
sh = error.split(' ')[3]
|
||||
return self._get_by_hash(sh[:40], data['RFC'])
|
||||
self.error = error
|
||||
return ''
|
||||
|
||||
tree = parseString(result.ComprobanteXML.DatosXML)
|
||||
xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
|
||||
return xml
|
||||
|
||||
def _get_epoch(self, date=None):
|
||||
if isinstance(date, str):
|
||||
f = '%Y-%m-%dT%H:%M:%S'
|
||||
e = int(time.mktime(time.strptime(date, f)))
|
||||
else:
|
||||
date = datetime.datetime.now()
|
||||
e = int(time.mktime(date.timetuple()))
|
||||
return e
|
||||
|
||||
def estatus_cuenta(self, rfc):
|
||||
#~ Codigos:
|
||||
#~ 100 = Cuenta encontrada
|
||||
#~ 101 = RFC no dado de alta en el sistema ECODEX
|
||||
token = self._get_token(rfc)
|
||||
if not token:
|
||||
return {}
|
||||
|
||||
data = {
|
||||
'RFC': rfc,
|
||||
'Token': token,
|
||||
'TransaccionID': self._get_epoch()
|
||||
}
|
||||
client = Client(URL['clients'],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.EstatusCuenta(**data)
|
||||
except Fault as e:
|
||||
log.error(str(e))
|
||||
return
|
||||
#~ print (result)
|
||||
return result.Estatus
|
||||
|
||||
|
||||
class Finkok(object):
|
||||
|
||||
def __init__(self, auth={}):
|
||||
self.codes = URL['codes']
|
||||
self.error = ''
|
||||
self.message = ''
|
||||
self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
|
||||
self._plugins = None
|
||||
self._history = None
|
||||
self.uuid = ''
|
||||
self.fecha = None
|
||||
if DEBUG:
|
||||
self._history = HistoryPlugin()
|
||||
self._plugins = [self._history]
|
||||
self._auth = AUTH
|
||||
else:
|
||||
self._auth = auth
|
||||
|
||||
def _debug(self):
|
||||
if not DEBUG:
|
||||
return
|
||||
print('SEND', self._history.last_sent)
|
||||
print('RESULT', self._history.last_received)
|
||||
return
|
||||
|
||||
def _check_result(self, method, result):
|
||||
# ~ print ('CODE', result.CodEstatus)
|
||||
# ~ print ('INCIDENCIAS', result.Incidencias)
|
||||
self.message = ''
|
||||
MSG = {
|
||||
'OK': 'Comprobante timbrado satisfactoriamente',
|
||||
'307': 'Comprobante timbrado previamente',
|
||||
}
|
||||
status = result.CodEstatus
|
||||
if status is None and result.Incidencias:
|
||||
for i in result.Incidencias['Incidencia']:
|
||||
self.error += 'Error: {}\n{}\n{}'.format(
|
||||
i['CodigoError'], i['MensajeIncidencia'], i['ExtraInfo'])
|
||||
return ''
|
||||
|
||||
if method == 'timbra' and status in (MSG['OK'], MSG['307']):
|
||||
#~ print ('UUID', result.UUID)
|
||||
#~ print ('FECHA', result.Fecha)
|
||||
if status == MSG['307']:
|
||||
self.message = MSG['307']
|
||||
tree = parseString(result.xml)
|
||||
response = tree.toprettyxml(encoding='utf-8').decode('utf-8')
|
||||
self.uuid = result.UUID
|
||||
self.fecha = result.Fecha
|
||||
|
||||
return response
|
||||
|
||||
def _load_file(self, path):
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
except Exception as e:
|
||||
self.error = str(e)
|
||||
return
|
||||
return data
|
||||
|
||||
def _validate_xml(self, file_xml):
|
||||
if os.path.isfile(file_xml):
|
||||
try:
|
||||
with open(file_xml, 'rb') as f:
|
||||
xml = f.read()
|
||||
except Exception as e:
|
||||
self.error = str(e)
|
||||
return False, ''
|
||||
else:
|
||||
xml = file_xml.encode('utf-8')
|
||||
return True, xml
|
||||
|
||||
def _validate_uuid(self, uuid):
|
||||
try:
|
||||
UUID(uuid)
|
||||
return True
|
||||
except ValueError:
|
||||
self.error = 'UUID no válido: {}'.format(uuid)
|
||||
return False
|
||||
|
||||
def timbra_xml(self, file_xml):
|
||||
self.error = ''
|
||||
|
||||
if not DEBUG and not self._auth:
|
||||
self.error = 'Sin datos para timbrar'
|
||||
return
|
||||
|
||||
method = 'timbra'
|
||||
ok, xml = self._validate_xml(file_xml)
|
||||
if not ok:
|
||||
return ''
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'xml': xml,
|
||||
}
|
||||
if URL['quick_stamp']:
|
||||
try:
|
||||
result = client.service.quick_stamp(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
result = client.service.stamp(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return
|
||||
except TransportError as e:
|
||||
if '413' in str(e):
|
||||
self.error = '413<BR><BR><b>Documento muy grande para timbrar</b>'
|
||||
else:
|
||||
self.error = str(e)
|
||||
return
|
||||
except ConnectionError as e:
|
||||
msg = '502 - Error de conexión'
|
||||
self.error = msg
|
||||
return
|
||||
|
||||
return self._check_result(method, result)
|
||||
|
||||
def _get_xml(self, uuid):
|
||||
if not self._validate_uuid(uuid):
|
||||
return ''
|
||||
|
||||
method = 'util'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'uuid': uuid,
|
||||
'taxpayer_id': self.rfc,
|
||||
'invoice_type': 'I',
|
||||
}
|
||||
try:
|
||||
result = client.service.get_xml(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
except TransportError as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
if result.error:
|
||||
self.error = result.error
|
||||
return ''
|
||||
|
||||
tree = parseString(result.xml)
|
||||
xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
|
||||
return xml
|
||||
|
||||
def recupera_xml(self, file_xml='', uuid=''):
|
||||
self.error = ''
|
||||
if uuid:
|
||||
return self._get_xml(uuid)
|
||||
|
||||
method = 'timbra'
|
||||
ok, xml = self._validate_xml(file_xml)
|
||||
if not ok:
|
||||
return ''
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.stamped(
|
||||
xml, self._auth['user'], self._auth['pass'])
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return self._check_result(method, result)
|
||||
|
||||
def estatus_xml(self, uuid):
|
||||
method = 'timbra'
|
||||
if not self._validate_uuid(uuid):
|
||||
return ''
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.query_pending(
|
||||
self._auth['USER'], self._auth['PASS'], uuid)
|
||||
return result.status
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
def cancel_xml(self, rfc, uuid, cer, key):
|
||||
# ~ for u in uuids:
|
||||
# ~ if not self._validate_uuid(u):
|
||||
# ~ return ''
|
||||
|
||||
method = 'cancel'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
uuid_type = client.get_type('ns1:UUIDS')
|
||||
sa = client.get_type('ns0:stringArray')
|
||||
|
||||
args = {
|
||||
'UUIDS': uuid_type(uuids=sa(string=uuid)),
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
'cer': cer,
|
||||
'key': key,
|
||||
'store_pending': False,
|
||||
}
|
||||
try:
|
||||
result = client.service.cancel(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
if result.CodEstatus and self.codes['205'] in result.CodEstatus:
|
||||
self.error = result.CodEstatus
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def cancel_signature(self, file_xml):
|
||||
method = 'cancel'
|
||||
if os.path.isfile(file_xml):
|
||||
root = etree.parse(file_xml).getroot()
|
||||
else:
|
||||
root = etree.fromstring(file_xml.encode())
|
||||
|
||||
xml = etree.tostring(root)
|
||||
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'xml': xml,
|
||||
'store_pending': False,
|
||||
}
|
||||
|
||||
try:
|
||||
result = client.service.cancel_signature(**args)
|
||||
return result
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
def get_acuse(self, rfc, uuids, type_acuse='C'):
|
||||
for u in uuids:
|
||||
if not self._validate_uuid(u):
|
||||
return ''
|
||||
|
||||
method = 'cancel'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
'uuid': '',
|
||||
'type': type_acuse,
|
||||
}
|
||||
try:
|
||||
result = []
|
||||
for u in uuids:
|
||||
args['uuid'] = u
|
||||
r = client.service.get_receipt(**args)
|
||||
result.append(r)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def estatus_cancel(self, uuids):
|
||||
for u in uuids:
|
||||
if not self._validate_uuid(u):
|
||||
return ''
|
||||
|
||||
method = 'cancel'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': self._auth['USER'],
|
||||
'password': self._auth['PASS'],
|
||||
'uuid': '',
|
||||
}
|
||||
try:
|
||||
result = []
|
||||
for u in uuids:
|
||||
args['uuid'] = u
|
||||
r = client.service.query_pending_cancellation(**args)
|
||||
result.append(r)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def add_token(self, rfc, email):
|
||||
"""Agrega un nuevo token al cliente para timbrado.
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del cliente, ya debe existir
|
||||
email (str): El correo del cliente, funciona como USER al timbrar
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'username': 'username',
|
||||
'status': True or False
|
||||
'name': 'name',
|
||||
'success': True or False
|
||||
'token': 'Token de timbrado',
|
||||
'message': None
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
method = 'util'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'username': auth['USER'],
|
||||
'password': auth['PASS'],
|
||||
'name': rfc,
|
||||
'token_username': email,
|
||||
'taxpayer_id': rfc,
|
||||
'status': True,
|
||||
}
|
||||
try:
|
||||
result = client.service.add_token(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def get_date(self):
|
||||
method = 'util'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
try:
|
||||
result = client.service.datetime(AUTH['USER'], AUTH['PASS'])
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
if result.error:
|
||||
self.error = result.error
|
||||
return
|
||||
|
||||
return result.datetime
|
||||
|
||||
def add_client(self, rfc, type_user=False):
|
||||
"""Agrega un nuevo cliente para timbrado.
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del nuevo cliente
|
||||
|
||||
Kwargs:
|
||||
type_user (bool): False == 'P' == Prepago or True == 'O' == On demand
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'message':
|
||||
'Account Created successfully'
|
||||
'Account Already exists'
|
||||
'success': True or False
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
tu = {False: 'P', True: 'O'}
|
||||
method = 'client'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'reseller_username': auth['USER'],
|
||||
'reseller_password': auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
'type_user': tu[type_user],
|
||||
'added': datetime.datetime.now().isoformat()[:19],
|
||||
}
|
||||
try:
|
||||
result = client.service.add(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def edit_client(self, rfc, status=True):
|
||||
"""
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
status = 'A' or 'S'
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
sv = {False: 'S', True: 'A'}
|
||||
method = 'client'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'reseller_username': auth['USER'],
|
||||
'reseller_password': auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
'status': sv[status],
|
||||
}
|
||||
try:
|
||||
result = client.service.edit(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def get_client(self, rfc):
|
||||
"""Regresa el estatus del cliente
|
||||
.
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del emisor
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'message': None,
|
||||
'users': {
|
||||
'ResellerUser': [
|
||||
{
|
||||
'status': 'A',
|
||||
'counter': 0,
|
||||
'taxpayer_id': '',
|
||||
'credit': 0
|
||||
}
|
||||
]
|
||||
} or None si no existe
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
method = 'client'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'reseller_username': auth['USER'],
|
||||
'reseller_password': auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
}
|
||||
|
||||
try:
|
||||
result = client.service.get(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
except TransportError as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def assign_client(self, rfc, credit):
|
||||
"""Agregar credito a un emisor
|
||||
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del emisor, debe existir
|
||||
credit (int): Cantidad de folios a agregar
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'success': True or False,
|
||||
'credit': nuevo credito despues de agregar or None
|
||||
'message':
|
||||
'Success, added {credit} of credit to {RFC}'
|
||||
'RFC no encontrado'
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
method = 'client'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'username': auth['USER'],
|
||||
'password': auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
'credit': credit,
|
||||
}
|
||||
try:
|
||||
result = client.service.assign(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return ''
|
||||
|
||||
return result
|
||||
|
||||
def client_get_timbres(self, rfc):
|
||||
method = 'client'
|
||||
client = Client(
|
||||
URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'reseller_username': self._auth['USER'],
|
||||
'reseller_password': self._auth['PASS'],
|
||||
'taxpayer_id': rfc,
|
||||
}
|
||||
|
||||
try:
|
||||
self.result = client.service.get(**args)
|
||||
except Fault as e:
|
||||
self.error = str(e)
|
||||
return 0
|
||||
except TransportError as e:
|
||||
self.error = str(e)
|
||||
return 0
|
||||
except ConnectionError:
|
||||
self.error = 'Verifica la conexión a internet'
|
||||
return 0
|
||||
|
||||
success = bool(self.result.users)
|
||||
if not success:
|
||||
self.error = self.result.message or 'RFC no existe'
|
||||
return 0
|
||||
|
||||
return self.result.users.ResellerUser[0].credit
|
||||
|
||||
|
||||
def _get_data_sat(path):
|
||||
BF = 'string(//*[local-name()="{}"]/@{})'
|
||||
NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
|
||||
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
tree = etree.parse(path).getroot()
|
||||
else:
|
||||
tree = etree.fromstring(path.encode())
|
||||
|
||||
data = {}
|
||||
emisor = escape(
|
||||
tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=NS_CFDI) or
|
||||
tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
|
||||
)
|
||||
receptor = escape(
|
||||
tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=NS_CFDI) or
|
||||
tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=NS_CFDI)
|
||||
)
|
||||
data['total'] = tree.get('total') or tree.get('Total')
|
||||
data['emisor'] = emisor
|
||||
data['receptor'] = receptor
|
||||
data['uuid'] = tree.xpath(BF.format('TimbreFiscalDigital', 'UUID'))
|
||||
except Exception as e:
|
||||
print (e)
|
||||
return {}
|
||||
|
||||
return '?re={emisor}&rr={receptor}&tt={total}&id={uuid}'.format(**data)
|
||||
|
||||
|
||||
def get_status_sat(xml):
|
||||
data = _get_data_sat(xml)
|
||||
if not data:
|
||||
return 'XML inválido'
|
||||
|
||||
URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl'
|
||||
client = Client(URL, transport=Transport(cache=SqliteCache()))
|
||||
try:
|
||||
result = client.service.Consulta(expresionImpresa=data)
|
||||
except Exception as e:
|
||||
return 'Error: {}'.format(str(e))
|
||||
|
||||
return result.Estado
|
||||
|
||||
|
||||
def main():
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from .comerciodigital import PACComercioDigital
|
||||
from .finkok import PACFinkok
|
|
@ -0,0 +1,350 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import datetime
|
||||
import getpass
|
||||
import hashlib
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import lxml.etree as ET
|
||||
import xmlsec
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.x509.oid import ExtensionOID
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
from conf import TOKEN
|
||||
|
||||
|
||||
class SATCertificate(object):
|
||||
|
||||
def __init__(self, cer=b'', key=b'', password=''):
|
||||
self._error = ''
|
||||
self._init_values()
|
||||
self._get_data_cer(cer)
|
||||
self._get_data_key(key, password)
|
||||
# ~ if not password:
|
||||
# ~ self._test()
|
||||
|
||||
# ~ def _test(self):
|
||||
# ~ key = self._get_key('')
|
||||
# ~ self._p = ''
|
||||
# ~ self._key_der = key.private_bytes(
|
||||
# ~ encoding=serialization.Encoding.DER,
|
||||
# ~ format=serialization.PrivateFormat.PKCS8,
|
||||
# ~ encryption_algorithm=serialization.BestAvailableEncryption(self._p.encode())
|
||||
# ~ )
|
||||
# ~ return
|
||||
|
||||
def _init_values(self):
|
||||
self._rfc = ''
|
||||
self._serial_number = ''
|
||||
self._serial_number2 = ''
|
||||
self._subject = ''
|
||||
self._issuer = ''
|
||||
self._not_before = None
|
||||
self._not_after = None
|
||||
self._is_fiel = False
|
||||
self._are_couple = False
|
||||
self._is_valid_time = False
|
||||
self._key = b''
|
||||
self._cer = b''
|
||||
self._cer_pem = ''
|
||||
self._cer_txt = ''
|
||||
self._key_enc = b''
|
||||
self._key_der = b''
|
||||
self._p12 = b''
|
||||
self._cer_modulus = 0
|
||||
self._key_modulus = 0
|
||||
return
|
||||
|
||||
def __str__(self):
|
||||
msg = '\tRFC: {}\n'.format(self.rfc)
|
||||
msg += '\tNo de Serie: {}\n'.format(self.serial_number)
|
||||
msg += '\tVálido desde: {}\n'.format(self.not_before)
|
||||
msg += '\tVálido hasta: {}\n'.format(self.not_after)
|
||||
msg += '\tEs vigente: {}\n'.format(self.is_valid_time)
|
||||
msg += '\tSon pareja: {}\n'.format(self.are_couple)
|
||||
msg += '\tEs FIEL: {}\n'.format(self.is_fiel)
|
||||
return msg
|
||||
|
||||
def __bool__(self):
|
||||
return self.is_valid
|
||||
|
||||
def _get_hash(self):
|
||||
digest = hashes.Hash(hashes.SHA512(), default_backend())
|
||||
digest.update(self._rfc.encode())
|
||||
digest.update(self._serial_number.encode())
|
||||
digest.update(TOKEN.encode())
|
||||
return digest.finalize()
|
||||
|
||||
def _get_data_cer(self, cer):
|
||||
self._cer = cer
|
||||
obj = x509.load_der_x509_certificate(cer, default_backend())
|
||||
|
||||
self._issuer = obj.issuer.rfc4514_string()
|
||||
self._subject = obj.subject.rfc4514_string()
|
||||
|
||||
self._rfc = obj.subject.get_attributes_for_oid(
|
||||
NameOID.X500_UNIQUE_IDENTIFIER)[0].value.split(' ')[0]
|
||||
self._serial_number2 = '{0:x}'.format(obj.serial_number)
|
||||
self._serial_number = self._serial_number2[1::2]
|
||||
self._not_before = obj.not_valid_before
|
||||
self._not_after = obj.not_valid_after
|
||||
now = datetime.datetime.utcnow()
|
||||
self._is_valid_time = (now > self.not_before) and (now < self.not_after)
|
||||
if not self._is_valid_time:
|
||||
msg = 'El certificado no es vigente'
|
||||
self._error = msg
|
||||
|
||||
self._is_fiel = obj.extensions.get_extension_for_oid(
|
||||
ExtensionOID.KEY_USAGE).value.key_agreement
|
||||
|
||||
self._cer_pem = obj.public_bytes(serialization.Encoding.PEM).decode()
|
||||
self._cer_txt = ''.join(self._cer_pem.split('\n')[1:-2])
|
||||
self._cer_modulus = obj.public_key().public_numbers().n
|
||||
return
|
||||
|
||||
def _get_data_key(self, key, password):
|
||||
self._key = key
|
||||
self._keyp = password
|
||||
self._key_enc = key
|
||||
if not key or not password:
|
||||
return
|
||||
|
||||
try:
|
||||
obj = serialization.load_der_private_key(
|
||||
key, password.encode(), default_backend())
|
||||
except ValueError:
|
||||
msg = 'La contraseña es incorrecta'
|
||||
self._error = msg
|
||||
return
|
||||
|
||||
p = self._get_hash()
|
||||
self._key_enc = obj.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.BestAvailableEncryption(p)
|
||||
)
|
||||
|
||||
self._key_modulus = obj.public_key().public_numbers().n
|
||||
self._are_couple = self._cer_modulus == self._key_modulus
|
||||
if not self._are_couple:
|
||||
msg = 'El CER y el KEY no son pareja'
|
||||
self._error = msg
|
||||
return
|
||||
|
||||
def _get_key(self, password):
|
||||
if not password:
|
||||
password = self._get_hash()
|
||||
private_key = serialization.load_pem_private_key(
|
||||
self._key_enc, password=password, backend=default_backend())
|
||||
return private_key
|
||||
|
||||
def _get_key_pem(self):
|
||||
obj = self._get_key('')
|
||||
key_pem = obj.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
return key_pem
|
||||
|
||||
# Not work
|
||||
def _get_p12(self):
|
||||
obj = serialization.pkcs12.serialize_key_and_certificates('test',
|
||||
self.key_pem, self.cer_pem, None,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
return obj
|
||||
|
||||
def sign(self, data, password=''):
|
||||
private_key = self._get_key(password)
|
||||
firma = private_key.sign(data, padding.PKCS1v15(), hashes.SHA256())
|
||||
return base64.b64encode(firma).decode()
|
||||
|
||||
def sign_xml(self, tree):
|
||||
node = xmlsec.tree.find_node(tree, xmlsec.constants.NodeSignature)
|
||||
ctx = xmlsec.SignatureContext()
|
||||
key = xmlsec.Key.from_memory(
|
||||
self.key_pem, xmlsec.constants.KeyDataFormatPem)
|
||||
ctx.key = key
|
||||
ctx.sign(node)
|
||||
|
||||
node = xmlsec.tree.find_node(tree, 'X509Certificate')
|
||||
node.text = self.cer_txt
|
||||
node = xmlsec.tree.find_node(tree, 'X509IssuerName')
|
||||
node.text = self.issuer
|
||||
node = xmlsec.tree.find_node(tree, 'X509SerialNumber')
|
||||
node.text = self.serial_number
|
||||
node = xmlsec.tree.find_node(tree, 'SignatureValue')
|
||||
node.text = node.text.replace('\n', '')
|
||||
node = xmlsec.tree.find_node(tree, 'Modulus')
|
||||
node.text = node.text.replace('\n', '')
|
||||
|
||||
# ~ xml_signed = ET.tostring(tree,
|
||||
# ~ xml_declaration=True, encoding='UTF-8').decode()
|
||||
xml_signed = ET.tostring(tree, encoding='UTF-8').decode().replace('\n', '')
|
||||
|
||||
return xml_signed
|
||||
|
||||
@property
|
||||
def rfc(self):
|
||||
return self._rfc
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
return self._serial_number
|
||||
|
||||
@property
|
||||
def serial_number2(self):
|
||||
return self._serial_number2
|
||||
|
||||
@property
|
||||
def issuer(self):
|
||||
return self._issuer
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return self._subject
|
||||
|
||||
@property
|
||||
def not_before(self):
|
||||
return self._not_before
|
||||
|
||||
@property
|
||||
def not_after(self):
|
||||
return self._not_after
|
||||
|
||||
@property
|
||||
def is_fiel(self):
|
||||
return self._is_fiel
|
||||
|
||||
@property
|
||||
def are_couple(self):
|
||||
return self._are_couple
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
return not bool(self.error)
|
||||
|
||||
@property
|
||||
def is_valid_time(self):
|
||||
return self._is_valid_time
|
||||
|
||||
@property
|
||||
def cer(self):
|
||||
return self._cer
|
||||
|
||||
@property
|
||||
def cer_pem(self):
|
||||
return self._cer_pem.encode()
|
||||
|
||||
@property
|
||||
def cer_txt(self):
|
||||
return self._cer_txt
|
||||
|
||||
@property
|
||||
def key_pem(self):
|
||||
return self._get_key_pem()
|
||||
|
||||
@property
|
||||
def key_enc(self):
|
||||
return self._key_enc
|
||||
|
||||
@property
|
||||
def p12(self):
|
||||
return self._get_p12()
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._error
|
||||
|
||||
@classmethod
|
||||
def save_cert(cls, obj):
|
||||
import hashlib
|
||||
|
||||
cer = x509.load_pem_x509_certificate(obj.cer_pem.encode(), default_backend())
|
||||
cer_der = cer.public_bytes(serialization.Encoding.DER)
|
||||
token = hashlib.md5(obj.rfc.encode()).hexdigest()
|
||||
path = Path(f'/tmp/{obj.rfc}.key')
|
||||
path.write_text(obj.key_enc)
|
||||
args = f'openssl rsa -inform PEM -outform PEM -in "{str(path)}" -passin pass:{token}'
|
||||
pem = subprocess.check_output(args, shell=True).decode()
|
||||
key = serialization.load_pem_private_key(
|
||||
pem.encode(), password=None, backend=default_backend())
|
||||
|
||||
digest = hashes.Hash(hashes.SHA512(), default_backend())
|
||||
digest.update(obj.rfc.encode())
|
||||
digest.update(obj.serie.encode())
|
||||
digest.update(TOKEN.encode())
|
||||
p = digest.finalize()
|
||||
|
||||
key_enc = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.BestAvailableEncryption(p)
|
||||
)
|
||||
|
||||
cert = SATCertificate(cer_der, key_enc)
|
||||
obj.key_enc = cert.key_enc
|
||||
obj.cer = cert.cer
|
||||
obj.serie = cert.serial_number
|
||||
obj.desde = cert.not_before
|
||||
obj.hasta = cert.not_after
|
||||
obj.save()
|
||||
return
|
||||
|
||||
|
||||
def main(args):
|
||||
# ~ contra = getpass.getpass('Introduce la contraseña del archivo KEY: ')
|
||||
contra = '12345678a'
|
||||
if not contra.strip():
|
||||
msg = 'La contraseña es requerida'
|
||||
print(msg)
|
||||
return
|
||||
|
||||
path_cer = Path(args.cer)
|
||||
path_key = Path(args.key)
|
||||
|
||||
if not path_cer.is_file():
|
||||
msg = 'El archivo CER es necesario'
|
||||
print(msg)
|
||||
return
|
||||
|
||||
if not path_key.is_file():
|
||||
msg = 'El archivo KEY es necesario'
|
||||
print(msg)
|
||||
return
|
||||
|
||||
cer = path_cer.read_bytes()
|
||||
key = path_key.read_bytes()
|
||||
cert = SATCertificate(cer, key, contra)
|
||||
|
||||
if cert.error:
|
||||
print(cert.error)
|
||||
else:
|
||||
print(cert)
|
||||
return
|
||||
|
||||
|
||||
def _process_command_line_arguments():
|
||||
parser = argparse.ArgumentParser(description='CFDI Certificados')
|
||||
|
||||
help = 'Archivo CER'
|
||||
parser.add_argument('-c', '--cer', help=help, default='')
|
||||
help = 'Archivo KEY'
|
||||
parser.add_argument('-k', '--key', help=help, default='')
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = _process_command_line_arguments()
|
||||
main(args)
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from .comercio import PACComercioDigital
|
|
@ -0,0 +1,431 @@
|
|||
#!/usr/bin/env python
|
||||
# ~
|
||||
# ~ PAC
|
||||
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
|
||||
# ~
|
||||
# ~ 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/>.
|
||||
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
import lxml.etree as ET
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from .conf import DEBUG
|
||||
# ~ , AUTH
|
||||
|
||||
|
||||
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
|
||||
LOG_DATE = '%d/%m/%Y %H:%M:%S'
|
||||
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
|
||||
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
|
||||
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
|
||||
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
logging.getLogger('requests').setLevel(logging.ERROR)
|
||||
|
||||
|
||||
TIMEOUT = 10
|
||||
|
||||
NAMESPACES = {
|
||||
'3.3': 'http://www.sat.gob.mx/cfd/3',
|
||||
'4.0': 'http://www.sat.gob.mx/cfd/4',
|
||||
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
||||
}
|
||||
|
||||
|
||||
def pretty_print_POST(req):
|
||||
"""
|
||||
At this point it is completely built and ready
|
||||
to be fired; it is "prepared".
|
||||
|
||||
However pay attention at the formatting used in
|
||||
this function because it is programmed to be pretty
|
||||
printed and may differ from the actual request.
|
||||
"""
|
||||
print('{}\n{}\r\n{}\r\n\r\n{}'.format(
|
||||
'-----------START-----------',
|
||||
req.method + ' ' + req.url,
|
||||
'\r\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()),
|
||||
req.body,
|
||||
))
|
||||
|
||||
|
||||
class PACComercioDigital(object):
|
||||
ws = 'https://{}.comercio-digital.mx/{}'
|
||||
api = 'https://app2.comercio-digital.mx/{}'
|
||||
URL = {
|
||||
'timbra': ws.format('ws', 'timbre4/timbrarV5'),
|
||||
'cancel': ws.format('cancela', 'cancela4/cancelarUuid'),
|
||||
'cancelxml': ws.format('cancela', 'cancela4/cancelarXml'),
|
||||
'status': ws.format('cancela', 'arws/consultaEstatus'),
|
||||
'client': api.format('x3/altaEmpresa'),
|
||||
'saldo': api.format('x3/saldo'),
|
||||
'timbres': api.format('x3/altaTimbres'),
|
||||
}
|
||||
CODES = {
|
||||
'000': '000 Exitoso',
|
||||
'004': '004 RFC {} ya esta dado de alta con Estatus=A',
|
||||
'704': '704 Usuario Invalido',
|
||||
'702': '702 Error rfc/empresa invalido',
|
||||
}
|
||||
NS_CFDI = {
|
||||
'cfdi': 'http://www.sat.gob.mx/cfd/4',
|
||||
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
ws = 'https://pruebas.comercio-digital.mx/{}'
|
||||
ws6 = 'https://pruebas6.comercio-digital.mx/arws/{}'
|
||||
URL = {
|
||||
'timbra': ws.format('timbre4/timbrarV5'),
|
||||
'cancel': ws.format('cancela4/cancelarUuid'),
|
||||
'cancelxml': ws.format('cancela4/cancelarXml'),
|
||||
'status': ws6.format('consultaEstatus'),
|
||||
'client': api.format('x3/altaEmpresa'),
|
||||
'saldo': api.format('x3/saldo'),
|
||||
'timbres': api.format('x3/altaTimbres'),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.error = ''
|
||||
|
||||
def _error(self, msg):
|
||||
self.error = str(msg)
|
||||
log.error(msg)
|
||||
return
|
||||
|
||||
def _post(self, url, data, headers={}):
|
||||
result = None
|
||||
headers['host'] = url.split('/')[2]
|
||||
headers['Content-type'] = 'text/plain'
|
||||
headers['Connection'] = 'Keep-Alive'
|
||||
headers['Expect'] = '100-continue'
|
||||
|
||||
if DEBUG:
|
||||
req = requests.Request('POST', url, headers=headers, data=data)
|
||||
prepared = req.prepare()
|
||||
pretty_print_POST(prepared)
|
||||
|
||||
try:
|
||||
result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT)
|
||||
except ConnectionError as e:
|
||||
self._error(e)
|
||||
|
||||
return result
|
||||
|
||||
def _validate_cfdi(self, xml):
|
||||
"""
|
||||
Comercio Digital solo soporta la declaración con doble comilla
|
||||
"""
|
||||
# ~ tree = ET.fromstring(xml.encode())
|
||||
# ~ xml = ET.tostring(tree,
|
||||
# ~ pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
|
||||
return xml.encode('utf-8')
|
||||
|
||||
def stamp(self, cfdi, auth):
|
||||
# ~ if DEBUG or not auth:
|
||||
# ~ auth = AUTH
|
||||
|
||||
url = self.URL['timbra']
|
||||
headers = {
|
||||
'usrws': auth['user'],
|
||||
'pwdws': auth['pass'],
|
||||
'tipo': 'XML',
|
||||
}
|
||||
cfdi = self._validate_cfdi(cfdi)
|
||||
result = self._post(url, cfdi, headers)
|
||||
|
||||
if result is None:
|
||||
return ''
|
||||
|
||||
if result.status_code != 200:
|
||||
return ''
|
||||
|
||||
if 'errmsg' in result.headers:
|
||||
self._error(result.headers['errmsg'])
|
||||
return ''
|
||||
|
||||
xml = result.content
|
||||
tree = ET.fromstring(xml)
|
||||
cfdi_uuid = tree.xpath(
|
||||
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
|
||||
namespaces=self.NS_CFDI)
|
||||
date_stamped = tree.xpath(
|
||||
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@FechaTimbrado)',
|
||||
namespaces=self.NS_CFDI)
|
||||
|
||||
data = {
|
||||
'xml': xml.decode(),
|
||||
'uuid': cfdi_uuid,
|
||||
'date': date_stamped,
|
||||
}
|
||||
return data
|
||||
|
||||
def _get_data_cancel(self, cfdi, info, auth):
|
||||
info['tipo'] = 'cfdi'
|
||||
info['key'] = base64.b64encode(info['key_enc']).decode()
|
||||
info['cer'] = base64.b64encode(info['cer_ori']).decode()
|
||||
|
||||
tree = ET.fromstring(cfdi.encode())
|
||||
version = tree.attrib['Version']
|
||||
|
||||
namespaces = {
|
||||
'cfdi': NAMESPACES[version],
|
||||
'tdf': NAMESPACES['tdf'],
|
||||
}
|
||||
|
||||
tipo = tree.xpath(
|
||||
'string(//cfdi:Comprobante/@TipoDeComprobante)',
|
||||
namespaces=namespaces)
|
||||
total = tree.xpath(
|
||||
'string(//cfdi:Comprobante/@Total)',
|
||||
namespaces=namespaces)
|
||||
rfc_emisor = tree.xpath(
|
||||
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
|
||||
namespaces=namespaces)
|
||||
rfc_receptor = tree.xpath(
|
||||
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
|
||||
namespaces=namespaces)
|
||||
uid = tree.xpath(
|
||||
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
|
||||
namespaces=namespaces)
|
||||
data = (
|
||||
f"USER={auth['user']}",
|
||||
f"PWDW={auth['pass']}",
|
||||
f"RFCE={rfc_emisor}",
|
||||
f"UUID={uid}",
|
||||
f"PWDK={info['pass']}",
|
||||
f"KEYF={info['key']}",
|
||||
f"CERT={info['cer']}",
|
||||
f"TIPO1={info['tipo']}",
|
||||
f"ACUS=SI",
|
||||
f"RFCR={rfc_receptor}",
|
||||
f"TIPOC={tipo}",
|
||||
f"TOTAL={total}",
|
||||
f"UUIDREL={info['args']['uuid']}",
|
||||
f"MOTIVO={info['args']['reason']}",
|
||||
)
|
||||
return '\n'.join(data)
|
||||
|
||||
def cancel(self, cfdi, info, auth):
|
||||
# ~ if DEBUG or not auth:
|
||||
# ~ auth = AUTH
|
||||
url = self.URL['cancel']
|
||||
data = self._get_data_cancel(cfdi, info, auth)
|
||||
|
||||
result = self._post(url, data)
|
||||
|
||||
if result is None:
|
||||
return ''
|
||||
|
||||
if result.status_code != 200:
|
||||
return ''
|
||||
|
||||
if result.headers['codigo'] != '000':
|
||||
self._error(result.headers['errmsg'])
|
||||
return ''
|
||||
|
||||
tree = ET.fromstring(result.text)
|
||||
date_cancel = tree.xpath('string(//Acuse/@Fecha)')[:19]
|
||||
|
||||
data = {
|
||||
'acuse': result.text,
|
||||
'date': date_cancel,
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
def _get_headers_cancel_xml(self, cfdi, info, auth):
|
||||
NS_CFDI = {
|
||||
'cfdi': 'http://www.sat.gob.mx/cfd/3',
|
||||
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
||||
}
|
||||
tree = ET.fromstring(cfdi.encode())
|
||||
tipocfdi = tree.xpath(
|
||||
'string(//cfdi:Comprobante/@TipoDeComprobante)',
|
||||
namespaces=NS_CFDI)
|
||||
total = tree.xpath(
|
||||
'string(//cfdi:Comprobante/@Total)',
|
||||
namespaces=NS_CFDI)
|
||||
rfc_receptor = tree.xpath(
|
||||
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
|
||||
namespaces=NS_CFDI)
|
||||
|
||||
headers = {
|
||||
'usrws': auth['user'],
|
||||
'pwdws': auth['pass'],
|
||||
'rfcr': rfc_receptor,
|
||||
'total': total,
|
||||
'tipocfdi': tipocfdi,
|
||||
}
|
||||
headers.update(info)
|
||||
|
||||
return headers
|
||||
|
||||
def cancel_xml(self, xml, auth, cfdi='', info={'tipo': 'cfdi'}):
|
||||
# ~ if DEBUG or not auth:
|
||||
# ~ auth = AUTH
|
||||
|
||||
url = self.URL['cancelxml']
|
||||
headers = self._get_headers_cancel_xml(cfdi, info, auth)
|
||||
result = self._post(url, xml, headers)
|
||||
|
||||
if result is None:
|
||||
return ''
|
||||
|
||||
if result.status_code != 200:
|
||||
return ''
|
||||
|
||||
if result.headers['codigo'] != '000':
|
||||
self._error(result.headers['errmsg'])
|
||||
return ''
|
||||
|
||||
tree = ET.fromstring(result.text)
|
||||
date_cancel = tree.xpath('string(//Acuse/@Fecha)')[:19]
|
||||
|
||||
data = {
|
||||
'acuse': result.text,
|
||||
'date': date_cancel,
|
||||
}
|
||||
return data
|
||||
|
||||
def status(self, data, auth):
|
||||
# ~ if not auth:
|
||||
# ~ auth = AUTH
|
||||
url = self.URL['status']
|
||||
|
||||
data = (
|
||||
f"USER={auth['user']}",
|
||||
f"PWDW={auth['pass']}",
|
||||
f"RFCR={data['rfc_receptor']}",
|
||||
f"RFCE={data['rfc_emisor']}",
|
||||
f"TOTAL={data['total']}",
|
||||
f"UUID={data['uuid']}",
|
||||
)
|
||||
data = '\n'.join(data)
|
||||
result = self._post(url, data)
|
||||
|
||||
if result is None:
|
||||
return ''
|
||||
|
||||
if result.status_code != 200:
|
||||
self._error(result.status_code)
|
||||
return self.error
|
||||
|
||||
return result.text
|
||||
|
||||
def _get_data_client(self, auth, values):
|
||||
data = [f"usr_ws={auth['user']}", f"pwd_ws={auth['pass']}"]
|
||||
fields = (
|
||||
'rfc_contribuyente',
|
||||
'nombre_contribuyente',
|
||||
'calle',
|
||||
'noExterior',
|
||||
'noInterior',
|
||||
'colonia',
|
||||
'localidad',
|
||||
'municipio',
|
||||
'estado',
|
||||
'pais',
|
||||
'cp',
|
||||
'contacto',
|
||||
'telefono',
|
||||
'email',
|
||||
'rep_nom',
|
||||
'rep_rfc',
|
||||
'email_fact',
|
||||
'pwd_asignado',
|
||||
)
|
||||
data += [f"{k}={values[k]}" for k in fields]
|
||||
|
||||
return '\n'.join(data)
|
||||
|
||||
def client_add(self, data, auth):
|
||||
# ~ auth = AUTH
|
||||
url = self.URL['client']
|
||||
data = self._get_data_client(auth, data)
|
||||
|
||||
result = self._post(url, data)
|
||||
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
if result.status_code != 200:
|
||||
self._error(f'Code: {result.status_code}')
|
||||
return False
|
||||
|
||||
if result.text != self.CODES['000']:
|
||||
self._error(result.text)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def client_balance(self, data, rfc=''):
|
||||
url = self.URL['saldo']
|
||||
host = url.split('/')[2]
|
||||
headers = {
|
||||
'Content-type': 'text/plain',
|
||||
'Host': host,
|
||||
'Connection' : 'Keep-Alive',
|
||||
}
|
||||
data = {'usr': data['user'], 'pwd': data['pass']}
|
||||
try:
|
||||
result = requests.get(url, params=data, headers=headers, timeout=TIMEOUT)
|
||||
except ConnectionError as e:
|
||||
self._error(e)
|
||||
return ''
|
||||
|
||||
if result.status_code != 200:
|
||||
return ''
|
||||
|
||||
if result.text == self.CODES['704']:
|
||||
self._error(result.text)
|
||||
return ''
|
||||
|
||||
if result.text == self.CODES['702']:
|
||||
self._error(result.text)
|
||||
return ''
|
||||
|
||||
return result.text
|
||||
|
||||
def client_add_timbres(self, data, auth):
|
||||
# ~ if not auth:
|
||||
# ~ auth = AUTH
|
||||
url = self.URL['timbres']
|
||||
data = '\n'.join((
|
||||
f"usr_ws={auth['user']}",
|
||||
f"pwd_ws={auth['pass']}",
|
||||
f"rfc_recibir={data['rfc']}",
|
||||
f"num_timbres={data['timbres']}"
|
||||
))
|
||||
|
||||
result = self._post(url, data)
|
||||
|
||||
if result is None:
|
||||
return False
|
||||
|
||||
if result.status_code != 200:
|
||||
self._error(f'Code: {result.status_code}')
|
||||
return False
|
||||
|
||||
if result.text != self.CODES['000']:
|
||||
self._error(result.text)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python
|
||||
# ~
|
||||
# ~ PAC
|
||||
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
|
||||
# ~
|
||||
# ~ 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/>.
|
||||
|
||||
|
||||
# ~ Siempre consulta la documentación de PAC
|
||||
# ~ AUTH = Las credenciales de timbrado proporcionadas por el PAC
|
||||
# ~ NO cambies las credenciales de prueba
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
AUTH = {
|
||||
'user': '',
|
||||
'pass': '',
|
||||
}
|
||||
|
||||
|
||||
if DEBUG:
|
||||
AUTH = {
|
||||
'user': 'AAA010101AAA',
|
||||
'pass': 'PWD',
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from .finkok import PACFinkok
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python
|
||||
# ~
|
||||
# ~ PAC
|
||||
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
|
||||
# ~
|
||||
# ~ 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/>.
|
||||
|
||||
|
||||
# ~ Siempre consulta la documentación de PAC
|
||||
# ~ AUTH = Las credenciales de timbrado proporcionadas por el PAC
|
||||
# ~ NO cambies las credenciales de prueba
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
AUTH = {
|
||||
'user': '',
|
||||
'pass': '',
|
||||
'RESELLER': {
|
||||
'user': '',
|
||||
'pass': ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if DEBUG:
|
||||
AUTH = {
|
||||
'user': 'pruebas-finkok@correolibre.net',
|
||||
'pass': '5c9a88da105bff9a8c430cb713f6d35269f51674bdc5963c1501b7316366',
|
||||
'RESELLER': {
|
||||
'user': '',
|
||||
'pass': ''
|
||||
}
|
||||
}
|
|
@ -0,0 +1,580 @@
|
|||
#!/usr/bin/env python
|
||||
# ~
|
||||
# ~ PAC
|
||||
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
|
||||
# ~
|
||||
# ~ 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/>.
|
||||
|
||||
# ~ import base64
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from io import BytesIO
|
||||
from xml.sax.saxutils import unescape
|
||||
|
||||
import lxml.etree as ET
|
||||
from zeep import Client
|
||||
from zeep.plugins import Plugin
|
||||
from zeep.cache import SqliteCache
|
||||
from zeep.transports import Transport
|
||||
from zeep.exceptions import Fault, TransportError
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from .conf import DEBUG, AUTH
|
||||
|
||||
|
||||
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
|
||||
LOG_DATE = '%d/%m/%Y %H:%M:%S'
|
||||
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
|
||||
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
|
||||
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
|
||||
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
|
||||
log = logging.getLogger(__name__)
|
||||
logging.getLogger('requests').setLevel(logging.ERROR)
|
||||
logging.getLogger('zeep').setLevel(logging.ERROR)
|
||||
|
||||
|
||||
TIMEOUT = 10
|
||||
DEBUG_SOAP = False
|
||||
|
||||
|
||||
class DebugPlugin(Plugin):
|
||||
|
||||
def _to_string(self, envelope, name):
|
||||
if DEBUG_SOAP:
|
||||
data = ET.tostring(envelope, pretty_print=True, encoding='utf-8').decode()
|
||||
path = f'/tmp/soap_{name}.xml'
|
||||
with open(path, 'w') as f:
|
||||
f.write(data)
|
||||
# ~ print(data)
|
||||
return
|
||||
|
||||
def egress(self, envelope, http_headers, operation, binding_options):
|
||||
self._to_string(envelope, 'request')
|
||||
return envelope, http_headers
|
||||
|
||||
def ingress(self, envelope, http_headers, operation):
|
||||
self._to_string(envelope, 'response')
|
||||
return envelope, http_headers
|
||||
|
||||
|
||||
class PACFinkok(object):
|
||||
WS = 'https://facturacion.finkok.com/servicios/soap/{}.wsdl'
|
||||
NS_TYPE = 'ns1'
|
||||
if DEBUG:
|
||||
WS = 'https://demo-facturacion.finkok.com/servicios/soap/{}.wsdl'
|
||||
NS_TYPE = 'ns0'
|
||||
URL = {
|
||||
'quick_stamp': False,
|
||||
'timbra': WS.format('stamp'),
|
||||
'cancel': WS.format('cancel'),
|
||||
'client': WS.format('registration'),
|
||||
'util': WS.format('utilities'),
|
||||
}
|
||||
CODE = {
|
||||
'200': 'Comprobante timbrado satisfactoriamente',
|
||||
'205': 'No Encontrado',
|
||||
'307': 'Comprobante timbrado previamente',
|
||||
'702': 'No se encontro el RFC del emisor',
|
||||
'IP': 'Invalid Passphrase',
|
||||
'IPMSG': 'Frase de paso inválida',
|
||||
'NE': 'No Encontrado',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._error = ''
|
||||
self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
|
||||
self._plugins = [DebugPlugin()]
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._error
|
||||
|
||||
def _validate_result(self, result):
|
||||
if hasattr(result, 'Incidencias') and not result.Incidencias is None:
|
||||
fault = result.Incidencias.Incidencia[0]
|
||||
cod_error = fault.CodigoError.encode('utf-8').decode()
|
||||
msg_error = fault.MensajeIncidencia.encode('utf-8').decode()
|
||||
error = 'Error: {}\n{}'.format(cod_error, msg_error)
|
||||
if cod_error == '307':
|
||||
return result
|
||||
|
||||
self._error = self.CODE.get(cod_error, error)
|
||||
return {}
|
||||
|
||||
if hasattr(result, 'CodEstatus'):
|
||||
ce = result.CodEstatus
|
||||
if ce is None:
|
||||
return result
|
||||
|
||||
if ce == self.CODE['IP']:
|
||||
self._error = self.CODE['IPMSG']
|
||||
return {}
|
||||
|
||||
if self.CODE['NE'] in ce:
|
||||
self._error = 'UUID ' + self.CODE['NE']
|
||||
return {}
|
||||
|
||||
if ce == 'UUID Not Found':
|
||||
self._error = 'UUID ' + self.CODE['NE']
|
||||
return {}
|
||||
|
||||
if self.CODE['200'] != ce:
|
||||
self._error = ce
|
||||
return {}
|
||||
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
def _get_result(self, client, method, args):
|
||||
self._error = ''
|
||||
try:
|
||||
result = getattr(client.service, method)(**args)
|
||||
except Fault as e:
|
||||
self._error = str(e)
|
||||
return {}
|
||||
except TransportError as e:
|
||||
if '413' in str(e):
|
||||
self._error = '413<BR><BR><b>Documento muy grande para timbrar</b>'
|
||||
else:
|
||||
self._error = str(e)
|
||||
return {}
|
||||
except ConnectionError as e:
|
||||
msg = '502 - Error de conexión'
|
||||
self._error = msg
|
||||
return {}
|
||||
|
||||
return self._validate_result(result)
|
||||
|
||||
def _to_string(self, data):
|
||||
root = ET.parse(BytesIO(data.encode('utf-8'))).getroot()
|
||||
xml = ET.tostring(root,
|
||||
pretty_print=True, xml_declaration=True, encoding='utf-8')
|
||||
return xml.decode('utf-8')
|
||||
|
||||
def stamp(self, cfdi, auth={}):
|
||||
if DEBUG or not auth:
|
||||
auth = AUTH
|
||||
|
||||
method = 'timbra'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'xml': cfdi.encode('utf-8'),
|
||||
}
|
||||
result = self._get_result(client, 'stamp', args)
|
||||
if self.error:
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
data = {
|
||||
'xml': self._to_string(result.xml),
|
||||
'uuid': result.UUID,
|
||||
'date': result.Fecha,
|
||||
}
|
||||
return data
|
||||
|
||||
def _get_data_cancel(self, cfdi):
|
||||
VERSIONS = {
|
||||
'3.3': 'http://www.sat.gob.mx/cfd/3',
|
||||
'4.0': 'http://www.sat.gob.mx/cfd/4',
|
||||
}
|
||||
NS_CFDI = {
|
||||
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
||||
}
|
||||
tree = ET.fromstring(cfdi.encode())
|
||||
|
||||
version = tree.attrib['Version']
|
||||
NS_CFDI['cfdi'] = VERSIONS[version]
|
||||
|
||||
rfc_emisor = tree.xpath(
|
||||
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
|
||||
namespaces=NS_CFDI)
|
||||
cfdi_uuid = tree.xpath(
|
||||
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
|
||||
namespaces=NS_CFDI)
|
||||
return rfc_emisor, cfdi_uuid
|
||||
|
||||
def cancel(self, cfdi, info, auth={}):
|
||||
if DEBUG or not auth:
|
||||
auth = AUTH
|
||||
|
||||
rfc_emisor, cfdi_uuid = self._get_data_cancel(cfdi)
|
||||
method = 'cancel'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
|
||||
uuid_type = client.get_type(f'{self.NS_TYPE}:UUIDS')
|
||||
ns1_uuid = client.get_type(f'{self.NS_TYPE}:UUID')
|
||||
|
||||
# ~ sa = client.get_type('ns0:stringArray')
|
||||
|
||||
data_uuid = {
|
||||
'UUID': cfdi_uuid,
|
||||
'FolioSustitucion': info['args']['uuid'],
|
||||
'Motivo': info['args']['reason'],
|
||||
}
|
||||
# ~ 'UUIDS': uuid_type(uuids=sa(string=cfdi_uuid)),
|
||||
|
||||
args = {
|
||||
'UUIDS': uuid_type(ns1_uuid(**data_uuid)),
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'taxpayer_id': rfc_emisor,
|
||||
'cer': info['cer'],
|
||||
'key': info['key'],
|
||||
'store_pending': False,
|
||||
}
|
||||
|
||||
result = self._get_result(client, 'cancel', args)
|
||||
if self.error:
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
folio = result['Folios']['Folio'][0]
|
||||
status = folio['EstatusUUID']
|
||||
if status != '201':
|
||||
log.debug(f'Cancel status: {status} - {cfdi_uuid}')
|
||||
|
||||
data = {
|
||||
'acuse': result['Acuse'],
|
||||
'date': result['Fecha'],
|
||||
}
|
||||
return data
|
||||
|
||||
def cancel_xml(self, xml, auth={}, cfdi=''):
|
||||
if DEBUG or not auth:
|
||||
auth = AUTH
|
||||
|
||||
method = 'cancel'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
client.set_ns_prefix('can', 'http://facturacion.finkok.com/cancel')
|
||||
args = {
|
||||
'xml': xml.encode(),
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'store_pending': False,
|
||||
}
|
||||
result = self._get_result(client, 'cancel_signature', args)
|
||||
if self.error:
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
folio = result['Folios']['Folio'][0]
|
||||
status = folio['EstatusUUID']
|
||||
|
||||
if status == '708':
|
||||
self._error = 'Error 708 del SAT, intenta más tarde.'
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
if status != '201':
|
||||
log.debug(f'Cancel status: {status} -')
|
||||
|
||||
data = {
|
||||
'acuse': result['Acuse'],
|
||||
'date': result['Fecha'],
|
||||
}
|
||||
return data
|
||||
|
||||
def client_add(self, rfc, type_user=False):
|
||||
"""Agrega un nuevo cliente para timbrado.
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del nuevo cliente
|
||||
|
||||
Kwargs:
|
||||
type_user (bool):
|
||||
False == 'P' == Prepago
|
||||
True == 'O' == On demand
|
||||
|
||||
Returns:
|
||||
True or False
|
||||
|
||||
origin PAC
|
||||
'message':
|
||||
'Account Created successfully'
|
||||
'Account Already exists'
|
||||
'success': True or False
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
tu = {True: 'O', False: 'P'}
|
||||
|
||||
method = 'client'
|
||||
client = Client(
|
||||
self.URL[method], transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'reseller_username': auth['user'],
|
||||
'reseller_password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
'type_user': tu[type_user],
|
||||
'added': datetime.datetime.now().isoformat()[:19],
|
||||
}
|
||||
|
||||
result = self._get_result(client, 'add', args)
|
||||
if self.error:
|
||||
return False
|
||||
|
||||
if not result.success:
|
||||
self.error = result.message
|
||||
return False
|
||||
|
||||
# ~ PAC success debería ser False
|
||||
msg = 'Account Already exists'
|
||||
if result.message == msg:
|
||||
self.error = msg
|
||||
return True
|
||||
|
||||
return result.success
|
||||
|
||||
def client_get_token(self, rfc, email):
|
||||
"""Genera un nuevo token al cliente para timbrado.
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del cliente, ya debe existir
|
||||
email (str): El correo del cliente, funciona como USER al timbrar
|
||||
|
||||
Returns:
|
||||
token (str): Es la contraseña para timbrar
|
||||
|
||||
origin PAC
|
||||
dict
|
||||
'username': 'username',
|
||||
'status': True or False
|
||||
'name': 'name',
|
||||
'success': True or False
|
||||
'token': 'Token de timbrado',
|
||||
'message': None
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
method = 'util'
|
||||
client = Client(
|
||||
self.URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'name': rfc,
|
||||
'token_username': email,
|
||||
'taxpayer_id': rfc,
|
||||
'status': True,
|
||||
}
|
||||
|
||||
result = self._get_result(client, 'add_token', args)
|
||||
if self.error:
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
if not result.success:
|
||||
self.error = result.message
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
return result.token
|
||||
|
||||
def client_add_timbres(self, rfc, credit):
|
||||
"""Agregar credito a un emisor
|
||||
|
||||
Se requiere cuenta de reseller
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del emisor, debe existir
|
||||
credit (int): Cantidad de folios a agregar
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'success': True or False,
|
||||
'credit': nuevo credito despues de agregar or None
|
||||
'message':
|
||||
'Success, added {credit} of credit to {RFC}.'
|
||||
'RFC no encontrado'
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
method = 'client'
|
||||
client = Client(
|
||||
self.URL[method], transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
'credit': credit,
|
||||
}
|
||||
|
||||
result = self._get_result(client, 'assign', args)
|
||||
if self.error:
|
||||
log.error(error)
|
||||
return ''
|
||||
|
||||
if not result.success:
|
||||
self.error = result.message
|
||||
return 0
|
||||
|
||||
return result.credit
|
||||
|
||||
def client_balance(self, auth={}, rfc=''):
|
||||
"""Regresa los timbres restantes del cliente
|
||||
Se pueden usar las credenciales de relleser o las credenciales del emisor
|
||||
|
||||
Args:
|
||||
auth (dict): Credenciales del emisor
|
||||
rfc (str): El RFC del emisor
|
||||
|
||||
Returns:
|
||||
int Cantidad de timbres restantes
|
||||
"""
|
||||
if not auth:
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
method = 'client'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
args = {
|
||||
'reseller_username': auth['user'],
|
||||
'reseller_password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
}
|
||||
result = self._get_result(client, 'get', args)
|
||||
|
||||
if self.error:
|
||||
log.error(self.error)
|
||||
return ''
|
||||
|
||||
success = bool(result.users)
|
||||
if not success:
|
||||
self._error = result.message or 'RFC no existe'
|
||||
log.error(self.error)
|
||||
return 0
|
||||
|
||||
return result.users.ResellerUser[0].credit
|
||||
|
||||
def client_set_status(self, rfc, status):
|
||||
"""Edita el estatus (Activo o Suspendido) de un cliente
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del cliente
|
||||
|
||||
Kwargs:
|
||||
status (bool):
|
||||
True == 'A' == Activo
|
||||
False == 'S' == Suspendido
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'message':
|
||||
'Account Created successfully'
|
||||
'Account Already exists'
|
||||
'success': True or False
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
ts = {True: 'A', False: 'S'}
|
||||
method = 'client'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'reseller_username': auth['user'],
|
||||
'reseller_password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
'status': ts[status],
|
||||
}
|
||||
result = self._get_result(client, 'edit', args)
|
||||
|
||||
if self.error:
|
||||
return False
|
||||
|
||||
if not result.success:
|
||||
self.error = result.message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def client_switch(self, rfc, type_user):
|
||||
"""Edita el tipo de timbrado (OnDemand o Prepago) de un cliente
|
||||
Se requiere cuenta de reseller para usar este método
|
||||
|
||||
Args:
|
||||
rfc (str): El RFC del cliente
|
||||
|
||||
Kwargs:
|
||||
status (bool):
|
||||
True == 'O' == OnDemand
|
||||
False == 'P' == Prepago
|
||||
|
||||
Returns:
|
||||
dict
|
||||
'message':
|
||||
'Account Created successfully'
|
||||
'Account Already exists'
|
||||
'success': True or False
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
tu = {True: 'O', False: 'P'}
|
||||
method = 'client'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
|
||||
args = {
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
'type_user': tu[type_user],
|
||||
}
|
||||
result = self._get_result(client, 'switch', args)
|
||||
|
||||
if self.error:
|
||||
return False
|
||||
|
||||
if not result.success:
|
||||
self.error = result.message
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def client_report_folios(self, rfc, date_from, date_to, invoice_type='I'):
|
||||
"""Obtiene un reporte del total de facturas timbradas
|
||||
"""
|
||||
auth = AUTH['RESELLER']
|
||||
|
||||
args = {
|
||||
'username': auth['user'],
|
||||
'password': auth['pass'],
|
||||
'taxpayer_id': rfc,
|
||||
'date_from': date_from,
|
||||
'date_to': date_to,
|
||||
'invoice_type': invoice_type,
|
||||
}
|
||||
|
||||
method = 'util'
|
||||
client = Client(self.URL[method],
|
||||
transport=self._transport, plugins=self._plugins)
|
||||
|
||||
result = self._get_result(client, 'report_total', args)
|
||||
|
||||
if result.result is None:
|
||||
# ~ PAC - Debería regresar RFC inexistente o sin registros
|
||||
self.error = 'RFC no existe o no tiene registros'
|
||||
return 0
|
||||
|
||||
total = result.result.ReportTotal[0].total
|
||||
|
||||
return total
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from decimal import Decimal, getcontext
|
||||
# ~ getcontext().prec = 6
|
||||
|
||||
import lxml.etree as ET
|
||||
from requests.structures import CaseInsensitiveDict as CIDict
|
||||
|
||||
|
||||
NS_CFDI = {
|
||||
'cfdi': 'http://www.sat.gob.mx/cfd/3',
|
||||
'tfd': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
||||
'nomina12': 'http://www.sat.gob.mx/nomina12',
|
||||
}
|
||||
PRE = '/cfdi:Comprobante'
|
||||
|
||||
|
||||
class CfdiRead(object):
|
||||
|
||||
def __init__(self, source):
|
||||
self._source = source
|
||||
self._data = {}
|
||||
self._error = ''
|
||||
self._rfc_emisor = ''
|
||||
self._rfc_receptor = ''
|
||||
self._parse()
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def rfc_emisor(self):
|
||||
return self._rfc_emisor
|
||||
|
||||
@property
|
||||
def rfc_receptor(self):
|
||||
return self._rfc_receptor
|
||||
|
||||
@property
|
||||
def error(self):
|
||||
return self._error
|
||||
|
||||
def _parse(self):
|
||||
self._tree = ET.fromstring(self.source)
|
||||
self._data['cfdi'] = dict(self._tree.attrib)
|
||||
|
||||
node_name = f'{PRE}/cfdi:Emisor'
|
||||
self._data['emisor'] = self._get_attr(node_name)
|
||||
self._rfc_emisor = self._data['emisor']['Rfc']
|
||||
|
||||
node_name = f'{PRE}/cfdi:Receptor'
|
||||
self._data['receptor'] = self._get_attr(node_name)
|
||||
self._rfc_receptor = self._data['receptor']['Rfc']
|
||||
|
||||
node_name = f'{PRE}/cfdi:Complemento/tfd:TimbreFiscalDigital'
|
||||
self._data['timbre'] = self._get_attr(node_name)
|
||||
|
||||
self._parse_details()
|
||||
return
|
||||
|
||||
def _get_attr(self, node_name):
|
||||
node = self._tree.xpath(node_name, namespaces=NS_CFDI)[0]
|
||||
attr = dict(node.attrib)
|
||||
return attr
|
||||
|
||||
def _parse_details(self):
|
||||
node_name = f'{PRE}/cfdi:Conceptos/cfdi:Concepto'
|
||||
details = self._tree.xpath(node_name, namespaces=NS_CFDI)
|
||||
rows = []
|
||||
for detail in details:
|
||||
row = dict(detail.attrib)
|
||||
for k, v in row.items():
|
||||
if k in ('Cantidad', 'ValorUnitario', 'Descuento', 'Importe'):
|
||||
row[k] = Decimal(v)
|
||||
# ~ row['taxes'] = self._get_taxes(detail)
|
||||
rows.append(row)
|
||||
self._data['conceptos'] = rows
|
||||
return
|
||||
|
||||
|
||||
class CfdiWrite(object):
|
||||
pass
|
|
@ -8,7 +8,6 @@ from middleware import (
|
|||
AuthMiddleware,
|
||||
JSONTranslator,
|
||||
ConnectionMiddleware,
|
||||
static,
|
||||
handle_404
|
||||
)
|
||||
from models.db import StorageEngine
|
||||
|
@ -18,7 +17,16 @@ from controllers.main import (AppEmpresas,
|
|||
AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco,
|
||||
AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina,
|
||||
AppInvoicePay, AppCfdiPay, AppSATBancos, AppSociosCuentasBanco,
|
||||
AppSATFormaPago
|
||||
AppSATFormaPago, AppSATLeyendaFiscales, AppCert, AppSucursales,
|
||||
AppPartnerProducts,
|
||||
AppInventoryEntries,
|
||||
AppTicketsDetails,
|
||||
AppUsers,
|
||||
AppWareHouse,
|
||||
AppWareHouseProduct,
|
||||
AppSATUnidadesPeso,
|
||||
AppSATRegimenes,
|
||||
AppSociosRegimenes,
|
||||
)
|
||||
|
||||
|
||||
|
@ -62,11 +70,20 @@ api.add_route('/cfdipay', AppCfdiPay(db))
|
|||
api.add_route('/satbancos', AppSATBancos(db))
|
||||
api.add_route('/satformapago', AppSATFormaPago(db))
|
||||
api.add_route('/socioscb', AppSociosCuentasBanco(db))
|
||||
api.add_route('/leyendasfiscales', AppSATLeyendaFiscales(db))
|
||||
api.add_route('/cert', AppCert(db))
|
||||
api.add_route('/sucursales', AppSucursales(db))
|
||||
api.add_route('/partnerproducts', AppPartnerProducts(db))
|
||||
api.add_route('/inventoryentries', AppInventoryEntries(db))
|
||||
api.add_route('/warehouse', AppWareHouse(db))
|
||||
api.add_route('/warehouseproduct', AppWareHouseProduct(db))
|
||||
api.add_route('/ticketsdetails', AppTicketsDetails(db))
|
||||
api.add_route('/users', AppUsers(db))
|
||||
api.add_route('/satunidadespeso', AppSATUnidadesPeso(db))
|
||||
api.add_route('/satregimenes', AppSATRegimenes(db))
|
||||
api.add_route('/sociosregimenes', AppSociosRegimenes(db))
|
||||
|
||||
|
||||
# ~ Activa si usas waitress y NO estas usando servidor web
|
||||
# ~ api.add_sink(static, '/static')
|
||||
|
||||
session_options = {
|
||||
'session.type': 'file',
|
||||
'session.cookie_expires': True,
|
||||
|
|
|
@ -8,4 +8,4 @@ threads = 4
|
|||
py-autoreload = 1
|
||||
thunder-lock = true
|
||||
static-map = /static=../static
|
||||
http-timeout = 300
|
||||
http-timeout = 300
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import falcon
|
||||
from controllers import util
|
||||
from models import main
|
||||
from settings import MV, PATH_STATIC
|
||||
from settings import MV
|
||||
|
||||
|
||||
def handle_404(req, resp):
|
||||
|
@ -20,18 +20,21 @@ def get_template(req, resp, resource):
|
|||
resp.body = util.get_template(resource.template, data)
|
||||
|
||||
|
||||
def static(req, res):
|
||||
path = PATH_STATIC + req.path
|
||||
if util.is_file(path):
|
||||
res.content_type = util.get_mimetype(path)
|
||||
res.stream, res.stream_len = util.get_stream(path)
|
||||
res.status = falcon.HTTP_200
|
||||
else:
|
||||
res.status = falcon.HTTP_404
|
||||
# ~ def static(req, res):
|
||||
# ~ path = PATH_STATIC + req.path
|
||||
# ~ if util.is_file(path):
|
||||
# ~ res.content_type = util.get_mimetype(path)
|
||||
# ~ res.stream, res.stream_len = util.get_stream(path)
|
||||
# ~ res.status = falcon.HTTP_200
|
||||
# ~ else:
|
||||
# ~ res.status = falcon.HTTP_404
|
||||
|
||||
|
||||
class AuthMiddleware(object):
|
||||
|
||||
def process_response(self, req, resp, resource):
|
||||
pass
|
||||
|
||||
def process_resource(self, req, resp, resource, params):
|
||||
session = req.env['beaker.session']
|
||||
user = session.get('userobj', None)
|
||||
|
@ -65,9 +68,15 @@ class JSONTranslator(object):
|
|||
def process_response(self, req, resp, resource):
|
||||
if 'result' not in req.context:
|
||||
return
|
||||
|
||||
if '/doc/' in req.path:
|
||||
resp.body = req.context['result']
|
||||
return
|
||||
|
||||
if 'blob' in req.context:
|
||||
resp.body = req.context['blob']
|
||||
return
|
||||
|
||||
resp.body = util.dumps(req.context['result'])
|
||||
|
||||
|
||||
|
|
|
@ -33,25 +33,19 @@ class StorageEngine(object):
|
|||
def get_nomina(self, values):
|
||||
return main.CfdiNomina.get_by(values)
|
||||
|
||||
def nomina(self, values):
|
||||
opt = values.pop('opt')
|
||||
if opt == 'cancel':
|
||||
return main.CfdiNomina.cancel(int(values['id']))
|
||||
|
||||
def empresa_agregar(self, values):
|
||||
return main.empresa_agregar(values['alta_rfc'], False)
|
||||
# ~ return main.empresa_agregar(values['alta_rfc'], False)
|
||||
return main._new_client(values['alta_rfc'], False)
|
||||
|
||||
def empresa_borrar(self, values):
|
||||
return main.empresa_borrar(values['rfc'])
|
||||
# ~ return main.empresa_borrar(values['rfc'])
|
||||
return main._delete_client(values['rfc'], False, False)
|
||||
|
||||
def respaldar_dbs(self):
|
||||
return main.respaldar_dbs()
|
||||
|
||||
def _get_empresas(self, values):
|
||||
return main.get_empresas()
|
||||
|
||||
def get_values(self, table, values=None, session=None):
|
||||
if table in ('allusuarios', 'usuarioupdate'):
|
||||
if table in ('allusuarios', 'usuarioupdate', 'main'):
|
||||
return getattr(self, '_get_{}'.format(table))(values, session)
|
||||
return getattr(self, '_get_{}'.format(table))(values)
|
||||
|
||||
|
@ -76,8 +70,11 @@ class StorageEngine(object):
|
|||
def _get_importinvoice(self, values):
|
||||
return main.import_invoice()
|
||||
|
||||
def _get_main(self, values):
|
||||
return main.config_main()
|
||||
def _get_importceods(self, values):
|
||||
return main.import_ceods()
|
||||
|
||||
def _get_main(self, values, session):
|
||||
return main.config_main(session['userobj'])
|
||||
|
||||
def _get_configtimbrar(self, values):
|
||||
return main.config_timbrar()
|
||||
|
@ -126,12 +123,6 @@ class StorageEngine(object):
|
|||
def enviar_prefac(self, values):
|
||||
return main.PreFacturas.enviar(values['id'])
|
||||
|
||||
# ~ def _get_cancelinvoice(self, values):
|
||||
# ~ return main.Facturas.cancel(values['id'])
|
||||
|
||||
def _get_statussat(self, values):
|
||||
return main.Facturas.get_status_sat(values['id'])
|
||||
|
||||
def _get_verifysat(self, values):
|
||||
return main.Facturas.get_verify_sat(values['id'])
|
||||
|
||||
|
@ -173,6 +164,9 @@ class StorageEngine(object):
|
|||
def _get_unidades(self, values):
|
||||
return main.SATUnidades.get_activos()
|
||||
|
||||
def _get_unitbykey(self, values):
|
||||
return main.SATUnidades.get_activos_by_key()
|
||||
|
||||
def add_moneda(self, values):
|
||||
return main.SATMonedas.add(values)
|
||||
|
||||
|
@ -209,6 +203,10 @@ class StorageEngine(object):
|
|||
def _get_allusoscfdi(self, values):
|
||||
return main.SATUsoCfdi.get_all()
|
||||
|
||||
def _get_allregimenes(self, values):
|
||||
filters = {'opt': 'all'}
|
||||
return main.SATRegimenes.get_data(filters, None)
|
||||
|
||||
def _get_allusuarios(self, values, session):
|
||||
return main.Usuarios.get_(session['userobj'])
|
||||
|
||||
|
@ -236,6 +234,9 @@ class StorageEngine(object):
|
|||
def _get_usocfdiupdate(self, values):
|
||||
return main.SATUsoCfdi.actualizar(values)
|
||||
|
||||
def _get_regimenesupdate(self, values):
|
||||
return main.SATRegimenes.actualizar(values)
|
||||
|
||||
def _get_emisorcuentasbanco(self, values):
|
||||
return main.CuentasBanco.emisor()
|
||||
|
||||
|
@ -248,6 +249,9 @@ class StorageEngine(object):
|
|||
def _get_satunidades(self, values):
|
||||
return main.get_sat_unidades(values['key'])
|
||||
|
||||
def _get_satunidadespeso(self, values):
|
||||
return main.get_sat_unidadespeso(values['key'])
|
||||
|
||||
def _get_satproductos(self, values):
|
||||
return main.get_sat_productos(values['key'])
|
||||
|
||||
|
@ -354,6 +358,9 @@ class StorageEngine(object):
|
|||
|
||||
return main.Facturas.add(values, user)
|
||||
|
||||
def invoice_put(self, values, user):
|
||||
return main.Facturas.put(values, user)
|
||||
|
||||
def preinvoice(self, values):
|
||||
id = int(values.pop('id', '0'))
|
||||
#~ if id:
|
||||
|
@ -375,17 +382,19 @@ class StorageEngine(object):
|
|||
if opt == 'add':
|
||||
return main.Tickets.add(values, user)
|
||||
if opt == 'cancel':
|
||||
return main.Tickets.cancel(values)
|
||||
return main.Tickets.cancel(values, user)
|
||||
if opt == 'invoice':
|
||||
return main.Tickets.invoice(values, user)
|
||||
if opt == 'print':
|
||||
return main.Tickets.printer(values)
|
||||
|
||||
def get_tickets(self, values):
|
||||
return main.Tickets.get_by(values)
|
||||
def get_tickets(self, values, user):
|
||||
return main.Tickets.get_by(values, user)
|
||||
|
||||
def get_invoices(self, values):
|
||||
return main.Facturas.get_(values)
|
||||
def get_invoices(self, filters, user):
|
||||
if filters.get('by', ''):
|
||||
return main.Facturas.get_by(filters, user)
|
||||
return main.Facturas.get_(filters)
|
||||
|
||||
def get_preinvoices(self, values):
|
||||
return main.PreFacturas.get_(values)
|
||||
|
@ -459,3 +468,81 @@ class StorageEngine(object):
|
|||
|
||||
def partners_accounts_bank(self, values):
|
||||
return main.SociosCuentasBanco.post(values)
|
||||
|
||||
def nomina(self, values, user):
|
||||
return main.CfdiNomina.post(values, user)
|
||||
|
||||
def sat_leyendas_fiscales_get(self, values):
|
||||
return main.SATLeyendasFiscales.get_values(values)
|
||||
|
||||
def sat_leyendas_fiscales_post(self, values):
|
||||
return main.SATLeyendasFiscales.post(values)
|
||||
|
||||
def sat_leyendas_fiscales_delete(self, values):
|
||||
return main.SATLeyendasFiscales.remove(values)
|
||||
|
||||
# ~ v2
|
||||
def cert_get(self, filters):
|
||||
return main.Certificado.get_data(filters)
|
||||
|
||||
def cert_post(self, filters):
|
||||
return main.Certificado.post(filters)
|
||||
|
||||
def sucursales_get(self, filters):
|
||||
return main.Sucursales.get_data(filters)
|
||||
|
||||
def sucursales_post(self, filters):
|
||||
return main.Sucursales.post(filters)
|
||||
|
||||
def partner_products_get(self, filters):
|
||||
return main.PartnerProducts.get_data(filters)
|
||||
|
||||
def inventory_entries_get(self, filters):
|
||||
return main.InventoryEntries.get_data(filters)
|
||||
|
||||
def inventory_entries_post(self, filters, user):
|
||||
return main.InventoryEntries.post(filters, user)
|
||||
|
||||
def warehouse_get(self, filters):
|
||||
return main.Almacenes.get_data(filters)
|
||||
|
||||
def warehouse_post(self, values, user):
|
||||
return main.Almacenes.post(values, user)
|
||||
|
||||
def warehouseproduct_get(self, filters, user):
|
||||
return main.WareHouseProduct.get_data(filters, user)
|
||||
|
||||
def warehouseproduct_post(self, filters, user):
|
||||
return main.WareHouseProduct.post(filters, user)
|
||||
|
||||
def users_get(self, filters, user):
|
||||
return main.Usuarios.get_data(filters, user)
|
||||
|
||||
def users_post(self, args, user):
|
||||
return main.Usuarios.post(args, user)
|
||||
|
||||
def ticketsdetails_get(self, filters, user):
|
||||
return main.TicketsDetalle.get_data(filters, user)
|
||||
|
||||
def products_get(self, filters, user):
|
||||
return main.Productos.get_data(filters, user)
|
||||
|
||||
def nomina_get(self, filters, user):
|
||||
return main.CfdiNomina.get_data(filters, user)
|
||||
|
||||
def sat_unidades_peso_get(self, filters, user):
|
||||
return main.SATUnidadesPeso.get_data(filters, user)
|
||||
|
||||
def sat_unidades_peso_post(self, args, user):
|
||||
return main.SATUnidadesPeso.post(args, user)
|
||||
|
||||
def sat_regimenes_get(self, filters, user):
|
||||
return main.SATRegimenes.get_data(filters, user)
|
||||
|
||||
def socios_regimenes_get(self, filters, user):
|
||||
return main.SociosRegimenes.get_data(filters, user)
|
||||
|
||||
# Companies only in MV
|
||||
def _get_empresas(self, values):
|
||||
return main.companies_get()
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# ~ Empresa Libre
|
||||
# ~ Copyright (C) 2016-2018 Mauricio Baeza Servin (web@correolibre.net)
|
||||
# ~ Copyright (C) 2016-2018 Mauricio Baeza Servin (publico@cuates.net)
|
||||
# ~
|
||||
# ~ 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
|
||||
|
@ -28,17 +28,9 @@ from conf import DEBUG, MV, LOG_PATH
|
|||
try:
|
||||
from conf import DEFAULT_PASSWORD
|
||||
except ImportError:
|
||||
DEFAULT_PASSWORD = 'salgueiro3.3'
|
||||
DEFAULT_PASSWORD = 'salgueiro4.0'
|
||||
|
||||
try:
|
||||
from conf import SEAFILE_SERVER
|
||||
except ImportError:
|
||||
SEAFILE_SERVER = {}
|
||||
|
||||
try:
|
||||
from conf import TITLE_APP
|
||||
except ImportError:
|
||||
TITLE_APP = 'Empresa Libre'
|
||||
TITLE_APP = 'Empresa Libre'
|
||||
|
||||
try:
|
||||
from conf import NO_HTTPS
|
||||
|
@ -47,13 +39,24 @@ except ImportError:
|
|||
|
||||
|
||||
DEBUG = DEBUG
|
||||
VERSION = '1.21.1'
|
||||
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
|
||||
VERSION = '2.3.2'
|
||||
|
||||
EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
|
||||
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
|
||||
|
||||
path_static = os.path.abspath(os.path.join(BASE_DIR, '..', 'static'))
|
||||
path_docs = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
|
||||
|
||||
path_css = os.path.join(path_static, 'css')
|
||||
path_img = os.path.join(path_static, 'img')
|
||||
path_user_template = os.path.join(path_docs, 'templates')
|
||||
path_user_logos = os.path.join(path_docs, 'logos')
|
||||
|
||||
# ~ PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..'))
|
||||
|
||||
PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..'))
|
||||
PATH_TEMPLATES = os.path.abspath(os.path.join(BASE_DIR, '..', 'templates'))
|
||||
PATH_MEDIA = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
|
||||
|
||||
|
@ -68,13 +71,16 @@ PATH_SESSIONS = {
|
|||
|
||||
IV = 'valores_iniciales.json'
|
||||
INIT_VALUES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', IV))
|
||||
CT = 'cancel_template.xml'
|
||||
TEMPLATE_CANCEL = os.path.abspath(os.path.join(PATH_TEMPLATES, CT))
|
||||
# ~ CT = 'cancel_template.xml'
|
||||
# ~ TEMPLATE_CANCEL = os.path.abspath(os.path.join(PATH_TEMPLATES, CT))
|
||||
|
||||
PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
|
||||
PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
|
||||
|
||||
template_lookup = TemplateLookup(directories=[PATH_TEMPLATES],
|
||||
PATH_TEMPLATES_USER = os.path.abspath(os.path.join(
|
||||
BASE_DIR, '..', 'docs', 'templates'))
|
||||
directories=[PATH_TEMPLATES, PATH_TEMPLATES_USER]
|
||||
template_lookup = TemplateLookup(directories=directories,
|
||||
input_encoding='utf-8',
|
||||
output_encoding='utf-8')
|
||||
|
||||
|
@ -119,12 +125,29 @@ if 'win' in sys.platform:
|
|||
PATH_XMLSEC = os.path.join(PATH_BIN, 'xmlsec.exe')
|
||||
|
||||
|
||||
PRE_DEFAULT = {
|
||||
'CFDI': {'VERSION': '4.0', 'PRE': '{http://www.sat.gob.mx/cfd/4}'},
|
||||
'NOMINA': {'VERSION': '1.2', 'PRE': '{http://www.sat.gob.mx/nomina12}'},
|
||||
'PAGOS': {'VERSION': '2.0', 'PRE': '{http://www.sat.gob.mx/Pagos20}'},
|
||||
'TIBRE': {'VERSION': '1.1', 'PRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}'},
|
||||
}
|
||||
|
||||
pre2 ='{http://www.sat.gob.mx/cfd/2}'
|
||||
pre3 ='{http://www.sat.gob.mx/cfd/3}'
|
||||
PRE_HISTORY = {
|
||||
'CFDI': {'2.0': pre2, '2.2': pre2,
|
||||
'3.0': pre3, '3.2': pre3, '3.3': pre3},
|
||||
'NOMINA': {'1.1': '{http://www.sat.gob.mx/nomina}'},
|
||||
'PAGOS': {'1.0': '{http://www.sat.gob.mx/Pagos}'},
|
||||
}
|
||||
|
||||
PRE = {
|
||||
'2.0': '{http://www.sat.gob.mx/cfd/2}',
|
||||
'2.2': '{http://www.sat.gob.mx/cfd/2}',
|
||||
'3.0': '{http://www.sat.gob.mx/cfd/3}',
|
||||
'3.2': '{http://www.sat.gob.mx/cfd/3}',
|
||||
'3.3': '{http://www.sat.gob.mx/cfd/3}',
|
||||
'4.0': '{http://www.sat.gob.mx/cfd/4}',
|
||||
'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}',
|
||||
'DONATARIA': '{http://www.sat.gob.mx/donat}',
|
||||
'INE': '{http://www.sat.gob.mx/ine}',
|
||||
|
@ -133,11 +156,15 @@ PRE = {
|
|||
'1.1': '{http://www.sat.gob.mx/nomina}',
|
||||
'1.2': '{http://www.sat.gob.mx/nomina12}',
|
||||
},
|
||||
'pagos': '{http://www.sat.gob.mx/Pagos}',
|
||||
'PAGOS': {
|
||||
'1.0': '{http://www.sat.gob.mx/Pagos}',
|
||||
}
|
||||
}
|
||||
|
||||
CURRENT_CFDI = '3.3'
|
||||
CURRENT_CFDI_NOMINA = '1.2'
|
||||
# To delete
|
||||
# ~ CURRENT_CFDI = '4.0'
|
||||
# ~ CURRENT_CFDI_NOMINA = '1.2'
|
||||
|
||||
DECIMALES = 2
|
||||
DECIMALES_TAX = 4
|
||||
DECIMALES_PRECIOS = 4
|
||||
|
@ -160,7 +187,8 @@ DEFAULT_CFDIPAY = {
|
|||
'TYPE': 'P',
|
||||
'WAYPAY': 'PPD',
|
||||
'CURRENCY': 'XXX',
|
||||
'USED': 'P01',
|
||||
'TC': '1',
|
||||
'USED': 'CP01',
|
||||
'KEYSAT': '84111506',
|
||||
'UNITKEY': 'ACT',
|
||||
'DESCRIPTION': 'Pago',
|
||||
|
@ -173,7 +201,7 @@ PUBLIC = 'Público en general'
|
|||
DEFAULT_SAT_NOMINA = {
|
||||
'SERIE': 'N',
|
||||
'FORMA_PAGO': '99',
|
||||
'USO_CFDI': 'P01',
|
||||
'USO_CFDI': 'CN01',
|
||||
'CLAVE': '84111505',
|
||||
'UNIDAD': 'ACT',
|
||||
'DESCRIPCION': 'Pago de nómina',
|
||||
|
@ -182,3 +210,110 @@ DEFAULT_SAT_NOMINA = {
|
|||
API = 'https://api.empresalibre.net{}'
|
||||
|
||||
CURRENCY_MN = 'MXN'
|
||||
|
||||
# ~ v2
|
||||
CANCEL_VERSION = ('3.3', '4.0')
|
||||
CFDI_VERSIONS = CANCEL_VERSION
|
||||
|
||||
IS_MV = MV
|
||||
DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
|
||||
path_bk = os.path.join(path_docs, 'tmp')
|
||||
path_local = 'facturas'
|
||||
path_sat = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'valores_iniciales.json'))
|
||||
|
||||
EXT = {
|
||||
'CSS': 'css',
|
||||
'HTML': 'html',
|
||||
'ODS': 'ods',
|
||||
'PNG': 'png',
|
||||
'JSON': 'json',
|
||||
}
|
||||
MXN = 'MXN'
|
||||
|
||||
CARTA_PORTE = {
|
||||
'MONEDA': 'XXX',
|
||||
}
|
||||
|
||||
PATHS = {
|
||||
'STATIC': path_static,
|
||||
'CSS': path_css,
|
||||
'IMG': path_img,
|
||||
'DOCS': path_docs,
|
||||
'USER': path_user_template,
|
||||
'LOGOS': path_user_logos,
|
||||
'BK': path_bk,
|
||||
'LOCAL': path_local,
|
||||
'SAT': path_sat,
|
||||
'xslt': PATH_XSLT,
|
||||
}
|
||||
|
||||
VALUES_PDF = {
|
||||
'CANCEL': {True: 'inline', False: 'none'},
|
||||
'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
|
||||
'TAX': {'001': 'ISR', '002': 'IVA', '003': 'IEPS'},
|
||||
'METHOD': {
|
||||
'PUE': 'Pago en una sola exhibición',
|
||||
'PPD': 'Pago en parcialidades o diferido',
|
||||
},
|
||||
}
|
||||
|
||||
RESICO = '626'
|
||||
|
||||
RFCS = {
|
||||
'PUBLIC': 'XAXX010101000',
|
||||
'FOREIGN': 'XEXX010101000',
|
||||
'CVD110412TF6': 'finkok',
|
||||
'SCD110105654': 'comercio',
|
||||
'AAA010101AAA': 'comercio',
|
||||
'SPR190613I52': 'comercio',
|
||||
}
|
||||
|
||||
URL = {
|
||||
'SEAFILE': 'https://seafile.cuates.net',
|
||||
}
|
||||
|
||||
DEFAULT_GLOBAL = {
|
||||
'cantidad': 1.00,
|
||||
'unidad': 'ACT',
|
||||
'descripcion': 'Venta',
|
||||
'clave_sat': '01010101',
|
||||
}
|
||||
|
||||
# ~ TEMPLATE_CANCEL = """<CancelaCFD xmlns="http://cancelacfd.sat.gob.mx">
|
||||
# ~ <Cancelacion xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" RfcEmisor="{rfc}" Fecha="{fecha}">
|
||||
# ~ <Folios>
|
||||
# ~ <Folio UUID="{uuid}" Motivo="{motivo}"{folio}/>
|
||||
# ~ </Folios>
|
||||
# ~ <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
|
||||
# ~ <SignedInfo>
|
||||
# ~ <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
|
||||
# ~ <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
|
||||
# ~ <Reference URI="">
|
||||
# ~ <Transforms>
|
||||
# ~ <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
|
||||
# ~ </Transforms>
|
||||
# ~ <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
|
||||
# ~ <DigestValue/>
|
||||
# ~ </Reference>
|
||||
# ~ </SignedInfo>
|
||||
# ~ <SignatureValue/>
|
||||
# ~ <KeyInfo>
|
||||
# ~ <X509Data>
|
||||
# ~ <X509IssuerSerial>
|
||||
# ~ <X509IssuerName/>
|
||||
# ~ <X509SerialNumber/>
|
||||
# ~ </X509IssuerSerial>
|
||||
# ~ <X509Certificate/>
|
||||
# ~ </X509Data>
|
||||
# ~ <KeyValue>
|
||||
# ~ <RSAKeyValue>
|
||||
# ~ <Modulus/>
|
||||
# ~ <Exponent/>
|
||||
# ~ </RSAKeyValue>
|
||||
# ~ </KeyValue>
|
||||
# ~ </KeyInfo>
|
||||
# ~ </Signature>
|
||||
# ~ </Cancelacion>
|
||||
# ~ </CancelaCFD>"""
|
||||
|
||||
TEMPLATE_CANCEL = """<CancelaCFD xmlns="http://cancelacfd.sat.gob.mx"><Cancelacion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Fecha="{fecha}" RfcEmisor="{rfc}" xmlns="http://cancelacfd.sat.gob.mx"><Folios><Folio UUID="{uuid}" Motivo="{motivo}"{folio}/></Folios><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /><DigestValue/></Reference></SignedInfo><SignatureValue/><KeyInfo><X509Data><X509IssuerSerial><X509IssuerName/><X509SerialNumber/></X509IssuerSerial><X509Certificate/></X509Data><KeyValue><RSAKeyValue><Modulus/><Exponent/></RSAKeyValue></KeyValue></KeyInfo></Signature></Cancelacion></CancelaCFD>"""
|
||||
|
|
BIN
source/db/cp.db
BIN
source/db/sat.db
|
@ -0,0 +1,473 @@
|
|||
id key name
|
||||
1 Tu Contenedor externo
|
||||
2 X1A Tambor de acero
|
||||
3 X1B Tambor de aluminio
|
||||
4 X1D Tambor contrachapado
|
||||
5 X1F Contenedor flexible
|
||||
6 X1G Tambor de fibra
|
||||
7 X1w Tambor de madera
|
||||
8 X2C Barril de madera
|
||||
9 X3A Bidón de acero
|
||||
10 X3H Bidón de plástico
|
||||
11 X43 Bolsa de gran tamaño
|
||||
12 X44 Bolsa de plástico
|
||||
13 X4A Caja de acero
|
||||
14 X4B Caja de aluminio
|
||||
15 X4C Caja de madera natural
|
||||
16 X4D Caja de contrachapado
|
||||
17 X4F Caja de madera reconstituida
|
||||
18 X4G Caja de cartón
|
||||
19 X4H Caja de plástico
|
||||
20 X5H Bolsa de plástico tejido
|
||||
21 X5L Bolsa textil
|
||||
22 X5M Bolsa de papel
|
||||
23 X6H Recipiente de plástico, Contenedor compuesto.
|
||||
24 X6P Recipiente de vidrio, Contenedor compuesto.
|
||||
25 X7A Estuche para carro
|
||||
26 X7B Estuche de madera
|
||||
27 X8A Pallet de madera
|
||||
28 X8B Cajón de madera
|
||||
29 X8C Madera flejada
|
||||
30 XAA Contenedor intermedio para gráneles de plástico rígido
|
||||
31 XAB Contenedor de fibra
|
||||
32 XAC Contenedor de papel
|
||||
33 XAD Contenedor de madera
|
||||
34 XAE Aerosol
|
||||
35 XAF Pallet modular con collares, 80cms * 60cms
|
||||
36 XAG Pallet o empaquetado
|
||||
37 XAH Pallet, 100cms X 110cms
|
||||
38 XAI Contenedor tipo concha
|
||||
39 XAJ Cono
|
||||
40 XAL Esfera
|
||||
41 XAM Ampolleta no protegida
|
||||
42 XAP Ampolleta protegida
|
||||
43 XAT Atomizador
|
||||
44 XAV Cápsula
|
||||
45 XB4 Cinturón
|
||||
46 XBA Barril
|
||||
47 XBB Bobina
|
||||
48 XBC Cajón para botellas / Estante para botellas
|
||||
49 XBD Tablero
|
||||
50 XBE Flejado
|
||||
51 XBF Globo no protegido
|
||||
52 XBG Bolso
|
||||
53 XBH Manojo
|
||||
54 XBI Compartimiento
|
||||
55 XBJ Cubeta
|
||||
56 XBK Cesta
|
||||
57 XBL Paca comprimida
|
||||
58 XBM Cuenco
|
||||
59 XBN Paca no comprimida
|
||||
60 XBO Botella no-protegida y cilíndrica
|
||||
61 XBP Globo protegido
|
||||
62 XBQ Botella cilíndrica protegida
|
||||
63 XBR Barra
|
||||
64 XBS Botella, no-protegida en forma de bulbo
|
||||
65 XBT Rollo de tela
|
||||
66 XBU Butt
|
||||
67 XBV Botella de bulbo protegido
|
||||
68 XBW Caja para líquidos
|
||||
69 XBX Caja
|
||||
70 XBY Tablero, con fleje/ agrupados/ armados
|
||||
71 XBZ Barras, con fleje/ agrupados/ armados
|
||||
72 XCA Lata rectangular
|
||||
73 XCB Cajón para cerveza
|
||||
74 XCC Mantequera
|
||||
75 XCD Lata con mango y boquilla
|
||||
76 XCE Cesto tejido
|
||||
77 XCF Cofre
|
||||
78 XCG Contenedor tipo Jaula
|
||||
79 XCH Cajonera
|
||||
80 XCI Frasco
|
||||
81 XCJ Ataúd
|
||||
82 XCK Barrica
|
||||
83 XCL Espiral
|
||||
84 XCM Paquete de tarjetas
|
||||
85 XCN Contenedor, no especificado como equipo de transporte
|
||||
86 XCO Garrafón no protegido
|
||||
87 XCP Garrafón protegido
|
||||
88 XCQ Cartucho
|
||||
89 XCR Cajón
|
||||
90 XCS Estuche
|
||||
91 XCT Cartón
|
||||
92 XCU Vaso
|
||||
93 XCV Cubierta
|
||||
94 XCW Jaula estilo rodillo
|
||||
95 XCX Lata cilíndrica
|
||||
96 XCY Cilindro
|
||||
97 XCZ Lona
|
||||
98 XDA Cajón multicapa de plástico
|
||||
99 XDB Cajón de varias capas de madera
|
||||
100 XDC Cajón multicapa de cartón
|
||||
101 XDG Jaula, Según la clasificación de la compañía (Commonwealth Handling Equipment Pool (CHEP))
|
||||
102 XDH Caja, Según la clasificación de la compañía (CHEP), Eurobox
|
||||
103 XDI Tambor de hierro
|
||||
104 XDJ damajuana o garrafa, no protegido
|
||||
105 XDK Cajón a granel, cartón
|
||||
106 XDL Cajas de plástico
|
||||
107 XDM Cajones a granel de madera
|
||||
108 XDN Dispensador
|
||||
109 XDP damajuana o garrafa, protegido
|
||||
110 XDR Tambor
|
||||
111 XDS Bandeja de una capa sin cubierta y de plástico
|
||||
112 XDT Bandeja de una capa sin cubierta y de madera
|
||||
113 XDU Bandeja de una capa sin cubierta y poliestireno
|
||||
114 XDV Bandeja de una capa sin cubierta y de cartón
|
||||
115 XDW Bandeja de dos capas sin tapa y con bandeja de plástico
|
||||
116 XDX Bandeja de dos capas sin cubierta y de madera
|
||||
117 XDY Bandeja de dos capas sin cubierta y de cartón
|
||||
118 XEC Bolsa de plástico
|
||||
119 XED Estuche, con pallet de base
|
||||
120 XEE Estuche, con pallet base de madera
|
||||
121 XEF Estuche, con pallet base de cartón
|
||||
122 XEG Estuche, con pallet base de plástico
|
||||
123 XEH Estuche, con pallet base de metal
|
||||
124 XEI Estuche isotérmico
|
||||
125 XEN Sobre
|
||||
126 XFB Bolsa flexible
|
||||
127 XFC Cajón para fruta
|
||||
128 XFD Cajón enmarcado
|
||||
129 XFE Tanque flexible
|
||||
130 XFI Firkin
|
||||
131 XFL Matraz
|
||||
132 XFO Cajón para zapatos
|
||||
133 XFP Caja auxiliar para película fotográfica
|
||||
134 XFR Marco
|
||||
135 XFT Contenedor para alimentos
|
||||
136 XFW Carro de cama plana
|
||||
137 XFX Bolsa flexible tipo contenedor
|
||||
138 XGB Botella para gas
|
||||
139 XGI Viga
|
||||
140 XGL Contenedor tipo galón
|
||||
141 XGR Recipiente de vidrio
|
||||
142 XGU Bandeja contenedor para apilar horizontalmente objetos planos
|
||||
143 XGY Costal de Yute
|
||||
144 XGZ Vigas con correas o agrupadas
|
||||
145 XHA Cesta con mango y de plástico
|
||||
146 XHB Cesta con mango y de madera
|
||||
147 XHC Cesta con asa y de cartón
|
||||
148 XHG Hogshead
|
||||
149 XHN Gancho
|
||||
150 XHR Cesto
|
||||
151 XIA Paquete con pantalla y de madera
|
||||
152 XIB Paquete con pantalla y de cartón
|
||||
153 XIC Paquete con pantalla y de plástico
|
||||
154 XID Paquete con pantalla y de metal
|
||||
155 XIE Paquete de mostrador.
|
||||
156 XIF Envase para alimentos
|
||||
157 XIG Paquete envuelto en papel
|
||||
158 XIH Tambor de plástico
|
||||
159 XIK Paquete de cartón con los agujeros para botellas
|
||||
160 XIL Bandeja rígida con tapa y apilable (CEN TS 14482: 2002)
|
||||
161 XIN Lingote
|
||||
162 XIZ Lingotes con correas/ agrupados
|
||||
163 XJB Bolsa jumbo
|
||||
164 XJC Bidón rectangular
|
||||
165 XJG Jarra
|
||||
166 XJR Tarro
|
||||
167 XJT Bolsa de yute
|
||||
168 XJY Bidón, cilíndrico
|
||||
169 XKG Barrilete
|
||||
170 XKI Kit (Conjunto de piezas)
|
||||
171 XLE Valijas
|
||||
172 XLG Bitácora
|
||||
173 XLT Lote
|
||||
174 XLU Caja de arrastre
|
||||
175 XLV Contenedor pequeño
|
||||
176 XLZ Registros con fleje/ agrupados/ armados
|
||||
177 XMA Cajón metálico
|
||||
178 XMB Múltiplo de bolsas
|
||||
179 XMC Cajón para leche
|
||||
180 XME Contenedor de metal
|
||||
181 XMR Recipiente de metal
|
||||
182 XMS Saco milti-pared
|
||||
183 XMT Tapete
|
||||
184 XMW Contenedor envuelto en plástico
|
||||
185 XMX Caja pequeña de cerillos
|
||||
186 XNA No disponible
|
||||
187 XNE Sin empaque o no empaquetado
|
||||
188 XNF Sin empaque o no empaquetado, unidad simple
|
||||
189 XNG Sin empaque o no empaquetado, unidades múltiples
|
||||
190 XNS Caja nido
|
||||
191 XNT Red
|
||||
192 XNU Red de plástico con tubo
|
||||
193 XNV Red textil con tubo
|
||||
194 XOA Pallet, Según la clasificación de la compañía (Commonwealth Handling Equipment Pool (CHEP) 40 cm x 60 cm
|
||||
195 XOB Pallet, Según la clasificación de la compañía (Commonwealth Handling Equipment Pool (CHEP) 80 cm x 120 cm
|
||||
196 XOC Pallet, Según la clasificación de la compañía (Commonwealth Handling Equipment Pool (CHEP) 100 cm x 120 cm
|
||||
197 XOD Pallet, AS 4068-1993
|
||||
198 XOE Pallet, ISO T11
|
||||
199 XOF Plataforma, peso o dimensión no especificada
|
||||
200 XOK Bloque
|
||||
201 XOT Octabin
|
||||
202 XP2 Charola
|
||||
203 XPA Cajetilla
|
||||
204 XPB Pallet, Caja combinada y abierta con caja y pallet.
|
||||
205 XPC Paquete postal
|
||||
206 XPD Pallet modular con collares (80cms * 100cms)
|
||||
207 XPE Pallet modular con collares (80cms * 120cms)
|
||||
208 XPF Corral
|
||||
209 XPG Placa
|
||||
210 XPH Cantaro
|
||||
211 XPI Pleca
|
||||
212 XPJ Canastilla
|
||||
213 XPK Paquete
|
||||
214 XPL Balde
|
||||
215 XPN Tablón
|
||||
216 XPO Bolsa pequeña
|
||||
217 XPR Contenedor de plástico
|
||||
218 XPT Maceta
|
||||
219 XPU Cacerola
|
||||
220 XPV Tubos, con fleje/ agrupados/ armados
|
||||
221 XPX Pallet
|
||||
222 XPY Placas con fleje/ agrupados/ armados
|
||||
223 XPZ Tablones con fleje/ agrupados/ armados
|
||||
224 XQA Tambor de acero con cabeza no desmontable
|
||||
225 XQB Tambor de acero con cabeza extraíble
|
||||
226 XQC Tambor de aluminio con cabeza no extraíble
|
||||
227 XQD Tambor de aluminio con cabeza extraíble
|
||||
228 XQF Tambor, plástico con cabeza no desmontable
|
||||
229 XQG Tambor, plástico, cabeza extraíble
|
||||
230 XQH Barril de madera con tapón
|
||||
231 XQJ Barril de madera con cabeza desprendible
|
||||
232 XQK Bidón de acero con cabeza no desmontable
|
||||
233 XQL Bidón de acero con cabeza desmontable
|
||||
234 XQM Bidón de plástico con cabeza no desmontable
|
||||
235 XQN Bidón de plástico con cabeza extraíble
|
||||
236 XQP Caja de madera natural estándar
|
||||
237 XQQ Caja de madera natural con muros a prueba de filtraciones
|
||||
238 XQR Caja de plástico expandida
|
||||
239 XQS Caja de plástico sólida
|
||||
240 XRD Rod
|
||||
241 XRG Anillo
|
||||
242 XRJ Estante, Perchero para ropa
|
||||
243 XRK Estante
|
||||
244 XRL Carrete
|
||||
245 XRO Rollo
|
||||
246 XRT Red Roja
|
||||
247 XRZ Varillas con fleje/ agrupados/ armados
|
||||
248 XSA Saco
|
||||
249 XSB Losa
|
||||
250 XSC Cajón poco profundo
|
||||
251 XSD Huso
|
||||
252 XSE Baúl
|
||||
253 XSH Bolsa pequeña hermética
|
||||
254 XSI Patín
|
||||
255 XSK Carcasa esqueleto
|
||||
256 XSL Hoja de deslizamiento
|
||||
257 XSM Hoja de metal
|
||||
258 XSO Carrete pequeño
|
||||
259 XSP Hoja de empaque de plástico
|
||||
260 XSS Cajón de acero
|
||||
261 XSU Maleta
|
||||
262 XSV Sobre de acero
|
||||
263 XSW Envoltorio
|
||||
264 XSY Manga
|
||||
265 XSZ Hojas con fleje/ agrupados/ armados
|
||||
266 XT1 Tableta
|
||||
267 XTB Tina
|
||||
268 XTC Caja para té
|
||||
269 XTD Tubo plegable
|
||||
270 XTG Contenedor de tanque genérico
|
||||
271 XTI Tierce
|
||||
272 XTK Tanque rectangular
|
||||
273 XTL Tina con tapa
|
||||
274 XTN Hojalata
|
||||
275 XTO Tonel
|
||||
276 XTR Maletero
|
||||
277 XTS Estructura
|
||||
278 XTT Bolsa de mano
|
||||
279 XTU Tubo
|
||||
280 XTV Tubo con boquilla
|
||||
281 XTW Pallet tricapa
|
||||
282 XTY Tanque cilíndrico
|
||||
283 XTZ Tubos con fleje/ agrupados/ armados
|
||||
284 XUC Sin empaque
|
||||
285 XUN Unidad
|
||||
286 XVA Tanque
|
||||
287 XVG Tanque de gas (a 1,031 mbar y 15° C)
|
||||
288 XVI Frasco pequeño
|
||||
289 XVK Paquete transportable
|
||||
290 XVL Contenedor para líquidos a granel
|
||||
291 XVN Vehículo
|
||||
292 XVO "Contenedor para sólido de partículas grandes a granel (""nódulos"")"
|
||||
293 XVP Envasado al vacío
|
||||
294 XVQ Tanque para Gas licuado (a temperatura / presión anormal)
|
||||
295 XVR Contenedor para sólidos de partículas granulares a granel (Granos)
|
||||
296 XVS Contenedor de chatarra a granel
|
||||
297 XVY "Contenedor para sólido de partículas finas a granel (""polvos"")"
|
||||
298 XWA Contenedor de granel intermedio
|
||||
299 XWB Botella de mimbre
|
||||
300 XWC Contenedor intermedio para gráneles y de acero
|
||||
301 XWD Contenedor intermedio para gráneles y de aluminio
|
||||
302 XWF Contenedor intermedio para gráneles y de metal
|
||||
303 XWG Contenedor intermedio para gráneles y de acero presurizado menor a 10 kpa
|
||||
304 XWH Contenedor intermedio para gráneles y de aluminio, presurizado menor a 10 kpa
|
||||
305 XWJ Contenedor intermedio para gráneles y de metal con una presión de 10 kpa
|
||||
306 XWK Contenedor intermedio para gráneles y de acero para líquido
|
||||
307 XWL Contenedor intermedio para gráneles y de aluminio para líquido
|
||||
308 XWM Contenedor intermedio para gráneles y de metal para líquido
|
||||
309 XWN Contenedor intermedio para gráneles con tejido plástico sin capa con revestimiento
|
||||
310 XWP Contenedor intermedio para gráneles con tejido plástico y recubierto
|
||||
311 XWQ Contenedor intermedio para gráneles con tejido plástico con revestimiento
|
||||
312 XWR Contenedor intermedio para gráneles con tejido plástico, revestido y con forro
|
||||
313 XWS Contenedor intermedio para gráneles con película de plástico
|
||||
314 XWT Contenedor intermedio para gráneles textil sin capa / forro
|
||||
315 XWU Contenedor intermedio para gráneles de madera natural con forro interior
|
||||
316 XWV Contenedor intermedio para gráneles textil recubierto
|
||||
317 XWW Contenedor intermedio para gráneles textil con revestimiento
|
||||
318 XWX Contenedor intermedio para gráneles textil recubierto y con forro
|
||||
319 XWY Contenedor intermedio para gráneles contrachapado con revestimiento interior
|
||||
320 XWZ Contenedor intermedio para gráneles de madera reconstituida con revestimiento interior
|
||||
321 XXA Bolsa de tejido plástico, sin abrigo interior ni forro
|
||||
322 XXB Bolsa de tejido plástico a prueba de filtraciones
|
||||
323 XXC Bolsa de tejido plástico resistente al agua
|
||||
324 XXD Bolsa con película de plástico
|
||||
325 XXF Bolsa textil sin capa ni forro interior
|
||||
326 XXG Bolsa textil a prueba de filtraciones
|
||||
327 XXH Bolsa textil resistente al agua
|
||||
328 XXJ Bolsa de papel multi-pared
|
||||
329 XXK Bolsa de papel multi-pared, resistente al agua
|
||||
330 XYA Empaque compuesto, recipiente de plástico en tambor de acero
|
||||
331 XYB Empaque compuesto, recipiente de plástico en cajas de acero
|
||||
332 XYC Empaque compuesto, recipiente de plástico en tambor de aluminio
|
||||
333 XYD Empaque compuesto, recipiente de plástico en cajón de aluminio
|
||||
334 XYF Empaque compuesto, recipiente de plástico en caja de madera
|
||||
335 XYG Empaque compuesto, recipiente de plástico en tambor de madera contrachapada
|
||||
336 XYH Empaque compuesto, recipiente de plástico en caja de madera contrachapada
|
||||
337 XYJ Empaque compuesto, recipiente de plástico en tambor de fibra
|
||||
338 XYK Empaque compuesto, recipiente de plástico en caja de cartón
|
||||
339 XYL Empaque compuesto, recipiente de plástico en el tambor de plástico
|
||||
340 XYM Empaque compuesto, recipiente de plástico en caja de plástico sólido
|
||||
341 XYN Empaque compuesto, receptáculo de vidrio en tambor de acero
|
||||
342 XYP Empaque compuesto, receptáculo de vidrio en caja de cajas de acero
|
||||
343 XYQ Empaque compuesto, recipiente de vidrio en tambor de aluminio
|
||||
344 XYR Empaque compuesto, receptáculo de vidrio en caja de aluminio
|
||||
345 XYS Empaque compuesto, recipiente de vidrio en caja de madera
|
||||
346 XYT Empaque compuesto, recipiente de vidrio en tambor de madera contrachapada
|
||||
347 Xyv Empaque compuesto, recipiente de vidrio en el cesto de mimbre
|
||||
348 XYW Empaque compuesto, recipiente de vidrio en tambor de fibra
|
||||
349 XYX Empaque compuesto, recipiente de vidrio en caja de cartón
|
||||
350 XYY Empaque compuesto, recipiente de vidrio en paquete de plástico expandible
|
||||
351 XYZ Empaque compuesto, recipiente de vidrio en paquete de plástico sólido
|
||||
352 XZA Contenedor de granel intermedio, papel, multi-pared
|
||||
353 XZB Bolsa grande
|
||||
354 XZC Contenedor intermedio para gráneles de papel, multi-pared y resistente al agua
|
||||
355 XZD Contenedor intermedio para gráneles de plástico rígido, con equipo estructural para sólidos
|
||||
356 XZF Contenedor intermedio para gráneles de plástico rígido, autoportante para sólidos
|
||||
357 XZG Contenedor intermedio para gráneles de plástico rígido, con equipo estructural, presurizado
|
||||
358 XZH Contenedor intermedio para gráneles de plástico rígido, autoportante y presurizado
|
||||
359 XZJ Contenedor intermedio para gráneles de plástico rígido, con equipo estructural para líquidos
|
||||
360 XZK Contenedor intermedio para gráneles de plástico rígido, autoportante, líquidos
|
||||
361 XZL Contenedor intermedio para gráneles, compuesto y de plástico rígido, sólidos
|
||||
362 XZM Contenedor intermedio para gráneles, compuesto y de plástico flexible, sólidos
|
||||
363 XZN Contenedor intermedio para gráneles, compuesto y de plástico rígido, presurizado
|
||||
364 XZP Contenedor intermedio para gráneles, compuesto y de plástico flexible, presurizado
|
||||
365 XZQ Contenedor intermedio para gráneles, compuesto y de plástico rígido, líquidos
|
||||
366 XZR Contenedor intermedio para gráneles, compuesto y de plástico flexible para líquidos
|
||||
367 XZS Contenedor intermedio para gráneles, compuesto
|
||||
368 XZT Contenedor intermedio para gráneles con tablero de fibras
|
||||
369 XZU Contenedor intermedio para gráneles flexible
|
||||
370 XZV Contenedor intermedio para gráneles de metal, distinto del acero
|
||||
371 XZW Contenedor intermedio para gráneles, de madera natural
|
||||
372 XZX Contenedor intermedio para gráneles, de contrachapado
|
||||
373 XZY Contenedor intermedio para gráneles, de madera reconstituida
|
||||
374 KGM Kilogramo
|
||||
375 MC Microgramo
|
||||
376 DJ Decagramo
|
||||
377 DG Decigramo
|
||||
378 GRM Gramo
|
||||
379 CGM Centigramo
|
||||
380 TNE Tonelada (tonelada métrica)
|
||||
381 DTN Decitonelada métrica
|
||||
382 MGM Miligramo
|
||||
383 HGM Hectogramo
|
||||
384 KTN Kilotonelada Métrica
|
||||
385 2U Megagramo
|
||||
386 LBR Libra
|
||||
387 GRN Grano
|
||||
388 ONZ Onza (avoirdupois)
|
||||
389 CWI Hundredweight
|
||||
390 CWA Hundred pound
|
||||
391 LTN Tonelada (UK) o tonelada larga (estados unidos)
|
||||
392 STI Estone (UK)
|
||||
393 STN Tonelada (estados unidos) o tonelada corta (UK y estados unidos)
|
||||
394 APZ Onza troy u onza farmacéutica
|
||||
395 F13 Slug
|
||||
396 K64 Libra (avoirdupois) por grado fahrenheit
|
||||
397 L69 Tonelada por kelvin
|
||||
398 L87 Tonelada corta por grado fahrenheit
|
||||
399 M85 Tonelada, ensayo
|
||||
400 M86 Libra Alemana
|
||||
401 J33 Microgramo por kilogramo
|
||||
402 L32 Nanogramo por kilogramo
|
||||
403 NA Miligramo por kilogramo
|
||||
404 M29 Kilogramo por kilogramo
|
||||
405 M91 Libra por libra
|
||||
406 Q29 Microgramo por hectogramo
|
||||
407 MTQ Metro cúbico
|
||||
408 MAL Megalitro
|
||||
409 LTR Litro
|
||||
410 MMQ Milímetro cúbico
|
||||
411 CMQ Centímetro cúbico
|
||||
412 DMQ Decímetro cúbico
|
||||
413 MLT Mililitro
|
||||
414 HLT Hectolitro
|
||||
415 CLT Centilitro
|
||||
416 DMA Decámetro cúbico
|
||||
417 H19 Hectómetro cúbico
|
||||
418 H20 Kilómetro cúbico
|
||||
419 M71 Metro cúbico por pascal (joules)
|
||||
420 DLT Decilitro
|
||||
421 4G Microlitro
|
||||
422 K6 Kilolitro
|
||||
423 A44 Decalitro
|
||||
424 G94 Centímetro cúbico por bar
|
||||
425 G95 Litro por bar
|
||||
426 G96 Metro cúbico por bar
|
||||
427 G97 Mililitro por bar
|
||||
428 5I Pies cúbicos estándar
|
||||
429 INQ Pulgada cúbica
|
||||
430 FTQ Pie cúbico
|
||||
431 YDQ Yarda cúbica
|
||||
432 GLI Galón (UK)
|
||||
433 GLL Galón (EUA)
|
||||
434 PT Pinta (US)
|
||||
435 PTI Pint (uk)
|
||||
436 QTI Cuarto (UK)
|
||||
437 PTL Pinta líquida (estados unidos)
|
||||
438 QTL Cuarto de líquido (estadis unidos)
|
||||
439 PTD Pinta seca (estados unidos)
|
||||
440 OZI Onza líquida (UK)
|
||||
441 QT Cuarto (EUA)
|
||||
442 J57 Barril (uk petróleo)
|
||||
443 K21 Pie cúbico por grado fahrenheit
|
||||
444 K23 Pie cúbico por psi (libra por pulgada cuadrada)
|
||||
445 L43 Peck (UK)
|
||||
446 L61 Pinta (US seco)
|
||||
447 L62 Cuarto de galón (seco de los EUA)
|
||||
448 L84 Tonelada (flota UK)
|
||||
449 L86 Tonelada (flota estados unidos)
|
||||
450 M11 Yarda cúbica por grado fahrenheit
|
||||
451 M14 Yarda cúbica por psi (libra por pulgada cuadrada)
|
||||
452 OZA Onza líquida (estados unidos)
|
||||
453 BUI Bushel (UK)
|
||||
454 BUA Bushel (EUA)
|
||||
455 BLL Barril (EUA)
|
||||
456 BLD Barril seco (EUA)
|
||||
457 GLD Galón seco (EUA)
|
||||
458 QTD Cuarto seco (estados unidos)
|
||||
459 G26 Estere
|
||||
460 G21 Taza (unidad de volumen)
|
||||
461 G24 Cucharada (estados unidos)
|
||||
462 G25 Cucharilla (estados unidos)
|
||||
463 G23 Peck
|
||||
464 M67 Acre-pie
|
||||
465 M68 Cordón
|
||||
466 M69 Milla cúbica (reino unido)
|
||||
467 M70 Unidad tradicional de capacidad de carga
|
||||
468 Q32 Femtolitro
|
||||
469 Q33 Picolitro
|
||||
470 Q34 Nanolitro
|
||||
471 NM3 Metro cúbico normalizado
|
||||
472 SM3 Metro cúbico estándar
|
|
|
@ -47,6 +47,13 @@
|
|||
font-size: 125%;
|
||||
}
|
||||
|
||||
.link_default {
|
||||
font-weight: bold;
|
||||
color: #610B0B;
|
||||
text-decoration: none;
|
||||
}
|
||||
.link_default:hover {text-decoration:underline;}
|
||||
|
||||
.link_forum {
|
||||
font-weight: bold;
|
||||
color: #610B0B;
|
||||
|
|
|
@ -0,0 +1,519 @@
|
|||
@page{
|
||||
size: Letter;
|
||||
margin: 0.5cm;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
|
||||
@media print {
|
||||
thead {display: table-header-group;}
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.emisor-cintilla{
|
||||
background-color: #975759;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fiscales-emisor{
|
||||
color: #7d1916;
|
||||
}
|
||||
|
||||
.fiscales-emisor .telefono, .fiscales-emisor .correo, .fiscales-emisor .web{
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header .titulo-vertical{
|
||||
background-color: #dbc5c6;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.receptor .nombre{
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.receptor .rfc{
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.receptor .direccion{
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.receptor .estado{
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cfdi .folio span{
|
||||
color: #ed483d;
|
||||
}
|
||||
|
||||
.cfdi .tipo span{
|
||||
color: #ed483d;
|
||||
}
|
||||
|
||||
.cfdi .folio-fiscal span{
|
||||
color: #ed483d;
|
||||
}
|
||||
|
||||
.conceptos th{
|
||||
background-color: #975759;
|
||||
}
|
||||
|
||||
.conceptos td.clave, .conceptos td.unidad, .conceptos td.valor-unitario{
|
||||
background-color: #dbc5c6;
|
||||
}
|
||||
|
||||
.conceptos td.descripcion, .conceptos td.cantidad, .conceptos td.importe{
|
||||
background-color: #f0e7e7;
|
||||
}
|
||||
|
||||
table.subtotal th{
|
||||
background-color: #dbc5c6;
|
||||
}
|
||||
|
||||
table.subtotal td{
|
||||
background-color: #f0e7e7;
|
||||
}
|
||||
|
||||
.sello .cadenas-sello .cadena div {
|
||||
background-color: #dbc5c6;
|
||||
color: #7d1916;
|
||||
}
|
||||
|
||||
.rfc-pac {
|
||||
background-color: #dbc5c6;
|
||||
color: #7d1916;
|
||||
}
|
||||
|
||||
.cancelada{
|
||||
color: #ed3833;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Avenir Next';
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot');
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff2') format('woff2'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff') format('woff'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.ttf') format('truetype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.svg#AvenirNextLTPro-Regular') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Avenir Next';
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot');
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff2') format('woff2'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff') format('woff'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.ttf') format('truetype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.svg#AvenirNextLTPro-Bold') format('svg');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Avenir Next';
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-It.eot');
|
||||
src: url('/static/fonts/avenir_next/AvenirNextLTPro-It.eot?#iefix') format('embedded-opentype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-It.woff2') format('woff2'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-It.woff') format('woff'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-It.ttf') format('truetype'),
|
||||
url('/static/fonts/avenir_next/AvenirNextLTPro-It.svg#AvenirNextLTPro-It') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
/*
|
||||
font-family: 'Avenir Next';
|
||||
*/
|
||||
}
|
||||
|
||||
#plantilla {
|
||||
border: 1px solid #fff;
|
||||
border-color: rgb(204,204,204);
|
||||
background: #fff;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 19.5cm;
|
||||
height: 25.9cm;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cancelada{
|
||||
-ms-transform: rotate(320deg);
|
||||
-webkit-transform: rotate(320deg);
|
||||
transform: rotate(320deg);
|
||||
color: #ed3833;
|
||||
font-size: 100px;
|
||||
font-weight: bold;
|
||||
left: 18%;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 30%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cancelada div{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.emisor-cintilla{
|
||||
background-color: #975759;
|
||||
color: #fff;
|
||||
font-size: 7pt;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.datos-emisor .logo{
|
||||
margin-left: 20px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.datos-emisor .fiscales-emisor{
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fiscales-emisor{
|
||||
color: #7d1916;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fiscales-emisor .nombre{
|
||||
font-size: 18px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.fiscales-emisor .rfc{
|
||||
font-size: 16px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.fiscales-emisor .regimen{
|
||||
font-size: 12px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.fiscales-emisor .telefono, .fiscales-emisor .correo, .fiscales-emisor .web{
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 15px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.fiscales-emisor img{
|
||||
margin-left: 10px;
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.clear{
|
||||
clear: both;
|
||||
}
|
||||
|
||||
header .titulo-vertical{
|
||||
background-color: #dbc5c6;
|
||||
-ms-transform: rotate(270deg);
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
left: -31px;
|
||||
line-height: 35px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 35px;
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
header .receptor{
|
||||
box-sizing: border-box;
|
||||
float: left;
|
||||
font-size: 10px;
|
||||
height: 106px;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 45px;
|
||||
padding-top: 5px;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.receptor .nombre{
|
||||
color: #000;
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.receptor .rfc{
|
||||
color: #000;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.receptor .direccion,
|
||||
.receptor .estado{
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.receptor img{
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.receptor .correo,
|
||||
.receptor .telefono{
|
||||
float: right;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.receptor .uso-cfdi{
|
||||
color: #000;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
header .cfdi{
|
||||
box-sizing: border-box;
|
||||
float: right;
|
||||
font-size: 10px;
|
||||
height: 100px;
|
||||
margin-top: 10px;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 45px;
|
||||
padding-top: 5px;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.cfdi .folio, .cfdi .tipo{
|
||||
color: #000;
|
||||
float: left;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.cfdi .folio span, .cfdi .tipo span{
|
||||
color: #ed483d;
|
||||
}
|
||||
|
||||
.cfdi .folio span{
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.cfdi .tipo span{
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.cfdi .folio-fiscal{
|
||||
color: #000;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cfdi .folio-fiscal span{
|
||||
color: #ed483d;
|
||||
}
|
||||
|
||||
.cfdi .fecha-emision, .cfdi .fecha-certificacion, .cfdi .lugar-expedicion{
|
||||
color: #333;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.conceptos{
|
||||
margin-bottom: 10px;
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conceptos th{
|
||||
background-color: #975759;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.conceptos .clave{
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.conceptos .descripcion{
|
||||
width: 45%;
|
||||
}
|
||||
.conceptos .descripcion div{
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.conceptos .unidad{
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.conceptos .cantidad{
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
.conceptos .valor-unitario{
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.conceptos .importe{
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.conceptos td{
|
||||
background-color: #975759;
|
||||
color: #000;
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
.conceptos td.clave, .conceptos td.unidad, .conceptos td.valor-unitario{
|
||||
background-color: #dbc5c6;
|
||||
}
|
||||
|
||||
.conceptos td.descripcion, .conceptos td.cantidad, .conceptos td.importe{
|
||||
background-color: #f0e7e7;
|
||||
}
|
||||
|
||||
.conceptos td.valor-unitario, .conceptos td.importe{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total-letras{
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
margin: 5px 0;
|
||||
width: 63%;
|
||||
}
|
||||
|
||||
table.subtotal{
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
text-align: right;
|
||||
width: 37%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.subtotal th{
|
||||
background-color: #f0e7e7;
|
||||
width: 60%;
|
||||
padding: 3px 3px 3px 3px;
|
||||
}
|
||||
|
||||
table.subtotal td{
|
||||
background-color: #dbc5c6;
|
||||
width: 40%;
|
||||
padding: 3px 3px 3px 3px;
|
||||
}
|
||||
|
||||
.condiciones-pago{
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.notas{
|
||||
float: left;
|
||||
font-size: 11px;
|
||||
margin: 20px 0;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.formapago-metodopago, .moneda-tipocambio, .tiporelacion, .relacionados{
|
||||
float: left;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.factura-info{
|
||||
float: left;
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.tipocomite, .tipoproceso, .idcontabilidad{
|
||||
float: right;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.sello{
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.sello .cbb{
|
||||
border: 1px solid #000;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
margin-right: 1px;
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.sello .cadenas-sello{
|
||||
color: #000;
|
||||
float: right;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
width: 79%;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sello .cadenas-sello .cadena{
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.sello .cadenas-sello .cadena div{
|
||||
font-weight: normal;
|
||||
background-color: #dbc5c6;
|
||||
color: #7d1916;
|
||||
}
|
||||
|
||||
.cadena-original{
|
||||
color: #000;
|
||||
float: right;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
line-height: 15px;
|
||||
width: 99%;
|
||||
margin: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cadena-original div{
|
||||
font-weight: normal;
|
||||
background-color: #dbc5c6;
|
||||
color: #7d1916;
|
||||
}
|
||||
|
||||
.rfc-pac{
|
||||
background-color: #dbc5c6;
|
||||
color: #7d1916;
|
||||
font-size: 10px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
margin: 5px;
|
||||
}
|
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 191 KiB |