Compare commits
600 Commits
Author | SHA1 | Date |
---|---|---|
app@dynamicweb-production | 92cfe71bbe | |
pcoder116 | 97d70cc5fd | |
PCoder | c4f6780a0e | |
pcoder116 | 0679a47eea | |
Nico Schottelius | 8b08357d05 | |
Nico Schottelius | 8b06c2c6e9 | |
Nico Schottelius | 26f3a1881d | |
nico14571 | dee2b1a90c | |
PCoder | 9e98125b13 | |
PCoder | 8d13629c8b | |
PCoder | b6ff2b62c1 | |
PCoder | 893c816846 | |
PCoder | f178be8395 | |
Nico Schottelius | 0d7f10776c | |
Nico Schottelius | 4eb470df16 | |
app@dynamicweb-production | 6262432b7a | |
app@dynamicweb-production | bd4d81c286 | |
Nico Schottelius | 169dc6b1ee | |
PCoder | fdb790bac7 | |
PCoder | 6a374f7fa0 | |
PCoder | 10d2c25556 | |
pcoder116 | 5b5239d1d4 | |
app@dynamicweb-production | 41461538a3 | |
app@dynamicweb-production | 7afa9d2f4c | |
PCoder | 96af882e6d | |
pcoder116 | 2e335fc574 | |
PCoder | 7d22086677 | |
PCoder | 83d1e54137 | |
PCoder | b6f9e6a706 | |
PCoder | 4e891ed0bb | |
PCoder | 4d3da3387a | |
PCoder | 94a81fc976 | |
PCoder | 36b091700e | |
PCoder | 3c48811548 | |
PCoder | 10c167e76b | |
PCoder | f31838dbe5 | |
PCoder | 92e5254679 | |
PCoder | a0ade926fb | |
PCoder | ea4ff961c2 | |
pcoder116 | e3b816d8d7 | |
PCoder | 5530b48d0d | |
pcoder116 | a38a4a86a4 | |
PCoder | a492c3de1b | |
app@dynamicweb-production | 325d1ffcb9 | |
app@dynamicweb-production | 3c3c614b6b | |
app@dynamicweb-production | c5f8660c55 | |
app@dynamicweb-production | 9f74c0286e | |
app@dynamicweb-production | dc6f5dcc5b | |
app@dynamicweb-production | c69affc9a1 | |
app@dynamicweb-production | 33aeaf7ddb | |
app@dynamicweb-production | 26d9a77ebf | |
app@dynamicweb-production | 5f1534c152 | |
app | 590f7391c4 | |
app@dynamicweb-production | 95c07fbed1 | |
Tomislav R | 338ff38bbb | |
Tomislav R | 66f3989c23 | |
app@dynamicweb-production | a21b4d6e3f | |
pcoder116 | d739a4a50e | |
PCoder | 138fd519b7 | |
Nico Schottelius | 8179ca4d22 | |
Nico Schottelius | c0333212aa | |
pcoder116 | 4861bee9d3 | |
amalelshihaby | 5ce283318a | |
pcoder116 | 79e96715b2 | |
PCoder | 47d5c63e3b | |
PCoder | d26f2b0f69 | |
pcoder116 | 7c2c3de1f6 | |
PCoder | 1d48dfb93b | |
PCoder | 63821813d4 | |
PCoder | 173c0fe9bf | |
PCoder | 6279fa4e7b | |
PCoder | 1fec2add72 | |
PCoder | d46deaa23a | |
PCoder | a5c83dd589 | |
PCoder | af09b343c0 | |
PCoder | 3b874901bc | |
PCoder | 640807eb62 | |
PCoder | 21f762d6b8 | |
pcoder116 | 9e640d0802 | |
PCoder | c58302d90e | |
PCoder | 1e67bef4f5 | |
PCoder | ec13a71866 | |
PCoder | 6c968fdbb8 | |
PCoder | 44ebb71916 | |
PCoder | 1d7c3f424c | |
PCoder | 8deed169ca | |
PCoder | 7cd485bc6d | |
PCoder | 31c5336e18 | |
PCoder | 36505db5a2 | |
PCoder | e7462289f6 | |
PCoder | 9b84461a29 | |
PCoder | 04003757dc | |
PCoder | e024a3a7a6 | |
PCoder | ba3c5ddd1d | |
PCoder | 2c3d00f03f | |
PCoder | d8a674da3d | |
PCoder | 9524e03762 | |
PCoder | a32a5af5a3 | |
PCoder | 9faf897818 | |
PCoder | 6e6a57b304 | |
PCoder | 7b71ba55f2 | |
PCoder | 8c72b56f6c | |
PCoder | d2ebd3c473 | |
PCoder | b36afcb828 | |
PCoder | a823efd8e2 | |
PCoder | 13f5f576b5 | |
PCoder | 2f98294eab | |
PCoder | 213b9a068e | |
PCoder | 33f741424d | |
PCoder | 7309b8416c | |
PCoder | ff7b20b0dc | |
PCoder | 37f82a48d5 | |
PCoder | 8827bd15ba | |
PCoder | 8e4b3ce96b | |
PCoder | 85757e01c9 | |
PCoder | f48a5cfe71 | |
PCoder | 52d1fb6a0e | |
PCoder | 12c9140b3a | |
PCoder | 7db0594778 | |
PCoder | 87b85c43b4 | |
PCoder | b2f0a45679 | |
PCoder | 9ae4b96968 | |
PCoder | 9077eb0cf2 | |
PCoder | 98b5d03d0b | |
PCoder | f628046417 | |
PCoder | 41de724904 | |
PCoder | ba92c8e416 | |
PCoder | 42c9ec6f28 | |
PCoder | c3286a68a5 | |
PCoder | 35cc9d4229 | |
PCoder | e9c596de66 | |
PCoder | ec1da8fbdf | |
PCoder | 1c4f297775 | |
PCoder | acba77976d | |
PCoder | 624cc45c12 | |
PCoder | 82359064cd | |
PCoder | 98628596f0 | |
PCoder | 39c8e35eca | |
PCoder | eefabe45b6 | |
PCoder | 968eaaf6a4 | |
PCoder | 6a7373523e | |
PCoder | 080a45f39c | |
PCoder | c8519058c4 | |
PCoder | a03e2dc006 | |
PCoder | c28bd9091a | |
PCoder | c0aeac4dc7 | |
PCoder | 377d12b5a5 | |
PCoder | d447f8d9e6 | |
PCoder | 799194152e | |
PCoder | 78b8191165 | |
PCoder | a9778076d6 | |
PCoder | a99924b94c | |
PCoder | 41e993a3d9 | |
PCoder | 0c1b7b1885 | |
PCoder | 480e38fbc9 | |
PCoder | 4962b72d1a | |
PCoder | 70c8ed6825 | |
PCoder | 259c509113 | |
PCoder | 981e68aa4f | |
PCoder | a4a5acd0e7 | |
PCoder | 812157b6c6 | |
PCoder | 9d765fcb6e | |
PCoder | f6f6482ce0 | |
PCoder | 2baa77a7d4 | |
PCoder | de6bc06eaf | |
PCoder | a5f49cf8be | |
PCoder | c70753767f | |
PCoder | 92bafed3b3 | |
PCoder | b1dd9988ce | |
PCoder | 95a1b8fa20 | |
PCoder | 50d9eb1c50 | |
PCoder | 1ed42e608c | |
PCoder | 17c8f9ca18 | |
PCoder | c4c918d591 | |
PCoder | 20c6703236 | |
PCoder | 2a84d20f35 | |
PCoder | 9f49c664fa | |
PCoder | 9e247cc556 | |
PCoder | ca7481cce0 | |
PCoder | 3e95a389bb | |
PCoder | cda241893b | |
PCoder | cb7a1ed4f4 | |
PCoder | 3389e69af1 | |
PCoder | a63fac1a20 | |
PCoder | d0d5fb0196 | |
PCoder | 0b0c932e5a | |
PCoder | 7fcf148cf4 | |
PCoder | 504681107a | |
PCoder | 57b6b18243 | |
PCoder | 22d3b1f83c | |
PCoder | 01d8cc1b9b | |
PCoder | 785091e4ff | |
PCoder | 890a83cfa6 | |
PCoder | 585e9cf146 | |
PCoder | a0ab436d9a | |
pcoder116 | abfbc3b69a | |
PCoder | 7bcca15f0b | |
PCoder | 9d85c058da | |
PCoder | 8c374af4ff | |
PCoder | ff28c6e8e8 | |
PCoder | bbb51b71a6 | |
PCoder | 591f5ff37b | |
PCoder | 81ba834b01 | |
PCoder | 082c0b00af | |
PCoder | 17557fd4c9 | |
PCoder | 352c780287 | |
PCoder | e9801eb9c4 | |
PCoder | e522ac0f61 | |
PCoder | bf1aad82b8 | |
pcoder116 | 555e13e631 | |
PCoder | d980fb0000 | |
PCoder | e8b79d6951 | |
PCoder | 52362cd0ea | |
pcoder116 | 73cb003353 | |
PCoder | 79cbfac092 | |
PCoder | 2973ef3b1d | |
PCoder | 81ec1125cb | |
PCoder | 4c7b9eaa52 | |
PCoder | 676a358832 | |
PCoder | 877553e442 | |
PCoder | 70bfef4738 | |
PCoder | c2e2e1828f | |
PCoder | 08bf163d21 | |
PCoder | df301a18fc | |
PCoder | ad5371a133 | |
PCoder | fb59ae4055 | |
PCoder | 050309a68e | |
pcoder116 | 18c494dfd7 | |
PCoder | c339c19cfd | |
PCoder | 58377319b9 | |
PCoder | 87a154bd0a | |
PCoder | 49a9fdd842 | |
PCoder | 0c39336653 | |
PCoder | 81fd129d48 | |
PCoder | 25255c862a | |
PCoder | 1b29a23ede | |
PCoder | 6b3ecfaff4 | |
PCoder | c4e7f99202 | |
PCoder | 0dc9c6cdca | |
PCoder | 38109e175a | |
PCoder | 081f81c41c | |
PCoder | ffae844ee5 | |
PCoder | 1ce28964a6 | |
PCoder | bc69cc49e5 | |
PCoder | a52215bb56 | |
PCoder | eadbebb796 | |
PCoder | 495e7d4022 | |
PCoder | 81eee87fb9 | |
PCoder | af36a49366 | |
PCoder | 4bff49dab6 | |
PCoder | 6131270b1d | |
PCoder | 17a8efb0b6 | |
pcoder116 | a665dbf9c8 | |
PCoder | c1473fa374 | |
PCoder | ac1170a0f1 | |
PCoder | f9906781ba | |
PCoder | 7db3dc4222 | |
PCoder | f089892c90 | |
PCoder | a395b7a4a6 | |
PCoder | 8443e03b1f | |
PCoder | 3aff4bb69a | |
PCoder | 84c3db7e52 | |
PCoder | d35403311f | |
PCoder | 8a3fa667a0 | |
PCoder | 132a5112fd | |
PCoder | b01f12c9ec | |
PCoder | 4869fd51df | |
PCoder | 4435eef077 | |
PCoder | 343a9440f0 | |
PCoder | 27aa0ea595 | |
PCoder | 70264d592d | |
PCoder | d0be07ecd5 | |
PCoder | cec7938c9c | |
PCoder | 7072420ea5 | |
pcoder116 | 1986978f9b | |
PCoder | cb3ff73100 | |
PCoder | 46c33cf107 | |
pcoder116 | 58680c1647 | |
PCoder | 9d96ecefea | |
PCoder | 580960548e | |
PCoder | f539967a22 | |
PCoder | b076debfee | |
PCoder | d21c5837f7 | |
PCoder | b9096de386 | |
PCoder | b44a7f98b5 | |
PCoder | e2c86116b2 | |
PCoder | 45af92e049 | |
PCoder | 42fb55dee1 | |
PCoder | 2cbf146ebc | |
PCoder | c058138044 | |
PCoder | f45f8dd51f | |
PCoder | dd2eae68e6 | |
PCoder | e322e58246 | |
PCoder | 3ca1a45217 | |
PCoder | c315030b06 | |
PCoder | e6de90e431 | |
PCoder | 00b434efb9 | |
PCoder | 5d977f32d3 | |
PCoder | 3061c1483c | |
PCoder | f68549d80d | |
PCoder | b607575c8c | |
PCoder | 368db61a2f | |
PCoder | 359a633047 | |
PCoder | 1630dc195b | |
PCoder | f0f8af2367 | |
PCoder | a5d393ad20 | |
PCoder | 70a3620598 | |
PCoder | 42a4a77c02 | |
PCoder | e094930d6e | |
PCoder | c0683d9f53 | |
PCoder | fc8c4579fb | |
PCoder | 9f86f44569 | |
PCoder | ffde015c31 | |
PCoder | c765698a0f | |
PCoder | 44dee625b4 | |
PCoder | 88a39ef85a | |
PCoder | 8fe689d993 | |
PCoder | b103cff0a6 | |
PCoder | c43afb7c59 | |
PCoder | 2058c660c0 | |
PCoder | 23b25002ae | |
PCoder | b645f9894b | |
PCoder | 9d21181073 | |
PCoder | d8482c52f9 | |
PCoder | 918d2b17e1 | |
PCoder | e6f00abd71 | |
PCoder | 838163bd59 | |
PCoder | b1acd3f25b | |
PCoder | f4393426d3 | |
PCoder | 7a9b315e2e | |
PCoder | 3141dc2793 | |
PCoder | 3d51e4fe32 | |
PCoder | f546c5cb4f | |
PCoder | 4128aeb64d | |
PCoder | 8ee4081f60 | |
PCoder | ad606c2c55 | |
PCoder | a81fdc8ec1 | |
PCoder | 6132638faa | |
PCoder | 48cc2b4939 | |
PCoder | 1f79ccd490 | |
PCoder | b8eca59e0d | |
PCoder | 970834cc38 | |
PCoder | 24740438f7 | |
PCoder | cde6c51d4b | |
PCoder | d1fd57b730 | |
PCoder | 156930ab26 | |
PCoder | 112f3e17a9 | |
PCoder | 38550ea75c | |
PCoder | e01b27835e | |
PCoder | ec00785068 | |
PCoder | 399f9ed6c9 | |
PCoder | a82c4d556c | |
PCoder | 0b2a305f57 | |
PCoder | a00a9f6ff0 | |
PCoder | fd4f61bc5c | |
PCoder | 9212c02cd7 | |
PCoder | 5fe1c21b57 | |
PCoder | f762cbe58c | |
PCoder | 4b8b0b0540 | |
PCoder | 5468d5436c | |
PCoder | 96e50ddc8a | |
PCoder | 235904d784 | |
PCoder | ceb7f9b0e6 | |
PCoder | 7d7bd60a7f | |
PCoder | b4a3c5e277 | |
PCoder | 5f81bc9091 | |
PCoder | 690952156d | |
PCoder | f4e84f62a4 | |
PCoder | 0d27bac3a8 | |
PCoder | 251676ead8 | |
PCoder | 2acb1fb418 | |
PCoder | 7130df9fd4 | |
PCoder | 18b04a70e6 | |
PCoder | ed1a4fc1a6 | |
PCoder | 0bca5113ca | |
PCoder | efaf75615b | |
PCoder | d4bfcbef47 | |
PCoder | 6674e70ded | |
PCoder | 8f2bd568db | |
PCoder | 0695d68903 | |
PCoder | 6ac6db8212 | |
PCoder | 7423a80670 | |
PCoder | ca5724f10f | |
PCoder | 2378410f2d | |
PCoder | 9078e46196 | |
PCoder | 3ca7e89f4f | |
PCoder | aba3092207 | |
PCoder | 7949ab274e | |
PCoder | b567b01362 | |
PCoder | 7397be98a5 | |
PCoder | 398a255965 | |
PCoder | 3202c83c68 | |
PCoder | 2a760639f6 | |
PCoder | c142d743d1 | |
PCoder | 74921fcd4a | |
PCoder | 12975565a5 | |
PCoder | d62986c91f | |
PCoder | fcdabd8dc3 | |
PCoder | a5c7865811 | |
PCoder | 69996f536b | |
PCoder | 7eed04ec73 | |
PCoder | cbf2f05d70 | |
PCoder | 9e87fa76c3 | |
PCoder | f2d738ae62 | |
PCoder | c6147c887c | |
PCoder | 064ea5be2f | |
PCoder | 32d9f06c18 | |
PCoder | 6d0a7f7049 | |
PCoder | 5ab0bf6993 | |
PCoder | ad52338653 | |
PCoder | d8c03a4364 | |
PCoder | 9aff248d31 | |
PCoder | 364f5599e6 | |
PCoder | ec5bfb18b3 | |
PCoder | c3b22992ea | |
PCoder | 2038d719f0 | |
PCoder | b284ed70a6 | |
PCoder | 7eff6fc92c | |
PCoder | 262bf3e2f7 | |
PCoder | 99e70d95c4 | |
PCoder | 7d9ab322c9 | |
PCoder | fe6ade38eb | |
PCoder | 242dbb2479 | |
PCoder | 833dc9bdcb | |
PCoder | 9310f72cf9 | |
PCoder | 74d1bbb6d3 | |
PCoder | 6fd0659c88 | |
PCoder | 4560c8bf83 | |
PCoder | 0e40ca6044 | |
PCoder | c393902396 | |
PCoder | 825e716625 | |
PCoder | 0d208d2bd9 | |
PCoder | 52201c0aa1 | |
PCoder | ca128dd8c4 | |
PCoder | 0b8315ca76 | |
PCoder | 4a40438edc | |
PCoder | 908c2e055c | |
PCoder | 4491a52bd9 | |
PCoder | b15ece7088 | |
PCoder | 3b654f1c49 | |
PCoder | 752b61a852 | |
PCoder | 110459b38d | |
PCoder | de0fe77779 | |
PCoder | 785f99311d | |
PCoder | 8cc766b62f | |
PCoder | 1b243822a9 | |
PCoder | 1400d27afa | |
PCoder | 8996254212 | |
PCoder | 80f01aec07 | |
PCoder | 127c83059f | |
PCoder | 772c7557e3 | |
PCoder | 55979f3701 | |
PCoder | f6832c090e | |
PCoder | cb065d36df | |
PCoder | 50043f8283 | |
PCoder | c6db34efdd | |
PCoder | 92570ada7f | |
PCoder | 8ebd12c420 | |
PCoder | e48ae6a39d | |
PCoder | 6eb4b03afe | |
PCoder | 31905695c9 | |
PCoder | 6e17742d03 | |
PCoder | 7b7f8fb191 | |
PCoder | 3ff9f25a7f | |
PCoder | 3b7183fc63 | |
PCoder | 3a0fe87a8e | |
PCoder | f1821954eb | |
PCoder | 3b6e5d448b | |
PCoder | 8409acf02d | |
PCoder | 32cfdea68c | |
PCoder | e3078f3ea9 | |
PCoder | 2d66ae6783 | |
PCoder | 202a514b1b | |
PCoder | b919b6cfbd | |
PCoder | f566aa8a2e | |
PCoder | c9de757bc7 | |
PCoder | 0c82525b7f | |
ahmadbilalkhalid | ed22a2261e | |
PCoder | 3f012b7514 | |
PCoder | 36d16ddd72 | |
PCoder | 034d2971cd | |
PCoder | 4ef4eaf785 | |
ahmadbilalkhalid | aa26458a8c | |
PCoder | a8cc07d95f | |
PCoder | ed74504270 | |
ahmadbilalkhalid | b382d9709f | |
PCoder | e2615de907 | |
PCoder | 78f901c48a | |
ahmadbilalkhalid | b29ff91b46 | |
ahmadbilalkhalid | 6c3f01003f | |
ahmadbilalkhalid | 75b08cfbf8 | |
PCoder | 24edf05e7a | |
PCoder | 6ea486b527 | |
PCoder | 568d874476 | |
PCoder | 5e97d70a5e | |
PCoder | 0f3acf5db4 | |
PCoder | cdaf498487 | |
PCoder | 3efd6087e2 | |
PCoder | e11882685f | |
PCoder | 33120d14f3 | |
PCoder | 490ceec47d | |
PCoder | c4013178f5 | |
PCoder | 69061c016b | |
PCoder | 922fea3bf4 | |
ahmadbilalkhalid | a8149edba5 | |
ahmadbilalkhalid | f9a9a24516 | |
PCoder | c1137c26a1 | |
PCoder | 9c96f2447c | |
PCoder | eda766dc6c | |
ahmadbilalkhalid | b52f2de8d7 | |
PCoder | 859249b894 | |
pcoder116 | 146e9faf53 | |
PCoder | 6666e40ec4 | |
PCoder | 7442cbd9ca | |
PCoder | 991908c37e | |
PCoder | 70f0fed63f | |
ahmadbilalkhalid | 2a1932e052 | |
ahmadbilalkhalid | b4995336c6 | |
ahmadbilalkhalid | c96aff16af | |
ahmadbilalkhalid | 37a3d21e0c | |
PCoder | 49453cacd4 | |
PCoder | 9970bd9925 | |
PCoder | fbfc1152b8 | |
ahmadbilalkhalid | db1da3af4c | |
ahmadbilalkhalid | 3b9322b929 | |
PCoder | a09f95d619 | |
PCoder | cc027c2497 | |
PCoder | fcc671a707 | |
PCoder | a6695a103f | |
PCoder | 744e76c5df | |
PCoder | d2d9eafa41 | |
PCoder | d0398ddec2 | |
PCoder | 52717c2ce7 | |
PCoder | e334b01ad4 | |
PCoder | 73b590f480 | |
PCoder | e940b468c4 | |
PCoder | d864f82e0f | |
PCoder | d8172d6bb2 | |
PCoder | b33271ce7d | |
PCoder | 3b0e479a70 | |
PCoder | b759471274 | |
PCoder | cc5d82ccac | |
pcoder116 | bed57786d7 | |
PCoder | b683a5ac44 | |
PCoder | a2635e6fb9 | |
PCoder | 987efe8f99 | |
PCoder | 9a84fc899e | |
PCoder | 3d28b17c71 | |
PCoder | 67d38df047 | |
_moep_ | 49ef761b2e | |
_moep_ | f82ed81b33 | |
PCoder | 1ff577ddcd | |
PCoder | 15ef20dbc1 | |
PCoder | dc507396eb | |
PCoder | aec2002a9f | |
PCoder | 7dd57fb116 | |
PCoder | 530e47586e | |
PCoder | e726f953a4 | |
PCoder | 5697e313df | |
PCoder | 1e57eb5fae | |
PCoder | a423dd9f49 | |
PCoder | 6eef592cd8 | |
PCoder | 93527fdc02 | |
PCoder | b790676940 | |
PCoder | 435cfa46a6 | |
PCoder | e493a9f3d1 | |
PCoder | 3bf2654b50 | |
PCoder | f0b604c6dc | |
PCoder | a33a344b40 | |
PCoder | 871cccc2ae | |
PCoder | 89418ca008 | |
PCoder | f6feb88708 | |
PCoder | 5954093999 | |
PCoder | 069556d9b6 | |
PCoder | f5372ecd1e | |
PCoder | 3599f0bff4 | |
PCoder | efe411933f | |
PCoder | 940eaf3a07 | |
PCoder | d399fe6e79 | |
PCoder | 582e952187 | |
PCoder | 76c2b9d16c | |
PCoder | 44a20a5029 | |
PCoder | e0b2a0b6e2 | |
PCoder | 7040d908dd | |
PCoder | b3dd57f189 | |
PCoder | 7038a36b4d | |
PCoder | c56d6bd627 | |
PCoder | 2d916936d6 | |
PCoder | 270a03e7c5 | |
PCoder | 4174c6226f | |
PCoder | 7aec4dd938 | |
PCoder | 6faa8b82e8 | |
PCoder | c29193f6c8 | |
PCoder | 72741f2188 | |
PCoder | b35a1a9e9b | |
pcoder116 | 0372e3d2cf | |
pcoder116 | b06c4d541f | |
pcoder116 | 7e398cf7b1 | |
PCoder | 6638d376b8 | |
PCoder | 6d8782415f |
|
@ -0,0 +1,2 @@
|
|||
.git
|
||||
.env
|
|
@ -10,7 +10,7 @@ __pycache__/
|
|||
.ropeproject/
|
||||
#django
|
||||
local_settings.py
|
||||
|
||||
Pipfile
|
||||
media/
|
||||
!media/keep
|
||||
/CACHE/
|
||||
|
@ -43,3 +43,4 @@ secret-key
|
|||
# to keep empty dirs
|
||||
!.gitkeep
|
||||
*.orig
|
||||
.vscode/settings.json
|
||||
|
|
151
Changelog
151
Changelog
|
@ -1,3 +1,154 @@
|
|||
3.4: 2022-04-14
|
||||
* 11566: Fix for ungleich.ch product section alignment
|
||||
3.2: 2021-02-07
|
||||
* 8816: Update order confirmation text to better prepared for payment dispute
|
||||
* supportticket#22990: Fix: can't add a deleted card
|
||||
3.1: 2021-01-11
|
||||
* 8781: Fix error is setting a default card (MR!746)
|
||||
3.0: 2021-01-07
|
||||
* 8393: Implement SCA for stripe payments (MR!745)
|
||||
* 8691: Implment check_vm_templates management command (MR!744)
|
||||
2.14: 2020-12-07
|
||||
* 8692: Create a script that fixes django db for the order after celery error (MR!743)
|
||||
2.13: 2020-12-02
|
||||
* 8654: Fix 500 error on invoices list for the user contact+devuanhosting.com@virus.media (MR!742)
|
||||
* 8593: Escape user's ssh key in xml-rpc call to create VM (MR!741)
|
||||
2.12.1: 2020-07-21
|
||||
* 8307: Introduce "Exclude vat calculations" for Generic Products (MR!740)
|
||||
* Change DE VAT rate to 16% from 19% (MR!739)
|
||||
2.12: 2020-06-23
|
||||
* 7894: Show one time payment invoices (MR!738)
|
||||
2.11: 2020-06-11
|
||||
* Bugfix: Correct the wrong constant name (caused payment to go thru and showing error and VMs not instantiated)
|
||||
2.10.8: 2020-06-10
|
||||
* #8102: Refactor MAX_TIME_TO_WAIT_FOR_VM_TERMINATE to increase time to poll whether VM has been terminated or not (MR!737)
|
||||
2.10.7: 2020-05-25
|
||||
* Bugfix: Handle VM templates deleted in OpenNebula but VM instances still existing (MR!736)
|
||||
Notes for deployment:
|
||||
When deploying define a UPDATED_TEMPLATES string represented dictionary value in .env
|
||||
```
|
||||
# Represents Template Ids that were
|
||||
# deleted and the new template Id to look for the template
|
||||
# definition
|
||||
UPDATED_TEMPLATES="{1: 100}"
|
||||
```
|
||||
2.10.6: 2020-03-25
|
||||
* Bugfix: Handle Nonetype for discount's name (MR!735)
|
||||
2.10.5: 2020-03-17
|
||||
* Introduce base price for VMs and let admins add stripe_coupon_id (MR!730)
|
||||
Notes for deployment:
|
||||
1. Add env variable `VM_BASE_PRICE`
|
||||
2. Migrate datacenterlight app. This introduces the stripe_coupon_code field in the VMPricing.
|
||||
3. Create a coupon in stripe with the desired value and note down the stripe's coupon id
|
||||
4. Update the discount amount and set the corresponding coupon id in the admin
|
||||
2.10.3b: 2020-03-05
|
||||
* #7773: Use username for communicating with opennebula all the time
|
||||
2.10.2b: 2020-02-25
|
||||
* #7764: Fix uid represented as bytestring
|
||||
* #7769: [hosting] ssh private key download feature does not work well on Firefox
|
||||
2.10.1: 2020-02-02:
|
||||
* Changes the pricing structure of generic products into the pre vat and with vat (like that for VM)
|
||||
* Shows product name (if exists) in the invoices list if it belongs to a generic product
|
||||
* Small bugfixes (right alignment of price in the invoice list, show prices with 2 decimal places etc)
|
||||
2.10: 2020-02-01
|
||||
* Feature: Introduce new design to show VAT exclusive/VAT inclusive pricing together
|
||||
* Feature: Separate VAT and discount in Stripe
|
||||
* Feature: Show Stripe invoices until we have a better way of showing them elegantly
|
||||
* Bugfix: Fix bug where VAT is set to 0 because user set a valid VAT number before but later chose not to use any VAT number
|
||||
2.9.5: 2020-01-20
|
||||
* Feature: Show invoices directly from stripe (MR!727)
|
||||
2.9.4: 2020-01-10
|
||||
* Bugfix: Buying VPN generic item caused 500 error
|
||||
2.9.3: 2020-01-05
|
||||
* Feature: Add StripeTaxRate model to save tax rates created in Stripe
|
||||
* Bugfix: Add vat rates for CY, IL and LI manually
|
||||
2.9.2: 2020-01-02
|
||||
* Bugfix: Improve admin email for terminate vm (include subscription details and subscription amount) (MR!726)
|
||||
2.9.1: 2019-12-31
|
||||
* Bugfix: Error handling tax_id updated webhook
|
||||
2.9: 2019-12-31
|
||||
* Feature: Enable saving user's VAT Number and validate it (MR!725)
|
||||
Notes for deployment:
|
||||
1. Migrate db for utils app
|
||||
./manage.py migrate utils
|
||||
|
||||
2. Uninstall old version and install a more recent version of stripe
|
||||
|
||||
```
|
||||
source venv/bin/activate
|
||||
./manage.py shell
|
||||
pip uninstall stripe
|
||||
pip install stripe==2.41.0
|
||||
```
|
||||
|
||||
3. Create tax id updated webhook
|
||||
```
|
||||
./manage.py webhook --create \
|
||||
--webhook_endpoint https://datacenterlight.ch/en-us/webhooks/ \
|
||||
--events_csv customer.tax_id.updated
|
||||
```
|
||||
|
||||
4. From the secret obtained in 3, setup an environment variable
|
||||
```
|
||||
WEBHOOK_SECRET='whsec......'
|
||||
```
|
||||
5. Deploy
|
||||
2.8.2: 2019-12-24
|
||||
* Bugfix: [dcl calculator plugin] Set the POST action url explicitly
|
||||
2.8.1: 2019-12-24
|
||||
* [dcl cms navbar plugin]: Provide an option to show non transparent navar always
|
||||
2.8: 2019-12-20
|
||||
* ldap_migration: Migrate django users to Ldap
|
||||
Notes for deployment:
|
||||
```
|
||||
1. Git Pull
|
||||
2. Ensure the newly dependencies in requirements.txt are installed
|
||||
3. Put new values in .env
|
||||
4. Run migrations
|
||||
5. Restart uwsgi
|
||||
```
|
||||
2.7.3: 2019-12-18
|
||||
* Bugfix: Swiss VAT being wrongly added to non-EU customers
|
||||
2.7.2: 2019-12-17
|
||||
* Add vat rates for AD, TK and IS
|
||||
* Improve billing address' string representation
|
||||
Notes for deployment:
|
||||
- Import the newly added vat rates into db
|
||||
```
|
||||
./manage.py import_vat_rates vat_rates.csv
|
||||
```
|
||||
2.7.1: 2019-12-14
|
||||
* feature: Add management command to list active VM customers (MR!723)
|
||||
2.7: 2019-12-9
|
||||
* feature: EU VAT for new subscriptions (MR!721)
|
||||
Notes for deployment:
|
||||
- Add the following to .env file
|
||||
- FIRST_VM_ID_AFTER_EU_VAT=<to VM_ID from which we begin EU VAT>
|
||||
- PRE_EU_VAT_RATE=whatever the rate was before introduction of EU VAT (7.7 for example)
|
||||
2.6.10: 2019-11-16
|
||||
* translation: Add DE translations for features in 2.6.{8,9} by moep (MR!719)
|
||||
2.6.9: 2019-11-15
|
||||
* feature: Allow creating yearly subscriptions for Generic Products (MR!718)
|
||||
Notes for deployment:
|
||||
- do a db migrate for new column added to Generic Product model
|
||||
./manage.py migrate hosting
|
||||
2.6.8: 2019-11-15
|
||||
* feature: [EU VAT] Add EU VAT feature for generic products (MR!717)
|
||||
Notes for deployment:
|
||||
- do a db migrate a to create VATRates table
|
||||
./manage.py migrate hosting
|
||||
- load vat_rates.csv
|
||||
./manage.py import_vat_rates vat_rates.csv
|
||||
2.6.7: 2019-11-04
|
||||
* bugfix: [admin] Improve dumpuser: show proper dates + bugfix
|
||||
* bugfix: [admin] Improve fetch_stripe_bills:
|
||||
- fix wrong assigment of string to num_invoice_created
|
||||
variable,
|
||||
- return None (do not handle the case) if we don't have an
|
||||
order
|
||||
* bugfix: [admin] Improve deleteuser: do not delete order, bill and vm_detail
|
||||
2.6.6: 2019-11-04
|
||||
* feature: [admin] Add dumpuser management command that dumps a user's data in json (MR!716)
|
||||
2.6.5: 2019-09-24
|
||||
* #7169: [hosting] Fix server error while vm terminate takes longer than 30 seconds
|
||||
* #7170: [hosting] Improve admin email body contents for hosting vm terminate error case
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# FROM python:3.10.0-alpine3.15
|
||||
FROM python:3.5-alpine3.12
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --update --no-cache \
|
||||
git \
|
||||
build-base \
|
||||
openldap-dev \
|
||||
python3-dev \
|
||||
postgresql-dev \
|
||||
jpeg-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
libmemcached-dev \
|
||||
zlib-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
## For alpine 3.15 replace postgresql-dev with libpq-dev
|
||||
|
||||
# FIX https://github.com/python-ldap/python-ldap/issues/432
|
||||
RUN echo 'INPUT ( libldap.so )' > /usr/lib/libldap_r.so
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
# Pillow seems to need LIBRARY_PATH set as follows: (see: https://github.com/python-pillow/Pillow/issues/1763#issuecomment-222383534)
|
||||
RUN LIBRARY_PATH=/lib:/usr/lib /bin/sh -c "pip install --no-cache-dir -r requirements.txt"
|
||||
|
||||
COPY ./ .
|
||||
COPY entrypoint.sh /
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh" ]
|
|
@ -10,13 +10,35 @@ Requirements
|
|||
|
||||
Install
|
||||
=======
|
||||
|
||||
.. note::
|
||||
lxml that is one of the dependency of dynamicweb couldn't
|
||||
get build on Python 3.7 so, please use Python 3.5.
|
||||
|
||||
|
||||
First install packages from requirements.archlinux.txt or
|
||||
requirements.debian.txt based on your distribution.
|
||||
|
||||
|
||||
The quick way:
|
||||
``pip install -r requirements.txt``
|
||||
|
||||
Next find the dump.db file on stagging server. Path for the file is under the base application folder.
|
||||
|
||||
or you can create one for yourself by running the following commands on dynamicweb server
|
||||
|
||||
.. code:: sh
|
||||
|
||||
sudo su - postgres
|
||||
pg_dump app > /tmp/postgres_db.bak
|
||||
exit
|
||||
cp /tmp/postgres_db.bak /root/postgres_db.bak
|
||||
|
||||
Now, you can download this using sftp.
|
||||
|
||||
|
||||
Install the postgresql server and import the database::
|
||||
``psql -d app < dump.db``
|
||||
``psql -d app -U root < dump.db``
|
||||
|
||||
**No migration is needed after a clean install, and You are ready to start developing.**
|
||||
|
||||
|
@ -25,9 +47,9 @@ Development
|
|||
Project is separated in master branch and development branch, and feature branches.
|
||||
Master branch is currently used on `Digital Glarus <https://digitalglarus.ungleich.ch/en-us/digitalglarus/>`_ and `Ungleich blog <https://digitalglarus.ungleich.ch/en-us/blog/>`_.
|
||||
|
||||
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
|
||||
If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch.
|
||||
|
||||
After You have complited the task create a pull request and ask someone to review the code from other developers.
|
||||
After You have completed the task, create a pull request and ask someone to review the code from other developers.
|
||||
|
||||
**Cheat sheet for branching and forking**:
|
||||
|
||||
|
|
6
Makefile
6
Makefile
|
@ -14,6 +14,12 @@ help:
|
|||
@echo ' make rsync_upload '
|
||||
@echo ' make install_debian_packages '
|
||||
|
||||
buildimage:
|
||||
docker build -t dynamicweb:$$(git describe) .
|
||||
|
||||
releaseimage: buildimage
|
||||
./release.sh
|
||||
|
||||
collectstatic:
|
||||
$(PY?) $(BASEDIR)/manage.py collectstatic
|
||||
|
||||
|
|
|
@ -31,9 +31,10 @@ class ContactView(FormView):
|
|||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
form.send_email(email_to='info@alplora.ch')
|
||||
messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
print("alplora contactusform")
|
||||
#form.save()
|
||||
#form.send_email(email_to='info@alplora.ch')
|
||||
#messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
return render(self.request, 'alplora/contact_success.html', {})
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "$0 imageversion [push]"
|
||||
echo "Version could be: $(git describe --always)"
|
||||
echo "If push is specified, also push to our harbor"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tagprefix=harbor.k8s.ungleich.ch/ungleich-public/dynamicweb
|
||||
version=$1; shift
|
||||
|
||||
tag=${tagprefix}:${version}
|
||||
|
||||
set -ex
|
||||
|
||||
docker build -t "${tag}" .
|
||||
|
||||
push=$1; shift
|
||||
|
||||
if [ "$push" ]; then
|
||||
docker push "${tag}"
|
||||
fi
|
|
@ -184,6 +184,11 @@ class DCLNavbarPluginModel(CMSPlugin):
|
|||
default=True,
|
||||
help_text='Uncheck this if you do not want to show login/dashboard.'
|
||||
)
|
||||
show_non_transparent_navbar_always = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Check this if you want to show non transparent navbar only.'
|
||||
'(Useful when we want to setup a simple page)'
|
||||
)
|
||||
|
||||
def get_logo_dark(self):
|
||||
# used only if atleast one logo exists
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from cms.plugin_base import CMSPluginBase
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from django.conf import settings
|
||||
|
||||
from .cms_models import (
|
||||
DCLBannerItemPluginModel, DCLBannerListPluginModel, DCLContactPluginModel,
|
||||
|
@ -100,6 +101,7 @@ class DCLCalculatorPlugin(CMSPluginBase):
|
|||
vm_type=instance.vm_type
|
||||
).order_by('name')
|
||||
context['instance'] = instance
|
||||
context['vm_base_price'] = settings.VM_BASE_PRICE
|
||||
context['min_ram'] = 0.5 if instance.enable_512mb_ram else 1
|
||||
return context
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-07-03 11:18+0000\n"
|
||||
"POT-Creation-Date: 2021-02-07 11:10+0000\n"
|
||||
"PO-Revision-Date: 2018-03-30 23:22+0000\n"
|
||||
"Last-Translator: b'Anonymous User <coder.purple+25@gmail.com>'\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -20,7 +20,7 @@ msgstr ""
|
|||
"X-Translated-Using: django-rosetta 0.8.1\n"
|
||||
|
||||
msgid "CMS Favicon"
|
||||
msgstr ""
|
||||
msgstr "CMS Favicon"
|
||||
|
||||
#, python-format
|
||||
msgid "Your New VM %(vm_name)s at Data Center Light"
|
||||
|
@ -52,7 +52,7 @@ msgid "Login"
|
|||
msgstr "Anmelden"
|
||||
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
msgstr "Dashboard"
|
||||
|
||||
msgid "Thank you for contacting us."
|
||||
msgstr "Nachricht gesendet."
|
||||
|
@ -64,7 +64,7 @@ msgid "Get in touch with us!"
|
|||
msgstr "Sende uns eine Nachricht."
|
||||
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
msgstr "Name"
|
||||
|
||||
msgid "Please enter your name."
|
||||
msgstr "Bitte gib Deinen Namen ein."
|
||||
|
@ -108,7 +108,7 @@ msgid "Your account details are as follows"
|
|||
msgstr "Deine Account Details sind unten aufgelistet"
|
||||
|
||||
msgid "Username"
|
||||
msgstr "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
msgid "Your email address"
|
||||
msgstr "Deine E-Mail-Adresse"
|
||||
|
@ -144,8 +144,8 @@ msgid ""
|
|||
"the heart of Switzerland."
|
||||
msgstr "Bei uns findest Du die günstiges VMs aus der Schweiz."
|
||||
|
||||
msgid "Try now, order a VM. VM price starts from only 15CHF per month."
|
||||
msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
|
||||
msgid "Try now, order a VM. VM price starts from only 11.5 CHF per month."
|
||||
msgstr "Unser Angebot beginnt bei 11.5 CHF pro Monat. Probier's jetzt aus!"
|
||||
|
||||
msgid "ORDER VM"
|
||||
msgstr "VM BESTELLEN"
|
||||
|
@ -155,7 +155,7 @@ msgid "Please enter a value in range %(min_ram)s - 200."
|
|||
msgstr "Bitte gib einen Wert von %(min_ram)s bis 200 ein."
|
||||
|
||||
msgid "VM hosting"
|
||||
msgstr ""
|
||||
msgstr "VM Hosting"
|
||||
|
||||
msgid "month"
|
||||
msgstr "Monat"
|
||||
|
@ -207,24 +207,24 @@ msgstr ""
|
|||
|
||||
msgid "Only wants you to pay for what you actually need."
|
||||
msgstr ""
|
||||
"Möchte, dass du nur bezahlst, was du auch wirklich brauchst: Wähle deine "
|
||||
"Du möchtest nur das bezahlen, was du auch wirklich brauchst: Wähle deine "
|
||||
"Ressourcen individuell aus!
"
|
||||
|
||||
msgid ""
|
||||
"Is creative, using a modern and alternative design for a data center in "
|
||||
"order to make it more sustainable and affordable at the same time."
|
||||
msgstr ""
|
||||
"Ist kreativ, indem es sich ein modernes und alternatives Layout zu Nutze "
|
||||
"macht um Nachhaltigkeit zu fördern und somit erschwingliche Preise bieten zu "
|
||||
"können.
"
|
||||
"Es ist kreativ, da es sich ein modernes und alternatives Layout zu "
|
||||
"Nutzemacht um Nachhaltigkeit zu fördern und somit erschwingliche Preise "
|
||||
"bieten zu können.
"
|
||||
|
||||
msgid ""
|
||||
"Cuts down the costs for you by using FOSS (Free Open Source Software) "
|
||||
"exclusively, wherefore we can save money from paying licenses."
|
||||
msgstr ""
|
||||
"Sorgt dafür, dass unnötige Kosten erspart werden, indem es ausschliesslich "
|
||||
"mit FOSS (Free Open Source Software) arbeitet und wir daher auf "
|
||||
"Lizenzgebühren verzichten können.
"
|
||||
"Um unnötige Kosten zu sparen werden, wird ausschliesslich Software aufBasis "
|
||||
"von FOSS (Free Open Source Software) eingesetzt und dadurch können auf "
|
||||
"Lizenzgebühren verzichtet werden.
"
|
||||
|
||||
msgid "Scale out"
|
||||
msgstr "Skalierung"
|
||||
|
@ -311,7 +311,7 @@ msgid "Billing Address"
|
|||
msgstr "Rechnungsadresse"
|
||||
|
||||
msgid "Make a payment"
|
||||
msgstr ""
|
||||
msgstr "Tätige eine Bezahlung"
|
||||
|
||||
msgid "Your Order"
|
||||
msgstr "Deine Bestellung"
|
||||
|
@ -375,6 +375,9 @@ msgstr "Letzten"
|
|||
msgid "Type"
|
||||
msgstr "Typ"
|
||||
|
||||
msgid "Expiry"
|
||||
msgstr "Ablaufdatum"
|
||||
|
||||
msgid "SELECT"
|
||||
msgstr "AUSWÄHLEN"
|
||||
|
||||
|
@ -403,6 +406,19 @@ msgstr "Datum"
|
|||
msgid "Billed to"
|
||||
msgstr "Rechnungsadresse"
|
||||
|
||||
msgid "VAT Number"
|
||||
msgstr "MwSt-Nummer"
|
||||
|
||||
msgid "Your VAT number has been verified"
|
||||
msgstr "Deine MwSt-Nummer wurde überprüft"
|
||||
|
||||
msgid ""
|
||||
"Your VAT number is under validation. VAT will be adjusted, once the "
|
||||
"validation is complete."
|
||||
msgstr ""
|
||||
"Deine MwSt-Nummer wird derzeit validiert. Die MwSt. wird angepasst, sobald "
|
||||
"die Validierung abgeschlossen ist."
|
||||
|
||||
msgid "Payment method"
|
||||
msgstr "Bezahlmethode"
|
||||
|
||||
|
@ -415,50 +431,63 @@ msgstr "Bestellungsübersicht"
|
|||
msgid "Product"
|
||||
msgstr "Produkt"
|
||||
|
||||
msgid "Amount"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
msgstr "Beschreibung"
|
||||
|
||||
msgid "Recurring"
|
||||
msgstr ""
|
||||
msgstr "Wiederholend"
|
||||
|
||||
msgid "Subtotal"
|
||||
msgstr "Zwischensumme"
|
||||
msgid "Price Before VAT"
|
||||
msgstr "Preis ohne MwSt."
|
||||
|
||||
msgid "VAT"
|
||||
msgstr "Mehrwertsteuer"
|
||||
msgid "Pre VAT"
|
||||
msgstr "Exkl. MwSt."
|
||||
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "By clicking \"Place order\" this plan will charge your credit card "
|
||||
#| "account with %(total_price)s CHF/month"
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with %(total_price)s CHF/month"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
msgid "VAT for"
|
||||
msgstr "MwSt für"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "By clicking \"Place order\" this payment will charge your credit card "
|
||||
#| "account with a one time amount of %(total_price)s CHF"
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this payment will charge your credit card "
|
||||
"account with a one time amount of %(total_price)s CHF"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
msgid "Your Price in Total"
|
||||
msgstr "Dein Gesamtpreis"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with %(vm_total_price)s CHF/month"
|
||||
" By clicking \"Place order\" you agree to our <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
|
||||
"this plan will charge your credit card account with %(total_price)s CHF/year"
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
"%(vm_total_price)s CHF pro Monat belastet"
|
||||
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Jahr belastet."
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" By clicking \"Place order\" you agree to "
|
||||
"our <a href=\"https://datacenterlight.ch/en-us/cms/terms-of-service/\">Terms "
|
||||
"of Service</a> and this plan will charge your credit card account with "
|
||||
"%(total_price)s CHF/month"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF/Monat belastet."
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" you agree to our <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
|
||||
"this plan will charge your credit card account with %(total_price)s CHF"
|
||||
msgstr ""
|
||||
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
|
||||
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(total_price)s CHF belastet."
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" you agree to our <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
|
||||
"this plan will charge your credit card account with %(vm_total_price)s CHF/"
|
||||
"month"
|
||||
msgstr ""
|
||||
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren <a href=\"https://"
|
||||
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_total_price)s CHF/Monat belastet"
|
||||
|
||||
msgid "Place order"
|
||||
msgstr "Bestellen"
|
||||
|
@ -470,10 +499,10 @@ msgid "Hold tight, we are processing your request"
|
|||
msgstr "Bitte warten - wir verarbeiten Deine Anfrage gerade"
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
msgstr "Ok"
|
||||
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
msgstr "Schliessen"
|
||||
|
||||
msgid "Some problem encountered. Please try again later."
|
||||
msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal."
|
||||
|
@ -485,7 +514,7 @@ msgid "Tech Stack"
|
|||
msgstr "Tech Stack"
|
||||
|
||||
msgid "We are seriously open source."
|
||||
msgstr "Wir sind vollends opensource."
|
||||
msgstr "Wir sind vollends Open Source."
|
||||
|
||||
msgid ""
|
||||
" Our full software stack is open source – We don't use anything that isn't "
|
||||
|
@ -555,13 +584,16 @@ msgid "Starting from only 15CHF per month. Try now."
|
|||
msgstr "Unser Angebot beginnt bei 15 CHF pro Monat. Probier's jetzt aus!"
|
||||
|
||||
msgid "Actions speak louder than words. Let's do it, try our VM now."
|
||||
msgstr "Tagen sagen mehr als Worte – Teste jetzt unsere VM!"
|
||||
msgstr "Taten sagen mehr als Worte – Teste jetzt unsere VM!"
|
||||
|
||||
msgid "See Invoice"
|
||||
msgstr "Siehe Rechnung"
|
||||
|
||||
msgid "Invalid number of cores"
|
||||
msgstr "Ungültige Anzahle CPU-Kerne"
|
||||
|
||||
msgid "Invalid calculator properties"
|
||||
msgstr ""
|
||||
msgstr "Ungültige Berechnungseigenschaften"
|
||||
|
||||
msgid "Invalid RAM size"
|
||||
msgstr "Ungültige RAM-Grösse"
|
||||
|
@ -572,17 +604,24 @@ msgstr "Ungültige Speicher-Grösse"
|
|||
#, python-brace-format
|
||||
msgid "Incorrect pricing name. Please contact support{support_email}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "{user} does not have permission to access the card"
|
||||
msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
|
||||
|
||||
msgid "An error occurred. Details: {}"
|
||||
msgstr "Ein Fehler ist aufgetreten. Details: {}"
|
||||
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
|
||||
|
||||
msgid "Confirm Order"
|
||||
msgstr "Bestellung Bestätigen"
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "Thank you!"
|
||||
msgid "Thank you !"
|
||||
msgstr "Vielen Dank!"
|
||||
|
||||
msgid "Your product will be provisioned as soon as we receive the payment."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "An error occurred while associating the card. Details: {details}"
|
||||
msgstr ""
|
||||
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
|
||||
|
||||
msgid "Error."
|
||||
msgstr ""
|
||||
|
||||
|
@ -593,16 +632,30 @@ msgstr ""
|
|||
"Es ist ein Fehler bei der Zahlung betreten. Du wirst nach dem Schliessen vom "
|
||||
"Popup zur Bezahlseite weitergeleitet."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "An error occurred while associating the card. Details: {details}"
|
||||
msgstr ""
|
||||
"Beim Verbinden der Karte ist ein Fehler aufgetreten. Details: {details}"
|
||||
msgid "Thank you for the order."
|
||||
msgstr "Danke für Deine Bestellung."
|
||||
|
||||
msgid "Confirmation of your payment"
|
||||
msgid ""
|
||||
"Your product will be provisioned as soon as we receive a payment "
|
||||
"confirmation from Stripe. We will send you a confirmation email. You can "
|
||||
"always contact us at support@datacenterlight.ch"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Your VM will be up and running in a few moments. We will send you a "
|
||||
"confirmation email as soon as it is ready."
|
||||
msgstr ""
|
||||
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
|
||||
"auf sie zugreifen kannst."
|
||||
|
||||
msgid " This is a monthly recurring plan."
|
||||
msgstr ""
|
||||
msgstr "Dies ist ein monatlich wiederkehrender Plan."
|
||||
|
||||
msgid " This is an yearly recurring plan."
|
||||
msgstr "Dies ist ein jährlich wiederkehrender Plan."
|
||||
|
||||
msgid "Confirmation of your payment"
|
||||
msgstr "Bestätigung deiner Zahlung"
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
|
@ -614,6 +667,14 @@ msgid ""
|
|||
"Cheers,\n"
|
||||
"Your Data Center Light team"
|
||||
msgstr ""
|
||||
"Hallo {name},\n"
|
||||
"\n"
|
||||
"vielen Dank für deine Bestellung!\n"
|
||||
"Wir haben deine Bezahlung in Höhe von {amount:.2f} CHF erhalten. "
|
||||
"{recurring}\n"
|
||||
"\n"
|
||||
"Grüsse\n"
|
||||
"Dein Data Center Light Team"
|
||||
|
||||
msgid "Thank you for the payment."
|
||||
msgstr "Danke für Deine Bestellung."
|
||||
|
@ -622,16 +683,49 @@ msgid ""
|
|||
"You will soon receive a confirmation email of the payment. You can always "
|
||||
"contact us at info@ungleich.ch for any question that you may have."
|
||||
msgstr ""
|
||||
"Du wirst bald eine Bestätigungs-E-Mail über die Zahlung erhalten. Du kannst "
|
||||
"jederzeit unter info@ungleich.ch kontaktieren."
|
||||
|
||||
msgid "Thank you for the order."
|
||||
msgstr "Danke für Deine Bestellung."
|
||||
#, python-format
|
||||
#~ msgid ""
|
||||
#~ "By clicking \"Place order\" this plan will charge your credit card "
|
||||
#~ "account with %(total_price)s CHF/month"
|
||||
#~ msgstr ""
|
||||
#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
#~ "%(total_price)s CHF pro Monat belastet"
|
||||
|
||||
msgid ""
|
||||
"Your VM will be up and running in a few moments. We will send you a "
|
||||
"confirmation email as soon as it is ready."
|
||||
msgstr ""
|
||||
"Deine VM ist gleich bereit. Wir senden Dir eine Bestätigungsemail, sobald Du "
|
||||
"auf sie zugreifen kannst."
|
||||
#, fuzzy, python-format
|
||||
#~| msgid ""
|
||||
#~| "By clicking \"Place order\" this payment will charge your credit card "
|
||||
#~| "account with a one time amount of %(total_price)s CHF"
|
||||
#~ msgid ""
|
||||
#~ "By clicking \"Place order\" this payment will charge your credit card "
|
||||
#~ "account with a one time amount of %(total_price)s CHF"
|
||||
#~ msgstr ""
|
||||
#~ "Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit "
|
||||
#~ "%(vm_total_price)s CHF pro Monat belastet"
|
||||
|
||||
#, python-brace-format
|
||||
#~ msgid "{user} does not have permission to access the card"
|
||||
#~ msgstr "{user} hat keine Erlaubnis auf diese Karte zuzugreifen"
|
||||
|
||||
#~ msgid "An error occurred. Details: {}"
|
||||
#~ msgstr "Ein Fehler ist aufgetreten. Details: {}"
|
||||
|
||||
#~ msgid "Price"
|
||||
#~ msgstr "Preise"
|
||||
|
||||
#~ msgid "Total Amount"
|
||||
#~ msgstr "Gesamtsumme"
|
||||
|
||||
#~ msgid "Amount"
|
||||
#~ msgstr "Betrag"
|
||||
|
||||
#~ msgid "Subtotal"
|
||||
#~ msgstr "Zwischensumme"
|
||||
|
||||
#~ msgid "VAT"
|
||||
#~ msgstr "Mehrwertsteuer"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "You are not making any payment yet. After submitting your card "
|
||||
|
@ -641,12 +735,6 @@ msgstr ""
|
|||
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
|
||||
#~ "hast."
|
||||
|
||||
#~ msgid "Card Number"
|
||||
#~ msgstr "Kreditkartennummer"
|
||||
|
||||
#~ msgid "Expiry Date"
|
||||
#~ msgstr "Ablaufdatum"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "You are not making any payment yet. After placing your order, you will be "
|
||||
#~ "taken to the Submit Payment Page."
|
||||
|
@ -655,9 +743,6 @@ msgstr ""
|
|||
#~ "ausgelöst, nachdem Du die Bestellung auf der nächsten Seite bestätigt "
|
||||
#~ "hast."
|
||||
|
||||
#~ msgid "Pricing"
|
||||
#~ msgstr "Preise"
|
||||
|
||||
#~ msgid "Order VM"
|
||||
#~ msgstr "VM bestellen"
|
||||
|
||||
|
@ -701,9 +786,6 @@ msgstr ""
|
|||
#~ "Wir werden dann sobald als möglich Ihren Beta-Zugang erstellen und Sie "
|
||||
#~ "daraufhin kontaktieren.Bis dahin bitten wir Sie um etwas Geduld."
|
||||
|
||||
#~ msgid "Thank you!"
|
||||
#~ msgstr "Vielen Dank!"
|
||||
|
||||
#~ msgid "Thank you for order! Our team will contact you via email"
|
||||
#~ msgstr ""
|
||||
#~ "Vielen Dank für die Bestellung. Unser Team setzt sich sobald wie möglich "
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from hosting.models import (
|
||||
HostingOrder, VMDetail
|
||||
)
|
||||
from membership.models import CustomUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Dumps the email addresses of all customers who have a VM'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('-a', '--all_registered', action='store_true',
|
||||
help='All registered users')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
all_registered = options['all_registered']
|
||||
all_customers_set = set()
|
||||
if all_registered:
|
||||
all_customers = CustomUser.objects.filter(
|
||||
is_admin=False, validated=True
|
||||
)
|
||||
for customer in all_customers:
|
||||
all_customers_set.add(customer.email)
|
||||
else:
|
||||
all_hosting_orders = HostingOrder.objects.filter()
|
||||
running_vm_details = VMDetail.objects.filter(terminated_at=None)
|
||||
running_vm_ids = [rvm.vm_id for rvm in running_vm_details]
|
||||
for order in all_hosting_orders:
|
||||
if order.vm_id in running_vm_ids:
|
||||
all_customers_set.add(order.customer.user.email)
|
||||
for cu in all_customers_set:
|
||||
print(cu)
|
||||
if all_registered:
|
||||
print("All registered users = %s" % len(all_customers_set))
|
||||
else:
|
||||
print("Total active customers = %s" % len(all_customers_set))
|
|
@ -0,0 +1,65 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from datacenterlight.models import VMTemplate
|
||||
from membership.models import CustomUser
|
||||
|
||||
from django.conf import settings
|
||||
from time import sleep
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Checks all VM templates to find if they can be instantiated'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('user_email', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
result_dict = {}
|
||||
user_email = options['user_email'] if 'user_email' in options else ""
|
||||
|
||||
if user_email:
|
||||
cu = CustomUser.objects.get(email=user_email)
|
||||
specs = {'cpu': 1, 'memory': 1, 'disk_size': 10}
|
||||
manager = OpenNebulaManager(email=user_email, password=cu.password)
|
||||
pub_keys = [settings.TEST_MANAGE_SSH_KEY_PUBKEY]
|
||||
PROJECT_PATH = os.path.abspath(os.path.dirname(__name__))
|
||||
if not os.path.exists("%s/outputs" % PROJECT_PATH):
|
||||
os.mkdir("%s/outputs" % PROJECT_PATH)
|
||||
for vm_template in VMTemplate.objects.all():
|
||||
vm_name = 'test-%s' % vm_template.name
|
||||
vm_id = manager.create_vm(
|
||||
template_id=vm_template.opennebula_vm_template_id,
|
||||
specs=specs,
|
||||
ssh_key='\n'.join(pub_keys),
|
||||
vm_name=vm_name
|
||||
)
|
||||
if vm_id and vm_id > 0:
|
||||
result_dict[vm_name] = "%s OK, created VM %s" % (
|
||||
'%s %s %s' % (vm_template.opennebula_vm_template_id,
|
||||
vm_template.name, vm_template.vm_type),
|
||||
vm_id
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(result_dict[vm_name]))
|
||||
manager.delete_vm(vm_id)
|
||||
else:
|
||||
result_dict[vm_name] = '''Error creating VM %s, template_id
|
||||
%s %s''' % (vm_name,
|
||||
vm_template.opennebula_vm_template_id,
|
||||
vm_template.vm_type)
|
||||
self.stdout.write(self.style.ERROR(result_dict[vm_name]))
|
||||
sleep(1)
|
||||
date_str = datetime.datetime.strftime(
|
||||
datetime.datetime.now(), '%Y%m%d%H%M%S'
|
||||
)
|
||||
with open("%s/outputs/check_vm_templates_%s.txt" %
|
||||
(PROJECT_PATH, date_str),
|
||||
'w',
|
||||
encoding='utf-8') as f:
|
||||
f.write(json.dumps(result_dict))
|
||||
self.stdout.write(self.style.SUCCESS("Done"))
|
|
@ -1,14 +1,17 @@
|
|||
import logging
|
||||
import oca
|
||||
import sys
|
||||
import stripe
|
||||
import uuid
|
||||
|
||||
import oca
|
||||
import stripe
|
||||
from django.core.management.base import BaseCommand
|
||||
from membership.models import CustomUser, DeletedUser
|
||||
|
||||
from hosting.models import (
|
||||
HostingOrder, HostingBill, VMDetail, UserCardDetail, UserHostingKey
|
||||
UserCardDetail, UserHostingKey
|
||||
)
|
||||
from membership.models import CustomUser, DeletedUser
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -79,86 +82,6 @@ class Command(BaseCommand):
|
|||
else:
|
||||
logger.error("Error while deleting the StripeCustomer")
|
||||
|
||||
hosting_orders = HostingOrder.objects.filter(
|
||||
customer=stripe_customer.id
|
||||
)
|
||||
|
||||
vm_ids = []
|
||||
for order in hosting_orders:
|
||||
vm_ids.append(order.vm_id)
|
||||
|
||||
# Delete Billing Address
|
||||
if order.billing_address is not None:
|
||||
logger.debug(
|
||||
"Billing Address {} associated with {} deleted"
|
||||
"".format(order.billing_address.id, email)
|
||||
)
|
||||
order.billing_address.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the billing_address")
|
||||
|
||||
# Delete Order Detail
|
||||
if order.order_detail is not None:
|
||||
logger.debug(
|
||||
"Order Detail {} associated with {} deleted"
|
||||
"".format(order.order_detail.id, email)
|
||||
)
|
||||
order.order_detail.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the order_detail. None")
|
||||
|
||||
# Delete order
|
||||
if order is not None:
|
||||
logger.debug(
|
||||
"Order {} associated with {} deleted"
|
||||
"".format(order.id, email)
|
||||
)
|
||||
order.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the Order")
|
||||
|
||||
hosting_bills = HostingBill.objects.filter(
|
||||
customer=stripe_customer.id
|
||||
)
|
||||
|
||||
# delete hosting bills
|
||||
for bill in hosting_bills:
|
||||
if bill.billing_address is not None:
|
||||
logger.debug(
|
||||
"HostingBills billing address {} associated with {} deleted"
|
||||
"".format(bill.billing_address.id, email)
|
||||
)
|
||||
bill.billing_address.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the HostingBill's Billing address")
|
||||
|
||||
if bill is not None:
|
||||
logger.debug(
|
||||
"HostingBill {} associated with {} deleted"
|
||||
"".format(bill.id, email)
|
||||
)
|
||||
bill.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the HostingBill")
|
||||
|
||||
# delete VMDetail
|
||||
for vm_id in vm_ids:
|
||||
vm_detail = VMDetail.objects.get(vm_id=vm_id)
|
||||
if vm_detail is not None:
|
||||
logger.debug(
|
||||
"vm_detail {} associated with {} deleted"
|
||||
"".format(vm_detail.id, email)
|
||||
)
|
||||
vm_detail.delete()
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the vm_detail")
|
||||
|
||||
# delete UserCardDetail
|
||||
ucds = UserCardDetail.objects.filter(
|
||||
stripe_customer=stripe_customer
|
||||
|
@ -190,8 +113,10 @@ class Command(BaseCommand):
|
|||
user_id = cus_user.id
|
||||
)
|
||||
|
||||
# delete CustomUser
|
||||
cus_user.delete()
|
||||
# reset CustomUser
|
||||
cus_user.email = str(uuid.uuid4())
|
||||
cus_user.validated = 0
|
||||
cus_user.save()
|
||||
|
||||
# remove user from OpenNebula
|
||||
manager = OpenNebulaManager()
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from membership.models import CustomUser
|
||||
from hosting.models import (
|
||||
HostingOrder, VMDetail, UserCardDetail, UserHostingKey
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Dumps the data of a customer into a json file'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('customer_email', nargs='+', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
for email in options['customer_email']:
|
||||
try:
|
||||
cus_user = CustomUser.objects.get(email=email)
|
||||
except CustomUser.DoesNotExist as dne:
|
||||
logger.error("CustomUser with email {} does "
|
||||
"not exist".format(email))
|
||||
sys.exit(1)
|
||||
|
||||
hosting_orders = HostingOrder.objects.filter(
|
||||
customer=cus_user.stripecustomer.id
|
||||
)
|
||||
|
||||
vm_ids = []
|
||||
orders_dict = {}
|
||||
for order in hosting_orders:
|
||||
order_dict = {}
|
||||
vm_ids.append(order.vm_id)
|
||||
order_dict["VM_ID"] = order.vm_id
|
||||
order_dict["Order Nr."] = order.id
|
||||
order_dict["Created On"] = str(order.created_at)
|
||||
order_dict["Price"] = order.price
|
||||
order_dict["Payment card details"] = {
|
||||
"last4": order.last4,
|
||||
"brand": order.cc_brand
|
||||
}
|
||||
if order.subscription_id is not None and order.stripe_charge_id is None:
|
||||
order_dict["Order type"] = "Monthly susbcription"
|
||||
else:
|
||||
order_dict["Order type"] = "One time payment"
|
||||
|
||||
# billing address
|
||||
if order.billing_address is not None:
|
||||
order_dict["Billing Address"] = {
|
||||
"Street": order.billing_address.street_address,
|
||||
"City": order.billing_address.city,
|
||||
"Country": order.billing_address.country,
|
||||
"Postal code": order.billing_address.postal_code,
|
||||
"Card holder name": order.billing_address.cardholder_name
|
||||
}
|
||||
else:
|
||||
logger.error(
|
||||
"did not find billing_address")
|
||||
|
||||
# Order Detail
|
||||
if order.order_detail is not None:
|
||||
order_dict["Specifications"] = {
|
||||
"RAM": "{} GB".format(order.order_detail.memory),
|
||||
"Cores": order.order_detail.cores,
|
||||
"Disk space (SSD)": "{} GB".format(
|
||||
order.order_detail.ssd_size)
|
||||
}
|
||||
else:
|
||||
logger.error(
|
||||
"Did not find order_detail. None")
|
||||
|
||||
vm_detail = VMDetail.objects.get(vm_id=order.vm_id)
|
||||
if vm_detail is not None:
|
||||
order_dict["VM Details"] = {
|
||||
"VM_ID": order.vm_id,
|
||||
"IPv4": vm_detail.ipv4,
|
||||
"IPv6": vm_detail.ipv6,
|
||||
"OS": vm_detail.configuration,
|
||||
}
|
||||
order_dict["Terminated on"] = str(vm_detail.terminated_at)
|
||||
|
||||
orders_dict[order.vm_id] = order_dict
|
||||
|
||||
|
||||
# UserCardDetail
|
||||
cards = {}
|
||||
ucds = UserCardDetail.objects.filter(
|
||||
stripe_customer=cus_user.stripecustomer
|
||||
)
|
||||
for ucd in ucds:
|
||||
card = {}
|
||||
if ucd is not None:
|
||||
card["Last 4"] = ucd.last4
|
||||
card["Brand"] = ucd.brand
|
||||
card["Expiry month"] = ucd.exp_month
|
||||
card["Expiry year"] = ucd.exp_year
|
||||
card["Preferred"] = ucd.preferred
|
||||
cards[ucd.id] = card
|
||||
else:
|
||||
logger.error(
|
||||
"Error while deleting the User Card Detail")
|
||||
|
||||
# UserHostingKey
|
||||
keys = {}
|
||||
uhks = UserHostingKey.objects.filter(
|
||||
user=cus_user
|
||||
)
|
||||
for uhk in uhks:
|
||||
key = {
|
||||
"Public key": uhk.public_key,
|
||||
"Name": uhk.name,
|
||||
"Created on": str(uhk.created_at)
|
||||
}
|
||||
if uhk.private_key:
|
||||
key["Private key"] = uhk.private_key
|
||||
keys[uhk.name] = key
|
||||
output_dict = {
|
||||
"User details": {
|
||||
"Name": cus_user.name,
|
||||
"Email": cus_user.email,
|
||||
"Activated": "yes" if cus_user.validated == 1 else "no",
|
||||
"Last login": str(cus_user.last_login)
|
||||
},
|
||||
"Orders": orders_dict,
|
||||
"Payment cards": cards,
|
||||
"SSH Keys": keys
|
||||
}
|
||||
print(json.dumps(output_dict, indent=4))
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
|
@ -0,0 +1,54 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from datacenterlight.tasks import handle_metadata_and_emails
|
||||
from datacenterlight.models import StripePlan
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from membership.models import CustomUser
|
||||
from hosting.models import GenericProduct
|
||||
import logging
|
||||
import json
|
||||
import sys
|
||||
import stripe
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Stripe plans created before version 3.4 saved the plan name like generic-{subscription_id}-amount. This
|
||||
command aims at replacing this with the actual product name
|
||||
'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cnt = 0
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'In Fix generic stripe plan product names'
|
||||
)
|
||||
)
|
||||
plans_to_change = StripePlan.objects.filter(stripe_plan_id__startswith='generic')
|
||||
for plan in plans_to_change:
|
||||
response = input("Press 'y' to continue: ")
|
||||
|
||||
# Check if the user entered 'y'
|
||||
if response.lower() == 'y':
|
||||
plan_name = plan.stripe_plan_id
|
||||
first_index_hyphen = plan_name.index("-") + 1
|
||||
product_id = plan_name[
|
||||
first_index_hyphen:(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen]
|
||||
gp = GenericProduct.objects.get(id=product_id)
|
||||
if gp:
|
||||
cnt += 1
|
||||
# update stripe
|
||||
sp = stripe.Plan.retrieve(plan_name)
|
||||
pr = stripe.Product.retrieve(sp.product)
|
||||
pr.name = gp.product_name
|
||||
pr.save()
|
||||
# update local
|
||||
spl = StripePlan.objects.get(stripe_plan_id=plan_name)
|
||||
spl.stripe_plan_name = gp.product_name
|
||||
spl.save()
|
||||
print("%s. %s => %s" % (cnt, plan_name, gp.product_name))
|
||||
else:
|
||||
print("Invalid input. Please try again.")
|
||||
sys.exit()
|
||||
|
||||
print("Done")
|
|
@ -0,0 +1,76 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from datacenterlight.tasks import handle_metadata_and_emails
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from membership.models import CustomUser
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Updates the DB after manual creation of VM'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('vm_id', type=int)
|
||||
parser.add_argument('order_id', type=int)
|
||||
parser.add_argument('user', type=str)
|
||||
parser.add_argument('specs', type=str)
|
||||
parser.add_argument('template', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
vm_id = options['vm_id']
|
||||
order_id = options['order_id']
|
||||
user_str = options['user']
|
||||
specs_str = options['specs']
|
||||
template_str = options['template']
|
||||
|
||||
json_acceptable_string = user_str.replace("'", "\"")
|
||||
user_dict = json.loads(json_acceptable_string)
|
||||
|
||||
json_acceptable_string = specs_str.replace("'", "\"")
|
||||
specs = json.loads(json_acceptable_string)
|
||||
|
||||
json_acceptable_string = template_str.replace("'", "\"")
|
||||
template = json.loads(json_acceptable_string)
|
||||
if vm_id <= 0:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
'vm_id can\'t be less than or 0. Given: %s' % vm_id))
|
||||
return
|
||||
if vm_id <= 0:
|
||||
self.stdout.write(self.style.ERROR(
|
||||
'order_id can\'t be less than or 0. Given: %s' % vm_id))
|
||||
return
|
||||
if specs_str is None or specs_str == "":
|
||||
self.stdout.write(
|
||||
self.style.ERROR('specs can\'t be empty or None'))
|
||||
return
|
||||
|
||||
user = {
|
||||
'name': user_dict['name'],
|
||||
'email': user_dict['email'],
|
||||
'username': user_dict['username'],
|
||||
'pass': user_dict['pass'],
|
||||
'request_scheme': user_dict['request_scheme'],
|
||||
'request_host': user_dict['request_host'],
|
||||
'language': user_dict['language'],
|
||||
}
|
||||
cu = CustomUser.objects.get(username=user.get('username'))
|
||||
# Create OpenNebulaManager
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'Connecting using %s' % (cu.username)
|
||||
)
|
||||
)
|
||||
manager = OpenNebulaManager(email=cu.username, password=cu.password)
|
||||
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
|
||||
template)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'Done handling metadata and emails for %s %s %s' % (
|
||||
order_id,
|
||||
vm_id,
|
||||
str(user)
|
||||
)
|
||||
)
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-12-24 03:34
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0029_auto_20190420_1022'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='dclnavbarpluginmodel',
|
||||
name='show_non_transparent_navbar_always',
|
||||
field=models.BooleanField(default=False, help_text='Check this if you want to show non transparent navbar only.(Useful when we want to setup a simple page)'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-02-04 03:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('datacenterlight', '0030_dclnavbarpluginmodel_show_non_transparent_navbar_always'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vmpricing',
|
||||
name='stripe_coupon_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
|
@ -54,6 +54,7 @@ class VMPricing(models.Model):
|
|||
discount_amount = models.DecimalField(
|
||||
max_digits=6, decimal_places=2, default=0
|
||||
)
|
||||
stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
display_str = self.name + ' => ' + ' - '.join([
|
||||
|
|
|
@ -191,3 +191,9 @@ footer .dcl-link-separator::before {
|
|||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media(max-width:767px) {
|
||||
.vspace-top {
|
||||
margin-top: 35px;
|
||||
}
|
||||
}
|
|
@ -532,6 +532,7 @@
|
|||
|
||||
.order-detail-container .total-price {
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
|
|
@ -77,16 +77,18 @@
|
|||
}
|
||||
|
||||
function _navScroll() {
|
||||
if ($(window).scrollTop() > 10) {
|
||||
$(".navbar").removeClass("navbar-transparent");
|
||||
$(".navbar-default .btn-link").css("color", "#777");
|
||||
$(".dropdown-menu").removeClass("navbar-transparent");
|
||||
$(".dropdown-menu > li > a").css("color", "#777");
|
||||
} else {
|
||||
$(".navbar").addClass("navbar-transparent");
|
||||
$(".navbar-default .btn-link").css("color", "#fff");
|
||||
$(".dropdown-menu").addClass("navbar-transparent");
|
||||
$(".dropdown-menu > li > a").css("color", "#fff");
|
||||
if (!window.non_transparent_navbar_always) {
|
||||
if ($(window).scrollTop() > 10) {
|
||||
$(".navbar").removeClass("navbar-transparent");
|
||||
$(".navbar-default .btn-link").css("color", "#777");
|
||||
$(".dropdown-menu").removeClass("navbar-transparent");
|
||||
$(".dropdown-menu > li > a").css("color", "#777");
|
||||
} else {
|
||||
$(".navbar").addClass("navbar-transparent");
|
||||
$(".navbar-default .btn-link").css("color", "#fff");
|
||||
$(".dropdown-menu").addClass("navbar-transparent");
|
||||
$(".dropdown-menu > li > a").css("color", "#fff");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,8 +225,8 @@
|
|||
}
|
||||
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
|
||||
(cardPricing['ram'].value * window.ramUnitPrice) +
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) -
|
||||
window.discountAmount;
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) +
|
||||
window.vmBasePrice - window.discountAmount;
|
||||
total = parseFloat(total.toFixed(2));
|
||||
$("#total").text(total);
|
||||
}
|
||||
|
|
|
@ -56,13 +56,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
|
|||
"Running create_vm_task on {}".format(current_task.request.hostname))
|
||||
vm_id = None
|
||||
try:
|
||||
final_price = (
|
||||
specs.get('total_price') if 'total_price' in specs
|
||||
else specs.get('price')
|
||||
)
|
||||
|
||||
if 'pass' in user:
|
||||
on_user = user.get('email')
|
||||
on_user = user.get('username')
|
||||
on_pass = user.get('pass')
|
||||
logger.debug("Using user {user} to create VM".format(user=on_user))
|
||||
vm_name = None
|
||||
|
@ -92,108 +87,8 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
|
|||
if vm_id is None:
|
||||
raise Exception("Could not create VM")
|
||||
|
||||
# Update HostingOrder with the created vm_id
|
||||
hosting_order = HostingOrder.objects.filter(id=order_id).first()
|
||||
error_msg = None
|
||||
|
||||
try:
|
||||
hosting_order.vm_id = vm_id
|
||||
hosting_order.save()
|
||||
logger.debug(
|
||||
"Updated hosting_order {} with vm_id={}".format(
|
||||
hosting_order.id, vm_id
|
||||
)
|
||||
)
|
||||
except Exception as ex:
|
||||
error_msg = (
|
||||
"HostingOrder with id {order_id} not found. This means that "
|
||||
"the hosting order was not created and/or it is/was not "
|
||||
"associated with VM with id {vm_id}. Details {details}".format(
|
||||
order_id=order_id, vm_id=vm_id, details=str(ex)
|
||||
)
|
||||
)
|
||||
logger.error(error_msg)
|
||||
|
||||
stripe_utils = StripeUtils()
|
||||
result = stripe_utils.set_subscription_metadata(
|
||||
subscription_id=hosting_order.subscription_id,
|
||||
metadata={"VM_ID": str(vm_id)}
|
||||
)
|
||||
|
||||
if result.get('error') is not None:
|
||||
emsg = "Could not update subscription metadata for {sub}".format(
|
||||
sub=hosting_order.subscription_id
|
||||
)
|
||||
logger.error(emsg)
|
||||
if error_msg:
|
||||
error_msg += ". " + emsg
|
||||
else:
|
||||
error_msg = emsg
|
||||
|
||||
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
|
||||
|
||||
context = {
|
||||
'name': user.get('name'),
|
||||
'email': user.get('email'),
|
||||
'cores': specs.get('cpu'),
|
||||
'memory': specs.get('memory'),
|
||||
'storage': specs.get('disk_size'),
|
||||
'price': final_price,
|
||||
'template': template.get('name'),
|
||||
'vm_name': vm.get('name'),
|
||||
'vm_id': vm['vm_id'],
|
||||
'order_id': order_id
|
||||
}
|
||||
|
||||
if error_msg:
|
||||
context['errors'] = error_msg
|
||||
if 'pricing_name' in specs:
|
||||
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
|
||||
name=specs['pricing_name']
|
||||
))
|
||||
email_data = {
|
||||
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['info@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in context.items()]),
|
||||
'reply_to': [context['email']],
|
||||
}
|
||||
email = EmailMessage(**email_data)
|
||||
email.send()
|
||||
|
||||
if 'pass' in user:
|
||||
lang = 'en-us'
|
||||
if user.get('language') is not None:
|
||||
logger.debug(
|
||||
"Language is set to {}".format(user.get('language')))
|
||||
lang = user.get('language')
|
||||
translation.activate(lang)
|
||||
# Send notification to the user as soon as VM has been booked
|
||||
context = {
|
||||
'base_url': "{0}://{1}".format(user.get('request_scheme'),
|
||||
user.get('request_host')),
|
||||
'order_url': reverse('hosting:orders',
|
||||
kwargs={'pk': order_id}),
|
||||
'page_header': _(
|
||||
'Your New VM %(vm_name)s at Data Center Light') % {
|
||||
'vm_name': vm.get('name')},
|
||||
'vm_name': vm.get('name')
|
||||
}
|
||||
email_data = {
|
||||
'subject': context.get('page_header'),
|
||||
'to': user.get('email'),
|
||||
'context': context,
|
||||
'template_name': 'new_booked_vm',
|
||||
'template_path': 'hosting/emails/',
|
||||
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
}
|
||||
email = BaseEmail(**email_data)
|
||||
email.send()
|
||||
|
||||
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
|
||||
if vm_id > 0:
|
||||
get_or_create_vm_detail(custom_user, manager, vm_id)
|
||||
handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
|
||||
template)
|
||||
except Exception as e:
|
||||
logger.error(str(e))
|
||||
try:
|
||||
|
@ -215,3 +110,127 @@ def create_vm_task(self, vm_template_id, user, specs, template, order_id):
|
|||
return
|
||||
|
||||
return vm_id
|
||||
|
||||
|
||||
def handle_metadata_and_emails(order_id, vm_id, manager, user, specs,
|
||||
template):
|
||||
"""
|
||||
Handle's setting up of the metadata in Stripe and database and sending of
|
||||
emails to the user after VM creation
|
||||
|
||||
:param order_id: the hosting order id
|
||||
:param vm_id: the id of the vm created
|
||||
:param manager: the OpenNebula Manager instance
|
||||
:param user: the user's dict passed to the celery task
|
||||
:param specs: the specification's dict passed to the celery task
|
||||
:param template: the template dict passed to the celery task
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
custom_user = CustomUser.objects.get(email=user.get('email'))
|
||||
final_price = (
|
||||
specs.get('total_price') if 'total_price' in specs
|
||||
else specs.get('price')
|
||||
)
|
||||
# Update HostingOrder with the created vm_id
|
||||
hosting_order = HostingOrder.objects.filter(id=order_id).first()
|
||||
error_msg = None
|
||||
|
||||
try:
|
||||
hosting_order.vm_id = vm_id
|
||||
hosting_order.save()
|
||||
logger.debug(
|
||||
"Updated hosting_order {} with vm_id={}".format(
|
||||
hosting_order.id, vm_id
|
||||
)
|
||||
)
|
||||
except Exception as ex:
|
||||
error_msg = (
|
||||
"HostingOrder with id {order_id} not found. This means that "
|
||||
"the hosting order was not created and/or it is/was not "
|
||||
"associated with VM with id {vm_id}. Details {details}".format(
|
||||
order_id=order_id, vm_id=vm_id, details=str(ex)
|
||||
)
|
||||
)
|
||||
logger.error(error_msg)
|
||||
|
||||
stripe_utils = StripeUtils()
|
||||
result = stripe_utils.set_subscription_metadata(
|
||||
subscription_id=hosting_order.subscription_id,
|
||||
metadata={"VM_ID": str(vm_id)}
|
||||
)
|
||||
|
||||
if result.get('error') is not None:
|
||||
emsg = "Could not update subscription metadata for {sub}".format(
|
||||
sub=hosting_order.subscription_id
|
||||
)
|
||||
logger.error(emsg)
|
||||
if error_msg:
|
||||
error_msg += ". " + emsg
|
||||
else:
|
||||
error_msg = emsg
|
||||
|
||||
vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
|
||||
|
||||
context = {
|
||||
'name': user.get('name'),
|
||||
'email': user.get('email'),
|
||||
'cores': specs.get('cpu'),
|
||||
'memory': specs.get('memory'),
|
||||
'storage': specs.get('disk_size'),
|
||||
'price': final_price,
|
||||
'template': template.get('name'),
|
||||
'vm_name': vm.get('name'),
|
||||
'vm_id': vm['vm_id'],
|
||||
'order_id': order_id
|
||||
}
|
||||
|
||||
if error_msg:
|
||||
context['errors'] = error_msg
|
||||
if 'pricing_name' in specs:
|
||||
context['pricing'] = str(VMPricing.get_vm_pricing_by_name(
|
||||
name=specs['pricing_name']
|
||||
))
|
||||
email_data = {
|
||||
'subject': settings.DCL_TEXT + " Order from %s" % context['email'],
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['dcl-orders@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in context.items()]),
|
||||
'reply_to': [context['email']],
|
||||
}
|
||||
email = EmailMessage(**email_data)
|
||||
email.send()
|
||||
|
||||
if 'pass' in user:
|
||||
lang = 'en-us'
|
||||
if user.get('language') is not None:
|
||||
logger.debug(
|
||||
"Language is set to {}".format(user.get('language')))
|
||||
lang = user.get('language')
|
||||
translation.activate(lang)
|
||||
# Send notification to the user as soon as VM has been booked
|
||||
context = {
|
||||
'base_url': "{0}://{1}".format(user.get('request_scheme'),
|
||||
user.get('request_host')),
|
||||
'order_url': reverse('hosting:invoices'),
|
||||
'page_header': _(
|
||||
'Your New VM %(vm_name)s at Data Center Light') % {
|
||||
'vm_name': vm.get('name')},
|
||||
'vm_name': vm.get('name')
|
||||
}
|
||||
email_data = {
|
||||
'subject': context.get('page_header'),
|
||||
'to': user.get('email'),
|
||||
'context': context,
|
||||
'template_name': 'new_booked_vm',
|
||||
'template_path': 'hosting/emails/',
|
||||
'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
}
|
||||
email = BaseEmail(**email_data)
|
||||
email.send()
|
||||
|
||||
logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
|
||||
if vm_id > 0:
|
||||
get_or_create_vm_detail(custom_user, manager, vm_id)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="price-calc-section">
|
||||
<div class="card">
|
||||
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing %}
|
||||
{% include "datacenterlight/includes/_calculator_form.html" with vm_pricing=instance.pricing vm_base_price=vm_base_price %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,9 @@
|
|||
{% load static i18n custom_tags cms_tags %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
|
||||
{% if instance.show_non_transparent_navbar_always %}
|
||||
<script>window.non_transparent_navbar_always=true;</script>
|
||||
{% endif %}
|
||||
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always != True %}navbar-transparent{% endif %}">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#dcl-topnav">
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
|
||||
</p>
|
||||
<p style="line-height: 1.75; font-family: Lato, Arial, sans-serif; font-weight: 300; margin: 0;">
|
||||
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
|
||||
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% trans "Welcome to Data Center Light!" %}
|
||||
|
||||
{% blocktrans %}Thanks for joining us! We provide the most affordable virtual machines from the heart of Switzerland.{% endblocktrans %}
|
||||
{% blocktrans %}Try now, order a VM. VM price starts from only 15CHF per month.{% endblocktrans %}
|
||||
{% blocktrans %}Try now, order a VM. VM price starts from only 11.5 CHF per month.{% endblocktrans %}
|
||||
|
||||
{{ base_url }}{% url 'hosting:create_virtual_machine' %}
|
||||
|
||||
|
|
|
@ -9,12 +9,13 @@
|
|||
window.ssdUnitPrice = {{vm_pricing.ssd_unit_price|default:0}};
|
||||
window.hddUnitPrice = {{vm_pricing.hdd_unit_price|default:0}};
|
||||
window.discountAmount = {{vm_pricing.discount_amount|default:0}};
|
||||
window.vmBasePrice = {{vm_base_price|default:0}};
|
||||
window.minRam = {{min_ram}};
|
||||
window.minRamErr = '{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}';
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<form id="order_form" method="POST" action="{{calculator_form_url}}" data-toggle="validator" role="form">
|
||||
<form id="order_form" method="POST" action="{% url 'datacenterlight:index' %}" data-toggle="validator" role="form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="pid" value="{{instance.id}}">
|
||||
<div class="title">
|
||||
|
@ -102,4 +103,4 @@
|
|||
</div>
|
||||
<input type="hidden" name="pricing_name" value="{% if vm_pricing.name %}{{vm_pricing.name}}{% else %}unknown{% endif%}"></input>
|
||||
<input type="submit" class="btn btn-primary disabled" value="{% trans 'Continue' %}"></input>
|
||||
</form>
|
||||
</form>
|
|
@ -1,7 +1,6 @@
|
|||
{% load staticfiles i18n custom_tags %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<nav class="navbar navbar-default navbar-fixed-top topnav navbar-transparent">
|
||||
<nav class="navbar navbar-default navbar-fixed-top topnav {% if instance.show_non_transparent_navbar_always is False %}navbar-transparent{% endif %}">
|
||||
<!-- Brand and toggle get grouped for better mobile display -->
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
|
||||
|
|
|
@ -13,6 +13,15 @@
|
|||
<!-- Credit card form -->
|
||||
<div class="dcl-order-container">
|
||||
<div class="payment-container">
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'vat_error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li><p class="card-warning-content card-warning-error">An error occurred while validating VAT number: {{ message|safe }}</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="dcl-payment-grid">
|
||||
<div class="dcl-payment-box">
|
||||
<div class="dcl-payment-section">
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
{% load staticfiles bootstrap3 i18n custom_tags humanize %}
|
||||
|
||||
{% block content %}
|
||||
<script>
|
||||
{% if payment_intent_secret %}
|
||||
console.log("payment_intent_secret");
|
||||
window.paymentIntentSecret = "{{payment_intent_secret}}";
|
||||
{% else %}
|
||||
console.log("No payment_intent_secret");
|
||||
{% endif %}
|
||||
</script>
|
||||
<div id="order-detail{{order.pk}}" class="order-detail-container">
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
|
@ -32,6 +40,16 @@
|
|||
{{billing_address.cardholder_name}}<br>
|
||||
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
|
||||
{{billing_address.city}}, {{billing_address.country}}
|
||||
{% if billing_address.vat_number %}
|
||||
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
|
||||
{% if vm.vat_validation_status != "ch_vat" and vm.vat_validation_status != "not_needed" %}
|
||||
{% if vm.vat_validation_status == "verified" %}
|
||||
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
</address>
|
||||
|
@ -48,38 +66,121 @@
|
|||
<hr>
|
||||
<div>
|
||||
<h4>{% trans "Order summary" %}</h4>
|
||||
<style>
|
||||
@media screen and (max-width:400px){
|
||||
.header-no-left-padding {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:767px){
|
||||
.cmf-ord-heading {
|
||||
font-size: 11px;
|
||||
}
|
||||
.order-detail-container .order-details {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:367px){
|
||||
.cmf-ord-heading {
|
||||
font-size: 11px;
|
||||
}
|
||||
.order-detail-container .order-details {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
{% if generic_payment_details %}
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ generic_payment_details.product_name }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<p>
|
||||
<span>{% trans "Amount" %}: </span>
|
||||
<strong class="pull-right">CHF {{generic_payment_details.amount|floatformat:2|intcomma}}</strong>
|
||||
</p>
|
||||
{% if generic_payment_details.description %}
|
||||
<p>
|
||||
<span>{% trans "Description" %}: </span>
|
||||
<strong class="pull-right">{{generic_payment_details.description}}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if generic_payment_details.recurring %}
|
||||
<p>
|
||||
<span>{% trans "Recurring" %}: </span>
|
||||
<strong class="pull-right">Yes</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if generic_payment_details.description %}
|
||||
<p>
|
||||
<strong>{% trans "Description" %}: </strong>
|
||||
<span class="pull-right">{{generic_payment_details.description}}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if generic_payment_details.recurring %}
|
||||
<p>
|
||||
<strong>{% trans "Recurring" %}: </strong>
|
||||
<span class="pull-right">Yes</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if generic_payment_details.exclude_vat_calculations %}
|
||||
{% else %}
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
|
||||
<strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span></span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{generic_payment_details.vat_country}} ({{generic_payment_details.vat_rate}}%)</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span>Price</span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><span class="pull-right" >{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><span class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><strong>Total</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><strong class="pull-right">{{generic_payment_details.amount_before_vat|floatformat:2|intcomma}} CHF</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><strong class="pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
|
||||
<strong class="total-price pull-right">{{generic_payment_details.amount|floatformat:2|intcomma}} CHF</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
{{ request.session.template.name }}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
<strong class="pull-right">{{vm.cpu|floatformat}}</strong>
|
||||
|
@ -96,38 +197,75 @@
|
|||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% if vm.vat > 0 or vm.discount.amount > 0 %}
|
||||
<div class="col-sm-6">
|
||||
<div class="subtotal-price">
|
||||
{% if vm.vat > 0 %}
|
||||
<p>
|
||||
<strong class="text-lg">{% trans "Subtotal" %} </strong>
|
||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if vm.discount.amount > 0 %}
|
||||
<p class="text-primary">
|
||||
{%trans "Discount" as discount_name %}
|
||||
<strong>{{ vm.discount.name|default:discount_name }} </strong>
|
||||
<strong class="pull-right">- {{ vm.discount.amount }} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
<p class="total-price">
|
||||
<strong>{% trans "Total" %} </strong>
|
||||
<strong class="pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
|
||||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span></span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%)</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span>Price</span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><span class="pull-right" >{{vm.price|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><span class="pull-right">{{vm.price_with_vat|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if vm.discount.amount > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span>{{vm.discount.name}}</span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><span class="pull-right">-{{vm.discount.amount|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><span class="pull-right">-{{vm.discount.amount_with_vat|floatformat:2|intcomma}} CHF</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><strong>Total</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><strong class="pull-right">{{vm.price_after_discount|floatformat:2|intcomma}} CHF</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><strong class="pull-right">{{vm.price_after_discount_with_vat|floatformat:2|intcomma}} CHF</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
|
||||
<strong class="total-price pull-right">{{vm.total_price|floatformat:2|intcomma}} CHF</strong>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -139,12 +277,17 @@
|
|||
<div class="col-sm-8">
|
||||
{% if generic_payment_details %}
|
||||
{% if generic_payment_details.recurring %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{total_price}} CHF/month{% endblocktrans %}.</div>
|
||||
{% if generic_payment_details.recurring_interval == 'year' %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %} By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/year{% endblocktrans %}.</div>
|
||||
{% else %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}
|
||||
By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF/month{% endblocktrans %}.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" this payment will charge your credit card account with a one time amount of {{total_price}} CHF{% endblocktrans %}.</div>
|
||||
<div class="dcl-place-order-text">{% blocktrans with total_price=generic_payment_details.amount|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ total_price }} CHF{% endblocktrans %}.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{vm_total_price}} CHF/month{% endblocktrans %}.</div>
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
|
@ -187,5 +330,14 @@
|
|||
<script type="text/javascript">
|
||||
{% trans "Some problem encountered. Please try again later." as err_msg %}
|
||||
var create_vm_error_message = '{{err_msg|safe}}';
|
||||
var pm_id = '{{id_payment_method}}';
|
||||
var error_url = '{{ error_msg.redirect }}';
|
||||
var error_msg = '{{ error_msg.msg_body }}';
|
||||
var error_title = '{{ error_msg.msg_title }}';
|
||||
var success_msg = '{{ success_msg.msg_body }}';
|
||||
var success_title = '{{ success_msg.msg_title }}';
|
||||
var success_url = '{{ success_msg.redirect }}';
|
||||
window.stripeKey = "{{stripe_key}}";
|
||||
window.isSubscription = ("{{is_subscription}}" === 'true');
|
||||
</script>
|
||||
{%endblock%}
|
||||
{%endblock%}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from django import template
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.utils.translation import activate, get_language
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import activate, get_language, ugettext_lazy as _
|
||||
|
||||
from hosting.models import GenericProduct, HostingOrder
|
||||
from utils.hosting_utils import get_ip_addresses
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -52,3 +61,115 @@ def escaped_line_break(value):
|
|||
:return:
|
||||
"""
|
||||
return value.replace("\\n", "\n")
|
||||
|
||||
|
||||
@register.filter('get_line_item_from_hosting_order_charge')
|
||||
def get_line_item_from_hosting_order_charge(hosting_order_id):
|
||||
"""
|
||||
Returns ready-to-use "html" line item to be shown for a charge in the
|
||||
invoice list page
|
||||
|
||||
:param hosting_order_id: the HostingOrder id
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
hosting_order = HostingOrder.objects.get(id = hosting_order_id)
|
||||
if hosting_order.stripe_charge_id:
|
||||
return mark_safe("""
|
||||
<td class="xs-td-inline">{product_name}</td>
|
||||
<td class="xs-td-inline">{created_at}</td>
|
||||
<td class="xs-td-inline">{total}</td>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{receipt_url}" target="_blank">{see_invoice_text}</a>
|
||||
</td>
|
||||
""".format(
|
||||
product_name=hosting_order.generic_product.product_name.capitalize(),
|
||||
created_at=hosting_order.created_at.strftime('%Y-%m-%d'),
|
||||
total='%.2f' % (hosting_order.price),
|
||||
receipt_url=reverse('hosting:orders',
|
||||
kwargs={'pk': hosting_order.id}),
|
||||
|
||||
see_invoice_text=_("See Invoice")
|
||||
))
|
||||
else:
|
||||
return ""
|
||||
except Exception as ex:
|
||||
logger.error("Error %s" % str(ex))
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter('get_line_item_from_stripe_invoice')
|
||||
def get_line_item_from_stripe_invoice(invoice):
|
||||
"""
|
||||
Returns ready-to-use "html" line item to be shown for an invoice in the
|
||||
invoice list page
|
||||
|
||||
:param invoice: the stripe Invoice object
|
||||
:return:
|
||||
"""
|
||||
start_date = 0
|
||||
end_date = 0
|
||||
is_first = True
|
||||
vm_id = -1
|
||||
plan_name = ""
|
||||
for line_data in invoice["lines"]["data"]:
|
||||
if is_first:
|
||||
plan_name = line_data.plan.name if line_data.plan is not None else ""
|
||||
start_date = line_data.period.start
|
||||
end_date = line_data.period.end
|
||||
is_first = False
|
||||
if hasattr(line_data.metadata, "VM_ID"):
|
||||
vm_id = line_data.metadata.VM_ID
|
||||
else:
|
||||
if line_data.period.start < start_date:
|
||||
start_date = line_data.period.start
|
||||
if line_data.period.end > end_date:
|
||||
end_date = line_data.period.end
|
||||
if hasattr(line_data.metadata, "VM_ID"):
|
||||
vm_id = line_data.metadata.VM_ID
|
||||
|
||||
try:
|
||||
vm_id = int(vm_id)
|
||||
except ValueError as ve:
|
||||
print(str(ve))
|
||||
if invoice["lines"]["data"]:
|
||||
return mark_safe("""
|
||||
<td class="xs-td-inline">{vm_id}</td>
|
||||
<td class="xs-td-inline">{ip_addresses}</td>
|
||||
<td class="xs-td-inline">{period}</td>
|
||||
<td class="xs-td-inline text-right dcl-text-right-padding">{total}</td>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{stripe_invoice_url}" target="_blank">{see_invoice_text}</a>
|
||||
</td>
|
||||
""".format(
|
||||
vm_id=vm_id if vm_id > 0 else "",
|
||||
ip_addresses=mark_safe(get_ip_addresses(vm_id)) if vm_id > 0 else
|
||||
mark_safe(get_product_name(plan_name)) if plan_name.startswith("generic-") else plan_name,
|
||||
period=mark_safe("%s — %s" % (
|
||||
datetime.datetime.fromtimestamp(start_date).strftime('%Y-%m-%d'),
|
||||
datetime.datetime.fromtimestamp(end_date).strftime('%Y-%m-%d'))),
|
||||
total='%.2f' % (invoice.total/100),
|
||||
stripe_invoice_url=invoice.hosted_invoice_url,
|
||||
see_invoice_text=_("See Invoice")
|
||||
))
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_product_name(plan_name):
|
||||
product_name = ""
|
||||
if plan_name and plan_name.startswith("generic-"):
|
||||
first_index_hyphen = plan_name.index("-") + 1
|
||||
product_id = plan_name[first_index_hyphen:(plan_name[first_index_hyphen:].index("-")) + first_index_hyphen]
|
||||
try:
|
||||
product = GenericProduct.objects.get(id=product_id)
|
||||
product_name = product.product_name
|
||||
except GenericProduct.DoesNotExist as dne:
|
||||
logger.error("Generic product id=%s does not exist" % product_id)
|
||||
product_name = plan_name
|
||||
except GenericProduct.MultipleObjectsReturned as mor:
|
||||
logger.error("Multiple products with id=%s exist" % product_id)
|
||||
product_name = "Unknown"
|
||||
else:
|
||||
logger.debug("Product name for plan %s does not exist" % plan_name)
|
||||
return product_name
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
import pyotp
|
||||
import requests
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
|
||||
|
@ -9,12 +11,19 @@ from datacenterlight.tasks import create_vm_task
|
|||
from hosting.models import HostingOrder, HostingBill, OrderDetail
|
||||
from membership.models import StripeCustomer
|
||||
from utils.forms import UserBillingAddressForm
|
||||
from utils.models import BillingAddress
|
||||
from utils.models import BillingAddress, UserBillingAddress
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from .cms_models import CMSIntegration
|
||||
from .models import VMPricing, VMTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
eu_countries = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk',
|
||||
'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it',
|
||||
'lv', 'lu', 'mt', 'nl', 'pl', 'pt', 'ro','sk', 'si', 'es',
|
||||
'se', 'gb']
|
||||
|
||||
|
||||
def get_cms_integration(name):
|
||||
current_site = Site.objects.get_current()
|
||||
try:
|
||||
|
@ -29,12 +38,14 @@ def get_cms_integration(name):
|
|||
def create_vm(billing_address_data, stripe_customer_id, specs,
|
||||
stripe_subscription_obj, card_details_dict, request,
|
||||
vm_template_id, template, user):
|
||||
logger.debug("In create_vm")
|
||||
billing_address = BillingAddress(
|
||||
cardholder_name=billing_address_data['cardholder_name'],
|
||||
street_address=billing_address_data['street_address'],
|
||||
city=billing_address_data['city'],
|
||||
postal_code=billing_address_data['postal_code'],
|
||||
country=billing_address_data['country']
|
||||
country=billing_address_data['country'],
|
||||
vat_number=billing_address_data['vat_number'],
|
||||
)
|
||||
billing_address.save()
|
||||
customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
|
||||
|
@ -92,8 +103,6 @@ def create_vm(billing_address_data, stripe_customer_id, specs,
|
|||
|
||||
create_vm_task.delay(vm_template_id, user, specs, template, order.id)
|
||||
|
||||
clear_all_session_vars(request)
|
||||
|
||||
|
||||
def clear_all_session_vars(request):
|
||||
if request.session is not None:
|
||||
|
@ -101,7 +110,9 @@ def clear_all_session_vars(request):
|
|||
'billing_address_data', 'card_id',
|
||||
'token', 'customer', 'generic_payment_type',
|
||||
'generic_payment_details', 'product_id',
|
||||
'order_confirm_url', 'new_user_hosting_key_id']:
|
||||
'order_confirm_url', 'new_user_hosting_key_id',
|
||||
'vat_validation_status', 'billing_address_id',
|
||||
'id_payment_method']:
|
||||
if session_var in request.session:
|
||||
del request.session[session_var]
|
||||
|
||||
|
@ -123,3 +134,172 @@ def check_otp(name, realm, token):
|
|||
data=data
|
||||
)
|
||||
return response.status_code
|
||||
|
||||
|
||||
def validate_vat_number(stripe_customer_id, billing_address_id,
|
||||
is_user_ba=False):
|
||||
if is_user_ba:
|
||||
try:
|
||||
billing_address = UserBillingAddress.objects.get(
|
||||
id=billing_address_id)
|
||||
except UserBillingAddress.DoesNotExist as dne:
|
||||
billing_address = None
|
||||
logger.debug(
|
||||
"UserBillingAddress does not exist for %s" % billing_address_id)
|
||||
except UserBillingAddress.MultipleObjectsReturned as mor:
|
||||
logger.debug(
|
||||
"Multiple UserBillingAddress exist for %s" % billing_address_id)
|
||||
billing_address = UserBillingAddress.objects.filter(
|
||||
id=billing_address_id).order_by('-id').first()
|
||||
else:
|
||||
try:
|
||||
billing_address = BillingAddress.objects.get(id=billing_address_id)
|
||||
except BillingAddress.DoesNotExist as dne:
|
||||
billing_address = None
|
||||
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
|
||||
except BillingAddress.MultipleObjectsReturned as mor:
|
||||
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
|
||||
billing_address = BillingAddress.objects.filter(id=billing_address_id).order_by('-id').first()
|
||||
if billing_address is not None:
|
||||
logger.debug("BillingAddress found: %s %s type=%s" % (
|
||||
billing_address_id, str(billing_address), type(billing_address)))
|
||||
if billing_address.country.lower().strip() not in eu_countries:
|
||||
return {
|
||||
"validated_on": "",
|
||||
"status": "not_needed"
|
||||
}
|
||||
if billing_address.vat_number_validated_on:
|
||||
logger.debug("billing_address verified on %s" %
|
||||
billing_address.vat_number_validated_on)
|
||||
return {
|
||||
"validated_on": billing_address.vat_number_validated_on,
|
||||
"status": "verified"
|
||||
}
|
||||
else:
|
||||
logger.debug("billing_address not yet verified, "
|
||||
"Checking if we already have a tax id")
|
||||
if billing_address.stripe_tax_id:
|
||||
logger.debug("We have a tax id %s" % billing_address.stripe_tax_id)
|
||||
tax_id_obj = stripe.Customer.retrieve_tax_id(
|
||||
stripe_customer_id,
|
||||
billing_address.stripe_tax_id,
|
||||
)
|
||||
if tax_id_obj.verification.status == "verified":
|
||||
logger.debug("Latest status on Stripe=%s. Updating" %
|
||||
tax_id_obj.verification.status)
|
||||
# update billing address
|
||||
billing_address.vat_number_validated_on = datetime.datetime.now()
|
||||
billing_address.vat_validation_status = tax_id_obj.verification.status
|
||||
billing_address.save()
|
||||
return {
|
||||
"status": "verified",
|
||||
"validated_on": billing_address.vat_number_validated_on
|
||||
}
|
||||
else:
|
||||
billing_address.vat_validation_status = tax_id_obj.verification.status
|
||||
billing_address.save()
|
||||
logger.debug(
|
||||
"Latest status on Stripe=%s" % str(tax_id_obj)
|
||||
)
|
||||
return {
|
||||
"status": tax_id_obj.verification.status if tax_id_obj
|
||||
else "unknown",
|
||||
"validated_on": ""
|
||||
}
|
||||
else:
|
||||
logger.debug("Creating a tax id")
|
||||
logger.debug("Billing address = %s" % str(billing_address))
|
||||
tax_id_obj = create_tax_id(
|
||||
stripe_customer_id, billing_address_id,
|
||||
"ch_vat" if billing_address.country.lower() == "ch" else "eu_vat",
|
||||
is_user_ba=is_user_ba
|
||||
)
|
||||
logger.debug("tax_id_obj = %s" % str(tax_id_obj))
|
||||
else:
|
||||
logger.debug("invalid billing address")
|
||||
return {
|
||||
"status": "invalid billing address",
|
||||
"validated_on": ""
|
||||
}
|
||||
|
||||
if 'response_object' in tax_id_obj:
|
||||
return tax_id_obj
|
||||
|
||||
return {
|
||||
"status": tax_id_obj.verification.status,
|
||||
"validated_on": datetime.datetime.now() if tax_id_obj.verification.status == "verified" else ""
|
||||
}
|
||||
|
||||
|
||||
def create_tax_id(stripe_customer_id, billing_address_id, type,
|
||||
is_user_ba=False):
|
||||
if is_user_ba:
|
||||
try:
|
||||
billing_address = UserBillingAddress.objects.get(
|
||||
id=billing_address_id)
|
||||
except UserBillingAddress.DoesNotExist as dne:
|
||||
billing_address = None
|
||||
logger.debug(
|
||||
"UserBillingAddress does not exist for %s" % billing_address_id)
|
||||
except UserBillingAddress.MultipleObjectsReturned as mor:
|
||||
logger.debug(
|
||||
"Multiple UserBillingAddress exist for %s" % billing_address_id)
|
||||
billing_address = UserBillingAddress.objects.filter(
|
||||
id=billing_address_id).order_by('-id').first()
|
||||
else:
|
||||
try:
|
||||
billing_address = BillingAddress.objects.get(id=billing_address_id)
|
||||
except BillingAddress.DoesNotExist as dne:
|
||||
billing_address = None
|
||||
logger.debug("BillingAddress does not exist for %s" % billing_address_id)
|
||||
except BillingAddress.MultipleObjectsReturned as mor:
|
||||
logger.debug("Multiple BillingAddress exist for %s" % billing_address_id)
|
||||
billing_address = BillingAddress.objects.filter(billing_address_id).order_by('-id').first()
|
||||
|
||||
tax_id_obj = None
|
||||
if billing_address:
|
||||
stripe_utils = StripeUtils()
|
||||
tax_id_response = stripe_utils.get_or_create_tax_id_for_user(
|
||||
stripe_customer_id,
|
||||
vat_number=billing_address.vat_number,
|
||||
type=type,
|
||||
country=billing_address.country
|
||||
)
|
||||
|
||||
tax_id_obj = tax_id_response.get('response_object')
|
||||
|
||||
if not tax_id_obj:
|
||||
logger.debug("Received none in tax_id_obj")
|
||||
return {
|
||||
'paid': False,
|
||||
'response_object': None,
|
||||
'error': "No such address found" if 'error' not in tax_id_response else
|
||||
tax_id_response["error"]
|
||||
}
|
||||
|
||||
try:
|
||||
stripe_customer = StripeCustomer.objects.get(stripe_id=stripe_customer_id)
|
||||
billing_address_set = set()
|
||||
logger.debug("Updating billing address")
|
||||
for ho in stripe_customer.hostingorder_set.all():
|
||||
if ho.billing_address.vat_number == billing_address.vat_number:
|
||||
billing_address_set.add(ho.billing_address)
|
||||
for b_address in billing_address_set:
|
||||
b_address.stripe_tax_id = tax_id_obj.id
|
||||
b_address.vat_validation_status = tax_id_obj.verification.status
|
||||
b_address.save()
|
||||
logger.debug("Updated billing_address %s" % str(b_address))
|
||||
|
||||
ub_addresses = stripe_customer.user.billing_addresses.filter(
|
||||
vat_number=billing_address.vat_number)
|
||||
for ub_address in ub_addresses:
|
||||
ub_address.stripe_tax_id = tax_id_obj.id
|
||||
ub_address.vat_validation_status = tax_id_obj.verification.status
|
||||
ub_address.save()
|
||||
logger.debug("Updated user_billing_address %s" % str(ub_address))
|
||||
except StripeCustomer.DoesNotExist as dne:
|
||||
logger.debug("StripeCustomer %s does not exist" % stripe_customer_id)
|
||||
billing_address.stripe_tax_id = tax_id_obj.id
|
||||
billing_address.vat_validation_status = tax_id_obj.verification.status
|
||||
billing_address.save()
|
||||
return tax_id_obj
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,7 @@ class MembershipBillingForm(BillingAddressForm):
|
|||
'city': _('City'),
|
||||
'postal_code': _('Postal Code'),
|
||||
'country': _('Country'),
|
||||
'vat_number': _('VAT Number'),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -376,8 +376,6 @@ msgid ""
|
|||
" digitalglarus.ch<br/>\n"
|
||||
" hack4lgarus.ch<br/>\n"
|
||||
" ipv6onlyhosting.com<br/>\n"
|
||||
" ipv6onlyhosting.ch<br/>\n"
|
||||
" ipv6onlyhosting.net<br/>\n"
|
||||
" django-hosting.ch<br/>\n"
|
||||
" rails-hosting.ch<br/>\n"
|
||||
" node-hosting.ch<br/>\n"
|
||||
|
@ -636,8 +634,8 @@ msgstr ""
|
|||
"Internetangebot der ungleich glarus ag, welches unter den nachfolgenden "
|
||||
"Domains erreichbar ist:<br/><br/>ungleich.ch<br/>datacenterlight.ch<br/"
|
||||
">devuanhosting.com<br/>devuanhosting.ch<br/>digitalglarus.ch<br/>hack4lgarus."
|
||||
"ch<br/>ipv6onlyhosting.com<br/>ipv6onlyhosting.ch<br/>ipv6onlyhosting.net<br/"
|
||||
">django-hosting.ch<br/>rails-hosting.ch<br/>node-hosting.ch<br/>blog."
|
||||
"ch<br/>ipv6onlyhosting.com<br/>django-hosting.ch<br/>rails-hosting.ch"
|
||||
"<br/>node-hosting.ch<br/>blog."
|
||||
"ungleich.ch<br/><br/>Der Datenschutzbeauftragte des Verantwortlichen ist:<br/"
|
||||
"><br/>Sanghee Kim<br/>ungleich glarus ag<br/>Bahnhofstrasse 1<br/>8783 "
|
||||
"Linthal (CH)<br/>E-Mail: <a href=\"mailto:sanghee.kim@ungleich.ch\">sanghee."
|
||||
|
@ -838,3 +836,4 @@ msgstr ""
|
|||
|
||||
#~ msgid "index/?$"
|
||||
#~ msgstr "index/?$"
|
||||
|
||||
|
|
|
@ -835,9 +835,10 @@ class ContactView(FormView):
|
|||
success_message = _('Message Successfully Sent')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
form.send_email()
|
||||
messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
print("digital glarus contactusform")
|
||||
#form.save()
|
||||
#form.send_email()
|
||||
#messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
return super(ContactView, self).form_valid(form)
|
||||
|
||||
|
||||
|
|
|
@ -52,10 +52,13 @@ PROJECT_DIR = os.path.abspath(
|
|||
)
|
||||
|
||||
# load .env file
|
||||
dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
|
||||
dotenv.load_dotenv("{0}/.env".format(PROJECT_DIR))
|
||||
|
||||
from multisite import SiteID
|
||||
|
||||
RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY')
|
||||
RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PRIVATE_KEY')
|
||||
|
||||
UNGLEICH_BLOG_SITE_ID = int_env("UNGLEICH_BLOG_SITE_ID")
|
||||
SITE_ID = SiteID(default=(UNGLEICH_BLOG_SITE_ID if
|
||||
UNGLEICH_BLOG_SITE_ID > 0 else 1))
|
||||
|
@ -125,6 +128,7 @@ INSTALLED_APPS = (
|
|||
'djangocms_file',
|
||||
'djangocms_picture',
|
||||
'djangocms_video',
|
||||
'django_recaptcha',
|
||||
# 'djangocms_flash',
|
||||
# 'djangocms_googlemap',
|
||||
# 'djangocms_inherit',
|
||||
|
@ -153,6 +157,7 @@ INSTALLED_APPS = (
|
|||
'rest_framework',
|
||||
'opennebula_api',
|
||||
'django_celery_results',
|
||||
'webhook',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
|
@ -244,8 +249,9 @@ DATABASES = {
|
|||
}
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'utils.backend.MyLDAPBackend',
|
||||
'guardian.backends.ObjectPermissionBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
|
||||
)
|
||||
|
||||
# Internationalization
|
||||
|
@ -629,8 +635,6 @@ GOOGLE_ANALYTICS_PROPERTY_IDS = {
|
|||
'datacenterlight.ch': 'UA-62285904-8',
|
||||
'devuanhosting.ch': 'UA-62285904-9',
|
||||
'devuanhosting.com': 'UA-62285904-9',
|
||||
'ipv6onlyhosting.ch': 'UA-62285904-10',
|
||||
'ipv6onlyhosting.net': 'UA-62285904-10',
|
||||
'ipv6onlyhosting.com': 'UA-62285904-10',
|
||||
'comic.ungleich.ch': 'UA-62285904-13',
|
||||
'127.0.0.1:8000': 'localhost',
|
||||
|
@ -703,7 +707,7 @@ if ENABLE_LOGGING:
|
|||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'standard': {
|
||||
'format': '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
||||
'format': '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s',
|
||||
}
|
||||
},
|
||||
'handlers': handlers_dict,
|
||||
|
@ -719,7 +723,35 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else
|
|||
X_FRAME_OPTIONS_ALLOW_FROM_URI.strip()
|
||||
))
|
||||
|
||||
WEBHOOK_SECRET = env('WEBHOOK_SECRET')
|
||||
|
||||
DEBUG = bool_env('DEBUG')
|
||||
ADD_TRIAL_PERIOD_TO_SUBSCRIPTION = bool_env('ADD_TRIAL_PERIOD_TO_SUBSCRIPTION')
|
||||
|
||||
|
||||
# LDAP setup
|
||||
LDAP_ADMIN_DN = env('LDAP_ADMIN_DN')
|
||||
LDAP_ADMIN_PASSWORD = env('LDAP_ADMIN_PASSWORD')
|
||||
AUTH_LDAP_SERVER = env('LDAPSERVER')
|
||||
|
||||
LDAP_CUSTOMER_DN = env('LDAP_CUSTOMER_DN')
|
||||
LDAP_CUSTOMER_GROUP_ID = int(env('LDAP_CUSTOMER_GROUP_ID'))
|
||||
LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH',
|
||||
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file')
|
||||
)
|
||||
LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID'))
|
||||
|
||||
# Search union over OUs
|
||||
AUTH_LDAP_START_TLS = bool(os.environ.get('LDAP_USE_TLS', False))
|
||||
|
||||
ENTIRE_SEARCH_BASE = env("ENTIRE_SEARCH_BASE")
|
||||
|
||||
|
||||
AUTH_LDAP_USER_ATTR_MAP = {
|
||||
"first_name": "givenName",
|
||||
"last_name": "sn",
|
||||
"email": "mail"
|
||||
}
|
||||
|
||||
READ_VM_REALM = env('READ_VM_REALM')
|
||||
AUTH_NAME = env('AUTH_NAME')
|
||||
|
@ -728,8 +760,26 @@ AUTH_REALM = env('AUTH_REALM')
|
|||
OTP_SERVER = env('OTP_SERVER')
|
||||
OTP_VERIFY_ENDPOINT = env('OTP_VERIFY_ENDPOINT')
|
||||
|
||||
FIRST_VM_ID_AFTER_EU_VAT = int_env('FIRST_VM_ID_AFTER_EU_VAT')
|
||||
PRE_EU_VAT_RATE = float(env('PRE_EU_VAT_RATE'))
|
||||
|
||||
VM_BASE_PRICE = float(env('VM_BASE_PRICE'))
|
||||
|
||||
UPDATED_TEMPLATES_STR = env('UPDATED_TEMPLATES')
|
||||
UPDATED_TEMPLATES_DICT = {}
|
||||
if UPDATED_TEMPLATES_STR:
|
||||
UPDATED_TEMPLATES_DICT = eval(UPDATED_TEMPLATES_STR)
|
||||
|
||||
MAX_TIME_TO_WAIT_FOR_VM_TERMINATE = int_env(
|
||||
'MAX_TIME_TO_WAIT_FOR_VM_TERMINATE', 15)
|
||||
|
||||
if DEBUG:
|
||||
from .local import * # flake8: noqa
|
||||
else:
|
||||
from .prod import * # flake8: noqa
|
||||
|
||||
# Try to load dynamic configuration, if it exists
|
||||
try:
|
||||
from .dynamic import * # flake8: noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
@ -28,9 +28,7 @@ ALLOWED_HOSTS = [
|
|||
".devuanhosting.ch",
|
||||
".devuanhosting.com",
|
||||
".digitalezukunft.ch",
|
||||
".ipv6onlyhosting.ch",
|
||||
".ipv6onlyhosting.com",
|
||||
".ipv6onlyhosting.net",
|
||||
".digitalglarus.ch",
|
||||
".hack4glarus.ch",
|
||||
".xn--nglarus-n2a.ch"
|
||||
|
|
|
@ -11,6 +11,7 @@ from hosting.views import (
|
|||
RailsHostingView, DjangoHostingView, NodeJSHostingView
|
||||
)
|
||||
from datacenterlight.views import PaymentOrderView
|
||||
from webhook import views as webhook_views
|
||||
from membership import urls as membership_urls
|
||||
from ungleich_page.views import LandingView
|
||||
from django.views.generic import RedirectView
|
||||
|
@ -62,6 +63,7 @@ urlpatterns += i18n_patterns(
|
|||
name='blog_list_view'),
|
||||
url(r'^cms/', include('cms.urls')),
|
||||
url(r'^blog/', include('djangocms_blog.urls', namespace='djangocms_blog')),
|
||||
url(r'^webhooks/', webhook_views.handle_webhook),
|
||||
url(r'^$', RedirectView.as_view(url='/cms') if REDIRECT_TO_CMS
|
||||
else LandingView.as_view()),
|
||||
url(r'^', include('ungleich_page.urls', namespace='ungleich_page')),
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -uex
|
||||
|
||||
cd /usr/src/app/
|
||||
cat > dynamicweb/settings/dynamic.py <<EOF
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': '${POSTGRES_DB}',
|
||||
'USER': '${POSTGRES_USER}',
|
||||
'PASSWORD': '${POSTGRES_PASSWORD}',
|
||||
'HOST': '${POSTGRES_HOST}',
|
||||
'PORT': '5432',
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
exec "$@"
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
import xml
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
@ -109,9 +110,14 @@ class ProductPaymentForm(GenericPaymentForm):
|
|||
)
|
||||
)
|
||||
if self.product.product_is_subscription:
|
||||
payment_type = "month"
|
||||
if self.product.product_subscription_interval == "month":
|
||||
payment_type = _('Monthly subscription')
|
||||
elif self.product.product_subscription_interval == "year":
|
||||
payment_type = _('Yearly subscription')
|
||||
self.fields['amount'].label = "{amt} ({payment_type})".format(
|
||||
amt=_('Amount in CHF'),
|
||||
payment_type=_('Monthly subscription')
|
||||
payment_type=payment_type
|
||||
)
|
||||
else:
|
||||
self.fields['amount'].label = "{amt} ({payment_type})".format(
|
||||
|
@ -202,7 +208,7 @@ class UserHostingKeyForm(forms.ModelForm):
|
|||
logger.debug(
|
||||
"Not a correct ssh format {error}".format(error=str(cpe)))
|
||||
raise forms.ValidationError(KEY_ERROR_MESSAGE)
|
||||
return openssh_pubkey_str
|
||||
return xml.sax.saxutils.escape(openssh_pubkey_str)
|
||||
|
||||
def clean_name(self):
|
||||
INVALID_NAME_MESSAGE = _("Comma not accepted in the name of the key")
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-09-15 03:39+0000\n"
|
||||
"POT-Creation-Date: 2021-02-07 10:19+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -28,28 +28,31 @@ msgid "User does not exist"
|
|||
msgstr "Der Benutzer existiert nicht"
|
||||
|
||||
msgid "Choose a product"
|
||||
msgstr ""
|
||||
msgstr "Wähle ein Produkt"
|
||||
|
||||
msgid "Amount in CHF"
|
||||
msgstr "Betrag"
|
||||
|
||||
msgid "Recurring monthly"
|
||||
msgstr ""
|
||||
msgstr "monatlich wiederkehrend"
|
||||
|
||||
msgid "Amount field does not match"
|
||||
msgstr ""
|
||||
msgstr "Betragsfeld stimmt nicht überein"
|
||||
|
||||
msgid "Recurring field does not match"
|
||||
msgstr ""
|
||||
msgstr "Betragsfeld stimmt nicht überein"
|
||||
|
||||
msgid "Product name"
|
||||
msgstr "Produkt"
|
||||
|
||||
msgid "Monthly subscription"
|
||||
msgstr ""
|
||||
msgstr "Monatliches Abonnement"
|
||||
|
||||
msgid "Yearly subscription"
|
||||
msgstr "Jährliches Abonnement"
|
||||
|
||||
msgid "One time payment"
|
||||
msgstr ""
|
||||
msgstr "Einmalzahlung"
|
||||
|
||||
msgid "Confirm Password"
|
||||
msgstr "Passwort Bestätigung"
|
||||
|
@ -73,7 +76,7 @@ msgid "Please input a proper SSH key"
|
|||
msgstr "Bitte verwende einen gültigen SSH-Key"
|
||||
|
||||
msgid "Comma not accepted in the name of the key"
|
||||
msgstr ""
|
||||
msgstr "Komma im Namen des Keys wird nicht akzeptiert"
|
||||
|
||||
msgid "All Rights Reserved"
|
||||
msgstr "Alle Rechte vorbehalten"
|
||||
|
@ -208,6 +211,9 @@ msgstr "Bezahlbares VM Hosting in der Schweiz"
|
|||
msgid "My Dashboard"
|
||||
msgstr "Mein Dashboard"
|
||||
|
||||
msgid "Welcome"
|
||||
msgstr ""
|
||||
|
||||
msgid "My VMs"
|
||||
msgstr "Meine VMs"
|
||||
|
||||
|
@ -239,7 +245,8 @@ msgid "You can view your VM detail by clicking the button below."
|
|||
msgstr "Um die Rechnung zu sehen, klicke auf den Button unten."
|
||||
|
||||
msgid "You can log in to your VM by the username <strong>puffy</strong>."
|
||||
msgstr "Du kannst Dich auf Deiner VM mit dem user <strong>puffy</strong> einloggen."
|
||||
msgstr ""
|
||||
"Du kannst Dich auf Deiner VM mit dem user <strong>puffy</strong> einloggen."
|
||||
|
||||
msgid "View Detail"
|
||||
msgstr "Details anzeigen"
|
||||
|
@ -360,6 +367,11 @@ msgstr "Abgelehnt"
|
|||
msgid "Billed to"
|
||||
msgstr "Rechnungsadresse"
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "Card Number"
|
||||
msgid "VAT Number"
|
||||
msgstr "Kreditkartennummer"
|
||||
|
||||
msgid "Payment method"
|
||||
msgstr "Bezahlmethode"
|
||||
|
||||
|
@ -387,6 +399,9 @@ msgstr "Festplattenkapazität"
|
|||
msgid "Subtotal"
|
||||
msgstr "Zwischensumme"
|
||||
|
||||
msgid "VAT for"
|
||||
msgstr ""
|
||||
|
||||
msgid "VAT"
|
||||
msgstr "Mehrwertsteuer"
|
||||
|
||||
|
@ -400,13 +415,19 @@ msgid "Amount"
|
|||
msgstr "Betrag"
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
msgstr "Beschreibung"
|
||||
|
||||
msgid "Recurring"
|
||||
msgstr ""
|
||||
msgstr "wiederkehrend"
|
||||
|
||||
msgid "of"
|
||||
msgstr "von"
|
||||
|
||||
msgid "each year"
|
||||
msgstr "jedes Jahr"
|
||||
|
||||
msgid "of every month"
|
||||
msgstr ""
|
||||
msgstr "jeden Monat"
|
||||
|
||||
msgid "BACK TO LIST"
|
||||
msgstr "ZURÜCK ZUR LISTE"
|
||||
|
@ -414,20 +435,21 @@ msgstr "ZURÜCK ZUR LISTE"
|
|||
msgid "Some problem encountered. Please try again later."
|
||||
msgstr "Ein Problem ist aufgetreten. Bitte versuche es später noch einmal."
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "Description"
|
||||
msgid "Subscriptions"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "One time payment"
|
||||
msgid "One-time payments"
|
||||
msgstr "Einmalzahlung"
|
||||
|
||||
msgid "VM ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "IP Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "See Invoice"
|
||||
msgstr "Siehe Rechnung"
|
||||
|
||||
msgid "Page"
|
||||
msgstr ""
|
||||
|
||||
msgid "of"
|
||||
msgstr ""
|
||||
msgstr "IP-Adresse"
|
||||
|
||||
msgid "Log in"
|
||||
msgstr "Anmelden"
|
||||
|
@ -473,11 +495,13 @@ msgstr "Bestellungsübersicht"
|
|||
|
||||
#, python-format
|
||||
msgid ""
|
||||
"By clicking \"Place order\" this plan will charge your credit card account "
|
||||
"with %(vm_price)s CHF/month"
|
||||
"By clicking \"Place order\" you agree to our <a href=\"https://"
|
||||
"datacenterlight.ch/en-us/cms/terms-of-service/\">Terms of Service</a> and "
|
||||
"this plan will charge your credit card account with %(vm_price)s CHF/month."
|
||||
msgstr ""
|
||||
"Wenn Du \"bestellen\" auswählst, wird Deine Kreditkarte mit %(vm_price)s CHF "
|
||||
"pro Monat belastet"
|
||||
"Indem Du auf \"Bestellung aufgeben\" klickst, erklärst Du dich mit unseren"
|
||||
" <a href=\"https://"
|
||||
"datacenterlight.ch/de/cms/terms-of-service/\">Nutzungsbedingungen</a> einverstanden und Dein Kreditkartenkonto wird mit %(vm_price)s CHF/Monat belastet."
|
||||
|
||||
msgid "Place order"
|
||||
msgstr "Bestellen"
|
||||
|
@ -489,7 +513,7 @@ msgid "Hold tight, we are processing your request"
|
|||
msgstr "Bitte warten - wir bearbeiten Deine Anfrage gerade"
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
msgstr "Ok"
|
||||
|
||||
msgid "Close"
|
||||
msgstr "Schliessen"
|
||||
|
@ -497,6 +521,12 @@ msgstr "Schliessen"
|
|||
msgid "Order Nr."
|
||||
msgstr "Bestellung Nr."
|
||||
|
||||
msgid "See Invoice"
|
||||
msgstr "Siehe Rechnung"
|
||||
|
||||
msgid "Page"
|
||||
msgstr "Seite"
|
||||
|
||||
msgid "Your Order"
|
||||
msgstr "Deine Bestellung"
|
||||
|
||||
|
@ -565,6 +595,19 @@ msgstr "Absenden"
|
|||
msgid "Password reset"
|
||||
msgstr "Passwort zurücksetzen"
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "Key name"
|
||||
msgid "My Username"
|
||||
msgstr "Key-Name"
|
||||
|
||||
msgid "Your VAT number has been verified"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Your VAT number is under validation. VAT will be adjusted, once the "
|
||||
"validation is complete."
|
||||
msgstr ""
|
||||
|
||||
msgid "UPDATE"
|
||||
msgstr "AKTUALISIEREN"
|
||||
|
||||
|
@ -766,21 +809,15 @@ msgstr "Dein Passwort konnte nicht zurückgesetzt werden."
|
|||
msgid "The reset password link is no longer valid."
|
||||
msgstr "Der Link zum Zurücksetzen Deines Passwortes ist nicht mehr gültig."
|
||||
|
||||
msgid "Could not set a default card."
|
||||
msgstr ""
|
||||
|
||||
msgid "Card deassociation successful"
|
||||
msgstr "Die Verbindung mit der Karte wurde erfolgreich aufgehoben"
|
||||
|
||||
msgid "You are not permitted to do this operation"
|
||||
msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
|
||||
|
||||
msgid "The selected card does not exist"
|
||||
msgstr "Die ausgewählte Karte existiert nicht"
|
||||
|
||||
msgid "Billing address updated successfully"
|
||||
msgstr "Die Rechnungsadresse wurde erfolgreich aktualisiert"
|
||||
|
||||
msgid "You seem to have already added this card"
|
||||
msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "An error occurred while associating the card. Details: {details}"
|
||||
msgstr ""
|
||||
|
@ -846,6 +883,7 @@ msgstr "Ungültige Speicher-Grösse"
|
|||
#, python-brace-format
|
||||
msgid "Incorrect pricing name. Please contact support{support_email}"
|
||||
msgstr ""
|
||||
"Ungültige Preisbezeichnung. Bitte kontaktiere den Support{support_email}"
|
||||
|
||||
msgid ""
|
||||
"We could not find the requested VM. Please "
|
||||
|
@ -865,6 +903,8 @@ msgid ""
|
|||
"VM terminate action timed out. Please contact support@datacenterlight.ch for "
|
||||
"further information."
|
||||
msgstr ""
|
||||
"VM beendet wegen Zeitüberschreitung. Bitte kontaktiere "
|
||||
"support@datacenterlight.ch für weitere Informationen."
|
||||
|
||||
#, python-format
|
||||
msgid "Virtual Machine %(vm_name)s Cancelled"
|
||||
|
@ -875,6 +915,15 @@ msgstr ""
|
|||
"Es gab einen Fehler bei der Bearbeitung Deine Anfrage. Bitte versuche es "
|
||||
"noch einmal."
|
||||
|
||||
#~ msgid "You are not permitted to do this operation"
|
||||
#~ msgstr "Du hast keine Erlaubnis um diese Operation durchzuführen"
|
||||
|
||||
#~ msgid "The selected card does not exist"
|
||||
#~ msgstr "Die ausgewählte Karte existiert nicht"
|
||||
|
||||
#~ msgid "You seem to have already added this card"
|
||||
#~ msgstr "Es scheint, als hättest du diese Karte bereits hinzugefügt"
|
||||
|
||||
#, python-format
|
||||
#~ msgid "This key exists already with the name \"%(name)s\""
|
||||
#~ msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
import datetime
|
||||
import csv
|
||||
import logging
|
||||
import stripe
|
||||
from hosting.models import VATRates
|
||||
from utils.hosting_utils import get_vat_rate_for_country
|
||||
from django.conf import settings
|
||||
from membership.models import CustomUser, StripeCustomer
|
||||
|
||||
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''CH vat rate changes on 2024-01-01 from 7.7% to 8.1%. This commands makes the necessary changes'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
MAKE_MODIFS=False
|
||||
try:
|
||||
country_to_change = 'CH'
|
||||
currency_to_change = 'CHF'
|
||||
new_rate = 0.081
|
||||
user_country_vat_rate = get_vat_rate_for_country(country_to_change)
|
||||
logger.debug("Existing VATRate for %s %s " % (country_to_change, user_country_vat_rate))
|
||||
vat_rate = VATRates.objects.get(
|
||||
territory_codes=country_to_change, start_date__isnull=False, stop_date=None
|
||||
)
|
||||
logger.debug("VAT rate for %s is %s" % (country_to_change, vat_rate.rate))
|
||||
logger.debug("vat_rate object = %s" % vat_rate)
|
||||
logger.debug("Create end date for the VATRate %s" % vat_rate.id)
|
||||
# if MAKE_MODIFS:
|
||||
# vat_rate.stop_date = datetime.date(2023, 12, 31)
|
||||
# vat_rate.save()
|
||||
# print("Creating a new VATRate for CH")
|
||||
# obj, created = VATRates.objects.get_or_create(
|
||||
# start_date=datetime.date(2024, 1, 1),
|
||||
# stop_date=None,
|
||||
# territory_codes=country_to_change,
|
||||
# currency_code=currency_to_change,
|
||||
# rate=new_rate,
|
||||
# rate_type="standard",
|
||||
# description="Switzerland standard VAT (added manually on %s)" % datetime.datetime.now()
|
||||
# )
|
||||
# if created:
|
||||
# logger.debug("Created new VAT Rate for %s with the new rate %s" % (country_to_change, new_rate))
|
||||
# logger.debug(obj)
|
||||
# else:
|
||||
# logger.debug("VAT Rate for %s already exists with the rate %s" % (country_to_change, new_rate))
|
||||
#
|
||||
logger.debug("Getting all subscriptions of %s that need a VAT Rate change")
|
||||
subscriptions = stripe.Subscription.list(limit=100) # Increase the limit to 100 per page (maximum)
|
||||
ch_subs = []
|
||||
|
||||
while subscriptions:
|
||||
for subscription in subscriptions:
|
||||
if len(subscription.default_tax_rates) > 0 and subscription.default_tax_rates[0].jurisdiction and subscription.default_tax_rates[0].jurisdiction.lower() == 'ch':
|
||||
ch_subs.append(subscription)
|
||||
elif len(subscription.default_tax_rates) > 0:
|
||||
print("subscription %s belongs to %s" % (subscription.id, subscription.default_tax_rates[0].jurisdiction))
|
||||
else:
|
||||
print("subscription %s does not have a tax rate" % subscription.id)
|
||||
if subscriptions.has_more:
|
||||
print("FETCHING MORE")
|
||||
subscriptions = stripe.Subscription.list(limit=100, starting_after=subscriptions.data[-1])
|
||||
else:
|
||||
break
|
||||
logger.debug("There are %s ch subscription that need VAT rate update" % len(ch_subs))
|
||||
|
||||
# CSV column headers
|
||||
csv_headers = [
|
||||
"customer_name",
|
||||
"customer_email",
|
||||
"stripe_customer_id",
|
||||
"subscription_id",
|
||||
"subscription_name",
|
||||
"amount",
|
||||
"vat_rate"
|
||||
]
|
||||
# CSV file name
|
||||
csv_filename = "ch_subscriptions_change_2024.csv"
|
||||
# Write subscription data to CSV file
|
||||
with open(csv_filename, mode='w', newline='') as csv_file:
|
||||
writer = csv.DictWriter(csv_file, fieldnames=csv_headers)
|
||||
writer.writeheader()
|
||||
|
||||
for subscription in ch_subs:
|
||||
subscription_id = subscription["id"]
|
||||
stripe_customer_id = subscription.get("customer", "")
|
||||
vat_rate = subscription.get("tax_percent", "")
|
||||
c_user = CustomUser.objects.get(
|
||||
id=StripeCustomer.objects.filter(stripe_id=stripe_customer_id)[0].user.id)
|
||||
if c_user:
|
||||
customer_name = c_user.name.encode('utf-8')
|
||||
customer_email = c_user.email
|
||||
items = subscription.get("items", {}).get("data", [])
|
||||
for item in items:
|
||||
subscription_name = item.get("plan", {}).get("id", "")
|
||||
amount = item.get("plan", {}).get("amount", "")
|
||||
|
||||
# Convert amount to a proper format (e.g., cents to dollars)
|
||||
amount_in_chf = amount / 100 # Adjust this conversion as needed
|
||||
|
||||
# Writing to CSV
|
||||
writer.writerow({
|
||||
"customer_name": customer_name,
|
||||
"customer_email": customer_email,
|
||||
"stripe_customer_id": stripe_customer_id,
|
||||
"subscription_id": subscription_id,
|
||||
"subscription_name": subscription_name,
|
||||
"amount": amount_in_chf,
|
||||
"vat_rate": vat_rate # Fill in VAT rate if available
|
||||
})
|
||||
else:
|
||||
print("No customuser for %s %s" % (stripe_customer_id, subscription_id))
|
||||
|
||||
|
||||
if MAKE_MODIFS:
|
||||
print("Making modifications now")
|
||||
tax_rate_obj = stripe.TaxRate.create(
|
||||
display_name="VAT",
|
||||
description="VAT for %s" % country_to_change,
|
||||
jurisdiction=country_to_change,
|
||||
percentage=new_rate,
|
||||
inclusive=False,
|
||||
)
|
||||
stripe_tax_rate = StripeTaxRate.objects.create(
|
||||
display_name=tax_rate_obj.display_name,
|
||||
description=tax_rate_obj.description,
|
||||
jurisdiction=tax_rate_obj.jurisdiction,
|
||||
percentage=tax_rate_obj.percentage,
|
||||
inclusive=False,
|
||||
tax_rate_id=tax_rate_obj.id
|
||||
)
|
||||
|
||||
for ch_sub in ch_subs:
|
||||
ch_sub.default_tax_rates = [stripe_tax_rate.tax_rate_id]
|
||||
ch_sub.save()
|
||||
logger.debug("Default tax rate updated for %s" % ch_sub.id)
|
||||
else:
|
||||
print("Not making any modifications because MAKE_MODIFS=False")
|
||||
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
|
@ -1,3 +1,4 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
@ -19,6 +20,10 @@ class Command(BaseCommand):
|
|||
def handle(self, *args, **options):
|
||||
try:
|
||||
for email in options['customer_email']:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
"---------------------------------------------")
|
||||
)
|
||||
stripe_utils = StripeUtils()
|
||||
user = CustomUser.objects.get(email=email)
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
|
@ -39,7 +44,9 @@ class Command(BaseCommand):
|
|||
)
|
||||
if all_invoices_response['error'] is not None:
|
||||
self.stdout.write(self.style.ERROR(all_invoices_response['error']))
|
||||
exit(1)
|
||||
user.import_stripe_bill_remark += "{}: {},".format(datetime.datetime.now(), all_invoices_response['error'])
|
||||
user.save()
|
||||
continue
|
||||
all_invoices = all_invoices_response['response_object']
|
||||
self.stdout.write(self.style.SUCCESS("Obtained {} invoices".format(len(all_invoices) if all_invoices is not None else 0)))
|
||||
num_invoice_created = 0
|
||||
|
@ -50,12 +57,25 @@ class Command(BaseCommand):
|
|||
logger.debug("Invoice %s exists already. Not importing." % invoice['invoice_id'])
|
||||
except MonthlyHostingBill.DoesNotExist as dne:
|
||||
logger.debug("Invoice id %s does not exist" % invoice['invoice_id'])
|
||||
num_invoice_created += 1 if MonthlyHostingBill.create(invoice) is not None else logger.error("Did not import invoice for %s" % str(invoice))
|
||||
|
||||
if MonthlyHostingBill.create(invoice) is not None:
|
||||
num_invoice_created += 1
|
||||
else:
|
||||
user.import_stripe_bill_remark += "{}: Import failed - {},".format(
|
||||
datetime.datetime.now(),
|
||||
invoice['invoice_id'])
|
||||
user.save()
|
||||
logger.error("Did not import invoice for %s"
|
||||
"" % str(invoice))
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS("Number of invoices imported = %s" % num_invoice_created)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'Customer email %s does not have a stripe customer.' % email))
|
||||
user.import_stripe_bill_remark += "{}: No stripecustomer,".format(
|
||||
datetime.datetime.now()
|
||||
)
|
||||
user.save()
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
import csv
|
||||
from hosting.models import VATRates
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv'''
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('csv_file', nargs='+', type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
for c_file in options['csv_file']:
|
||||
print("c_file = %s" % c_file)
|
||||
with open(c_file, mode='r') as csv_file:
|
||||
csv_reader = csv.DictReader(csv_file)
|
||||
line_count = 0
|
||||
for row in csv_reader:
|
||||
if line_count == 0:
|
||||
line_count += 1
|
||||
obj, created = VATRates.objects.get_or_create(
|
||||
start_date=row["start_date"],
|
||||
stop_date=row["stop_date"] if row["stop_date"] is not "" else None,
|
||||
territory_codes=row["territory_codes"],
|
||||
currency_code=row["currency_code"],
|
||||
rate=row["rate"],
|
||||
rate_type=row["rate_type"],
|
||||
description=row["description"]
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
'%s. %s - %s - %s - %s' % (
|
||||
line_count,
|
||||
obj.start_date,
|
||||
obj.stop_date,
|
||||
obj.territory_codes,
|
||||
obj.rate
|
||||
)
|
||||
))
|
||||
line_count+=1
|
||||
|
||||
except Exception as e:
|
||||
print(" *** Error occurred. Details {}".format(str(e)))
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-10-26 04:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0055_auto_20190701_1614'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='hostingbilllineitem',
|
||||
name='amount',
|
||||
field=models.IntegerField(),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-11-15 05:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0056_auto_20191026_0454'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VATRates',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('stop_date', models.DateField(blank=True, null=True)),
|
||||
('territory_codes', models.TextField(blank=True, default='')),
|
||||
('currency_code', models.CharField(max_length=10)),
|
||||
('rate', models.FloatField()),
|
||||
('rate_type', models.TextField(blank=True, default='')),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-11-15 14:57
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0057_vatrates'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='genericproduct',
|
||||
name='product_subscription_interval',
|
||||
field=models.CharField(default='month', help_text='Choose between `year` and `month`', max_length=10),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-01-05 04:29
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0058_genericproduct_product_subscription_interval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StripeTaxRate',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('tax_rate_id', models.CharField(max_length=100, unique=True)),
|
||||
('jurisdiction', models.CharField(max_length=10)),
|
||||
('inclusive', models.BooleanField(default=False)),
|
||||
('display_name', models.CharField(max_length=100)),
|
||||
('percentage', models.FloatField(default=0)),
|
||||
('description', models.CharField(max_length=100)),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-06-30 19:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0059_stripetaxrate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
sql=["update hosting_vatrates set stop_date = '2020-06-30' where territory_codes = 'DE' and rate = '0.19'"],
|
||||
reverse_sql=[
|
||||
"update hosting_vatrates set stop_date = null where stop_date = '2020-06-30' and territory_codes = 'DE' and rate = '0.19'"],
|
||||
),
|
||||
migrations.RunSQL(
|
||||
sql=[
|
||||
"insert into hosting_vatrates (start_date, stop_date, territory_codes, currency_code, rate, rate_type, description) values ('2020-07-01',null,'DE', 'EUR', '0.16', 'standard', 'Germany (member state) standard VAT rate - COVID 19 reduced rate')"],
|
||||
reverse_sql=[
|
||||
"delete from hosting_vatrates where description = 'Germany (member state) standard VAT rate - COVID 19 reduced rate' and start_date = '2020-07-01' and territory_codes = 'DE'" ],
|
||||
),
|
||||
|
||||
migrations.RunSQL(
|
||||
sql=[
|
||||
"update hosting_stripetaxrate set description = 'VAT for DE pre-COVID-19' where description = 'VAT for DE'"],
|
||||
reverse_sql=[
|
||||
"update hosting_stripetaxrate set description = 'VAT for DE' where description = 'VAT for DE pre-COVID-19'"],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-07-21 16:32
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0060_update_DE_vat_covid-19'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='genericproduct',
|
||||
name='exclude_vat_calculations',
|
||||
field=models.BooleanField(default=False, help_text='When checked VAT calculations are excluded for this product'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-12-23 05:36
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0061_genericproduct_exclude_vat_calculations'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IncompleteSubscriptions',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('completed_at', models.DateTimeField()),
|
||||
('subscription_id', models.CharField(max_length=100)),
|
||||
('subscription_status', models.CharField(max_length=30)),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('request', models.TextField()),
|
||||
('stripe_api_cus_id', models.CharField(max_length=30)),
|
||||
('card_details_response', models.TextField()),
|
||||
('stripe_subscription_obj', models.TextField()),
|
||||
('stripe_onetime_charge', models.TextField()),
|
||||
('gp_details', models.TextField()),
|
||||
('specs', models.TextField()),
|
||||
('vm_template_id', models.PositiveIntegerField(default=0)),
|
||||
('template', models.TextField()),
|
||||
('billing_address_data', models.TextField()),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-12-23 06:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0062_incompletesubscriptions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='incompletesubscriptions',
|
||||
name='completed_at',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-12-31 10:13
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import utils.mixins
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0063_auto_20201223_0612'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IncompletePaymentIntents',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('completed_at', models.DateTimeField(null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('payment_intent_id', models.CharField(max_length=100)),
|
||||
('request', models.TextField()),
|
||||
('stripe_api_cus_id', models.CharField(max_length=30)),
|
||||
('card_details_response', models.TextField()),
|
||||
('stripe_subscription_id', models.TextField()),
|
||||
('stripe_charge_id', models.TextField()),
|
||||
('gp_details', models.TextField()),
|
||||
('billing_address_data', models.TextField()),
|
||||
],
|
||||
bases=(utils.mixins.AssignPermissionsMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2020-12-31 10:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0064_incompletepaymentintents'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='incompletepaymentintents',
|
||||
name='stripe_charge_id',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='incompletepaymentintents',
|
||||
name='stripe_subscription_id',
|
||||
field=models.CharField(max_length=100, null=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2023-07-27 08:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hosting', '0065_auto_20201231_1041'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='genericproduct',
|
||||
name='product_price',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='genericproduct',
|
||||
name='product_vat',
|
||||
field=models.DecimalField(decimal_places=4, default=0, max_digits=10),
|
||||
),
|
||||
]
|
|
@ -75,17 +75,28 @@ class GenericProduct(AssignPermissionsMixin, models.Model):
|
|||
)
|
||||
product_description = models.CharField(max_length=500, default="")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
product_price = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
product_vat = models.DecimalField(max_digits=6, decimal_places=4, default=0)
|
||||
product_price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
product_vat = models.DecimalField(max_digits=10, decimal_places=4, default=0)
|
||||
product_is_subscription = models.BooleanField(default=True)
|
||||
product_subscription_interval = models.CharField(
|
||||
max_length=10, default="month",
|
||||
help_text="Choose between `year` and `month`")
|
||||
exclude_vat_calculations = models.BooleanField(
|
||||
default=False,
|
||||
help_text="When checked VAT calculations are excluded for this product"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
def get_actual_price(self):
|
||||
return round(
|
||||
self.product_price + (self.product_price * self.product_vat), 2
|
||||
)
|
||||
def get_actual_price(self, vat_rate=None):
|
||||
if self.exclude_vat_calculations:
|
||||
return round(float(self.product_price), 2)
|
||||
else:
|
||||
VAT = vat_rate if vat_rate is not None else self.product_vat
|
||||
return round(
|
||||
float(self.product_price) + float(self.product_price) * float(VAT), 2
|
||||
)
|
||||
|
||||
|
||||
class HostingOrder(AssignPermissionsMixin, models.Model):
|
||||
|
@ -158,8 +169,12 @@ class HostingOrder(AssignPermissionsMixin, models.Model):
|
|||
|
||||
def set_stripe_charge(self, stripe_charge):
|
||||
self.stripe_charge_id = stripe_charge.id
|
||||
self.last4 = stripe_charge.source.last4
|
||||
self.cc_brand = stripe_charge.source.brand
|
||||
if stripe_charge.source is None:
|
||||
self.last4 = stripe_charge.payment_method_details.card.last4
|
||||
self.cc_brand = stripe_charge.payment_method_details.card.brand
|
||||
else:
|
||||
self.last4 = stripe_charge.source.last4
|
||||
self.cc_brand = stripe_charge.source.brand
|
||||
self.save()
|
||||
|
||||
def set_subscription_id(self, subscription_id, cc_details):
|
||||
|
@ -319,7 +334,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
|
|||
logger.debug("Neither subscription id nor vm_id available")
|
||||
logger.debug("Can't import invoice")
|
||||
return None
|
||||
|
||||
if args['order'] is None:
|
||||
logger.error(
|
||||
"Order is None for {}".format(args['invoice_id']))
|
||||
return None
|
||||
instance = cls.objects.create(
|
||||
created=datetime.utcfromtimestamp(
|
||||
args['created']).replace(tzinfo=pytz.utc),
|
||||
|
@ -337,7 +355,10 @@ class MonthlyHostingBill(AssignPermissionsMixin, models.Model):
|
|||
args['period_start']).replace(tzinfo=pytz.utc),
|
||||
period_end=datetime.utcfromtimestamp(
|
||||
args['period_end']).replace(tzinfo=pytz.utc),
|
||||
billing_reason=args['billing_reason'],
|
||||
billing_reason=(
|
||||
args['billing_reason']
|
||||
if args['billing_reason'] is not None else ''
|
||||
),
|
||||
discount=args['discount'],
|
||||
total=args['total'],
|
||||
lines_data_count=args['lines_data_count'],
|
||||
|
@ -466,7 +487,7 @@ class HostingBillLineItem(AssignPermissionsMixin, models.Model):
|
|||
on_delete=models.CASCADE)
|
||||
stripe_plan = models.ForeignKey(StripePlan, null=True,
|
||||
on_delete=models.CASCADE)
|
||||
amount = models.PositiveSmallIntegerField()
|
||||
amount = models.IntegerField()
|
||||
description = models.CharField(max_length=255)
|
||||
discountable = models.BooleanField()
|
||||
metadata = models.CharField(max_length=128)
|
||||
|
@ -655,7 +676,11 @@ class UserCardDetail(AssignPermissionsMixin, models.Model):
|
|||
stripe_utils = StripeUtils()
|
||||
cus_response = stripe_utils.get_customer(stripe_api_cus_id)
|
||||
cu = cus_response['response_object']
|
||||
cu.default_source = stripe_source_id
|
||||
if stripe_source_id.startswith("pm"):
|
||||
# card is a payment method
|
||||
cu.invoice_settings.default_payment_method = stripe_source_id
|
||||
else:
|
||||
cu.default_source = stripe_source_id
|
||||
cu.save()
|
||||
UserCardDetail.save_default_card_local(
|
||||
stripe_api_cus_id, stripe_source_id
|
||||
|
@ -704,3 +729,54 @@ class UserCardDetail(AssignPermissionsMixin, models.Model):
|
|||
return ucd
|
||||
except UserCardDetail.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class VATRates(AssignPermissionsMixin, models.Model):
|
||||
start_date = models.DateField(blank=True, null=True)
|
||||
stop_date = models.DateField(blank=True, null=True)
|
||||
territory_codes = models.TextField(blank=True, default='')
|
||||
currency_code = models.CharField(max_length=10)
|
||||
rate = models.FloatField()
|
||||
rate_type = models.TextField(blank=True, default='')
|
||||
description = models.TextField(blank=True, default='')
|
||||
|
||||
|
||||
class StripeTaxRate(AssignPermissionsMixin, models.Model):
|
||||
tax_rate_id = models.CharField(max_length=100, unique=True)
|
||||
jurisdiction = models.CharField(max_length=10)
|
||||
inclusive = models.BooleanField(default=False)
|
||||
display_name = models.CharField(max_length=100)
|
||||
percentage = models.FloatField(default=0)
|
||||
description = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class IncompletePaymentIntents(AssignPermissionsMixin, models.Model):
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
payment_intent_id = models.CharField(max_length=100)
|
||||
request = models.TextField()
|
||||
stripe_api_cus_id = models.CharField(max_length=30)
|
||||
card_details_response = models.TextField()
|
||||
stripe_subscription_id = models.CharField(max_length=100, null=True)
|
||||
stripe_charge_id = models.CharField(max_length=100, null=True)
|
||||
gp_details = models.TextField()
|
||||
billing_address_data = models.TextField()
|
||||
|
||||
|
||||
class IncompleteSubscriptions(AssignPermissionsMixin, models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
completed_at = models.DateTimeField(null=True)
|
||||
subscription_id = models.CharField(max_length=100)
|
||||
subscription_status = models.CharField(max_length=30)
|
||||
name = models.CharField(max_length=50)
|
||||
email = models.EmailField()
|
||||
request = models.TextField()
|
||||
stripe_api_cus_id = models.CharField(max_length=30)
|
||||
card_details_response = models.TextField()
|
||||
stripe_subscription_obj = models.TextField()
|
||||
stripe_onetime_charge = models.TextField()
|
||||
gp_details = models.TextField()
|
||||
specs = models.TextField()
|
||||
vm_template_id = models.PositiveIntegerField(default=0)
|
||||
template = models.TextField()
|
||||
billing_address_data = models.TextField()
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
.hosting-dashboard .dashboard-container-head {
|
||||
color: #fff;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.hosting-dashboard-item {
|
||||
|
|
|
@ -2,4 +2,10 @@
|
|||
|
||||
.orders-container .table > tbody > tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media screen and (min-width:767px){
|
||||
.dcl-text-right {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
|
@ -248,6 +248,9 @@
|
|||
.dashboard-title-thin {
|
||||
font-size: 22px;
|
||||
}
|
||||
.dashboard-greetings-thin {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-vm-invoice {
|
||||
|
@ -315,6 +318,11 @@
|
|||
font-size: 32px;
|
||||
}
|
||||
|
||||
.dashboard-greetings-thin {
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.dashboard-title-thin .un-icon {
|
||||
height: 34px;
|
||||
margin-right: 5px;
|
||||
|
@ -411,6 +419,9 @@
|
|||
.dashboard-title-thin {
|
||||
font-size: 22px;
|
||||
}
|
||||
.dashboard-greetings-thin {
|
||||
font-size: 16px;
|
||||
}
|
||||
.dashboard-title-thin .un-icon {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
|
|
|
@ -266,8 +266,8 @@ $( document ).ready(function() {
|
|||
}
|
||||
var total = (cardPricing['cpu'].value * window.coresUnitPrice) +
|
||||
(cardPricing['ram'].value * window.ramUnitPrice) +
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) -
|
||||
window.discountAmount;
|
||||
(cardPricing['storage'].value * window.ssdUnitPrice) +
|
||||
window.vmBasePrice - window.discountAmount;
|
||||
total = parseFloat(total.toFixed(2));
|
||||
$("#total").text(total);
|
||||
}
|
||||
|
|
|
@ -84,68 +84,72 @@ $(document).ready(function () {
|
|||
var hasCreditcard = window.hasCreditcard || false;
|
||||
if (!hasCreditcard && window.stripeKey) {
|
||||
var stripe = Stripe(window.stripeKey);
|
||||
var element_style = {
|
||||
fonts: [{
|
||||
family: 'lato-light',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
|
||||
}, {
|
||||
family: 'lato-regular',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
|
||||
}
|
||||
],
|
||||
locale: window.current_lan
|
||||
};
|
||||
var elements = stripe.elements(element_style);
|
||||
var credit_card_text_style = {
|
||||
base: {
|
||||
iconColor: '#666EE8',
|
||||
color: '#31325F',
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-light', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
color: '#777'
|
||||
if (window.pm_id) {
|
||||
|
||||
} else {
|
||||
var element_style = {
|
||||
fonts: [{
|
||||
family: 'lato-light',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
|
||||
}, {
|
||||
family: 'lato-regular',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
iconColor: '#eb4d5c',
|
||||
color: '#eb4d5c',
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-regular', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
],
|
||||
locale: window.current_lan
|
||||
};
|
||||
var elements = stripe.elements(element_style);
|
||||
var credit_card_text_style = {
|
||||
base: {
|
||||
iconColor: '#666EE8',
|
||||
color: '#31325F',
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-light', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
color: '#777'
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
iconColor: '#eb4d5c',
|
||||
color: '#eb4d5c',
|
||||
fontWeight: 400
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-regular', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
color: '#eb4d5c',
|
||||
fontWeight: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var enter_ccard_text = "Enter your credit card number";
|
||||
if (typeof window.enter_your_card_text !== 'undefined') {
|
||||
enter_ccard_text = window.enter_your_card_text;
|
||||
var enter_ccard_text = "Enter your credit card number";
|
||||
if (typeof window.enter_your_card_text !== 'undefined') {
|
||||
enter_ccard_text = window.enter_your_card_text;
|
||||
}
|
||||
var cardNumberElement = elements.create('cardNumber', {
|
||||
style: credit_card_text_style,
|
||||
placeholder: enter_ccard_text
|
||||
});
|
||||
cardNumberElement.mount('#card-number-element');
|
||||
|
||||
var cardExpiryElement = elements.create('cardExpiry', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardExpiryElement.mount('#card-expiry-element');
|
||||
|
||||
var cardCvcElement = elements.create('cardCvc', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardCvcElement.mount('#card-cvc-element');
|
||||
cardNumberElement.on('change', function (event) {
|
||||
if (event.brand) {
|
||||
setBrandIcon(event.brand);
|
||||
}
|
||||
});
|
||||
}
|
||||
var cardNumberElement = elements.create('cardNumber', {
|
||||
style: credit_card_text_style,
|
||||
placeholder: enter_ccard_text
|
||||
});
|
||||
cardNumberElement.mount('#card-number-element');
|
||||
|
||||
var cardExpiryElement = elements.create('cardExpiry', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardExpiryElement.mount('#card-expiry-element');
|
||||
|
||||
var cardCvcElement = elements.create('cardCvc', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardCvcElement.mount('#card-cvc-element');
|
||||
cardNumberElement.on('change', function (event) {
|
||||
if (event.brand) {
|
||||
setBrandIcon(event.brand);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var submit_form_btn = $('#payment_button_with_creditcard');
|
||||
|
@ -163,7 +167,7 @@ $(document).ready(function () {
|
|||
if (parts.length === 2) return parts.pop().split(";").shift();
|
||||
}
|
||||
|
||||
function submitBillingForm() {
|
||||
function submitBillingForm(pmId) {
|
||||
var billing_form = $('#billing-form');
|
||||
var recurring_input = $('#id_generic_payment_form-recurring');
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-product_name" value="' + $('#id_generic_payment_form-product_name').val() + '" />');
|
||||
|
@ -174,11 +178,40 @@ $(document).ready(function () {
|
|||
billing_form.append('<input type="hidden" name="generic_payment_form-recurring" value="' + (recurring_input.prop('checked') ? 'on' : '') + '" />');
|
||||
}
|
||||
billing_form.append('<input type="hidden" name="generic_payment_form-description" value="' + $('#id_generic_payment_form-description').val() + '" />');
|
||||
billing_form.append('<input type="hidden" name="id_payment_method" value="' + pmId + '" />');
|
||||
billing_form.submit();
|
||||
}
|
||||
|
||||
var $form_new = $('#payment-form-new');
|
||||
$form_new.submit(payWithStripe_new);
|
||||
$form_new.submit(payWithPaymentIntent);
|
||||
window.result = "";
|
||||
window.card = "";
|
||||
function payWithPaymentIntent(e) {
|
||||
e.preventDefault();
|
||||
|
||||
function stripePMHandler(paymentMethod) {
|
||||
// Insert the token ID into the form so it gets submitted to the server
|
||||
console.log(paymentMethod);
|
||||
$('#id_payment_method').val(paymentMethod.id);
|
||||
submitBillingForm(paymentMethod.id);
|
||||
}
|
||||
stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardNumberElement,
|
||||
})
|
||||
.then(function(result) {
|
||||
// Handle result.error or result.paymentMethod
|
||||
window.result = result;
|
||||
if(result.error) {
|
||||
var errorElement = document.getElementById('card-errors');
|
||||
errorElement.textContent = result.error.message;
|
||||
} else {
|
||||
console.log("created paymentMethod " + result.paymentMethod.id);
|
||||
stripePMHandler(result.paymentMethod);
|
||||
}
|
||||
});
|
||||
window.card = cardNumberElement;
|
||||
}
|
||||
function payWithStripe_new(e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -197,7 +230,7 @@ $(document).ready(function () {
|
|||
} else {
|
||||
var process_text = "Processing";
|
||||
if (typeof window.processing_text !== 'undefined') {
|
||||
process_text = window.processing_text
|
||||
process_text = window.processing_text;
|
||||
}
|
||||
|
||||
$form_new.find('[type=submit]').html(process_text + ' <i class="fa fa-spinner fa-pulse"></i>');
|
||||
|
|
|
@ -92,50 +92,117 @@ $(document).ready(function() {
|
|||
});
|
||||
|
||||
var create_vm_form = $('#virtual_machine_create_form');
|
||||
create_vm_form.submit(function () {
|
||||
$('#btn-create-vm').prop('disabled', true);
|
||||
$.ajax({
|
||||
url: create_vm_form.attr('action'),
|
||||
type: 'POST',
|
||||
data: create_vm_form.serialize(),
|
||||
init: function(){
|
||||
ok_btn = $('#createvm-modal-done-btn');
|
||||
close_btn = $('#createvm-modal-close-btn');
|
||||
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
|
||||
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
|
||||
},
|
||||
success: function (data) {
|
||||
fa_icon = $('.modal-icon > .fa');
|
||||
modal_btn = $('#createvm-modal-done-btn');
|
||||
$('#createvm-modal-title').text(data.msg_title);
|
||||
$('#createvm-modal-body').html(data.msg_body);
|
||||
if (data.redirect) {
|
||||
modal_btn.attr('href', data.redirect).removeClass('hide');
|
||||
} else {
|
||||
modal_btn.attr('href', "");
|
||||
}
|
||||
if (data.status === true) {
|
||||
fa_icon.attr('class', 'checkmark');
|
||||
} else {
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
|
||||
}
|
||||
},
|
||||
error: function (xmlhttprequest, textstatus, message) {
|
||||
if (window.isSubscription) {
|
||||
create_vm_form.submit(function () {
|
||||
$('#btn-create-vm').prop('disabled', true);
|
||||
$.ajax({
|
||||
url: create_vm_form.attr('action'),
|
||||
type: 'POST',
|
||||
data: create_vm_form.serialize(),
|
||||
init: function () {
|
||||
ok_btn = $('#createvm-modal-done-btn');
|
||||
close_btn = $('#createvm-modal-close-btn');
|
||||
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
|
||||
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
|
||||
},
|
||||
success: function (data) {
|
||||
fa_icon = $('.modal-icon > .fa');
|
||||
modal_btn = $('#createvm-modal-done-btn');
|
||||
if (data.showSCA) {
|
||||
console.log("Show SCA");
|
||||
var stripe = Stripe(data.STRIPE_PUBLISHABLE_KEY);
|
||||
stripe.confirmCardPayment(data.payment_intent_secret).then(function (result) {
|
||||
if (result.error) {
|
||||
// Display error.message in your UI.
|
||||
modal_btn.attr('href', data.error.redirect).removeClass('hide');
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
|
||||
$('#createvm-modal-title').text(data.error.msg_title);
|
||||
$('#createvm-modal-body').html(data.error.msg_body);
|
||||
} else {
|
||||
// The payment has succeeded. Display a success message.
|
||||
modal_btn.attr('href', data.success.redirect).removeClass('hide');
|
||||
fa_icon.attr('class', 'checkmark');
|
||||
$('#createvm-modal-title').text(data.success.msg_title);
|
||||
$('#createvm-modal-body').html(data.success.msg_body);
|
||||
}
|
||||
});
|
||||
$('#3Dsecure-modal').show();
|
||||
} else {
|
||||
$('#createvm-modal-title').text(data.msg_title);
|
||||
$('#createvm-modal-body').html(data.msg_body);
|
||||
if (data.redirect) {
|
||||
modal_btn.attr('href', data.redirect).removeClass('hide');
|
||||
} else {
|
||||
modal_btn.attr('href', "");
|
||||
}
|
||||
if (data.status === true) {
|
||||
fa_icon.attr('class', 'checkmark');
|
||||
} else {
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (xmlhttprequest, textstatus, message) {
|
||||
fa_icon = $('.modal-icon > .fa');
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
if (typeof(create_vm_error_message) !== 'undefined') {
|
||||
if (typeof (create_vm_error_message) !== 'undefined') {
|
||||
$('#createvm-modal-body').text(create_vm_error_message);
|
||||
}
|
||||
$('#btn-create-vm').prop('disabled', false);
|
||||
$('#createvm-modal-close-btn').removeClass('hide');
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
create_vm_form.submit(placeOrderPaymentIntent);
|
||||
function placeOrderPaymentIntent(e) {
|
||||
e.preventDefault();
|
||||
var stripe = Stripe(window.stripeKey);
|
||||
stripe.confirmCardPayment(
|
||||
window.paymentIntentSecret,
|
||||
{
|
||||
payment_method: window.pm_id
|
||||
}
|
||||
).then(function(result) {
|
||||
window.result = result;
|
||||
fa_icon = $('.modal-icon > .fa');
|
||||
modal_btn = $('#createvm-modal-done-btn');
|
||||
if (result.error) {
|
||||
// Display error.message in your UI.
|
||||
modal_btn.attr('href', error_url).removeClass('hide');
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
|
||||
$('#createvm-modal-title').text(error_title);
|
||||
$('#createvm-modal-body').html(result.error.message + " " + error_msg);
|
||||
} else {
|
||||
// The payment has succeeded
|
||||
// Display a success message
|
||||
modal_btn.attr('href', success_url).removeClass('hide');
|
||||
fa_icon.attr('class', 'checkmark');
|
||||
$('#createvm-modal-title').text(success_title);
|
||||
$('#createvm-modal-body').html(success_msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
$('#createvm-modal').on('hidden.bs.modal', function () {
|
||||
$(this).find('.modal-footer .btn').addClass('hide');
|
||||
})
|
||||
});
|
||||
|
||||
// Toggle subscription and one-time payments div
|
||||
$('#li-one-time-charges').click(function() {
|
||||
console.log("li-one-time-charges clicked");
|
||||
$('#subscriptions').hide();
|
||||
$('#one-time-charges').show();
|
||||
});
|
||||
$('#li-subscriptions').click(function() {
|
||||
console.log("li-subscriptions clicked");
|
||||
$('#one-time-charges').hide();
|
||||
$('#subscriptions').show();
|
||||
});
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
<div class="dashboard-container-head">
|
||||
<h1 class="dashboard-title-thin">{% trans "My Dashboard" %}</h1>
|
||||
</div>
|
||||
<div style="color:#fff; font-size: 18px; font-weight:300; padding: 0 8px; margin-top: 30px; margin-bottom: 30px;">
|
||||
{% trans "Welcome" %} {{request.user.name}}
|
||||
</div>
|
||||
<div class="hosting-dashboard-content">
|
||||
<a href="{% url 'hosting:create_virtual_machine' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "Create VM" %}</h2>
|
||||
|
@ -26,7 +29,7 @@
|
|||
<img class="svg-img" src="{% static 'hosting/img/key.svg' %}">
|
||||
</div>
|
||||
</a>
|
||||
<a href="{% if has_invoices %}{% url 'hosting:invoices' %}{% else %}{% url 'hosting:orders' %}{% endif %}" class="hosting-dashboard-item">
|
||||
<a href="{% url 'hosting:invoices' %}" class="hosting-dashboard-item">
|
||||
<h2>{% trans "My Bills" %}</h2>
|
||||
<div class="hosting-dashboard-image">
|
||||
<img class="svg-img" src="{% static 'hosting/img/billing.svg' %}">
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
{% if messages %}
|
||||
<ul class="list-unstyled msg-list">
|
||||
{% for message in messages %}
|
||||
<div class="alert {% if message.tags and message.tags == 'error' %} alert-danger {% else %} alert-{{message.tags}} {% endif %}">{{ message|safe }}</div>
|
||||
{% if message.tags and 'error' in message.tags %}
|
||||
<p class="card-warning-content card-warning-error">{{ message|safe }}</p>
|
||||
{% elif message.tags %}
|
||||
<div class="alert alert-{{message.tags}}">{{ message|safe }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
|
@ -26,7 +26,7 @@
|
|||
</li>
|
||||
<li class="dropdown highlights-dropdown">
|
||||
<a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-fw fa-user"></i> {{request.user.name}} <span class="fa fa-fw fa-caret-down"></span>
|
||||
<i class="fa fa-fw fa-user"></i> {{request.user.username}} <span class="fa fa-fw fa-caret-down"></span>
|
||||
</a>
|
||||
<ul id="g-account-menu" class="dropdown-menu" role="menu">
|
||||
<li><a href="{% url 'hosting:logout' %}">{% trans "Logout"%}</a></li>
|
||||
|
|
|
@ -70,6 +70,9 @@
|
|||
{{invoice.order.billing_address.postal_code}}<br>
|
||||
{{invoice.order.billing_address.city}},
|
||||
{{invoice.order.billing_address.country}}
|
||||
{% if invoice.order.billing_address.vat_number %}
|
||||
<br/>{% trans "VAT Number" %} {{invoice.order.billing_address.vat_number}}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</address>
|
||||
|
@ -93,10 +96,10 @@
|
|||
<table>
|
||||
<tr><th style="width: 35%">Product</th><th style="width: 20%">Period</th><th style="text-align: center; width: 10%">Qty</th><th align="center" style="width: 10%; text-align: center;">Unit Price</th><th style="width: 10%; text-align: right;">Total</th></tr>
|
||||
{% for line_item in line_items %}
|
||||
<tr class="border_bottom"><td>{% if line_item.description|length > 0 %}{{line_item.description}}{% elif line_item.stripe_plan.stripe_plan_name|length > 0 %}{{line_item.stripe_plan.stripe_plan_name}}{% else %}{{line_item.get_item_detail_str|safe}}{% endif %}</td><td>{{ line_item.period_start | date:'Y-m-d' }} — {{ line_item.period_end | date:'Y-m-d' }}</td><td align="center">{{line_item.quantity}}</td><td align="center">{{line_item.unit_amount_in_chf}}</td><td align="right">{{line_item.amount_in_chf}}</td></tr>
|
||||
<tr class="border_bottom"><td>{% if line_item.description|length > 0 %}{{line_item.description}}{% elif line_item.stripe_plan.stripe_plan_name|length > 0 %}{{line_item.stripe_plan.stripe_plan_name}}{% else %}{{line_item.get_item_detail_str|safe}}{% endif %}</td><td>{{ line_item.period_start | date:'Y-m-d' }} — {{ line_item.period_end | date:'Y-m-d' }}</td><td align="center">{{line_item.quantity}}</td><td align="center">{{line_item.unit_amount_in_chf}}</td><td align="right">{{line_item.amount_in_chf|floatformat:2}}</td></tr>
|
||||
|
||||
{% endfor %}
|
||||
<tr class="grand-total-padding"><td colspan="4">Grand Total</td><td align="right">{{total_in_chf}}</td></tr>
|
||||
<tr class="grand-total-padding"><td colspan="4">Grand Total</td><td align="right">{{total_in_chf|floatformat:2}}</td></tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>
|
||||
|
@ -147,8 +150,12 @@
|
|||
CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
|
||||
{% if vm.after_eu_vat_intro %}
|
||||
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
|
||||
{% else %}
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
|
||||
</small>
|
||||
{% endif %}
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
@ -195,8 +202,13 @@
|
|||
{% if invoice.order.subscription_id %}
|
||||
<p>
|
||||
<span>{% trans "Recurring" %}: </span>
|
||||
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}}
|
||||
{% if invoice.order.generic_product.product_subscription_interval == 'year' %}
|
||||
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}} {% trans "of" %} {{invoice.order.created_at|date:'b'|title}}
|
||||
{% trans "each year" %}</strong>
|
||||
{% else %}
|
||||
<strong class="pull-right">{{invoice.order.created_at|date:'d'|ordinal}}
|
||||
{% trans "of every month" %}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -15,47 +15,116 @@
|
|||
<div class="dashboard-subtitle"></div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active" id="li-subscriptions"><a href="#">{% trans "Subscriptions" %}</a></li>
|
||||
<li id="li-one-time-charges"><a href="#">{% trans "One-time payments" %}</a></li>
|
||||
</ul>
|
||||
<div class="subscriptions" id="subscriptions">
|
||||
<table class="table table-switch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "VM ID" %}</th>
|
||||
<th>{% trans "IP Address" %}</th>
|
||||
<th>{% trans "IP Address" %}/{% trans "Product" %}</th>
|
||||
<th>{% trans "Period" %}</th>
|
||||
<th>{% trans "Amount" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invoice in invoices %}
|
||||
{% for inv_data in invs %}
|
||||
<tr>
|
||||
<td class="xs-td-inline" data-header="{% trans 'VM ID' %}">{{ invoice.order.vm_id }}</td>
|
||||
<td class="xs-td-inline" data-header="{% trans 'IP Address' %}">{{ ips|get_value_from_dict:invoice.invoice_number|join:"<br/>" }}</td>
|
||||
{% with period|get_value_from_dict:invoice.invoice_number as period_to_show %}
|
||||
<td class="xs-td-inline" data-header="{% trans 'Period' %}">{{ period_to_show.period_start | date:'Y-m-d' }} — {{ period_to_show.period_end | date:'Y-m-d' }}</td>
|
||||
{% endwith %}
|
||||
<td class="xs-td-inline" data-header="{% trans 'Amount' %}">{{ invoice.total_in_chf|floatformat:2|intcomma }}</td>
|
||||
<td class="text-right last-td">
|
||||
<a class="btn btn-order-detail" href="{% url 'hosting:invoices' invoice.invoice_number %}">{% trans 'See Invoice' %}</a>
|
||||
</td>
|
||||
{{ inv_data | get_line_item_from_stripe_invoice }}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="pagination">
|
||||
<span class="page-links">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="{{request.path}}?page={{ page_obj.previous_page_number }}">{% trans "previous" %}</a>
|
||||
{% endif %}
|
||||
<span class="page-current">
|
||||
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}.
|
||||
</span>
|
||||
{% if page_obj.has_next %}
|
||||
<a href="{{request.path}}?page={{ page_obj.next_page_number }}">{% trans "next" %}</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if invs.has_other_pages %}
|
||||
<ul class="pagination">
|
||||
{% if invs.has_previous %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ invs.previous_page_number }}&user_email={{user_email}}">«</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ invs.previous_page_number }}">«</a></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="disabled"><span>«</span></li>
|
||||
{% endif %}
|
||||
{% for i in invs.paginator.page_range %}
|
||||
{% if invs.number == i %}
|
||||
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
|
||||
{% else %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ i }}">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if invs.has_next %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ invs.next_page_number }}&user_email={{user_email}}">»</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ invs.next_page_number }}">»</a></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="disabled"><span>»</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="one-time-charges" class="one-time-charges" style="display: none;">
|
||||
<table class="table table-switch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Product" %}</th>
|
||||
<th>{% trans "Date" %}</th>
|
||||
<th>{% trans "Amount" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ho, stripe_charge_data in invs_charge %}
|
||||
<tr>
|
||||
{{ ho.id | get_line_item_from_hosting_order_charge }}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if invs_charge.has_other_pages %}
|
||||
<ul class="pagination">
|
||||
{% if invs_charge.has_previous %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ invs_charge.previous_page_number }}&user_email={{user_email}}">«</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ invs_charge.previous_page_number }}">«</a></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="disabled"><span>«</span></li>
|
||||
{% endif %}
|
||||
{% for i in invs_charge.paginator.page_range %}
|
||||
{% if invs_charge.number == i %}
|
||||
<li class="active"><span>{{ i }} <span class="sr-only">(current)</span></span></li>
|
||||
{% else %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ i }}&user_email={{user_email}}">{{ i }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ i }}">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if invs_charge.has_next %}
|
||||
{% if user_email %}
|
||||
<li><a href="?page={{ invs_charge.next_page_number }}&user_email={{user_email}}">»</a></li>
|
||||
{% else %}
|
||||
<li><a href="?page={{ invs_charge.next_page_number }}">»</a></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="disabled"><span>»</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -62,11 +62,17 @@
|
|||
{{user.name}}<br>
|
||||
{{order.billing_address.street_address}}, {{order.billing_address.postal_code}}<br>
|
||||
{{order.billing_address.city}}, {{order.billing_address.country}}
|
||||
{% if order.billing_address.vat_number %}
|
||||
<br/>{% trans "VAT Number" %} {{order.billing_address.vat_number}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with request.session.billing_address_data as billing_address %}
|
||||
{{billing_address.cardholder_name}}<br>
|
||||
{{billing_address.street_address}}, {{billing_address.postal_code}}<br>
|
||||
{{billing_address.city}}, {{billing_address.country}}
|
||||
{% if billing_address.vat_number %}
|
||||
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
@ -142,7 +148,12 @@
|
|||
<strong class="pull-right">{{vm.price|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
<p>
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%) </small>
|
||||
{% if vm.after_eu_vat_intro %}
|
||||
<small>{% trans "VAT for" %} {{vm.vat_country}} ({{vm.vat_percent}}%) : </small>
|
||||
{% else %}
|
||||
<small>{% trans "VAT" %} ({{ vm.vat_percent|floatformat:2|intcomma }}%)
|
||||
</small>
|
||||
{% endif %}
|
||||
<strong class="pull-right">{{vm.vat|floatformat:2|intcomma}} CHF</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
@ -186,7 +197,13 @@
|
|||
{% if order.subscription_id %}
|
||||
<p>
|
||||
<span>{% trans "Recurring" %}: </span>
|
||||
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of every month" %}</strong>
|
||||
{% if order.generic_product.product_subscription_interval == 'year' %}
|
||||
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}} {% trans "of" %} {{order.created_at|date:'b'|title}}
|
||||
{% trans "each year" %}</strong>
|
||||
{% else %}
|
||||
<strong class="pull-right">{{order.created_at|date:'d'|ordinal}}
|
||||
{% trans "of every month" %}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -201,7 +218,7 @@
|
|||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" this plan will charge your credit card account with {{ vm_price }} CHF/month{% endblocktrans %}.</div>
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_price=vm.total_price|floatformat:2|intcomma %}By clicking "Place order" you agree to our <a href="https://datacenterlight.ch/en-us/cms/terms-of-service/">Terms of Service</a> and this plan will charge your credit card account with {{ vm_price }} CHF/month.{% endblocktrans %}.</div>
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
<button class="btn choice-btn" id="btn-create-vm" data-href="{% url 'hosting:order-confirmation' %}" data-toggle="modal" data-target="#createvm-modal">
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
<div class="settings-container">
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-6 billing dcl-billing">
|
||||
<h3><b>{%trans "My Username"%}</b></h3>
|
||||
<hr class="top-hr">
|
||||
<p>{{request.user.username}}</p>
|
||||
<br>
|
||||
<h3>{%trans "Billing Address" %}</h3>
|
||||
<hr>
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
|
@ -22,6 +26,16 @@
|
|||
{% for field in form %}
|
||||
{% bootstrap_field field show_label=False type='fields' bound_css_class='' %}
|
||||
{% endfor %}
|
||||
{% if form.instance.vat_number %}
|
||||
{% if form.instance.vat_validation_status != "ch_vat" and form.instance.vat_validation_status != "not_needed" %}
|
||||
|
||||
{% if form.instance.vat_validation_status == "verified" %}
|
||||
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
|
||||
{% elif form.instance.vat_validation_status == "pending" %}
|
||||
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-vm-contact btn-wide" name="billing-form">{% trans "UPDATE" %}</button>
|
||||
</div>
|
||||
|
|
|
@ -81,12 +81,10 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if user_key.private_key %}
|
||||
<form action="{{ user_key.private_key.url }}">
|
||||
<button style="color: #717274" type="submit" class="btn btn-default">
|
||||
<a class="btn btn-default" style='color: #717274;' href="{{ user_key.private_key.url }}" download="{{user_key.private_key.name}}">
|
||||
<span class="pc-only">{% trans "Download" %}</span>
|
||||
<span class="mob-only"><i class="fa fa-download"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -45,8 +45,8 @@
|
|||
<h2 class="vm-detail-title">{% trans "Billing" %} <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon"></h2>
|
||||
<div class="vm-vmid">
|
||||
<div class="vm-item-subtitle">{% trans "Current Pricing" %}</div>
|
||||
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% trans "Month" %}</div>
|
||||
<a class="btn btn-vm-invoice" href="{% url 'hosting:orders' order.pk %}">{% trans "See Invoice" %}</a>
|
||||
<div class="vm-item-lg">{{order.price|floatformat:2|intcomma}} CHF/{% if order.generic_product %}{% trans order.generic_product.product_subscription_interval %}{% else %}{% trans "Month" %}{% endif %}</div>
|
||||
{% if inv_url %}<a class="btn btn-vm-invoice" href="{{inv_url}}" target="_blank">{% trans "See Invoice" %}</a>{%else%}{% trans "No invoice as of now" %}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vm-detail-item">
|
||||
|
|
|
@ -51,7 +51,7 @@ urlpatterns = [
|
|||
name='choice_ssh_keys'),
|
||||
url(r'delete_ssh_key/(?P<pk>\d+)/?$', SSHKeyDeleteView.as_view(),
|
||||
name='delete_ssh_key'),
|
||||
url(r'delete_card/(?P<pk>\d+)/?$', SettingsView.as_view(),
|
||||
url(r'delete_card/(?P<pk>[\w\-]+)/$', SettingsView.as_view(),
|
||||
name='delete_card'),
|
||||
url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
|
||||
name='create_ssh_key'),
|
||||
|
|
657
hosting/views.py
657
hosting/views.py
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
from time import sleep
|
||||
|
||||
import stripe
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
|
@ -10,7 +12,9 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.core.urlresolvers import reverse_lazy, reverse
|
||||
from django.db.models import Q
|
||||
from django.http import (
|
||||
Http404, HttpResponseRedirect, HttpResponse, JsonResponse
|
||||
)
|
||||
|
@ -37,8 +41,10 @@ from stored_messages.settings import stored_messages_settings
|
|||
|
||||
from datacenterlight.cms_models import DCLCalculatorPluginModel
|
||||
from datacenterlight.models import VMTemplate, VMPricing
|
||||
from datacenterlight.utils import create_vm, get_cms_integration, check_otp
|
||||
from hosting.models import UserCardDetail
|
||||
from datacenterlight.utils import (
|
||||
create_vm, get_cms_integration, check_otp, validate_vat_number
|
||||
)
|
||||
from hosting.models import UserCardDetail, StripeTaxRate
|
||||
from membership.models import CustomUser, StripeCustomer
|
||||
from opennebula_api.models import OpenNebulaManager
|
||||
from opennebula_api.serializers import (
|
||||
|
@ -50,7 +56,11 @@ from utils.forms import (
|
|||
ResendActivationEmailForm
|
||||
)
|
||||
from utils.hosting_utils import get_all_public_keys
|
||||
from utils.hosting_utils import get_vm_price_with_vat, HostingUtils
|
||||
from utils.hosting_utils import (
|
||||
get_vm_price_with_vat, get_vm_price_for_given_vat, HostingUtils,
|
||||
get_vat_rate_for_country
|
||||
)
|
||||
from utils.ldap_manager import LdapManager
|
||||
from utils.mailer import BaseEmail
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.tasks import send_plain_email_task
|
||||
|
@ -65,7 +75,7 @@ from .forms import (
|
|||
from .mixins import ProcessVMSelectionMixin, HostingContextMixin
|
||||
from .models import (
|
||||
HostingOrder, HostingBill, HostingPlan, UserHostingKey, VMDetail,
|
||||
GenericProduct, MonthlyHostingBill, HostingBillLineItem
|
||||
GenericProduct, MonthlyHostingBill
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -378,7 +388,7 @@ class PasswordResetConfirmView(HostingContextMixin,
|
|||
user = CustomUser.objects.get(pk=uid)
|
||||
|
||||
opennebula_client = OpenNebulaManager(
|
||||
email=user.email,
|
||||
email=user.username,
|
||||
password=user.password,
|
||||
)
|
||||
|
||||
|
@ -391,21 +401,30 @@ class PasswordResetConfirmView(HostingContextMixin,
|
|||
if user is not None and default_token_generator.check_token(user,
|
||||
token):
|
||||
if form.is_valid():
|
||||
ldap_manager = LdapManager()
|
||||
new_password = form.cleaned_data['new_password2']
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
messages.success(request, _('Password has been reset.'))
|
||||
|
||||
# Change opennebula password
|
||||
opennebula_client.change_user_password(user.password)
|
||||
# Make sure the user have an ldap account already
|
||||
user.create_ldap_account(new_password)
|
||||
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
messages.error(
|
||||
request, _('Password reset has not been successful.'))
|
||||
form.add_error(None,
|
||||
_('Password reset has not been successful.'))
|
||||
return self.form_invalid(form)
|
||||
# We are changing password in ldap before changing in database because
|
||||
# ldap have more chances of failure than local database
|
||||
if ldap_manager.change_password(user.username, new_password):
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
messages.success(request, _('Password has been reset.'))
|
||||
|
||||
# Change opennebula password
|
||||
opennebula_client.change_user_password(user.password)
|
||||
|
||||
return self.form_valid(form)
|
||||
|
||||
messages.error(
|
||||
request, _('Password reset has not been successful.'))
|
||||
form.add_error(None,
|
||||
_('Password reset has not been successful.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
else:
|
||||
error_msg = _('The reset password link is no longer valid.')
|
||||
|
@ -461,7 +480,7 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
|
|||
def delete(self, request, *args, **kwargs):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email,
|
||||
email=owner.username,
|
||||
password=owner.password
|
||||
)
|
||||
pk = self.kwargs.get('pk')
|
||||
|
@ -515,7 +534,7 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
|
|||
ssh_key.private_key.save(filename, content)
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email,
|
||||
email=owner.username,
|
||||
password=owner.password
|
||||
)
|
||||
keys = get_all_public_keys(request.user)
|
||||
|
@ -535,20 +554,31 @@ class SettingsView(LoginRequiredMixin, FormView):
|
|||
Check if the user already saved contact details. If so, then show
|
||||
the form populated with those details, to let user change them.
|
||||
"""
|
||||
username = self.request.GET.get('username')
|
||||
if self.request.user.is_admin and username:
|
||||
user = CustomUser.objects.get(username=username)
|
||||
else:
|
||||
user = self.request.user
|
||||
return form_class(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
instance=user.billing_addresses.first(),
|
||||
**self.get_form_kwargs())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SettingsView, self).get_context_data(**kwargs)
|
||||
# Get user
|
||||
user = self.request.user
|
||||
username = self.request.GET.get('username')
|
||||
if self.request.user.is_admin and username:
|
||||
user = CustomUser.objects.get(username=username)
|
||||
else:
|
||||
user = self.request.user
|
||||
stripe_customer = None
|
||||
if hasattr(user, 'stripecustomer'):
|
||||
stripe_customer = user.stripecustomer
|
||||
cards_list = UserCardDetail.get_all_cards_list(
|
||||
stripe_customer=stripe_customer
|
||||
stripe_utils = StripeUtils()
|
||||
cards_list_request = stripe_utils.get_available_payment_methods(
|
||||
stripe_customer
|
||||
)
|
||||
cards_list = cards_list_request.get('response_object')
|
||||
context.update({
|
||||
'cards_list': cards_list,
|
||||
'stripe_key': settings.STRIPE_API_PUBLIC_KEY
|
||||
|
@ -559,108 +589,155 @@ class SettingsView(LoginRequiredMixin, FormView):
|
|||
def post(self, request, *args, **kwargs):
|
||||
if 'card' in request.POST and request.POST['card'] is not '':
|
||||
card_id = escape(request.POST['card'])
|
||||
user_card_detail = UserCardDetail.objects.get(id=card_id)
|
||||
UserCardDetail.set_default_card(
|
||||
stripe_api_cus_id=request.user.stripecustomer.stripe_id,
|
||||
stripe_source_id=user_card_detail.card_id
|
||||
stripe_source_id=card_id
|
||||
)
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_payment_method(
|
||||
card_id
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
logger.debug("Could not find card %s in stripe" % card_id)
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_("Could not set a default card."))
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
card_details_response = card_details['response_object']
|
||||
msg = _(
|
||||
("Your {brand} card ending in {last4} set as "
|
||||
"default card").format(
|
||||
brand=user_card_detail.brand,
|
||||
last4=user_card_detail.last4
|
||||
brand=card_details_response['brand'],
|
||||
last4=card_details_response['last4']
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
if 'delete_card' in request.POST:
|
||||
try:
|
||||
card = UserCardDetail.objects.get(pk=self.kwargs.get('pk'))
|
||||
if (request.user.has_perm(self.permission_required[0], card)
|
||||
and
|
||||
request.user
|
||||
.stripecustomer
|
||||
.usercarddetail_set
|
||||
.count() > 1):
|
||||
if card.card_id is not None:
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_utils.dissociate_customer_card(
|
||||
request.user.stripecustomer.stripe_id,
|
||||
card.card_id
|
||||
)
|
||||
if card.preferred:
|
||||
UserCardDetail.set_default_card_from_stripe(
|
||||
request.user.stripecustomer.stripe_id
|
||||
)
|
||||
card.delete()
|
||||
msg = _("Card deassociation successful")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
msg = _("You are not permitted to do this operation")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
except UserCardDetail.DoesNotExist:
|
||||
msg = _("The selected card does not exist")
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
card = self.kwargs.get('pk')
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_utils.dissociate_customer_card(
|
||||
request.user.stripecustomer.stripe_id,
|
||||
card
|
||||
)
|
||||
msg = _("Card deassociation successful")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return HttpResponseRedirect(reverse_lazy('hosting:settings'))
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
if 'billing-form' in request.POST:
|
||||
current_billing_address = self.request.user.billing_addresses.last()
|
||||
billing_address_data = form.cleaned_data
|
||||
billing_address_data.update({
|
||||
'user': self.request.user.id
|
||||
})
|
||||
billing_address_user_form = UserBillingAddressForm(
|
||||
instance=self.request.user.billing_addresses.first(),
|
||||
instance=self.request.user.billing_addresses.order_by('-id').first(),
|
||||
data=billing_address_data)
|
||||
billing_address_user_form.save()
|
||||
msg = _("Billing address updated successfully")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
billing_address = billing_address_user_form.save()
|
||||
billing_address.stripe_tax_id = ''
|
||||
billing_address.vat_number_validated_on = None
|
||||
billing_address.vat_validation_status = ''
|
||||
billing_address.save()
|
||||
vat_number = billing_address_user_form.cleaned_data.get(
|
||||
'vat_number').strip()
|
||||
logger.debug("Vat number = %s" % vat_number)
|
||||
if vat_number:
|
||||
try:
|
||||
stripe_customer = request.user.stripecustomer
|
||||
except StripeCustomer.DoesNotExist as dne:
|
||||
logger.debug(
|
||||
"User %s does not have a stripecustomer. "
|
||||
"Creating one." % request.user.email)
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=request.user.email,
|
||||
token=None)
|
||||
request.user.stripecustomer = stripe_customer
|
||||
request.user.save()
|
||||
validate_result = validate_vat_number(
|
||||
stripe_customer_id=request.user.stripecustomer.stripe_id,
|
||||
billing_address_id=billing_address.id,
|
||||
is_user_ba=True
|
||||
)
|
||||
logger.debug("validate_result = %s" % str(validate_result))
|
||||
if 'error' in validate_result and validate_result['error']:
|
||||
messages.add_message(
|
||||
request, messages.ERROR,
|
||||
"VAT Number validation error: %s" % validate_result["error"],
|
||||
extra_tags='error'
|
||||
)
|
||||
billing_address = current_billing_address
|
||||
if billing_address:
|
||||
billing_address.save()
|
||||
email_data = {
|
||||
'subject': "%s updated VAT number to %s but failed" %
|
||||
(request.user.email, vat_number),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in
|
||||
validate_result.items()]),
|
||||
}
|
||||
else:
|
||||
email_data = {
|
||||
'subject': "%s updated VAT number to %s" % (
|
||||
request.user.email, vat_number
|
||||
),
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': settings.DCL_ERROR_EMAILS_TO_LIST,
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in
|
||||
validate_result.items()]),
|
||||
}
|
||||
msg = _("Billing address updated successfully")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
send_plain_email_task.delay(email_data)
|
||||
else:
|
||||
msg = _("Billing address updated successfully")
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
else:
|
||||
token = form.cleaned_data.get('token')
|
||||
id_payment_method = request.POST.get('id_payment_method', None)
|
||||
stripe_utils = StripeUtils()
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
token
|
||||
card_details = stripe_utils.get_cards_details_from_payment_method(
|
||||
id_payment_method
|
||||
)
|
||||
if not card_details.get('response_object'):
|
||||
form.add_error("__all__", card_details.get('error'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
stripe_customer = StripeCustomer.get_or_create(
|
||||
email=request.user.email, token=token
|
||||
email=request.user.email, id_payment_method=id_payment_method
|
||||
)
|
||||
card = card_details['response_object']
|
||||
if UserCardDetail.get_user_card_details(stripe_customer, card):
|
||||
msg = _('You seem to have already added this card')
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
else:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
request.user.stripecustomer.stripe_id, token
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
preferred = False
|
||||
if stripe_customer.usercarddetail_set.count() == 0:
|
||||
preferred = True
|
||||
UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card['last4'],
|
||||
brand=card['brand'],
|
||||
fingerprint=card['fingerprint'],
|
||||
exp_month=card['exp_month'],
|
||||
exp_year=card['exp_year'],
|
||||
card_id=card['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
request.user.stripecustomer.stripe_id,
|
||||
id_payment_method,
|
||||
set_as_default=True
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
msg = _(
|
||||
"Successfully associated the card with your account"
|
||||
'An error occurred while associating the card.'
|
||||
' Details: {details}'.format(
|
||||
details=acc_result['error']
|
||||
)
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
messages.add_message(request, messages.ERROR, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
preferred = False
|
||||
if stripe_customer.usercarddetail_set.count() == 0:
|
||||
preferred = True
|
||||
UserCardDetail.create(
|
||||
stripe_customer=stripe_customer,
|
||||
last4=card['last4'],
|
||||
brand=card['brand'],
|
||||
fingerprint=card['fingerprint'],
|
||||
exp_month=card['exp_month'],
|
||||
exp_year=card['exp_year'],
|
||||
card_id=card['card_id'],
|
||||
preferred=preferred
|
||||
)
|
||||
msg = _(
|
||||
"Successfully associated the card with your account"
|
||||
)
|
||||
messages.add_message(request, messages.SUCCESS, msg)
|
||||
return self.render_to_response(self.get_context_data())
|
||||
else:
|
||||
billing_address_data = form.cleaned_data
|
||||
|
@ -673,7 +750,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
form_class = BillingAddressForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
current_billing_address = self.request.user.billing_addresses.first()
|
||||
current_billing_address = self.request.user.billing_addresses.last()
|
||||
form_kwargs = super(PaymentVMView, self).get_form_kwargs()
|
||||
if not current_billing_address:
|
||||
return form_kwargs
|
||||
|
@ -685,6 +762,7 @@ class PaymentVMView(LoginRequiredMixin, FormView):
|
|||
'city': current_billing_address.city,
|
||||
'postal_code': current_billing_address.postal_code,
|
||||
'country': current_billing_address.country,
|
||||
'vat_number': current_billing_address.vat_number
|
||||
}
|
||||
})
|
||||
return form_kwargs
|
||||
|
@ -845,40 +923,66 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
context['vm'] = vm_detail.__dict__
|
||||
context['vm']['name'] = '{}-{}'.format(
|
||||
context['vm']['configuration'], context['vm']['vm_id'])
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
user_vat_country = obj.billing_address.country
|
||||
user_country_vat_rate = get_vat_rate_for_country(
|
||||
user_vat_country)
|
||||
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
if obj.vm_pricing else 'default'),
|
||||
vat_rate= (
|
||||
user_country_vat_rate * 100
|
||||
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else settings.PRE_EU_VAT_RATE
|
||||
)
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = price + vat - discount['amount']
|
||||
context['vm']["after_eu_vat_intro"] = (
|
||||
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else False
|
||||
)
|
||||
context['vm']["price"] = price
|
||||
context['vm']["vat"] = vat
|
||||
context['vm']["vat_percent"] = vat_percent
|
||||
context['vm']["vat_country"] = user_vat_country
|
||||
context['vm']["discount"] = discount
|
||||
context['vm']["total_price"] = round(
|
||||
price + vat - discount['amount'], 2)
|
||||
context['subscription_end_date'] = vm_detail.end_date()
|
||||
except VMDetail.DoesNotExist:
|
||||
try:
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email, password=owner.password
|
||||
email=owner.username, password=owner.password
|
||||
)
|
||||
vm = manager.get_vm(obj.vm_id)
|
||||
context['vm'] = VirtualMachineSerializer(vm).data
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
user_vat_country = obj.billing_address.country
|
||||
user_country_vat_rate = get_vat_rate_for_country(
|
||||
user_vat_country)
|
||||
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.vm_pricing.name
|
||||
if obj.vm_pricing else 'default')
|
||||
if obj.vm_pricing else 'default'),
|
||||
vat_rate=(
|
||||
user_country_vat_rate * 100
|
||||
if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else settings.PRE_EU_VAT_RATE
|
||||
)
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = (
|
||||
price + vat - discount['amount']
|
||||
context['vm']["after_eu_vat_intro"] = (
|
||||
True if obj.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else False
|
||||
)
|
||||
context['vm']["price"] = price
|
||||
context['vm']["vat"] = vat
|
||||
context['vm']["vat_percent"] = vat_percent
|
||||
context['vm']["vat_country"] = user_vat_country
|
||||
context['vm']["discount"] = discount
|
||||
context['vm']["total_price"] = round(
|
||||
price + vat - discount['amount'], 2)
|
||||
except WrongIdError:
|
||||
messages.error(
|
||||
self.request,
|
||||
|
@ -916,7 +1020,27 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
context['cc_exp_year'] = card_detail.exp_year
|
||||
context['cc_exp_month'] = '{:02d}'.format(card_detail.exp_month)
|
||||
context['site_url'] = reverse('hosting:create_virtual_machine')
|
||||
context['vm'] = self.request.session.get('specs')
|
||||
vm_specs = self.request.session.get('specs')
|
||||
user_vat_country = (
|
||||
self.request.session.get('billing_address_data').get("country")
|
||||
)
|
||||
user_country_vat_rate = get_vat_rate_for_country(user_vat_country)
|
||||
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
|
||||
cpu=vm_specs['cpu'],
|
||||
memory=vm_specs['memory'],
|
||||
ssd_size=vm_specs['disk_size'],
|
||||
pricing_name=vm_specs['pricing_name'],
|
||||
vat_rate=user_country_vat_rate * 100
|
||||
)
|
||||
vm_specs["price"] = price
|
||||
vm_specs["vat"] = vat
|
||||
vm_specs["vat_percent"] = vat_percent
|
||||
vm_specs["vat_country"] = user_vat_country
|
||||
vm_specs["discount"] = discount
|
||||
vm_specs["total_price"] = round(price + vat - discount['amount'],
|
||||
2)
|
||||
vm_specs["after_eu_vat_intro"] = True
|
||||
context['vm'] = vm_specs
|
||||
return context
|
||||
|
||||
@method_decorator(decorators)
|
||||
|
@ -954,7 +1078,13 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
billing_address_data = request.session.get('billing_address_data')
|
||||
vm_template_id = template.get('id', 1)
|
||||
stripe_api_cus_id = request.user.stripecustomer.stripe_id
|
||||
if 'token' in self.request.session:
|
||||
logger.debug("template=%s specs=%s stripe_customer_id=%s "
|
||||
"billing_address_data=%s vm_template_id=%s "
|
||||
"stripe_api_cus_id=%s" % (
|
||||
template, specs, stripe_customer_id, billing_address_data,
|
||||
vm_template_id, stripe_api_cus_id)
|
||||
)
|
||||
if 'id_payment_method' in self.request.session:
|
||||
card_details = stripe_utils.get_cards_details_from_token(
|
||||
request.session['token']
|
||||
)
|
||||
|
@ -971,7 +1101,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
)
|
||||
if not ucd:
|
||||
acc_result = stripe_utils.associate_customer_card(
|
||||
stripe_api_cus_id, request.session['token'],
|
||||
stripe_api_cus_id, request.session['id_payment_method'],
|
||||
set_as_default=True
|
||||
)
|
||||
if acc_result['response_object'] is None:
|
||||
|
@ -1012,7 +1142,8 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
cpu = specs.get('cpu')
|
||||
memory = specs.get('memory')
|
||||
disk_size = specs.get('disk_size')
|
||||
amount_to_be_charged = specs.get('total_price')
|
||||
amount_to_be_charged = specs.get('price')
|
||||
discount = specs.get('discount')
|
||||
plan_name = StripeUtils.get_stripe_plan_name(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
|
@ -1031,11 +1162,61 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
amount=amount_to_be_charged,
|
||||
name=plan_name,
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
# Create StripeTaxRate if applicable to the user
|
||||
stripe_tax_rate = None
|
||||
if specs["vat_percent"] > 0:
|
||||
try:
|
||||
stripe_tax_rate = StripeTaxRate.objects.get(
|
||||
description="VAT for %s" % specs["vat_country"]
|
||||
)
|
||||
print("Stripe Tax Rate exists")
|
||||
except StripeTaxRate.DoesNotExist as dne:
|
||||
print("StripeTaxRate does not exist")
|
||||
tax_rate_obj = stripe.TaxRate.create(
|
||||
display_name="VAT",
|
||||
description="VAT for %s" % specs["vat_country"],
|
||||
jurisdiction=specs["vat_country"],
|
||||
percentage=specs["vat_percent"] * 100,
|
||||
inclusive=False,
|
||||
)
|
||||
stripe_tax_rate = StripeTaxRate.objects.create(
|
||||
display_name=tax_rate_obj.display_name,
|
||||
description=tax_rate_obj.description,
|
||||
jurisdiction=tax_rate_obj.jurisdiction,
|
||||
percentage=tax_rate_obj.percentage,
|
||||
inclusive=False,
|
||||
tax_rate_id=tax_rate_obj.id
|
||||
)
|
||||
logger.debug("Created StripeTaxRate %s" %
|
||||
stripe_tax_rate.tax_rate_id)
|
||||
subscription_result = stripe_utils.subscribe_customer_to_plan(
|
||||
stripe_api_cus_id,
|
||||
[{"plan": stripe_plan.get(
|
||||
'response_object').stripe_plan_id}])
|
||||
[{"plan": stripe_plan.get('response_object').stripe_plan_id}],
|
||||
coupon=(discount['stripe_coupon_id']
|
||||
if 'name' in discount and
|
||||
discount['name'] is not None and
|
||||
'ipv6' in discount['name'].lower() and
|
||||
discount['stripe_coupon_id']
|
||||
else ""),
|
||||
tax_rates=[stripe_tax_rate.tax_rate_id] if stripe_tax_rate else [],
|
||||
default_payment_method=request.session['id_payment_method']
|
||||
)
|
||||
stripe_subscription_obj = subscription_result.get('response_object')
|
||||
latest_invoice = stripe.Invoice.retrieve(stripe_subscription_obj.latest_invoice)
|
||||
ret = stripe.PaymentIntent.confirm(
|
||||
latest_invoice.payment_intent
|
||||
)
|
||||
if ret.status == 'requires_source_action' or ret.status == 'requires_action':
|
||||
pi = stripe.PaymentIntent.retrieve(
|
||||
latest_invoice.payment_intent
|
||||
)
|
||||
context = {
|
||||
'sid': stripe_subscription_obj.id,
|
||||
'payment_intent_secret': pi.client_secret,
|
||||
'STRIPE_PUBLISHABLE_KEY': settings.STRIPE_API_PUBLIC_KEY,
|
||||
'showSCA': True
|
||||
}
|
||||
return JsonResponse(context)
|
||||
# Check if the subscription was approved and is active
|
||||
if (stripe_subscription_obj is None or
|
||||
stripe_subscription_obj.status != 'active'):
|
||||
|
@ -1074,6 +1255,7 @@ class OrdersHostingDetailView(LoginRequiredMixin, DetailView, FormView):
|
|||
user = {
|
||||
'name': self.request.user.name,
|
||||
'email': self.request.user.email,
|
||||
'username': self.request.user.username,
|
||||
'pass': self.request.user.password,
|
||||
'request_scheme': request.scheme,
|
||||
'request_host': request.get_host(),
|
||||
|
@ -1117,7 +1299,7 @@ class OrdersHostingListView(LoginRequiredMixin, ListView):
|
|||
return super(OrdersHostingListView, self).get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class InvoiceListView(LoginRequiredMixin, ListView):
|
||||
class InvoiceListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "hosting/invoices.html"
|
||||
login_url = reverse_lazy('hosting:login')
|
||||
context_object_name = "invoices"
|
||||
|
@ -1125,10 +1307,14 @@ class InvoiceListView(LoginRequiredMixin, ListView):
|
|||
ordering = '-created'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
page = self.request.GET.get('page', 1)
|
||||
context = super(InvoiceListView, self).get_context_data(**kwargs)
|
||||
invs_page = None
|
||||
invs_page_charges = None
|
||||
if ('user_email' in self.request.GET
|
||||
and self.request.user.email == settings.ADMIN_EMAIL):
|
||||
user_email = self.request.GET['user_email']
|
||||
context['user_email'] = '%s' % quote(user_email)
|
||||
logger.debug(
|
||||
"user_email = {}".format(user_email)
|
||||
)
|
||||
|
@ -1137,53 +1323,66 @@ class InvoiceListView(LoginRequiredMixin, ListView):
|
|||
except CustomUser.DoesNotExist as dne:
|
||||
logger.debug("User does not exist")
|
||||
cu = self.request.user
|
||||
mhbs = MonthlyHostingBill.objects.filter(customer__user=cu)
|
||||
else:
|
||||
mhbs = MonthlyHostingBill.objects.filter(
|
||||
customer__user=self.request.user
|
||||
)
|
||||
ips_dict = {}
|
||||
line_item_period_dict = {}
|
||||
for mhb in mhbs:
|
||||
invs = stripe.Invoice.list(customer=cu.stripecustomer.stripe_id,
|
||||
count=100,
|
||||
status='paid')
|
||||
paginator = Paginator(invs.data, 10)
|
||||
try:
|
||||
vm_detail = VMDetail.objects.get(vm_id=mhb.order.vm_id)
|
||||
ips_dict[mhb.invoice_number] = [vm_detail.ipv6, vm_detail.ipv4]
|
||||
all_line_items = HostingBillLineItem.objects.filter(monthly_hosting_bill=mhb)
|
||||
for line_item in all_line_items:
|
||||
if line_item.get_item_detail_str() != "":
|
||||
line_item_period_dict[mhb.invoice_number] = {
|
||||
"period_start": line_item.period_start,
|
||||
"period_end": line_item.period_end
|
||||
}
|
||||
break
|
||||
except VMDetail.DoesNotExist as dne:
|
||||
ips_dict[mhb.invoice_number] = ['--']
|
||||
logger.debug("VMDetail for {} doesn't exist".format(
|
||||
mhb.order.vm_id
|
||||
))
|
||||
context['ips'] = ips_dict
|
||||
context['period'] = line_item_period_dict
|
||||
return context
|
||||
invs_page = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
invs_page = paginator.page(1)
|
||||
except EmptyPage:
|
||||
invs_page = paginator.page(paginator.num_pages)
|
||||
hosting_orders = HostingOrder.objects.filter(
|
||||
customer=cu.stripecustomer).filter(
|
||||
Q(subscription_id=None) | Q(subscription_id='')
|
||||
).order_by('-created_at')
|
||||
stripe_chgs = []
|
||||
for ho in hosting_orders:
|
||||
stripe_chgs.append({ho.id: stripe.Charge.retrieve(ho.stripe_charge_id)})
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
if ('user_email' in self.request.GET
|
||||
and self.request.user.email == settings.ADMIN_EMAIL):
|
||||
user_email = self.request.GET['user_email']
|
||||
logger.debug(
|
||||
"user_email = {}".format(user_email)
|
||||
)
|
||||
paginator_charges = Paginator(stripe_chgs, 10)
|
||||
try:
|
||||
cu = CustomUser.objects.get(email=user_email)
|
||||
except CustomUser.DoesNotExist as dne:
|
||||
logger.debug("User does not exist")
|
||||
cu = self.request.user
|
||||
self.queryset = MonthlyHostingBill.objects.filter(customer__user=cu)
|
||||
invs_page_charges = paginator_charges.page(page)
|
||||
except PageNotAnInteger:
|
||||
invs_page_charges = paginator_charges.page(1)
|
||||
except EmptyPage:
|
||||
invs_page_charges = paginator_charges.page(paginator_charges.num_pages)
|
||||
else:
|
||||
self.queryset = MonthlyHostingBill.objects.filter(
|
||||
customer__user=self.request.user
|
||||
)
|
||||
return super(InvoiceListView, self).get_queryset()
|
||||
try:
|
||||
invs = stripe.Invoice.list(
|
||||
customer=self.request.user.stripecustomer.stripe_id,
|
||||
count=100
|
||||
)
|
||||
paginator = Paginator(invs.data, 10)
|
||||
try:
|
||||
invs_page = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
invs_page = paginator.page(1)
|
||||
except EmptyPage:
|
||||
invs_page = paginator.page(paginator.num_pages)
|
||||
hosting_orders = HostingOrder.objects.filter(
|
||||
customer=self.request.user.stripecustomer).filter(
|
||||
Q(subscription_id=None) | Q(subscription_id='')
|
||||
).order_by('-created_at')
|
||||
stripe_chgs = []
|
||||
for ho in hosting_orders:
|
||||
stripe_chgs.append(
|
||||
{ho: stripe.Charge.retrieve(ho.stripe_charge_id)})
|
||||
paginator_charges = Paginator(stripe_chgs, 10)
|
||||
try:
|
||||
invs_page_charges = paginator_charges.page(page)
|
||||
except PageNotAnInteger:
|
||||
invs_page_charges = paginator_charges.page(1)
|
||||
except EmptyPage:
|
||||
invs_page_charges = paginator_charges.page(
|
||||
paginator_charges.num_pages)
|
||||
except Exception as ex:
|
||||
logger.error(str(ex))
|
||||
invs_page = None
|
||||
context["invs"] = invs_page
|
||||
context["invs_charge"] = invs_page_charges
|
||||
return context
|
||||
|
||||
@method_decorator(decorators)
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
@ -1233,41 +1432,71 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView):
|
|||
context['vm'] = vm_detail.__dict__
|
||||
context['vm']['name'] = '{}-{}'.format(
|
||||
context['vm']['configuration'], context['vm']['vm_id'])
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
user_vat_country = obj.order.billing_address.country
|
||||
user_country_vat_rate = get_vat_rate_for_country(
|
||||
user_vat_country)
|
||||
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.order.vm_pricing.name
|
||||
if obj.order.vm_pricing else 'default')
|
||||
if obj.order.vm_pricing else 'default'),
|
||||
vat_rate=(
|
||||
user_country_vat_rate * 100
|
||||
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else settings.PRE_EU_VAT_RATE
|
||||
)
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = price + vat - discount['amount']
|
||||
context['vm']["after_eu_vat_intro"] = (
|
||||
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else False
|
||||
)
|
||||
context['vm']["price"] = price
|
||||
context['vm']["vat"] = vat
|
||||
context['vm']["vat_percent"] = vat_percent
|
||||
context['vm']["vat_country"] = user_vat_country
|
||||
context['vm']["discount"] = discount
|
||||
context['vm']["total_price"] = round(
|
||||
price + vat - discount['amount'], 2)
|
||||
except VMDetail.DoesNotExist:
|
||||
# fallback to get it from the infrastructure
|
||||
try:
|
||||
manager = OpenNebulaManager(
|
||||
email=self.request.user.email,
|
||||
email=self.request.user.username,
|
||||
password=self.request.user.password
|
||||
)
|
||||
vm = manager.get_vm(vm_id)
|
||||
context['vm'] = VirtualMachineSerializer(vm).data
|
||||
price, vat, vat_percent, discount = get_vm_price_with_vat(
|
||||
user_vat_country = obj.order.billing_address.country
|
||||
user_country_vat_rate = get_vat_rate_for_country(
|
||||
user_vat_country)
|
||||
price, vat, vat_percent, discount = get_vm_price_for_given_vat(
|
||||
cpu=context['vm']['cores'],
|
||||
ssd_size=context['vm']['disk_size'],
|
||||
memory=context['vm']['memory'],
|
||||
pricing_name=(obj.order.vm_pricing.name
|
||||
if obj.order.vm_pricing else 'default')
|
||||
if obj.order.vm_pricing else 'default'),
|
||||
vat_rate=(
|
||||
user_country_vat_rate * 100
|
||||
if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else settings.PRE_EU_VAT_RATE
|
||||
)
|
||||
)
|
||||
context['vm']['vat'] = vat
|
||||
context['vm']['price'] = price
|
||||
context['vm']['discount'] = discount
|
||||
context['vm']['vat_percent'] = vat_percent
|
||||
context['vm']['total_price'] = (
|
||||
price + vat - discount['amount']
|
||||
context['vm']["after_eu_vat_intro"] = (
|
||||
True if obj.order.vm_id >= settings.FIRST_VM_ID_AFTER_EU_VAT
|
||||
else False
|
||||
)
|
||||
context['vm']["price"] = price
|
||||
context['vm']["vat"] = vat
|
||||
context['vm']["vat_percent"] = vat_percent
|
||||
context['vm']["vat_country"] = user_vat_country
|
||||
context['vm']["discount"] = discount
|
||||
context['vm']["total_price"] = round(
|
||||
price + vat - discount['amount'], 2)
|
||||
except TypeError:
|
||||
logger.error("Type error. Probably we "
|
||||
"came from a generic product. "
|
||||
"Invoice ID %s" % obj.invoice_id)
|
||||
except WrongIdError:
|
||||
logger.error("WrongIdError while accessing "
|
||||
"invoice {}".format(obj.invoice_id))
|
||||
|
@ -1315,8 +1544,13 @@ class VirtualMachinesPlanListView(LoginRequiredMixin, ListView):
|
|||
ordering = '-id'
|
||||
|
||||
def get_queryset(self):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
username = self.request.GET.get('username')
|
||||
if self.request.user.is_admin and username:
|
||||
user = CustomUser.objects.get(username=username)
|
||||
else:
|
||||
user = self.request.user
|
||||
owner = user
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
try:
|
||||
queryset = manager.get_vms()
|
||||
|
@ -1474,10 +1708,14 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
login_url = reverse_lazy('hosting:login')
|
||||
|
||||
def get_object(self):
|
||||
owner = self.request.user
|
||||
username = self.request.GET.get('username')
|
||||
if self.request.user.is_admin and username:
|
||||
owner = CustomUser.objects.get(username=username)
|
||||
else:
|
||||
owner = self.request.user
|
||||
vm = None
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email,
|
||||
email=owner.username,
|
||||
password=owner.password
|
||||
)
|
||||
vm_id = self.kwargs.get('pk')
|
||||
|
@ -1521,13 +1759,31 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
context = None
|
||||
try:
|
||||
serializer = VirtualMachineSerializer(vm)
|
||||
hosting_order = HostingOrder.objects.get(
|
||||
vm_id=serializer.data['vm_id']
|
||||
)
|
||||
inv_url = None
|
||||
if hosting_order.subscription_id:
|
||||
stripe_obj = stripe.Invoice.list(
|
||||
subscription=hosting_order.subscription_id,
|
||||
count=1
|
||||
)
|
||||
if stripe_obj.data:
|
||||
inv_url = stripe_obj.data[0].hosted_invoice_url
|
||||
else:
|
||||
inv_url = ''
|
||||
elif hosting_order.stripe_charge_id:
|
||||
stripe_obj = stripe.Charge.retrieve(
|
||||
hosting_order.stripe_charge_id
|
||||
)
|
||||
inv_url = stripe_obj.receipt_url
|
||||
context = {
|
||||
'virtual_machine': serializer.data,
|
||||
'order': HostingOrder.objects.get(
|
||||
vm_id=serializer.data['vm_id']
|
||||
),
|
||||
'keys': UserHostingKey.objects.filter(user=request.user)
|
||||
'order': hosting_order,
|
||||
'keys': UserHostingKey.objects.filter(user=request.user),
|
||||
'inv_url': inv_url
|
||||
}
|
||||
|
||||
except Exception as ex:
|
||||
logger.debug("Exception generated {}".format(str(ex)))
|
||||
messages.error(self.request,
|
||||
|
@ -1546,7 +1802,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
vm = self.get_object()
|
||||
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email,
|
||||
email=owner.username,
|
||||
password=owner.password
|
||||
)
|
||||
try:
|
||||
|
@ -1559,6 +1815,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
# Cancel Stripe subscription
|
||||
stripe_utils = StripeUtils()
|
||||
hosting_order = None
|
||||
stripe_subscription_obj = None
|
||||
try:
|
||||
hosting_order = HostingOrder.objects.get(
|
||||
vm_id=vm.id
|
||||
|
@ -1573,7 +1830,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
error_msg = result.get('error')
|
||||
logger.error(
|
||||
'Error canceling subscription for {user} and vm id '
|
||||
'{vm_id}'.format(user=owner.email, vm_id=vm.id)
|
||||
'{vm_id}'.format(user=owner.username, vm_id=vm.id)
|
||||
)
|
||||
logger.error(error_msg)
|
||||
admin_email_body['stripe_error_msg'] = error_msg
|
||||
|
@ -1595,7 +1852,7 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
)
|
||||
response['text'] = str(_('Error terminating VM')) + str(vm.id)
|
||||
else:
|
||||
for t in range(15):
|
||||
for t in range(settings.MAX_TIME_TO_WAIT_FOR_VM_TERMINATE):
|
||||
try:
|
||||
manager.get_vm(vm.id)
|
||||
except WrongIdError:
|
||||
|
@ -1618,6 +1875,10 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
)
|
||||
break
|
||||
else:
|
||||
logger.debug(
|
||||
'Sleeping 2 seconds for terminate action on VM %s' %
|
||||
vm.id
|
||||
)
|
||||
sleep(2)
|
||||
if not response['status']:
|
||||
response['text'] = str(_("VM terminate action timed out. "
|
||||
|
@ -1645,19 +1906,33 @@ class VirtualMachineView(LoginRequiredMixin, View):
|
|||
email.send()
|
||||
admin_email_body.update(response)
|
||||
admin_email_body["customer_email"] = owner.email
|
||||
admin_email_body["customer_username"] = owner.username
|
||||
admin_email_body["VM_ID"] = vm.id
|
||||
admin_email_body["VM_created_at"] = (str(hosting_order.created_at) if
|
||||
hosting_order is not None
|
||||
else "unknown")
|
||||
admin_msg_sub = "VM and Subscription for VM {} and user: {}".format(
|
||||
content = ""
|
||||
total_amount = 0
|
||||
if stripe_subscription_obj:
|
||||
for line_item in stripe_subscription_obj["items"]["data"]:
|
||||
total_amount += (line_item["quantity"] *
|
||||
line_item.plan["amount"])
|
||||
content += " %s => %s x %s => %s\n" % (
|
||||
line_item.plan["name"], line_item["quantity"],
|
||||
line_item.plan["amount"]/100,
|
||||
(line_item["quantity"] * line_item.plan["amount"])/100
|
||||
)
|
||||
admin_email_body["subscription_amount"] = total_amount/100
|
||||
admin_email_body["subscription_detail"] = content
|
||||
admin_msg_sub = "VM and Subscription for VM {} and user: {}, {}".format(
|
||||
vm.id,
|
||||
owner.email
|
||||
owner.email, owner.username
|
||||
)
|
||||
email_to_admin_data = {
|
||||
'subject': ("Deleted " if response['status']
|
||||
else "ERROR deleting ") + admin_msg_sub,
|
||||
'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
|
||||
'to': ['info@ungleich.ch'],
|
||||
'to': ['dcl-orders@ungleich.ch'],
|
||||
'body': "\n".join(
|
||||
["%s=%s" % (k, v) for (k, v) in admin_email_body.items()]),
|
||||
}
|
||||
|
@ -1697,7 +1972,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin,
|
|||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
# Get vms
|
||||
queryset = manager.get_vms()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-11-28 07:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('membership', '0009_deleteduser'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='import_stripe_bill_remark',
|
||||
field=models.TextField(default='', help_text='Indicates any issues while importing stripe bills'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-12-18 10:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import membership.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('membership', '0010_customuser_import_stripe_bill_remark'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='in_ldap',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customuser',
|
||||
name='username',
|
||||
field=models.CharField(max_length=60, null=True, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customuser',
|
||||
name='name',
|
||||
field=models.CharField(max_length=50, validators=[membership.models.validate_name]),
|
||||
),
|
||||
]
|
|
@ -1,5 +1,8 @@
|
|||
from datetime import datetime
|
||||
import logging
|
||||
import random
|
||||
import unicodedata
|
||||
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
|
||||
|
@ -7,13 +10,17 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
|
|||
from django.contrib.sites.models import Site
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db import models, IntegrityError
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from utils.mailer import BaseEmail
|
||||
from utils.mailer import DigitalGlarusRegistrationMailer
|
||||
from utils.stripe_utils import StripeUtils
|
||||
from utils.ldap_manager import LdapManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REGISTRATION_MESSAGE = {'subject': "Validation mail",
|
||||
'message': 'Please validate Your account under this link '
|
||||
|
@ -42,6 +49,7 @@ class MyUserManager(BaseUserManager):
|
|||
user.is_admin = False
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
user.create_ldap_account(password)
|
||||
return user
|
||||
|
||||
def create_superuser(self, email, name, password):
|
||||
|
@ -63,13 +71,54 @@ def get_validation_slug():
|
|||
return make_password(None)
|
||||
|
||||
|
||||
def get_first_and_last_name(full_name):
|
||||
first_name, *last_name = full_name.split(" ")
|
||||
last_name = " ".join(last_name)
|
||||
return first_name, last_name
|
||||
|
||||
|
||||
def assign_username(user):
|
||||
if not user.username:
|
||||
ldap_manager = LdapManager()
|
||||
|
||||
# Try to come up with a username
|
||||
first_name, last_name = get_first_and_last_name(user.name)
|
||||
user.username = unicodedata.normalize('NFKD', first_name + last_name)
|
||||
user.username = "".join([char for char in user.username if char.isalnum()]).lower()
|
||||
exist = True
|
||||
while exist:
|
||||
# Check if it exists
|
||||
exist, entries = ldap_manager.check_user_exists(user.username)
|
||||
if exist:
|
||||
# If username exists in ldap, come up with a new user name and check it again
|
||||
user.username = user.username + str(random.randint(0, 2 ** 10))
|
||||
else:
|
||||
# If username does not exists in ldap, try to save it in database
|
||||
try:
|
||||
user.save()
|
||||
except IntegrityError:
|
||||
# If username exists in database then come up with a new username
|
||||
user.username = user.username + str(random.randint(0, 2 ** 10))
|
||||
exist = True
|
||||
|
||||
|
||||
def validate_name(value):
|
||||
valid_chars = [char for char in value if (char.isalpha() or char == "-" or char == " ")]
|
||||
if len(valid_chars) < len(value):
|
||||
raise ValidationError(
|
||||
_('%(value)s is not a valid name. A valid name can only include letters, spaces or -'),
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
|
||||
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||
VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
|
||||
site = models.ForeignKey(Site, default=1)
|
||||
name = models.CharField(max_length=50)
|
||||
name = models.CharField(max_length=50, validators=[validate_name])
|
||||
email = models.EmailField(unique=True)
|
||||
|
||||
username = models.CharField(max_length=60, unique=True, null=True)
|
||||
validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0)
|
||||
in_ldap = models.BooleanField(default=False)
|
||||
# By default, we initialize the validation_slug with appropriate value
|
||||
# This is required for User(page) admin
|
||||
validation_slug = models.CharField(
|
||||
|
@ -82,6 +131,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||
help_text=_(
|
||||
'Designates whether the user can log into this admin site.'),
|
||||
)
|
||||
import_stripe_bill_remark = models.TextField(
|
||||
default="",
|
||||
help_text="Indicates any issues while importing stripe bills"
|
||||
)
|
||||
|
||||
objects = MyUserManager()
|
||||
|
||||
|
@ -160,6 +213,38 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||
# The user is identified by their email address
|
||||
return self.email
|
||||
|
||||
def create_ldap_account(self, password):
|
||||
# create ldap account for user if it does not exists already.
|
||||
if self.in_ldap:
|
||||
return
|
||||
|
||||
assign_username(self)
|
||||
ldap_manager = LdapManager()
|
||||
try:
|
||||
user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.username)
|
||||
except Exception:
|
||||
logger.exception("Exception occur while searching for user in LDAP")
|
||||
else:
|
||||
if not user_exists_in_ldap:
|
||||
# IF no ldap account
|
||||
first_name, last_name = get_first_and_last_name(self.name)
|
||||
if not last_name:
|
||||
last_name = first_name
|
||||
ldap_manager.create_user(self.username, password=password,
|
||||
firstname=first_name, lastname=last_name,
|
||||
email=self.email)
|
||||
else:
|
||||
# User exists already in LDAP, but with a dummy credential
|
||||
# We are here implies that the user has successfully
|
||||
# authenticated against Django db, and a corresponding user
|
||||
# exists in LDAP.
|
||||
# We just update the LDAP credentials once again, assuming it
|
||||
# was set to a dummy value while migrating users from Django to
|
||||
# LDAP
|
||||
ldap_manager.change_password(self.username, password)
|
||||
self.in_ldap = True
|
||||
self.save()
|
||||
|
||||
def __str__(self): # __unicode__ on Python 2
|
||||
return self.email
|
||||
|
||||
|
@ -192,7 +277,7 @@ class StripeCustomer(models.Model):
|
|||
return "%s - %s" % (self.stripe_id, self.user.email)
|
||||
|
||||
@classmethod
|
||||
def create_stripe_api_customer(cls, email=None, token=None,
|
||||
def create_stripe_api_customer(cls, email=None, id_payment_method=None,
|
||||
customer_name=None):
|
||||
"""
|
||||
This method creates a Stripe API customer with the given
|
||||
|
@ -203,7 +288,8 @@ class StripeCustomer(models.Model):
|
|||
stripe user.
|
||||
"""
|
||||
stripe_utils = StripeUtils()
|
||||
stripe_data = stripe_utils.create_customer(token, email, customer_name)
|
||||
stripe_data = stripe_utils.create_customer(
|
||||
id_payment_method, email, customer_name)
|
||||
if stripe_data.get('response_object'):
|
||||
stripe_cus_id = stripe_data.get('response_object').get('id')
|
||||
return stripe_cus_id
|
||||
|
@ -211,7 +297,7 @@ class StripeCustomer(models.Model):
|
|||
return None
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, email=None, token=None):
|
||||
def get_or_create(cls, email=None, token=None, id_payment_method=None):
|
||||
"""
|
||||
Check if there is a registered stripe customer with that email
|
||||
or create a new one
|
||||
|
|
|
@ -154,6 +154,8 @@ class OpenNebulaManager():
|
|||
protocol=settings.OPENNEBULA_PROTOCOL)
|
||||
)
|
||||
raise ConnectionRefusedError
|
||||
except Exception as ex:
|
||||
logger.error(str(ex))
|
||||
|
||||
def _get_user_pool(self):
|
||||
try:
|
||||
|
@ -427,8 +429,12 @@ class OpenNebulaManager():
|
|||
template_id = int(template_id)
|
||||
try:
|
||||
template_pool = self._get_template_pool()
|
||||
if template_id in settings.UPDATED_TEMPLATES_DICT.keys():
|
||||
template_id = settings.UPDATED_TEMPLATES_DICT[template_id]
|
||||
return template_pool.get_by_id(template_id)
|
||||
except:
|
||||
except Exception as ex:
|
||||
logger.debug("Template Id we are looking for : %s" % template_id)
|
||||
logger.error(str(ex))
|
||||
raise ConnectionRefusedError
|
||||
|
||||
def create_template(self, name, cores, memory, disk_size, core_price,
|
||||
|
@ -485,9 +491,15 @@ class OpenNebulaManager():
|
|||
)
|
||||
|
||||
def change_user_password(self, passwd_hash):
|
||||
if type(self.opennebula_user) == int:
|
||||
logger.debug("opennebula_user is int and has value = %s" %
|
||||
self.opennebula_user)
|
||||
else:
|
||||
logger.debug("opennebula_user is object and corresponding id is %s"
|
||||
% self.opennebula_user.id)
|
||||
self.oneadmin_client.call(
|
||||
oca.User.METHODS['passwd'],
|
||||
self.opennebula_user.id,
|
||||
self.opennebula_user if type(self.opennebula_user) == int else self.opennebula_user.id,
|
||||
passwd_hash
|
||||
)
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ class VirtualMachineSerializer(serializers.Serializer):
|
|||
}
|
||||
|
||||
try:
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password,
|
||||
)
|
||||
opennebula_id = manager.create_vm(template_id=template_id,
|
||||
|
|
|
@ -19,7 +19,7 @@ class VmCreateView(generics.ListCreateAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
# We may have ConnectionRefusedError if we don't have a
|
||||
# connection to OpenNebula. For now, we raise ServiceUnavailable
|
||||
|
@ -42,7 +42,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
def get_queryset(self):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
# We may have ConnectionRefusedError if we don't have a
|
||||
# connection to OpenNebula. For now, we raise ServiceUnavailable
|
||||
|
@ -54,7 +54,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
def get_object(self):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
# We may have ConnectionRefusedError if we don't have a
|
||||
# connection to OpenNebula. For now, we raise ServiceUnavailable
|
||||
|
@ -66,7 +66,7 @@ class VmDetailsView(generics.RetrieveUpdateDestroyAPIView):
|
|||
|
||||
def perform_destroy(self, instance):
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(email=owner.email,
|
||||
manager = OpenNebulaManager(email=owner.username,
|
||||
password=owner.password)
|
||||
# We may have ConnectionRefusedError if we don't have a
|
||||
# connection to OpenNebula. For now, we raise ServiceUnavailable
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
# Nico Schottelius, 2021-12-17
|
||||
|
||||
current=$(git describe --dirty)
|
||||
last_tag=$(git describe --tags --abbrev=0)
|
||||
registry=harbor.ungleich.svc.p10.k8s.ooo/ungleich-public
|
||||
image_url=$registry/dynamicweb:${current}
|
||||
|
||||
if echo $current | grep -q -e 'dirty$'; then
|
||||
echo Refusing to release a dirty tree build
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$current" != "$last_tag" ]; then
|
||||
echo "Last tag ($last_tag) is not current version ($current)"
|
||||
echo "Only release proper versions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker tag dynamicweb:${current} ${image_url}
|
||||
docker push ${image_url}
|
|
@ -1 +1,2 @@
|
|||
base-devel
|
||||
libmemcached
|
||||
|
|
|
@ -23,7 +23,7 @@ django-classy-tags==0.7.2
|
|||
django-cms==3.2.5
|
||||
django-compressor==2.0
|
||||
django-debug-toolbar==1.4
|
||||
django-dotenv==1.4.1
|
||||
python-dotenv==0.10.3
|
||||
django-extensions==1.6.7
|
||||
django-filer==1.2.0
|
||||
django-filter==0.13.0
|
||||
|
@ -63,6 +63,7 @@ djangocms-text-ckeditor==2.9.3
|
|||
djangocms-video==1.0.0
|
||||
easy-thumbnails==2.3
|
||||
html5lib==0.9999999
|
||||
ldap3==2.6.1
|
||||
lxml==3.6.0
|
||||
model-mommy==1.2.6
|
||||
phonenumbers==7.4.0
|
||||
|
@ -78,11 +79,11 @@ requests==2.10.0
|
|||
rjsmin==1.0.12
|
||||
six==1.10.0
|
||||
sqlparse==0.1.19
|
||||
stripe==1.33.0
|
||||
stripe==2.41.0
|
||||
wheel==0.29.0
|
||||
django-admin-honeypot==1.0.0
|
||||
coverage==4.3.4
|
||||
git+https://github.com/ungleich/python-oca.git#egg=python-oca
|
||||
git+https://github.com/ungleich/python-oca.git#egg=oca
|
||||
djangorestframework==3.6.3
|
||||
flake8==3.3.0
|
||||
python-memcached==1.58
|
||||
|
|
|
@ -134,8 +134,6 @@
|
|||
digitalglarus.ch<br/>
|
||||
hack4lgarus.ch<br/>
|
||||
ipv6onlyhosting.com<br/>
|
||||
ipv6onlyhosting.ch<br/>
|
||||
ipv6onlyhosting.net<br/>
|
||||
django-hosting.ch<br/>
|
||||
rails-hosting.ch<br/>
|
||||
node-hosting.ch<br/>
|
||||
|
|
|
@ -19,13 +19,15 @@
|
|||
|
||||
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var equalizer = ".sameheight-{{product_instance.pk}}"
|
||||
var heights = $(equalizer).map(function() {
|
||||
return $(this).height();
|
||||
}).get(),
|
||||
|
||||
maxHeight = Math.max.apply(null, heights);
|
||||
$(equalizer).height(maxHeight);
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var equalizer = ".sameheight-{{product_instance.pk}}";
|
||||
var elements = document.querySelectorAll(equalizer);
|
||||
var heights = Array.from(elements).map(function(el) {
|
||||
return el.offsetHeight;
|
||||
});
|
||||
var maxHeight = Math.max(...heights);
|
||||
Array.from(elements).forEach(function(el) {
|
||||
el.style.height = maxHeight + "px";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -25,9 +25,10 @@ class ContactView(FormView):
|
|||
success_message = _('Message Successfully Sent')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
form.send_email()
|
||||
messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
print("ungleich_page contactusform")
|
||||
#form.save()
|
||||
#form.send_email()
|
||||
#messages.add_message(self.request, messages.SUCCESS, self.success_message)
|
||||
return super(ContactView, self).form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MyLDAPBackend(ModelBackend):
|
||||
def authenticate(self, username=None, password=None, **kwargs):
|
||||
user = super().authenticate(username, password, **kwargs)
|
||||
if user:
|
||||
user.create_ldap_account(password)
|
||||
return user
|
|
@ -1,7 +1,8 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
|
||||
# http://xml.coverpages.org/country3166.html
|
||||
# Old: http://xml.coverpages.org/country3166.html
|
||||
# 2023-12-29: Updated list of countries from https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
|
||||
COUNTRIES = (
|
||||
('AD', _('Andorra')),
|
||||
('AE', _('United Arab Emirates')),
|
||||
|
@ -10,7 +11,6 @@ COUNTRIES = (
|
|||
('AI', _('Anguilla')),
|
||||
('AL', _('Albania')),
|
||||
('AM', _('Armenia')),
|
||||
('AN', _('Netherlands Antilles')),
|
||||
('AO', _('Angola')),
|
||||
('AQ', _('Antarctica')),
|
||||
('AR', _('Argentina')),
|
||||
|
@ -18,6 +18,7 @@ COUNTRIES = (
|
|||
('AT', _('Austria')),
|
||||
('AU', _('Australia')),
|
||||
('AW', _('Aruba')),
|
||||
('AX', _('Aland Islands')),
|
||||
('AZ', _('Azerbaijan')),
|
||||
('BA', _('Bosnia and Herzegovina')),
|
||||
('BB', _('Barbados')),
|
||||
|
@ -28,11 +29,13 @@ COUNTRIES = (
|
|||
('BH', _('Bahrain')),
|
||||
('BI', _('Burundi')),
|
||||
('BJ', _('Benin')),
|
||||
('BL', _('St. Barts')),
|
||||
('BM', _('Bermuda')),
|
||||
('BN', _('Brunei Darussalam')),
|
||||
('BN', _('Brunei')),
|
||||
('BO', _('Bolivia')),
|
||||
('BQ', _('Caribbean Netherlands')),
|
||||
('BR', _('Brazil')),
|
||||
('BS', _('Bahama')),
|
||||
('BS', _('Bahamas')),
|
||||
('BT', _('Bhutan')),
|
||||
('BV', _('Bouvet Island')),
|
||||
('BW', _('Botswana')),
|
||||
|
@ -40,11 +43,12 @@ COUNTRIES = (
|
|||
('BZ', _('Belize')),
|
||||
('CA', _('Canada')),
|
||||
('CC', _('Cocos (Keeling) Islands')),
|
||||
('CD', _('Congo - Kinshasa')),
|
||||
('CF', _('Central African Republic')),
|
||||
('CG', _('Congo')),
|
||||
('CG', _('Congo - Brazzaville')),
|
||||
('CH', _('Switzerland')),
|
||||
('CI', _('Ivory Coast')),
|
||||
('CK', _('Cook Iislands')),
|
||||
('CK', _('Cook Islands')),
|
||||
('CL', _('Chile')),
|
||||
('CM', _('Cameroon')),
|
||||
('CN', _('China')),
|
||||
|
@ -52,9 +56,10 @@ COUNTRIES = (
|
|||
('CR', _('Costa Rica')),
|
||||
('CU', _('Cuba')),
|
||||
('CV', _('Cape Verde')),
|
||||
('CW', _('Curacao')),
|
||||
('CX', _('Christmas Island')),
|
||||
('CY', _('Cyprus')),
|
||||
('CZ', _('Czech Republic')),
|
||||
('CZ', _('Czechia')),
|
||||
('DE', _('Germany')),
|
||||
('DJ', _('Djibouti')),
|
||||
('DK', _('Denmark')),
|
||||
|
@ -70,16 +75,16 @@ COUNTRIES = (
|
|||
('ET', _('Ethiopia')),
|
||||
('FI', _('Finland')),
|
||||
('FJ', _('Fiji')),
|
||||
('FK', _('Falkland Islands (Malvinas)')),
|
||||
('FK', _('Falkland Islands')),
|
||||
('FM', _('Micronesia')),
|
||||
('FO', _('Faroe Islands')),
|
||||
('FR', _('France')),
|
||||
('FX', _('France, Metropolitan')),
|
||||
('GA', _('Gabon')),
|
||||
('GB', _('United Kingdom (Great Britain)')),
|
||||
('GB', _('United Kingdom')),
|
||||
('GD', _('Grenada')),
|
||||
('GE', _('Georgia')),
|
||||
('GF', _('French Guiana')),
|
||||
('GG', _('Guernsey')),
|
||||
('GH', _('Ghana')),
|
||||
('GI', _('Gibraltar')),
|
||||
('GL', _('Greenland')),
|
||||
|
@ -93,7 +98,7 @@ COUNTRIES = (
|
|||
('GU', _('Guam')),
|
||||
('GW', _('Guinea-Bissau')),
|
||||
('GY', _('Guyana')),
|
||||
('HK', _('Hong Kong')),
|
||||
('HK', _('Hong Kong SAR China')),
|
||||
('HM', _('Heard & McDonald Islands')),
|
||||
('HN', _('Honduras')),
|
||||
('HR', _('Croatia')),
|
||||
|
@ -102,12 +107,14 @@ COUNTRIES = (
|
|||
('ID', _('Indonesia')),
|
||||
('IE', _('Ireland')),
|
||||
('IL', _('Israel')),
|
||||
('IM', _('Isle of Man')),
|
||||
('IN', _('India')),
|
||||
('IO', _('British Indian Ocean Territory')),
|
||||
('IQ', _('Iraq')),
|
||||
('IR', _('Islamic Republic of Iran')),
|
||||
('IR', _('Iran')),
|
||||
('IS', _('Iceland')),
|
||||
('IT', _('Italy')),
|
||||
('JE', _('Jersey')),
|
||||
('JM', _('Jamaica')),
|
||||
('JO', _('Jordan')),
|
||||
('JP', _('Japan')),
|
||||
|
@ -117,14 +124,14 @@ COUNTRIES = (
|
|||
('KI', _('Kiribati')),
|
||||
('KM', _('Comoros')),
|
||||
('KN', _('St. Kitts and Nevis')),
|
||||
('KP', _('Korea, Democratic People\'s Republic of')),
|
||||
('KR', _('Korea, Republic of')),
|
||||
('KP', _('North Korea')),
|
||||
('KR', _('South Korea')),
|
||||
('KW', _('Kuwait')),
|
||||
('KY', _('Cayman Islands')),
|
||||
('KZ', _('Kazakhstan')),
|
||||
('LA', _('Lao People\'s Democratic Republic')),
|
||||
('LA', _('Laos')),
|
||||
('LB', _('Lebanon')),
|
||||
('LC', _('Saint Lucia')),
|
||||
('LC', _('St. Lucia')),
|
||||
('LI', _('Liechtenstein')),
|
||||
('LK', _('Sri Lanka')),
|
||||
('LR', _('Liberia')),
|
||||
|
@ -132,20 +139,23 @@ COUNTRIES = (
|
|||
('LT', _('Lithuania')),
|
||||
('LU', _('Luxembourg')),
|
||||
('LV', _('Latvia')),
|
||||
('LY', _('Libyan Arab Jamahiriya')),
|
||||
('LY', _('Libya')),
|
||||
('MA', _('Morocco')),
|
||||
('MC', _('Monaco')),
|
||||
('MD', _('Moldova, Republic of')),
|
||||
('MD', _('Moldova')),
|
||||
('ME', _('Montenegro')),
|
||||
('MF', _('St. Martin')),
|
||||
('MG', _('Madagascar')),
|
||||
('MH', _('Marshall Islands')),
|
||||
('MK', _('North Macedonia')),
|
||||
('ML', _('Mali')),
|
||||
('MM', _('Myanmar (Burma)')),
|
||||
('MN', _('Mongolia')),
|
||||
('MM', _('Myanmar')),
|
||||
('MO', _('Macau')),
|
||||
('MO', _('Macao SAR China')),
|
||||
('MP', _('Northern Mariana Islands')),
|
||||
('MQ', _('Martinique')),
|
||||
('MR', _('Mauritania')),
|
||||
('MS', _('Monserrat')),
|
||||
('MS', _('Montserrat')),
|
||||
('MT', _('Malta')),
|
||||
('MU', _('Mauritius')),
|
||||
('MV', _('Maldives')),
|
||||
|
@ -174,15 +184,17 @@ COUNTRIES = (
|
|||
('PK', _('Pakistan')),
|
||||
('PL', _('Poland')),
|
||||
('PM', _('St. Pierre & Miquelon')),
|
||||
('PN', _('Pitcairn')),
|
||||
('PN', _('Pitcairn Islands')),
|
||||
('PR', _('Puerto Rico')),
|
||||
('PS', _('Palestinian Territories')),
|
||||
('PT', _('Portugal')),
|
||||
('PW', _('Palau')),
|
||||
('PY', _('Paraguay')),
|
||||
('QA', _('Qatar')),
|
||||
('RE', _('Reunion')),
|
||||
('RO', _('Romania')),
|
||||
('RU', _('Russian Federation')),
|
||||
('RS', _('Serbia')),
|
||||
('RU', _('Russia')),
|
||||
('RW', _('Rwanda')),
|
||||
('SA', _('Saudi Arabia')),
|
||||
('SB', _('Solomon Islands')),
|
||||
|
@ -192,17 +204,19 @@ COUNTRIES = (
|
|||
('SG', _('Singapore')),
|
||||
('SH', _('St. Helena')),
|
||||
('SI', _('Slovenia')),
|
||||
('SJ', _('Svalbard & Jan Mayen Islands')),
|
||||
('SJ', _('Svalbard and Jan Mayen')),
|
||||
('SK', _('Slovakia')),
|
||||
('SL', _('Sierra Leone')),
|
||||
('SM', _('San Marino')),
|
||||
('SN', _('Senegal')),
|
||||
('SO', _('Somalia')),
|
||||
('SR', _('Suriname')),
|
||||
('SS', _('South Sudan')),
|
||||
('ST', _('Sao Tome & Principe')),
|
||||
('SV', _('El Salvador')),
|
||||
('SY', _('Syrian Arab Republic')),
|
||||
('SZ', _('Swaziland')),
|
||||
('SX', _('Sint Maarten')),
|
||||
('SY', _('Syria')),
|
||||
('SZ', _('Eswatini')),
|
||||
('TC', _('Turks & Caicos Islands')),
|
||||
('TD', _('Chad')),
|
||||
('TF', _('French Southern Territories')),
|
||||
|
@ -210,38 +224,35 @@ COUNTRIES = (
|
|||
('TH', _('Thailand')),
|
||||
('TJ', _('Tajikistan')),
|
||||
('TK', _('Tokelau')),
|
||||
('TL', _('Timor-Leste')),
|
||||
('TM', _('Turkmenistan')),
|
||||
('TN', _('Tunisia')),
|
||||
('TO', _('Tonga')),
|
||||
('TP', _('East Timor')),
|
||||
('TR', _('Turkey')),
|
||||
('TT', _('Trinidad & Tobago')),
|
||||
('TV', _('Tuvalu')),
|
||||
('TW', _('Taiwan, Province of China')),
|
||||
('TZ', _('Tanzania, United Republic of')),
|
||||
('TW', _('Taiwan')),
|
||||
('TZ', _('Tanzania')),
|
||||
('UA', _('Ukraine')),
|
||||
('UG', _('Uganda')),
|
||||
('UM', _('United States Minor Outlying Islands')),
|
||||
('US', _('United States of America')),
|
||||
('UM', _('U.S. Outlying Islands')),
|
||||
('US', _('United States')),
|
||||
('UY', _('Uruguay')),
|
||||
('UZ', _('Uzbekistan')),
|
||||
('VA', _('Vatican City State (Holy See)')),
|
||||
('VC', _('St. Vincent & the Grenadines')),
|
||||
('VA', _('Vatican City')),
|
||||
('VC', _('St. Vincent & Grenadines')),
|
||||
('VE', _('Venezuela')),
|
||||
('VG', _('British Virgin Islands')),
|
||||
('VI', _('United States Virgin Islands')),
|
||||
('VN', _('Viet Nam')),
|
||||
('VI', _('U.S. Virgin Islands')),
|
||||
('VN', _('Vietnam')),
|
||||
('VU', _('Vanuatu')),
|
||||
('WF', _('Wallis & Futuna Islands')),
|
||||
('WF', _('Wallis & Futuna')),
|
||||
('WS', _('Samoa')),
|
||||
('YE', _('Yemen')),
|
||||
('YT', _('Mayotte')),
|
||||
('YU', _('Yugoslavia')),
|
||||
('ZA', _('South Africa')),
|
||||
('ZM', _('Zambia')),
|
||||
('ZR', _('Zaire')),
|
||||
('ZW', _('Zimbabwe')),
|
||||
('ZZ', _('Unknown or unspecified country')),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ from django.core.mail import EmailMultiAlternatives
|
|||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django_recaptcha.fields import ReCaptchaField
|
||||
|
||||
from membership.models import CustomUser
|
||||
from .models import ContactMessage, BillingAddress, UserBillingAddress
|
||||
|
||||
|
@ -124,13 +126,14 @@ class BillingAddressForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = BillingAddress
|
||||
fields = ['cardholder_name', 'street_address',
|
||||
'city', 'postal_code', 'country']
|
||||
'city', 'postal_code', 'country', 'vat_number']
|
||||
labels = {
|
||||
'cardholder_name': _('Cardholder Name'),
|
||||
'street_address': _('Street Address'),
|
||||
'city': _('City'),
|
||||
'postal_code': _('Postal Code'),
|
||||
'Country': _('Country'),
|
||||
'VAT Number': _('VAT Number')
|
||||
}
|
||||
|
||||
|
||||
|
@ -142,7 +145,7 @@ class BillingAddressFormSignup(BillingAddressForm):
|
|||
class Meta:
|
||||
model = BillingAddress
|
||||
fields = ['name', 'email', 'cardholder_name', 'street_address',
|
||||
'city', 'postal_code', 'country']
|
||||
'city', 'postal_code', 'country', 'vat_number']
|
||||
labels = {
|
||||
'name': 'Name',
|
||||
'email': _('Email'),
|
||||
|
@ -151,6 +154,7 @@ class BillingAddressFormSignup(BillingAddressForm):
|
|||
'city': _('City'),
|
||||
'postal_code': _('Postal Code'),
|
||||
'Country': _('Country'),
|
||||
'vat_number': _('VAT Number')
|
||||
}
|
||||
|
||||
def clean_email(self):
|
||||
|
@ -173,18 +177,20 @@ class UserBillingAddressForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = UserBillingAddress
|
||||
fields = ['cardholder_name', 'street_address',
|
||||
'city', 'postal_code', 'country', 'user']
|
||||
'city', 'postal_code', 'country', 'user', 'vat_number']
|
||||
labels = {
|
||||
'cardholder_name': _('Cardholder Name'),
|
||||
'street_address': _('Street Building'),
|
||||
'city': _('City'),
|
||||
'postal_code': _('Postal Code'),
|
||||
'Country': _('Country'),
|
||||
'vat_number': _('VAT Number'),
|
||||
}
|
||||
|
||||
|
||||
class ContactUsForm(forms.ModelForm):
|
||||
error_css_class = 'autofocus'
|
||||
captcha = ReCaptchaField()
|
||||
|
||||
class Meta:
|
||||
model = ContactMessage
|
||||
|
@ -203,11 +209,12 @@ class ContactUsForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
def send_email(self, email_to='info@digitalglarus.ch'):
|
||||
text_content = render_to_string(
|
||||
'emails/contact.txt', {'data': self.cleaned_data})
|
||||
html_content = render_to_string(
|
||||
'emails/contact.html', {'data': self.cleaned_data})
|
||||
email = EmailMultiAlternatives('Subject', text_content)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.to = [email_to]
|
||||
email.send()
|
||||
pass
|
||||
#text_content = render_to_string(
|
||||
# 'emails/contact.txt', {'data': self.cleaned_data})
|
||||
#html_content = render_to_string(
|
||||
# 'emails/contact.html', {'data': self.cleaned_data})
|
||||
#email = EmailMultiAlternatives('Subject', text_content)
|
||||
#email.attach_alternative(html_content, "text/html")
|
||||
#email.to = [email_to]
|
||||
#email.send()
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import decimal
|
||||
import logging
|
||||
import math
|
||||
import subprocess
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from oca.pool import WrongIdError
|
||||
|
||||
from datacenterlight.models import VMPricing
|
||||
from hosting.models import UserHostingKey, VMDetail
|
||||
from hosting.models import UserHostingKey, VMDetail, VATRates
|
||||
from opennebula_api.serializers import VirtualMachineSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -78,12 +81,55 @@ def get_vm_price(cpu, memory, disk_size, hdd_size=0, pricing_name='default'):
|
|||
price = ((decimal.Decimal(cpu) * pricing.cores_unit_price) +
|
||||
(decimal.Decimal(memory) * pricing.ram_unit_price) +
|
||||
(decimal.Decimal(disk_size) * pricing.ssd_unit_price) +
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price))
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
|
||||
decimal.Decimal(settings.VM_BASE_PRICE))
|
||||
cents = decimal.Decimal('.01')
|
||||
price = price.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
return round(float(price), 2)
|
||||
|
||||
|
||||
def get_vm_price_for_given_vat(cpu, memory, ssd_size, hdd_size=0,
|
||||
pricing_name='default', vat_rate=0):
|
||||
try:
|
||||
pricing = VMPricing.objects.get(name=pricing_name)
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
"Error getting VMPricing object for {pricing_name}."
|
||||
"Details: {details}".format(
|
||||
pricing_name=pricing_name, details=str(ex)
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
price = (
|
||||
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
|
||||
(decimal.Decimal(memory) * pricing.ram_unit_price) +
|
||||
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
|
||||
decimal.Decimal(settings.VM_BASE_PRICE)
|
||||
)
|
||||
|
||||
discount_name = pricing.discount_name
|
||||
discount_amount = round(float(pricing.discount_amount), 2)
|
||||
vat = price * decimal.Decimal(vat_rate) * decimal.Decimal(0.01)
|
||||
vat_percent = vat_rate
|
||||
|
||||
cents = decimal.Decimal('.01')
|
||||
price = price.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01))
|
||||
discount_amount_with_vat = discount_amount_with_vat.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
discount = {
|
||||
'name': discount_name,
|
||||
'amount': discount_amount,
|
||||
'amount_with_vat': round(float(discount_amount_with_vat), 2),
|
||||
'stripe_coupon_id': pricing.stripe_coupon_id
|
||||
}
|
||||
return (round(float(price), 2), round(float(vat), 2),
|
||||
round(float(vat_percent), 2), discount)
|
||||
|
||||
|
||||
|
||||
def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
||||
pricing_name='default'):
|
||||
"""
|
||||
|
@ -113,7 +159,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
|||
(decimal.Decimal(cpu) * pricing.cores_unit_price) +
|
||||
(decimal.Decimal(memory) * pricing.ram_unit_price) +
|
||||
(decimal.Decimal(ssd_size) * pricing.ssd_unit_price) +
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price)
|
||||
(decimal.Decimal(hdd_size) * pricing.hdd_unit_price) +
|
||||
decimal.Decimal(settings.VM_BASE_PRICE)
|
||||
)
|
||||
if pricing.vat_inclusive:
|
||||
vat = decimal.Decimal(0)
|
||||
|
@ -127,7 +174,8 @@ def get_vm_price_with_vat(cpu, memory, ssd_size, hdd_size=0,
|
|||
vat = vat.quantize(cents, decimal.ROUND_HALF_UP)
|
||||
discount = {
|
||||
'name': pricing.discount_name,
|
||||
'amount': round(float(pricing.discount_amount), 2)
|
||||
'amount': round(float(pricing.discount_amount), 2),
|
||||
'stripe_coupon_id': pricing.stripe_coupon_id
|
||||
}
|
||||
return (round(float(price), 2), round(float(vat), 2),
|
||||
round(float(vat_percent), 2), discount)
|
||||
|
@ -150,6 +198,30 @@ def ping_ok(host_ipv6):
|
|||
return True
|
||||
|
||||
|
||||
def get_vat_rate_for_country(country):
|
||||
vat_rate = None
|
||||
try:
|
||||
vat_rate = VATRates.objects.get(
|
||||
territory_codes=country, start_date__isnull=False, stop_date=None
|
||||
)
|
||||
logger.debug("VAT rate for %s is %s" % (country, vat_rate.rate))
|
||||
return vat_rate.rate
|
||||
except VATRates.DoesNotExist as dne:
|
||||
logger.debug(str(dne))
|
||||
logger.debug("Did not find VAT rate for %s, returning 0" % country)
|
||||
return 0
|
||||
|
||||
|
||||
def get_ip_addresses(vm_id):
|
||||
try:
|
||||
vm_detail = VMDetail.objects.get(vm_id=vm_id)
|
||||
return "%s <br/>%s" % (vm_detail.ipv6, vm_detail.ipv4)
|
||||
except VMDetail.DoesNotExist as dne:
|
||||
logger.error(str(dne))
|
||||
logger.error("VMDetail for %s does not exist" % vm_id)
|
||||
return "--"
|
||||
|
||||
|
||||
class HostingUtils:
|
||||
@staticmethod
|
||||
def clear_items_from_list(from_list, items_list):
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import random
|
||||
import ldap3
|
||||
import logging
|
||||
import unicodedata
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LdapManager:
|
||||
__instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if LdapManager.__instance is None:
|
||||
LdapManager.__instance = object.__new__(cls)
|
||||
return LdapManager.__instance
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the LDAP subsystem.
|
||||
"""
|
||||
self.rng = random.SystemRandom()
|
||||
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER)
|
||||
|
||||
def get_admin_conn(self):
|
||||
"""
|
||||
Return a bound :class:`ldap3.Connection` instance which has write
|
||||
permissions on the dn in which the user accounts reside.
|
||||
"""
|
||||
conn = self.get_conn(user=settings.LDAP_ADMIN_DN,
|
||||
password=settings.LDAP_ADMIN_PASSWORD,
|
||||
raise_exceptions=True)
|
||||
conn.bind()
|
||||
return conn
|
||||
|
||||
def get_conn(self, **kwargs):
|
||||
"""
|
||||
Return an unbound :class:`ldap3.Connection` which talks to the configured
|
||||
LDAP server.
|
||||
|
||||
The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and
|
||||
can be used to set *user*, *password* and other useful arguments.
|
||||
"""
|
||||
return ldap3.Connection(self.server, **kwargs)
|
||||
|
||||
def _ssha_password(self, password):
|
||||
"""
|
||||
Apply the SSHA password hashing scheme to the given *password*.
|
||||
*password* must be a :class:`bytes` object, containing the utf-8
|
||||
encoded password.
|
||||
|
||||
Return a :class:`bytes` object containing ``ascii``-compatible data
|
||||
which can be used as LDAP value, e.g. after armoring it once more using
|
||||
base64 or decoding it to unicode from ``ascii``.
|
||||
"""
|
||||
SALT_BYTES = 15
|
||||
|
||||
sha1 = hashlib.sha1()
|
||||
salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little")
|
||||
sha1.update(password)
|
||||
sha1.update(salt)
|
||||
|
||||
digest = sha1.digest()
|
||||
passwd = b"{SSHA}" + base64.b64encode(digest + salt)
|
||||
return passwd
|
||||
|
||||
def create_user(self, user, password, firstname, lastname, email):
|
||||
conn = self.get_admin_conn()
|
||||
uidNumber = self._get_max_uid() + 1
|
||||
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
|
||||
user_exists = True
|
||||
while user_exists:
|
||||
user_exists, _ = self.check_user_exists(
|
||||
"",
|
||||
'(&(objectClass=inetOrgPerson)(objectClass=posixAccount)'
|
||||
'(objectClass=top)(uidNumber={uidNumber}))'.format(
|
||||
uidNumber=uidNumber
|
||||
)
|
||||
)
|
||||
if user_exists:
|
||||
logger.debug(
|
||||
"{uid} exists. Trying next.".format(uid=uidNumber)
|
||||
)
|
||||
uidNumber += 1
|
||||
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
|
||||
self._set_max_uid(uidNumber)
|
||||
try:
|
||||
uid = user
|
||||
conn.add("uid={uid},{customer_dn}".format(
|
||||
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
|
||||
),
|
||||
["inetOrgPerson", "posixAccount", "ldapPublickey"],
|
||||
{
|
||||
"uid": [uid],
|
||||
"sn": [lastname.encode("utf-8")],
|
||||
"givenName": [firstname.encode("utf-8")],
|
||||
"cn": [uid],
|
||||
"displayName": ["{} {}".format(firstname, lastname).encode("utf-8")],
|
||||
"uidNumber": [str(uidNumber)],
|
||||
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
|
||||
"loginShell": ["/bin/bash"],
|
||||
"homeDirectory": ["/home/{}".format(unicodedata.normalize('NFKD', user).encode('ascii','ignore'))],
|
||||
"mail": email.encode("utf-8"),
|
||||
"userPassword": [self._ssha_password(
|
||||
password.encode("utf-8")
|
||||
)]
|
||||
}
|
||||
)
|
||||
logger.debug('Created user %s %s' % (user.encode('utf-8'),
|
||||
uidNumber))
|
||||
except Exception as ex:
|
||||
logger.debug('Could not create user %s' % user.encode('utf-8'))
|
||||
logger.error("Exception: " + str(ex))
|
||||
raise Exception(ex)
|
||||
finally:
|
||||
conn.unbind()
|
||||
|
||||
def change_password(self, uid, new_password):
|
||||
"""
|
||||
Changes the password of the user identified by user_dn
|
||||
|
||||
:param uid: str The uid that identifies the user
|
||||
:param new_password: str The new password string
|
||||
:return: True if password was changed successfully False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
|
||||
# Make sure the user exists first to change his/her details
|
||||
user_exists, entries = self.check_user_exists(
|
||||
uid=uid,
|
||||
search_base=settings.ENTIRE_SEARCH_BASE
|
||||
)
|
||||
return_val = False
|
||||
if user_exists:
|
||||
try:
|
||||
return_val = conn.modify(
|
||||
entries[0].entry_dn,
|
||||
{
|
||||
"userpassword": (
|
||||
ldap3.MODIFY_REPLACE,
|
||||
[self._ssha_password(new_password.encode("utf-8"))]
|
||||
)
|
||||
}
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.error("Exception: " + str(ex))
|
||||
else:
|
||||
logger.error("User {} not found".format(uid))
|
||||
|
||||
conn.unbind()
|
||||
return return_val
|
||||
|
||||
|
||||
def change_user_details(self, uid, details):
|
||||
"""
|
||||
Updates the user details as per given values in kwargs of the user
|
||||
identified by user_dn.
|
||||
|
||||
Assumes that all attributes passed in kwargs are valid.
|
||||
|
||||
:param uid: str The uid that identifies the user
|
||||
:param details: dict A dictionary containing the new values
|
||||
:return: True if user details were updated successfully False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
|
||||
# Make sure the user exists first to change his/her details
|
||||
user_exists, entries = self.check_user_exists(
|
||||
uid=uid,
|
||||
search_base=settings.ENTIRE_SEARCH_BASE
|
||||
)
|
||||
|
||||
return_val = False
|
||||
if user_exists:
|
||||
details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for
|
||||
k, v in details.items()}
|
||||
try:
|
||||
return_val = conn.modify(entries[0].entry_dn, details_dict)
|
||||
msg = "success"
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
logger.error("Exception: " + msg)
|
||||
finally:
|
||||
conn.unbind()
|
||||
else:
|
||||
msg = "User {} not found".format(uid)
|
||||
logger.error(msg)
|
||||
conn.unbind()
|
||||
return return_val, msg
|
||||
|
||||
def check_user_exists(self, uid, search_filter="", attributes=None,
|
||||
search_base=settings.LDAP_CUSTOMER_DN, search_attr="uid"):
|
||||
"""
|
||||
Check if the user with the given uid exists in the customer group.
|
||||
|
||||
:param uid: str representing the user
|
||||
:param search_filter: str representing the filter condition to find
|
||||
users. If its empty, the search finds the user with
|
||||
the given uid.
|
||||
:param attributes: list A list of str representing all the attributes
|
||||
to be obtained in the result entries
|
||||
:param search_base: str
|
||||
:return: tuple (bool, [ldap3.abstract.entry.Entry ..])
|
||||
A bool indicating if the user exists
|
||||
A list of all entries obtained in the search
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
entries = []
|
||||
try:
|
||||
result = conn.search(
|
||||
search_base=search_base,
|
||||
search_filter=search_filter if len(search_filter) > 0 else
|
||||
'(uid={uid})'.format(uid=uid),
|
||||
attributes=attributes
|
||||
)
|
||||
entries = conn.entries
|
||||
finally:
|
||||
conn.unbind()
|
||||
return result, entries
|
||||
|
||||
def delete_user(self, uid):
|
||||
"""
|
||||
Deletes the user with the given uid from ldap
|
||||
|
||||
:param uid: str representing the user
|
||||
:return: True if the delete was successful False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
try:
|
||||
return_val = conn.delete(
|
||||
("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid),
|
||||
)
|
||||
msg = "success"
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
logger.error("Exception: " + msg)
|
||||
return_val = False
|
||||
finally:
|
||||
conn.unbind()
|
||||
return return_val, msg
|
||||
|
||||
def _set_max_uid(self, max_uid):
|
||||
"""
|
||||
a utility function to save max_uid value to a file
|
||||
|
||||
:param max_uid: an integer representing the max uid
|
||||
:return:
|
||||
"""
|
||||
with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler:
|
||||
handler.write(str(max_uid))
|
||||
|
||||
def _get_max_uid(self):
|
||||
"""
|
||||
A utility function to read the max uid value that was previously set
|
||||
|
||||
:return: An integer representing the max uid value that was previously
|
||||
set
|
||||
"""
|
||||
try:
|
||||
with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler:
|
||||
try:
|
||||
return_value = int(handler.read())
|
||||
except ValueError as ve:
|
||||
logger.error(
|
||||
"Error reading int value from {}. {}"
|
||||
"Returning default value {} instead".format(
|
||||
settings.LDAP_MAX_UID_FILE_PATH,
|
||||
str(ve),
|
||||
settings.LDAP_DEFAULT_START_UID
|
||||
)
|
||||
)
|
||||
return_value = settings.LDAP_DEFAULT_START_UID
|
||||
return return_value
|
||||
except FileNotFoundError as fnfe:
|
||||
logger.error("File not found : " + str(fnfe))
|
||||
return_value = settings.LDAP_DEFAULT_START_UID
|
||||
logger.error("So, returning UID={}".format(return_value))
|
||||
return return_value
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.4 on 2019-12-26 14:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('utils', '0007_auto_20191226_0610'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='billingaddress',
|
||||
name='vat_validation_status',
|
||||
field=models.CharField(blank=True, default='', max_length=25),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userbillingaddress',
|
||||
name='vat_validation_status',
|
||||
field=models.CharField(blank=True, default='', max_length=25),
|
||||
),
|
||||
]
|
|
@ -13,6 +13,11 @@ class BaseBillingAddress(models.Model):
|
|||
city = models.CharField(max_length=50)
|
||||
postal_code = models.CharField(max_length=50)
|
||||
country = CountryField()
|
||||
vat_number = models.CharField(max_length=100, default="", blank=True)
|
||||
stripe_tax_id = models.CharField(max_length=100, default="", blank=True)
|
||||
vat_number_validated_on = models.DateTimeField(blank=True, null=True)
|
||||
vat_validation_status = models.CharField(max_length=25, default="",
|
||||
blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
@ -20,7 +25,18 @@ class BaseBillingAddress(models.Model):
|
|||
|
||||
class BillingAddress(BaseBillingAddress):
|
||||
def __str__(self):
|
||||
return self.street_address
|
||||
if self.vat_number:
|
||||
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
|
||||
self.cardholder_name, self.street_address, self.city,
|
||||
self.postal_code, self.country, self.vat_number,
|
||||
self.stripe_tax_id, self.vat_number_validated_on,
|
||||
self.vat_validation_status
|
||||
)
|
||||
else:
|
||||
return "%s, %s, %s, %s, %s" % (
|
||||
self.cardholder_name, self.street_address, self.city,
|
||||
self.postal_code, self.country
|
||||
)
|
||||
|
||||
|
||||
class UserBillingAddress(BaseBillingAddress):
|
||||
|
@ -28,7 +44,18 @@ class UserBillingAddress(BaseBillingAddress):
|
|||
current = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.street_address
|
||||
if self.vat_number:
|
||||
return "%s, %s, %s, %s, %s, %s %s %s %s" % (
|
||||
self.cardholder_name, self.street_address, self.city,
|
||||
self.postal_code, self.country, self.vat_number,
|
||||
self.stripe_tax_id, self.vat_number_validated_on,
|
||||
self.vat_validation_status
|
||||
)
|
||||
else:
|
||||
return "%s, %s, %s, %s, %s" % (
|
||||
self.cardholder_name, self.street_address, self.city,
|
||||
self.postal_code, self.country
|
||||
)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
@ -37,6 +64,7 @@ class UserBillingAddress(BaseBillingAddress):
|
|||
'City': self.city,
|
||||
'Postal Code': self.postal_code,
|
||||
'Country': self.country,
|
||||
'VAT Number': self.vat_number
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
|
||||
from datacenterlight.models import StripePlan
|
||||
|
||||
stripe.api_key = settings.STRIPE_API_PRIVATE_KEY
|
||||
|
@ -32,32 +34,33 @@ def handleStripeError(f):
|
|||
logger.error(str(e))
|
||||
return response
|
||||
except stripe.error.RateLimitError as e:
|
||||
logger.error(str(e))
|
||||
response.update(
|
||||
{'error': "Too many requests made to the API too quickly"})
|
||||
return response
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
logger.error(str(e))
|
||||
response.update({'error': "Invalid parameters"})
|
||||
response.update({'error': str(e._message)})
|
||||
return response
|
||||
except stripe.error.AuthenticationError as e:
|
||||
# Authentication with Stripe's API failed
|
||||
# (maybe you changed API keys recently)
|
||||
logger.error(str(e))
|
||||
response.update({'error': common_message})
|
||||
response.update({'error': str(e)})
|
||||
return response
|
||||
except stripe.error.APIConnectionError as e:
|
||||
logger.error(str(e))
|
||||
response.update({'error': common_message})
|
||||
response.update({'error': str(e)})
|
||||
return response
|
||||
except stripe.error.StripeError as e:
|
||||
# maybe send email
|
||||
logger.error(str(e))
|
||||
response.update({'error': common_message})
|
||||
response.update({'error': str(e)})
|
||||
return response
|
||||
except Exception as e:
|
||||
# maybe send email
|
||||
logger.error(str(e))
|
||||
response.update({'error': common_message})
|
||||
response.update({'error': str(e)})
|
||||
return response
|
||||
|
||||
return handleProblems
|
||||
|
@ -67,7 +70,7 @@ class StripeUtils(object):
|
|||
CURRENCY = 'chf'
|
||||
INTERVAL = 'month'
|
||||
SUCCEEDED_STATUS = 'succeeded'
|
||||
STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
|
||||
RESOURCE_ALREADY_EXISTS_ERROR_CODE = 'resource_already_exists'
|
||||
STRIPE_NO_SUCH_PLAN = 'No such plan'
|
||||
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
|
||||
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
|
||||
|
@ -80,20 +83,31 @@ class StripeUtils(object):
|
|||
customer.save()
|
||||
|
||||
@handleStripeError
|
||||
def associate_customer_card(self, stripe_customer_id, token,
|
||||
def associate_customer_card(self, stripe_customer_id, id_payment_method,
|
||||
set_as_default=False):
|
||||
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||
card = customer.sources.create(source=token)
|
||||
stripe.PaymentMethod.attach(
|
||||
id_payment_method,
|
||||
customer=stripe_customer_id,
|
||||
)
|
||||
if set_as_default:
|
||||
customer.default_source = card.id
|
||||
customer.invoice_settings.default_payment_method = id_payment_method
|
||||
customer.save()
|
||||
return True
|
||||
|
||||
@handleStripeError
|
||||
def dissociate_customer_card(self, stripe_customer_id, card_id):
|
||||
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||
card = customer.sources.retrieve(card_id)
|
||||
card.delete()
|
||||
if card_id.startswith("pm"):
|
||||
logger.debug("PaymentMethod %s detached %s" % (card_id,
|
||||
stripe_customer_id))
|
||||
pm = stripe.PaymentMethod.retrieve(card_id)
|
||||
stripe.PaymentMethod.detach(card_id)
|
||||
pm.delete()
|
||||
else:
|
||||
logger.debug("card %s detached %s" % (card_id, stripe_customer_id))
|
||||
card = customer.sources.retrieve(card_id)
|
||||
card.delete()
|
||||
|
||||
@handleStripeError
|
||||
def update_customer_card(self, customer_id, token):
|
||||
|
@ -185,6 +199,24 @@ class StripeUtils(object):
|
|||
}
|
||||
return card_details
|
||||
|
||||
@handleStripeError
|
||||
def get_cards_details_from_payment_method(self, payment_method_id):
|
||||
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
|
||||
# payment_method does not always seem to have a card with id
|
||||
# if that is the case, fallback to payment_method_id for card_id
|
||||
card_id = payment_method_id
|
||||
if hasattr(payment_method.card, 'id'):
|
||||
card_id = payment_method.card.id
|
||||
card_details = {
|
||||
'last4': payment_method.card.last4,
|
||||
'brand': payment_method.card.brand,
|
||||
'exp_month': payment_method.card.exp_month,
|
||||
'exp_year': payment_method.card.exp_year,
|
||||
'fingerprint': payment_method.card.fingerprint,
|
||||
'card_id': card_id
|
||||
}
|
||||
return card_details
|
||||
|
||||
def check_customer(self, stripe_cus_api_id, user, token):
|
||||
try:
|
||||
customer = stripe.Customer.retrieve(stripe_cus_api_id)
|
||||
|
@ -204,11 +236,11 @@ class StripeUtils(object):
|
|||
return customer
|
||||
|
||||
@handleStripeError
|
||||
def create_customer(self, token, email, name=None):
|
||||
def create_customer(self, id_payment_method, email, name=None):
|
||||
if name is None or name.strip() == "":
|
||||
name = email
|
||||
customer = self.stripe.Customer.create(
|
||||
source=token,
|
||||
payment_method=id_payment_method,
|
||||
description=name,
|
||||
email=email
|
||||
)
|
||||
|
@ -226,7 +258,8 @@ class StripeUtils(object):
|
|||
return charge
|
||||
|
||||
@handleStripeError
|
||||
def get_or_create_stripe_plan(self, amount, name, stripe_plan_id):
|
||||
def get_or_create_stripe_plan(self, amount, name, stripe_plan_id,
|
||||
interval=""):
|
||||
"""
|
||||
This function checks if a StripePlan with the given
|
||||
stripe_plan_id already exists. If it exists then the function
|
||||
|
@ -238,6 +271,10 @@ class StripeUtils(object):
|
|||
:param stripe_plan_id: The id of the Stripe plan to be
|
||||
created. Use get_stripe_plan_id_string function to
|
||||
obtain the name of the plan to be created
|
||||
:param interval: str representing the interval of the Plan
|
||||
Specifies billing frequency. Either day, week, month or year.
|
||||
Ref: https://stripe.com/docs/api/plans/create#create_plan-interval
|
||||
The default is month
|
||||
:return: The StripePlan object if it exists else creates a
|
||||
Plan object in Stripe and a local StripePlan and
|
||||
returns it. Returns None in case of Stripe error
|
||||
|
@ -245,6 +282,7 @@ class StripeUtils(object):
|
|||
_amount = float(amount)
|
||||
amount = int(_amount * 100) # stripe amount unit, in cents
|
||||
stripe_plan_db_obj = None
|
||||
plan_interval = interval if interval is not "" else self.INTERVAL
|
||||
try:
|
||||
stripe_plan_db_obj = StripePlan.objects.get(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
|
@ -252,18 +290,24 @@ class StripeUtils(object):
|
|||
try:
|
||||
self.stripe.Plan.create(
|
||||
amount=amount,
|
||||
interval=self.INTERVAL,
|
||||
interval=plan_interval,
|
||||
name=name,
|
||||
currency=self.CURRENCY,
|
||||
id=stripe_plan_id)
|
||||
stripe_plan_db_obj = StripePlan.objects.create(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
if self.STRIPE_PLAN_ALREADY_EXISTS in str(e):
|
||||
logger.error(str(e))
|
||||
logger.error("error_code = %s" % str(e.__dict__))
|
||||
if self.RESOURCE_ALREADY_EXISTS_ERROR_CODE in e.error.code:
|
||||
logger.debug(
|
||||
self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
|
||||
stripe_plan_db_obj = StripePlan.objects.create(
|
||||
stripe_plan_db_obj, c = StripePlan.objects.get_or_create(
|
||||
stripe_plan_id=stripe_plan_id)
|
||||
if c:
|
||||
logger.debug("Created stripe plan %s" % stripe_plan_id)
|
||||
else:
|
||||
logger.debug("Plan %s exists already" % stripe_plan_id)
|
||||
return stripe_plan_db_obj
|
||||
|
||||
@handleStripeError
|
||||
|
@ -291,10 +335,15 @@ class StripeUtils(object):
|
|||
return return_value
|
||||
|
||||
@handleStripeError
|
||||
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
|
||||
def subscribe_customer_to_plan(self, customer, plans, trial_end=None,
|
||||
coupon="", tax_rates=list(),
|
||||
default_payment_method=""):
|
||||
"""
|
||||
Subscribes the given customer to the list of given plans
|
||||
|
||||
:param default_payment_method:
|
||||
:param tax_rates:
|
||||
:param coupon:
|
||||
:param customer: The stripe customer identifier
|
||||
:param plans: A list of stripe plans.
|
||||
:param trial_end: An integer representing when the Stripe subscription
|
||||
|
@ -308,10 +357,17 @@ class StripeUtils(object):
|
|||
]
|
||||
:return: The subscription StripeObject
|
||||
"""
|
||||
|
||||
logger.debug("Subscribing %s to plan %s : coupon = %s" % (
|
||||
customer, str(plans), str(coupon)
|
||||
))
|
||||
subscription_result = self.stripe.Subscription.create(
|
||||
customer=customer, items=plans, trial_end=trial_end
|
||||
customer=customer, items=plans, trial_end=trial_end,
|
||||
coupon=coupon,
|
||||
default_tax_rates=tax_rates,
|
||||
payment_behavior='allow_incomplete',
|
||||
default_payment_method=default_payment_method
|
||||
)
|
||||
logger.debug("Done subscribing")
|
||||
return subscription_result
|
||||
|
||||
@handleStripeError
|
||||
|
@ -342,7 +398,7 @@ class StripeUtils(object):
|
|||
|
||||
@staticmethod
|
||||
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
|
||||
price=None):
|
||||
price=None, excl_vat=True):
|
||||
"""
|
||||
Returns the Stripe plan id string of the form
|
||||
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
|
||||
|
@ -369,13 +425,16 @@ class StripeUtils(object):
|
|||
plan=dcl_plan_string
|
||||
)
|
||||
if price is not None:
|
||||
stripe_plan_id_string_with_price = '{}-{}chf'.format(
|
||||
stripe_plan_id_string = '{}-{}chf'.format(
|
||||
stripe_plan_id_string,
|
||||
round(price, 2)
|
||||
)
|
||||
return stripe_plan_id_string_with_price
|
||||
else:
|
||||
return stripe_plan_id_string
|
||||
if excl_vat:
|
||||
stripe_plan_id_string = '{}-{}'.format(
|
||||
stripe_plan_id_string,
|
||||
"excl_vat"
|
||||
)
|
||||
return stripe_plan_id_string
|
||||
|
||||
@staticmethod
|
||||
def get_vm_config_from_stripe_id(stripe_id):
|
||||
|
@ -404,18 +463,27 @@ class StripeUtils(object):
|
|||
|
||||
|
||||
@staticmethod
|
||||
def get_stripe_plan_name(cpu, memory, disk_size, price):
|
||||
def get_stripe_plan_name(cpu, memory, disk_size, price, excl_vat=True):
|
||||
"""
|
||||
Returns the Stripe plan name
|
||||
:return:
|
||||
"""
|
||||
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
|
||||
"{price} CHF".format(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=round(price, 2)
|
||||
)
|
||||
if excl_vat:
|
||||
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
|
||||
"{price} CHF Excl. VAT".format(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=round(price, 2)
|
||||
)
|
||||
else:
|
||||
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
|
||||
"{price} CHF".format(
|
||||
cpu=cpu,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
price=round(price, 2)
|
||||
)
|
||||
|
||||
@handleStripeError
|
||||
def set_subscription_meta_data(self, subscription_id, meta_data):
|
||||
|
@ -428,3 +496,78 @@ class StripeUtils(object):
|
|||
subscription = stripe.Subscription.retrieve(subscription_id)
|
||||
subscription.metadata = meta_data
|
||||
subscription.save()
|
||||
|
||||
@handleStripeError
|
||||
def get_or_create_tax_id_for_user(self, stripe_customer_id, vat_number,
|
||||
type="eu_vat", country=""):
|
||||
tax_ids_list = stripe.Customer.list_tax_ids(
|
||||
stripe_customer_id,
|
||||
limit=100,
|
||||
)
|
||||
for tax_id_obj in tax_ids_list.data:
|
||||
if self.compare_vat_numbers(tax_id_obj.value, vat_number):
|
||||
logger.debug("tax id obj exists already")
|
||||
return tax_id_obj
|
||||
else:
|
||||
logger.debug(
|
||||
"{val1} is not equal to {val2} or {con1} not same as "
|
||||
"{con2}".format(val1=tax_id_obj.value, val2=vat_number,
|
||||
con1=tax_id_obj.country.lower(),
|
||||
con2=country.lower().strip()))
|
||||
logger.debug(
|
||||
"tax id obj does not exist for {val}. Creating a new one".format(
|
||||
val=vat_number
|
||||
))
|
||||
tax_id_obj = stripe.Customer.create_tax_id(
|
||||
stripe_customer_id,
|
||||
type=type,
|
||||
value=vat_number,
|
||||
)
|
||||
return tax_id_obj
|
||||
|
||||
@handleStripeError
|
||||
def get_payment_intent(self, amount, customer):
|
||||
""" Create a stripe PaymentIntent of the given amount and return it
|
||||
:param amount: the amount of payment_intent
|
||||
:return:
|
||||
"""
|
||||
payment_intent_obj = stripe.PaymentIntent.create(
|
||||
amount=amount,
|
||||
currency='chf',
|
||||
customer=customer,
|
||||
setup_future_usage='off_session'
|
||||
)
|
||||
return payment_intent_obj
|
||||
|
||||
@handleStripeError
|
||||
def get_available_payment_methods(self, customer):
|
||||
""" Retrieves all payment methods of the given customer
|
||||
:param customer: StripeCustomer object
|
||||
:return: a list of available payment methods
|
||||
"""
|
||||
return_list = []
|
||||
if customer is None:
|
||||
return return_list
|
||||
cu = stripe.Customer.retrieve(customer.stripe_id)
|
||||
pms = stripe.PaymentMethod.list(
|
||||
customer=customer.stripe_id,
|
||||
type="card",
|
||||
)
|
||||
default_source = None
|
||||
if cu.default_source:
|
||||
default_source = cu.default_source
|
||||
else:
|
||||
default_source = cu.invoice_settings.default_payment_method
|
||||
for pm in pms.data:
|
||||
return_list.append({
|
||||
'last4': pm.card.last4, 'brand': pm.card.brand, 'id': pm.id,
|
||||
'exp_year': pm.card.exp_year,
|
||||
'exp_month': '{:02d}'.format(pm.card.exp_month),
|
||||
'preferred': pm.id == default_source
|
||||
})
|
||||
return return_list
|
||||
|
||||
def compare_vat_numbers(self, vat1, vat2):
|
||||
_vat1 = vat1.replace(" ", "").replace(".", "").replace("-","")
|
||||
_vat2 = vat2.replace(" ", "").replace(".", "").replace("-","")
|
||||
return True if _vat1 == _vat2 else False
|
||||
|
|
|
@ -228,7 +228,7 @@ class SSHKeyCreateView(FormView):
|
|||
if self.request.user.is_authenticated():
|
||||
owner = self.request.user
|
||||
manager = OpenNebulaManager(
|
||||
email=owner.email,
|
||||
email=owner.username,
|
||||
password=owner.password
|
||||
)
|
||||
keys_to_save = get_all_public_keys(self.request.user)
|
||||
|
|
|
@ -0,0 +1,324 @@
|
|||
start_date,stop_date,territory_codes,currency_code,rate,rate_type,description
|
||||
2011-01-04,,AI,XCD,0,standard,Anguilla (British overseas territory) is exempted of VAT.
|
||||
1984-01-01,,AT,EUR,0.2,standard,Austria (member state) standard VAT rate.
|
||||
1976-01-01,1984-01-01,AT,EUR,0.18,standard,
|
||||
1973-01-01,1976-01-01,AT,EUR,0.16,standard,
|
||||
1984-01-01,,"AT-6691
|
||||
DE-87491",EUR,0.19,standard,Jungholz (Austrian town) special VAT rate.
|
||||
1984-01-01,,"AT-6991
|
||||
AT-6992
|
||||
AT-6993
|
||||
DE-87567
|
||||
DE-87568
|
||||
DE-87569",EUR,0.19,standard,Mittelberg (Austrian town) special VAT rate.
|
||||
1996-01-01,,BE,EUR,0.21,standard,Belgium (member state) standard VAT rate.
|
||||
1994-01-01,1996-01-01,BE,EUR,0.205,standard,
|
||||
1992-04-01,1994-01-01,BE,EUR,0.195,standard,
|
||||
1983-01-01,1992-04-01,BE,EUR,0.19,standard,
|
||||
1981-07-01,1983-01-01,BE,EUR,0.17,standard,
|
||||
1978-07-01,1981-07-01,BE,EUR,0.16,standard,
|
||||
1971-07-01,1978-07-01,BE,EUR,0.18,standard,
|
||||
1999-01-01,,BG,BGN,0.2,standard,Bulgaria (member state) standard VAT rate.
|
||||
1996-07-01,1999-01-01,BG,BGN,0.22,standard,
|
||||
1994-04-01,1996-07-01,BG,BGN,0.18,standard,
|
||||
2011-01-04,,BM,BMD,0,standard,Bermuda (British overseas territory) is exempted of VAT.
|
||||
2014-01-13,,"CY
|
||||
GB-BFPO 57
|
||||
GB-BFPO 58
|
||||
GB-BFPO 59
|
||||
UK-BFPO 57
|
||||
UK-BFPO 58
|
||||
UK-BFPO 59",EUR,0.19,standard,"Cyprus (member state) standard VAT rate.
|
||||
Akrotiri and Dhekelia (British overseas territory) is subjected to Cyprus' standard VAT rate."
|
||||
2013-01-14,2014-01-13,CY,EUR,0.18,standard,
|
||||
2012-03-01,2013-01-14,CY,EUR,0.17,standard,
|
||||
2003-01-01,2012-03-01,CY,EUR,0.15,standard,
|
||||
2002-07-01,2003-01-01,CY,EUR,0.13,standard,
|
||||
2000-07-01,2002-07-01,CY,EUR,0.1,standard,
|
||||
1993-10-01,2000-07-01,CY,EUR,0.08,standard,
|
||||
1992-07-01,1993-10-01,CY,EUR,0.05,standard,
|
||||
2013-01-01,,CZ,CZK,0.21,standard,Czech Republic (member state) standard VAT rate.
|
||||
2010-01-01,2013-01-01,CZ,CZK,0.2,standard,
|
||||
2004-05-01,2010-01-01,CZ,CZK,0.19,standard,
|
||||
1995-01-01,2004-05-01,CZ,CZK,0.22,standard,
|
||||
1993-01-01,1995-01-01,CZ,CZK,0.23,standard,
|
||||
2007-01-01,,DE,EUR,0.19,standard,Germany (member state) standard VAT rate.
|
||||
1998-04-01,2007-01-01,DE,EUR,0.16,standard,
|
||||
1993-01-01,1998-04-01,DE,EUR,0.15,standard,
|
||||
1983-07-01,1993-01-01,DE,EUR,0.14,standard,
|
||||
1979-07-01,1983-07-01,DE,EUR,0.13,standard,
|
||||
1978-01-01,1979-07-01,DE,EUR,0.12,standard,
|
||||
1968-07-01,1978-01-01,DE,EUR,0.11,standard,
|
||||
1968-01-01,1968-07-01,DE,EUR,0.1,standard,
|
||||
2007-01-01,,DE-27498,EUR,0,standard,Heligoland (German island) is exempted of VAT.
|
||||
2007-01-01,,"DE-78266
|
||||
CH-8238",EUR,0,standard,Busingen am Hochrhein (German territory) is exempted of VAT.
|
||||
1992-01-01,,DK,DKK,0.25,standard,Denmark (member state) standard VAT rate.
|
||||
1980-06-30,1992-01-01,DK,DKK,0.22,standard,
|
||||
1978-10-30,1980-06-30,DK,DKK,0.2025,standard,
|
||||
1977-10-03,1978-10-30,DK,DKK,0.18,standard,
|
||||
1970-06-29,1977-10-03,DK,DKK,0.15,standard,
|
||||
1968-04-01,1970-06-29,DK,DKK,0.125,standard,
|
||||
1967-07-03,1968-04-01,DK,DKK,0.1,standard,
|
||||
2009-07-01,,EE,EUR,0.2,standard,Estonia (member state) standard VAT rate.
|
||||
1993-01-01,2009-07-01,EE,EUR,0.18,standard,
|
||||
1991-01-01,1993-01-01,EE,EUR,0.1,standard,
|
||||
2016-06-01,,"GR
|
||||
EL",EUR,0.24,standard,Greece (member state) standard VAT rate.
|
||||
2010-07-01,2016-06-01,"GR
|
||||
EL",EUR,0.23,standard,
|
||||
2010-03-15,2010-07-01,"GR
|
||||
EL",EUR,0.21,standard,
|
||||
2005-04-01,2010-03-15,"GR
|
||||
EL",EUR,0.19,standard,
|
||||
1990-04-28,2005-04-01,"GR
|
||||
EL",EUR,0.18,standard,
|
||||
1988-01-01,1990-04-28,"GR
|
||||
EL",EUR,0.16,standard,
|
||||
1987-01-01,1988-01-01,"GR
|
||||
EL",EUR,0.18,standard,
|
||||
2012-09-01,,ES,EUR,0.21,standard,Spain (member state) standard VAT rate.
|
||||
2010-07-01,2012-09-01,ES,EUR,0.18,standard,
|
||||
1995-01-01,2010-07-01,ES,EUR,0.16,standard,
|
||||
1992-08-01,1995-01-01,ES,EUR,0.15,standard,
|
||||
1992-01-01,1992-08-01,ES,EUR,0.13,standard,
|
||||
1986-01-01,1992-01-01,ES,EUR,0.12,standard,
|
||||
2012-09-01,,"ES-CN
|
||||
ES-GC
|
||||
ES-TF
|
||||
IC",EUR,0,standard,Canary Islands (Spanish autonomous community) is exempted of VAT.
|
||||
2012-09-01,,"ES-ML
|
||||
ES-CE
|
||||
EA",EUR,0,standard,Ceuta and Melilla (Spanish autonomous cities) is exempted of VAT.
|
||||
2013-01-01,,FI,EUR,0.24,standard,Finland (member state) standard VAT rate.
|
||||
2010-07-01,2013-01-01,FI,EUR,0.23,standard,
|
||||
1994-06-01,2010-07-01,FI,EUR,0.22,standard,
|
||||
2013-01-01,,"FI-01
|
||||
AX",EUR,0,standard,Aland Islands (Finish autonomous region) is exempted of VAT.
|
||||
2011-01-04,,FK,FKP,0,standard,Falkland Islands (British overseas territory) is exempted of VAT.
|
||||
1992-01-01,,FO,DKK,0,standard,Faroe Islands (Danish autonomous country) is exempted of VAT.
|
||||
2014-01-01,,"FR
|
||||
MC",EUR,0.2,standard,"France (member state) standard VAT rate.
|
||||
Monaco (sovereign city-state) is member of the EU VAT area and subjected to France's standard VAT rate."
|
||||
2000-04-01,2014-01-01,"FR
|
||||
MC",EUR,0.196,standard,
|
||||
1995-08-01,2000-04-01,"FR
|
||||
MC",EUR,0.206,standard,
|
||||
1982-07-01,1995-08-01,"FR
|
||||
MC",EUR,0.186,standard,
|
||||
1977-01-01,1982-07-01,"FR
|
||||
MC",EUR,0.176,standard,
|
||||
1973-01-01,1977-01-01,"FR
|
||||
MC",EUR,0.2,standard,
|
||||
1970-01-01,1973-01-01,"FR
|
||||
MC",EUR,0.23,standard,
|
||||
1968-12-01,1970-01-01,"FR
|
||||
MC",EUR,0.19,standard,
|
||||
1968-01-01,1968-12-01,"FR
|
||||
MC",EUR,0.1666,standard,
|
||||
2014-01-01,,"FR-BL
|
||||
BL",EUR,0,standard,Saint Barthelemy (French overseas collectivity) is exempted of VAT.
|
||||
2014-01-01,,"FR-GF
|
||||
GF",EUR,0,standard,Guiana (French overseas department) is exempted of VAT.
|
||||
2014-01-01,,"FR-GP
|
||||
GP",EUR,0.085,standard,Guadeloupe (French overseas department) special VAT rate.
|
||||
2014-01-01,,"FR-MF
|
||||
MF",EUR,0,standard,Saint Martin (French overseas collectivity) is subjected to France's standard VAT rate.
|
||||
2014-01-01,,"FR-MQ
|
||||
MQ",EUR,0.085,standard,Martinique (French overseas department) special VAT rate.
|
||||
2014-01-01,,"FR-NC
|
||||
NC",XPF,0,standard,New Caledonia (French special collectivity) is exempted of VAT.
|
||||
2014-01-01,,"FR-PF
|
||||
PF",XPF,0,standard,French Polynesia (French overseas collectivity) is exempted of VAT.
|
||||
2014-01-01,,"FR-PM
|
||||
PM",EUR,0,standard,Saint Pierre and Miquelon (French overseas collectivity) is exempted of VAT.
|
||||
2014-01-01,,"FR-RE
|
||||
RE",EUR,0.085,standard,Reunion (French overseas department) special VAT rate.
|
||||
2014-01-01,,"FR-TF
|
||||
TF",EUR,0,standard,French Southern and Antarctic Lands (French overseas territory) is exempted of VAT.
|
||||
2014-01-01,,"FR-WF
|
||||
WF",XPF,0,standard,Wallis and Futuna (French overseas collectivity) is exempted of VAT.
|
||||
2014-01-01,,"FR-YT
|
||||
YT",EUR,0,standard,Mayotte (French overseas department) is exempted of VAT.
|
||||
2011-01-04,,GG,GBP,0,standard,Guernsey (British Crown dependency) is exempted of VAT.
|
||||
2011-01-04,,GI,GIP,0,standard,Gibraltar (British overseas territory) is exempted of VAT.
|
||||
1992-01-01,,GL,DKK,0,standard,Greenland (Danish autonomous country) is exempted of VAT.
|
||||
2010-07-01,2016-06-01,"GR-34007
|
||||
EL-34007",EUR,0.16,standard,Skyros (Greek island) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-37002
|
||||
GR-37003
|
||||
GR-37005
|
||||
EL-37002
|
||||
EL-37003
|
||||
EL-37005",EUR,0.16,standard,Northern Sporades (Greek islands) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-64004
|
||||
EL-64004",EUR,0.16,standard,Thasos (Greek island) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-68002
|
||||
EL-68002",EUR,0.16,standard,Samothrace (Greek island) special VAT rate.
|
||||
2010-07-01,,"GR-69
|
||||
EL-69",EUR,0,standard,Mount Athos (Greek self-governed part) is exempted of VAT.
|
||||
2010-07-01,2016-06-01,"GR-81
|
||||
EL-81",EUR,0.16,standard,Dodecanese (Greek department) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-82
|
||||
EL-82",EUR,0.16,standard,Cyclades (Greek department) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-83
|
||||
EL-83",EUR,0.16,standard,Lesbos (Greek department) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-84
|
||||
EL-84",EUR,0.16,standard,Samos (Greek department) special VAT rate.
|
||||
2010-07-01,2016-06-01,"GR-85
|
||||
EL-85",EUR,0.16,standard,Chios (Greek department) special VAT rate.
|
||||
2011-01-04,,GS,GBP,0,standard,South Georgia and the South Sandwich Islands (British overseas territory) is exempted of VAT.
|
||||
2012-03-01,,HR,HRK,0.25,standard,Croatia (member state) standard VAT rate.
|
||||
2009-08-01,2012-03-01,HR,HRK,0.23,standard,
|
||||
1998-08-01,2009-08-01,HR,HRK,0.22,standard,
|
||||
2012-01-01,,HU,HUF,0.27,standard,Hungary (member state) standard VAT rate.
|
||||
2009-07-01,2012-01-01,HU,HUF,0.25,standard,
|
||||
2006-01-01,2009-07-01,HU,HUF,0.2,standard,
|
||||
1988-01-01,2006-01-01,HU,HUF,0.25,standard,
|
||||
2012-01-01,,IE,EUR,0.23,standard,Republic of Ireland (member state) standard VAT rate.
|
||||
2010-01-01,2012-01-01,IE,EUR,0.21,standard,
|
||||
2008-12-01,2010-01-01,IE,EUR,0.215,standard,
|
||||
2002-03-01,2008-12-01,IE,EUR,0.21,standard,
|
||||
2001-01-01,2002-03-01,IE,EUR,0.2,standard,
|
||||
1991-03-01,2001-01-01,IE,EUR,0.21,standard,
|
||||
1990-03-01,1991-03-01,IE,EUR,0.23,standard,
|
||||
1986-03-01,1990-03-01,IE,EUR,0.25,standard,
|
||||
1983-05-01,1986-03-01,IE,EUR,0.23,standard,
|
||||
1983-03-01,1983-05-01,IE,EUR,0.35,standard,
|
||||
1982-05-01,1983-03-01,IE,EUR,0.3,standard,
|
||||
1980-05-01,1982-05-01,IE,EUR,0.25,standard,
|
||||
1976-03-01,1980-05-01,IE,EUR,0.2,standard,
|
||||
1973-09-03,1976-03-01,IE,EUR,0.195,standard,
|
||||
1972-11-01,1973-09-03,IE,EUR,0.1637,standard,
|
||||
2011-01-04,,IO,GBP,0,standard,British Indian Ocean Territory (British overseas territory) is exempted of VAT.
|
||||
2013-10-01,,IT,EUR,0.22,standard,Italy (member state) standard VAT rate.
|
||||
2011-09-17,2013-10-01,IT,EUR,0.21,standard,
|
||||
1997-10-01,2011-09-17,IT,EUR,0.2,standard,
|
||||
1988-08-01,1997-10-01,IT,EUR,0.19,standard,
|
||||
1982-08-05,1988-08-01,IT,EUR,0.18,standard,
|
||||
1981-01-01,1982-08-05,IT,EUR,0.15,standard,
|
||||
1980-11-01,1981-01-01,IT,EUR,0.14,standard,
|
||||
1980-07-03,1980-11-01,IT,EUR,0.15,standard,
|
||||
1977-02-08,1980-07-03,IT,EUR,0.14,standard,
|
||||
1973-01-01,1977-02-08,IT,EUR,0.12,standard,
|
||||
2013-10-01,,"IT-22060
|
||||
CH-6911",CHF,0,standard,Campione (Italian town) is exempted of VAT.
|
||||
2013-10-01,,IT-23030,EUR,0,standard,Livigno (Italian town) is exempted of VAT.
|
||||
2011-01-04,,JE,GBP,0,standard,Jersey (British Crown dependency) is exempted of VAT.
|
||||
2011-01-04,,KY,KYD,0,standard,Cayman Islands (British overseas territory) is exempted of VAT.
|
||||
2009-09-01,,LT,EUR,0.21,standard,Lithuania (member state) standard VAT rate.
|
||||
2009-01-01,2009-09-01,LT,EUR,0.19,standard,
|
||||
1994-05-01,2009-01-01,LT,EUR,0.18,standard,
|
||||
2015-01-01,,LU,EUR,0.17,standard,Luxembourg (member state) standard VAT rate.
|
||||
1992-01-01,2015-01-01,LU,EUR,0.15,standard,
|
||||
1983-07-01,1992-01-01,LU,EUR,0.12,standard,
|
||||
1971-01-01,1983-07-01,LU,EUR,0.1,standard,
|
||||
1970-01-01,1971-01-01,LU,EUR,0.8,standard,
|
||||
2012-07-01,,LV,EUR,0.21,standard,Latvia (member state) standard VAT rate.
|
||||
2011-01-01,2012-07-01,LV,EUR,0.22,standard,
|
||||
2009-01-01,2011-01-01,LV,EUR,0.21,standard,
|
||||
1995-05-01,2009-01-01,LV,EUR,0.18,standard,
|
||||
2011-01-04,,MS,XCD,0,standard,Montserrat (British overseas territory) is exempted of VAT.
|
||||
2004-01-01,,MT,EUR,0.18,standard,Malta (member state) standard VAT rate.
|
||||
1995-01-01,2004-01-01,MT,EUR,0.15,standard,
|
||||
2012-10-01,,NL,EUR,0.21,standard,Netherlands (member state) standard VAT rate.
|
||||
2001-01-01,2012-10-01,NL,EUR,0.19,standard,
|
||||
1992-10-01,2001-01-01,NL,EUR,0.175,standard,
|
||||
1989-01-01,1992-10-01,NL,EUR,0.185,standard,
|
||||
1986-10-01,1989-01-01,NL,EUR,0.2,standard,
|
||||
1984-01-01,1986-10-01,NL,EUR,0.19,standard,
|
||||
1976-01-01,1984-01-01,NL,EUR,0.18,standard,
|
||||
1973-01-01,1976-01-01,NL,EUR,0.16,standard,
|
||||
1971-01-01,1973-01-01,NL,EUR,0.14,standard,
|
||||
1969-01-01,1971-01-01,NL,EUR,0.12,standard,
|
||||
2012-10-01,,"NL-AW
|
||||
AW",AWG,0,standard,Aruba (Dutch country) are exempted of VAT.
|
||||
2012-10-01,,"NL-CW
|
||||
NL-SX
|
||||
CW
|
||||
SX",ANG,0,standard,Curacao and Sint Maarten (Dutch countries) are exempted of VAT.
|
||||
2012-10-01,,"NL-BQ1
|
||||
NL-BQ2
|
||||
NL-BQ3
|
||||
BQ
|
||||
BQ-BO
|
||||
BQ-SA
|
||||
BQ-SE",USD,0,standard,"Bonaire, Saba and Sint Eustatius (Dutch special municipalities) are exempted of VAT."
|
||||
2011-01-01,,PL,PLN,0.23,standard,Poland (member state) standard VAT rate.
|
||||
1993-01-08,2011-01-01,PL,PLN,0.22,standard,
|
||||
2011-01-04,,PN,NZD,0,standard,Pitcairn Islands (British overseas territory) is exempted of VAT.
|
||||
2011-01-01,,PT,EUR,0.23,standard,Portugal (member state) standard VAT rate.
|
||||
2010-07-01,2011-01-01,PT,EUR,0.21,standard,
|
||||
2008-07-01,2010-07-01,PT,EUR,0.2,standard,
|
||||
2005-07-01,2008-07-01,PT,EUR,0.21,standard,
|
||||
2002-06-05,2005-07-01,PT,EUR,0.19,standard,
|
||||
1995-01-01,2002-06-05,PT,EUR,0.17,standard,
|
||||
1992-03-24,1995-01-01,PT,EUR,0.16,standard,
|
||||
1988-02-01,1992-03-24,PT,EUR,0.17,standard,
|
||||
1986-01-01,1988-02-01,PT,EUR,0.16,standard,
|
||||
2011-01-01,,PT-20,EUR,0.18,standard,Azores (Portuguese autonomous region) special VAT rate.
|
||||
2011-01-01,,PT-30,EUR,0.22,standard,Madeira (Portuguese autonomous region) special VAT rate.
|
||||
2017-01-01,,RO,RON,0.19,standard,Romania (member state) standard VAT rate.
|
||||
2016-01-01,2017-01-01,RO,RON,0.2,standard,Romania (member state) standard VAT rate.
|
||||
2010-07-01,2016-01-01,RO,RON,0.24,standard,
|
||||
2000-01-01,2010-07-01,RO,RON,0.19,standard,
|
||||
1998-02-01,2000-01-01,RO,RON,0.22,standard,
|
||||
1993-07-01,1998-02-01,RO,RON,0.18,standard,
|
||||
1990-07-01,,SE,SEK,0.25,standard,Sweden (member state) standard VAT rate.
|
||||
1983-01-01,1990-07-01,SE,SEK,0.2346,standard,
|
||||
1981-11-16,1983-01-01,SE,SEK,0.2151,standard,
|
||||
1980-09-08,1981-11-16,SE,SEK,0.2346,standard,
|
||||
1977-06-01,1980-09-08,SE,SEK,0.2063,standard,
|
||||
1971-01-01,1977-06-01,SE,SEK,0.1765,standard,
|
||||
1969-01-01,1971-01-01,SE,SEK,0.1111,standard,
|
||||
2011-01-04,,"AC
|
||||
SH
|
||||
SH-AC
|
||||
SH-HL",SHP,0,standard,Ascension and Saint Helena (British overseas territory) is exempted of VAT.
|
||||
2011-01-04,,"TA
|
||||
SH-TA",GBP,0,standard,Tristan da Cunha (British oversea territory) is exempted of VAT.
|
||||
2013-07-01,,SI,EUR,0.22,standard,Slovenia (member state) standard VAT rate.
|
||||
2002-01-01,2013-07-01,SI,EUR,0.2,standard,
|
||||
1999-07-01,2002-01-01,SI,EUR,0.19,standard,
|
||||
2011-01-01,,SK,EUR,0.2,standard,Slovakia (member state) standard VAT rate.
|
||||
2004-01-01,2011-01-01,SK,EUR,0.19,standard,
|
||||
2003-01-01,2004-01-01,SK,EUR,0.2,standard,
|
||||
1996-01-01,2003-01-01,SK,EUR,0.23,standard,
|
||||
1993-08-01,1996-01-01,SK,EUR,0.25,standard,
|
||||
1993-01-01,1993-08-01,SK,EUR,0.23,standard,
|
||||
2011-01-04,,TC,USD,0,standard,Turks and Caicos Islands (British overseas territory) is exempted of VAT.
|
||||
2011-01-04,,"GB
|
||||
UK
|
||||
IM",GBP,0.2,standard,"United Kingdom (member state) standard VAT rate.
|
||||
Isle of Man (British self-governing dependency) is member of the EU VAT area and subjected to UK's standard VAT rate."
|
||||
2010-01-01,2011-01-04,"GB
|
||||
UK
|
||||
IM",GBP,0.175,standard,
|
||||
2008-12-01,2010-01-01,"GB
|
||||
UK
|
||||
IM",GBP,0.15,standard,
|
||||
1991-04-01,2008-12-01,"GB
|
||||
UK
|
||||
IM",GBP,0.175,standard,
|
||||
1979-06-18,1991-04-01,"GB
|
||||
UK
|
||||
IM",GBP,0.15,standard,
|
||||
1974-07-29,1979-06-18,"GB
|
||||
UK
|
||||
IM",GBP,0.08,standard,
|
||||
1973-04-01,1974-07-29,"GB
|
||||
UK
|
||||
IM",GBP,0.1,standard,
|
||||
2011-01-04,,VG,USD,0,standard,British Virgin Islands (British overseas territory) is exempted of VAT.
|
||||
2014-01-01,,CP,EUR,0,standard,Clipperton Island (French overseas possession) is exempted of VAT.
|
||||
2019-11-15,,CH,CHF,0.077,standard,Switzerland standard VAT (added manually)
|
||||
2019-11-15,,MC,EUR,0.196,standard,Monaco standard VAT (added manually)
|
||||
2019-11-15,,FR,EUR,0.2,standard,France standard VAT (added manually)
|
||||
2019-11-15,,GR,EUR,0.24,standard,Greece standard VAT (added manually)
|
||||
2019-11-15,,GB,EUR,0.2,standard,UK standard VAT (added manually)
|
||||
2019-12-17,,AD,EUR,0.045,standard,Andorra standard VAT (added manually)
|
||||
2019-12-17,,TK,EUR,0.18,standard,Turkey standard VAT (added manually)
|
||||
2019-12-17,,IS,EUR,0.24,standard,Iceland standard VAT (added manually)
|
||||
2019-12-17,,FX,EUR,0.20,standard,France metropolitan standard VAT (added manually)
|
||||
2020-01-04,,CY,EUR,0.19,standard,Cyprus standard VAT (added manually)
|
||||
2019-01-04,,LI,EUR,0.077,standard,Liechtenstein standard VAT (added manually)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue