Initial commit: Hr packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 62531cd146
2820 changed files with 1432848 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
<defs>
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="98.616%">
<stop offset="0%" stop-color="#797C79"/>
<stop offset="100%" stop-color="#545554"/>
</linearGradient>
<path id="icon-d" d="M29.2777778,17.2348485 C34.4714536,17.2348485 38.681713,21.506348 38.681713,26.7755682 C38.681713,32.0447884 34.4714536,36.3162879 29.2777778,36.3162879 C24.0841019,36.3162879 19.8738426,32.0447884 19.8738426,26.7755682 C19.8738426,21.506348 24.0841019,17.2348485 29.2777778,17.2348485 Z M42.7933813,37.84729 C42.7933813,41.2291455 53.1142387,40.370791 53.1141828,46.7993457 C53.1141828,49.8785938 50.8543359,52.5088721 47.1832578,53.0575293 L47.1832578,55.6054688 C47.1832578,55.9614111 46.8831254,56.25 46.5129453,56.25 L44.2785703,56.25 C43.9083902,56.25 43.6082578,55.9614111 43.6082578,55.6054688 L43.6082578,53.0156885 C41.4017008,52.6548584 39.4354508,51.6754395 38.0920887,50.4390137 C37.8475922,50.2139648 37.8167578,49.8485693 38.0193598,49.5878564 L39.7168703,47.4035937 C39.944609,47.1106006 40.3787481,47.0596289 40.6753613,47.2886523 C42.0671535,48.3632471 43.8647641,49.2161768 45.5824957,49.2161768 C47.5829875,49.2161768 48.4941098,48.0702539 48.4941098,47.005542 C48.4941098,43.8578662 38.1732523,44.5412305 38.1732523,37.9061572 C38.1732523,35.0744092 40.3334461,32.784873 43.6083137,32.0710547 L43.6083137,29.3945312 C43.6083137,29.0385889 43.9084461,28.75 44.2786262,28.75 L46.5130012,28.75 C46.8831813,28.75 47.1833137,29.0385889 47.1833137,29.3945312 L47.1833137,31.9317822 C48.979416,32.1313721 50.9263387,32.8088281 52.2826602,33.9623779 C52.5156496,34.1605176 52.5744695,34.4877783 52.4264981,34.7508545 L51.1108981,37.0899121 C50.9184625,37.4321045 50.4576227,37.5328125 50.1291695,37.3056689 C48.8809918,36.4425342 47.3510035,35.783877 45.8581059,35.783877 C43.9963688,35.783877 42.7933813,36.5938379 42.7933813,37.84729 Z M35.2962963,36.7272727 C35.2962963,36.3141183 26.1175854,38.5736818 22.6967864,36.0773525 L18.5053937,37.1404272 C15.9936026,37.7775087 14.2314815,40.0671622 14.2314815,42.6939608 L14.2314815,44.9029356 C14.2314815,46.4837136 15.4945475,47.7651515 17.052662,47.7651515 C29.017554,47.7651515 35,47.7651515 35,47.7651515 C33.1481481,41.4242424 35.2962963,37.1404272 35.2962963,36.7272727 Z"/>
<path id="icon-e" d="M29.2777778,15.2348485 C34.4714536,15.2348485 38.681713,19.506348 38.681713,24.7755682 C38.681713,30.0447884 34.4714536,34.3162879 29.2777778,34.3162879 C24.0841019,34.3162879 19.8738426,30.0447884 19.8738426,24.7755682 C19.8738426,19.506348 24.0841019,15.2348485 29.2777778,15.2348485 Z M42.7933813,35.84729 C42.7933813,39.2291455 53.1142387,38.370791 53.1141828,44.7993457 C53.1141828,47.8785938 50.8543359,50.5088721 47.1832578,51.0575293 L47.1832578,53.6054688 C47.1832578,53.9614111 46.8831254,54.25 46.5129453,54.25 L44.2785703,54.25 C43.9083902,54.25 43.6082578,53.9614111 43.6082578,53.6054688 L43.6082578,51.0156885 C41.4017008,50.6548584 39.4354508,49.6754395 38.0920887,48.4390137 C37.8475922,48.2139648 37.8167578,47.8485693 38.0193598,47.5878564 L39.7168703,45.4035937 C39.944609,45.1106006 40.3787481,45.0596289 40.6753613,45.2886523 C42.0671535,46.3632471 43.8647641,47.2161768 45.5824957,47.2161768 C47.5829875,47.2161768 48.4941098,46.0702539 48.4941098,45.005542 C48.4941098,41.8578662 38.1732523,42.5412305 38.1732523,35.9061572 C38.1732523,33.0744092 40.3334461,30.784873 43.6083137,30.0710547 L43.6083137,27.3945312 C43.6083137,27.0385889 43.9084461,26.75 44.2786262,26.75 L46.5130012,26.75 C46.8831813,26.75 47.1833137,27.0385889 47.1833137,27.3945312 L47.1833137,29.9317822 C48.979416,30.1313721 50.9263387,30.8088281 52.2826602,31.9623779 C52.5156496,32.1605176 52.5744695,32.4877783 52.4264981,32.7508545 L51.1108981,35.0899121 C50.9184625,35.4321045 50.4576227,35.5328125 50.1291695,35.3056689 C48.8809918,34.4425342 47.3510035,33.783877 45.8581059,33.783877 C43.9963688,33.783877 42.7933813,34.5938379 42.7933813,35.84729 Z M35.2962963,34.7272727 C35.2962963,34.3141183 26.1175854,36.5736818 22.6967864,34.0773525 L18.5053937,35.1404272 C15.9936026,35.7775087 14.2314815,38.0671622 14.2314815,40.6939608 L14.2314815,42.9029356 C14.2314815,44.4837136 15.4945475,45.7651515 17.052662,45.7651515 C29.017554,45.7651515 35,45.7651515 35,45.7651515 C33.1481481,39.4242424 35.2962963,35.1404272 35.2962963,34.7272727 Z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="icon-b" fill="#fff">
<use xlink:href="#icon-a"/>
</mask>
<g mask="url(#icon-b)">
<rect width="70" height="70" fill="url(#icon-c)"/>
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
<path fill="#393939" d="M40.2790698,48 L4,48 C2,48 -7.10542736e-15,47.8556793 0,43.9590217 L2.0734159e-16,21.6246135 L20.6658658,0.0963982452 L37.783693,7.00604553 L34.121375,14.2273604 L43.799671,6.01962237 L50.8814479,14.2273604 L46.6827849,19.2273136 L52.6062041,25.9680455 L40.2790698,48 Z" opacity=".324" transform="translate(0 21)"/>
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1,11 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="5" width="36" height="60" rx="1" stroke="#875B7B" stroke-width="2"/>
<rect x="18" y="13" width="35" height="2" fill="#875B7B"/>
<rect x="18" y="54" width="35" height="2" fill="#875B7B"/>
<circle cx="35" cy="60" r="2" stroke="#C0A5BD" stroke-width="2"/>
<rect x="31" y="9" width="6" height="2" rx="1" fill="#C0A5BD"/>
<rect x="38" y="9" width="2" height="2" rx="1" fill="#C0A5BD"/>
<path d="M53 16H55V20H53V16Z" fill="#875B7B"/>
<path d="M53 21H55V27H53V21Z" fill="#875B7B"/>
<path d="M34.7313 43.5949C33.4391 43.5736 32.1821 43.4782 30.9604 43.3084C29.7386 43.1175 28.7518 42.8629 28 42.5447V39.8397C28.7988 40.1791 29.8209 40.4761 31.0661 40.7307C32.3113 40.9853 33.533 41.1232 34.7313 41.1444V34.716C33.58 34.419 32.5698 34.1008 31.7004 33.7613C30.8546 33.4007 30.1615 32.9976 29.6211 32.552C29.0808 32.1065 28.6696 31.5973 28.3877 31.0245C28.1292 30.4304 28 29.7515 28 28.9878C28 27.9482 28.2702 27.0571 28.8106 26.3146C29.3744 25.572 30.1615 24.9886 31.1718 24.5643C32.1821 24.1187 33.3686 23.8641 34.7313 23.8005V21H36.9868V23.7687C38.232 23.7899 39.3598 23.9172 40.37 24.1506C41.4038 24.3627 42.3436 24.6279 43.1894 24.9461L42.2379 27.3011C41.486 27.0253 40.652 26.7919 39.7357 26.601C38.8429 26.3888 37.9266 26.2509 36.9868 26.1873V32.5838C38.5374 32.9869 39.8297 33.4219 40.8634 33.8886C41.8972 34.3341 42.6725 34.8964 43.1894 35.5753C43.7298 36.233 44 37.0922 44 38.153C44 39.6381 43.3891 40.8474 42.1674 41.7809C40.9457 42.6932 39.2188 43.2554 36.9868 43.4676V47H34.7313V43.5949ZM36.9868 40.9853C38.373 40.858 39.3833 40.5716 40.0176 40.1261C40.652 39.6593 40.9692 39.0653 40.9692 38.3439C40.9692 37.8135 40.8517 37.3786 40.6167 37.0392C40.3818 36.6785 39.9706 36.3709 39.3833 36.1163C38.8194 35.8617 38.0206 35.6177 36.9868 35.3843V40.9853ZM34.7313 26.2509C33.8855 26.2934 33.1924 26.4313 32.652 26.6646C32.1116 26.8768 31.7004 27.1632 31.4185 27.5239C31.1601 27.8845 31.0308 28.2982 31.0308 28.765C31.0308 29.3166 31.1366 29.794 31.348 30.1971C31.583 30.5789 31.9706 30.9078 32.511 31.1836C33.0514 31.4382 33.7915 31.6716 34.7313 31.8837V26.2509Z" fill="#C0A5BD"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,10 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43 31H62L61.1219 63.0274C61.1071 63.5688 60.6639 64 60.1223 64H44.8777C44.3361 64 43.8929 63.5688 43.8781 63.0274L43 31Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M41 23C41 22.4477 41.4477 22 42 22H63C63.5523 22 64 22.4477 64 23V31H41V23Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M52.7567 6.64888C52.903 6.25857 53.2762 6 53.693 6H55.557C56.2552 6 56.7385 6.69737 56.4933 7.35112L54.0637 13.8302C54.0216 13.9425 54 14.0614 54 14.1813V21C54 21.5523 53.5523 22 53 22H51C50.4477 22 50 21.5523 50 21V14.1813C50 14.0614 50.0216 13.9425 50.0637 13.8302L52.7567 6.64888Z" fill="#C0A5BD"/>
<path d="M50.0637 13.8302L51 14.1813L50.0637 13.8302ZM54.0637 13.8302L55 14.1813L54.0637 13.8302ZM53.693 7H55.557V5H53.693V7ZM53 21H51V23H53V21ZM55.557 7L53.1273 13.4791L55 14.1813L57.4297 7.70225L55.557 7ZM53 14.1813V21H55V14.1813H53ZM51 21V14.1813H49V21H51ZM51 14.1813L53.693 7L51.8203 6.29775L49.1273 13.4791L51 14.1813ZM51 14.1813L51 14.1813L49.1273 13.4791C49.0431 13.7036 49 13.9415 49 14.1813H51ZM53.1273 13.4791C53.0431 13.7036 53 13.9415 53 14.1813H55L55 14.1813L53.1273 13.4791ZM51 21V21H49C49 22.1046 49.8954 23 51 23V21ZM53 23C54.1046 23 55 22.1046 55 21H53V21V23ZM55.557 7L55.557 7L57.4297 7.70225C57.92 6.39475 56.9534 5 55.557 5V7ZM53.693 5C52.8593 5 52.1131 5.51714 51.8203 6.29775L53.693 7L53.693 7V5Z" fill="#875B7B"/>
<path d="M8 59H35V63C35 63.5523 34.5523 64 34 64H9C8.44771 64 8 63.5523 8 63V59Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<rect x="8" y="55" width="27" height="4" rx="1" fill="#C0A5BD" stroke="#875B7B" stroke-width="2"/>
<path d="M21.5 36C28.8348 36 35 37.7845 35 46H8C8 37.7845 14.1652 36 21.5 36Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M5 53L6.63826 50.9522C7.43891 49.9514 8.96109 49.9514 9.76174 50.9522L9.83826 51.0478C10.6389 52.0486 12.1611 52.0486 12.9617 51.0478L13.0383 50.9522C13.8389 49.9514 15.3611 49.9514 16.1617 50.9522L16.2383 51.0478C17.0389 52.0486 18.5611 52.0486 19.3617 51.0478L19.4383 50.9522C20.2389 49.9514 21.7611 49.9514 22.5617 50.9522L22.6383 51.0478C23.4389 52.0486 24.9611 52.0486 25.7617 51.0478L25.8383 50.9522C26.6389 49.9514 28.1611 49.9514 28.9617 50.9522L29.0383 51.0478C29.8389 52.0486 31.3611 52.0486 32.1617 51.0478L32.2383 50.9522C33.0389 49.9514 34.5611 49.9514 35.3617 50.9522L37 53" stroke="#875B7B" stroke-width="2" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,11 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="11" y="33" width="48" height="30" rx="1" fill="white" stroke="#875B7B" stroke-width="2"/>
<rect x="9" y="21" width="52" height="12" rx="1" fill="white" stroke="#875B7B" stroke-width="2"/>
<rect x="32" y="33" width="6" height="29" fill="#C0A5BD" stroke="#875B7B" stroke-width="2"/>
<rect x="31" y="21" width="8" height="12" fill="#C0A5BD" stroke="#875B7B" stroke-width="2"/>
<path d="M37 14C37 10.134 40.134 7 44 7V7C47.866 7 51 10.134 51 14V14C51 17.866 47.866 21 44 21H37V14Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M33 14C33 10.134 29.866 7 26 7V7C22.134 7 19 10.134 19 14V14C19 17.866 22.134 21 26 21H33V14Z" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M50.1214 44.7782C49.9339 44.5907 49.8285 44.3363 49.8285 44.0711L49.8285 39.8284C49.8285 39.2762 50.2762 38.8284 50.8285 38.8284L55.0711 38.8284C55.3364 38.8284 55.5907 38.9338 55.7783 39.1213L61.8493 45.1924C62.2398 45.5829 62.2398 46.2161 61.8493 46.6066L57.6067 50.8493C57.2162 51.2398 56.583 51.2398 56.1925 50.8493L50.1214 44.7782Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.8284 39.8284C48.8284 38.7239 49.7238 37.8284 50.8284 37.8284L55.0711 37.8284C55.6015 37.8284 56.1102 38.0392 56.4853 38.4142L62.5563 44.4853C63.3374 45.2664 63.3374 46.5327 62.5563 47.3137L58.3137 51.5564C57.5326 52.3374 56.2663 52.3374 55.4853 51.5564L49.4142 45.4853C49.0391 45.1102 48.8284 44.6015 48.8284 44.0711L48.8284 39.8284ZM50.8284 39.8284L50.8284 44.0711L56.8995 50.1422L61.1421 45.8995L55.0711 39.8284L50.8284 39.8284Z" fill="#875B7B"/>
<path d="M43.179 33.5932L44.5932 32.179L53.7856 41.3714L52.3713 42.7856L43.179 33.5932Z" fill="#875B7B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,7 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="35" cy="35" r="29" fill="white" stroke="#875B7B" stroke-width="2"/>
<path d="M34 24C34 23.4477 34.4477 23 35 23V23C35.5523 23 36 23.4477 36 24V35C36 35.5523 35.5523 36 35 36V36C34.4477 36 34 35.5523 34 35V24Z" fill="#875B7B"/>
<circle cx="35" cy="39" r="3" stroke="#875B7B" stroke-width="2"/>
<path d="M25 49C25 48.4477 25.4477 48 26 48H44C44.5523 48 45 48.4477 45 49V53C45 53.5523 44.5523 54 44 54H26C25.4477 54 25 53.5523 25 53V49Z" fill="#C0A5BD" stroke="#875B7B" stroke-width="2"/>
<path d="M34 14.0234C28.1407 14.2981 22.9093 16.974 19.2678 21.0893L21.1247 22.9463L19.9462 24.1248L18.2083 22.3869C15.5661 25.8989 14 30.2666 14 35H17V37H12V35C12 22.2975 22.2975 12 35 12C47.7025 12 58 22.2975 58 35V37H53V35H56C56 30.0425 54.2822 25.4863 51.4093 21.894L49.1785 24.1248L48 22.9463L50.3149 20.6314C46.6965 16.7762 41.6391 14.2878 36 14.0234V17H34V14.0234Z" fill="#875B7B"/>
</svg>

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View file

@ -0,0 +1,23 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_42_408)">
<path d="M40.4601 57.92V53.56C40.4601 53.56 44.8201 50.91 44.8201 45.92V26.27L27.0001 24.08L20.0001 29.78C19.2314 30.4959 18.6188 31.3628 18.2006 32.3264C17.7825 33.29 17.5678 34.3296 17.5701 35.38V57.92H40.4601Z" fill="white" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M41.5501 57.92H15.3501V65.56H41.5501V57.92Z" fill="white"/>
<path d="M15.3501 65.57V57.92H41.5501V65.57" fill="#C0A5BD"/>
<path d="M15.3501 65.57V57.92H41.5501V65.57" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M18.6299 62.29H21.8999" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M52.4701 4.42999H29.5401C28.961 4.43264 28.4065 4.66454 27.998 5.07495C27.5895 5.48537 27.3601 6.0409 27.3601 6.61999V39.37L30.6301 42.64H52.4701C53.0475 42.6374 53.6004 42.4068 54.0087 41.9986C54.417 41.5903 54.6475 41.0374 54.6501 40.46V6.61999C54.6501 6.0409 54.4208 5.48537 54.0122 5.07495C53.6037 4.66454 53.0492 4.43264 52.4701 4.42999V4.42999Z" fill="white" stroke="#875B7B" stroke-miterlimit="10"/>
<path d="M30.6301 42.64H52.4701C53.0475 42.6374 53.6004 42.4068 54.0087 41.9986C54.417 41.5903 54.6475 41.0374 54.6501 40.46V6.61999C54.6501 6.0409 54.4208 5.48537 54.0122 5.07495C53.6037 4.66454 53.0492 4.43264 52.4701 4.42999H29.5401C28.961 4.43264 28.4065 4.66454 27.998 5.07495C27.5895 5.48537 27.3601 6.0409 27.3601 6.61999V39.37" fill="white"/>
<path d="M30.6301 42.64H52.4701C53.0475 42.6374 53.6004 42.4068 54.0087 41.9986C54.417 41.5903 54.6475 41.0374 54.6501 40.46V6.61999C54.6501 6.0409 54.4208 5.48537 54.0122 5.07495C53.6037 4.66454 53.0492 4.43264 52.4701 4.42999H29.5401C28.961 4.43264 28.4065 4.66454 27.998 5.07495C27.5895 5.48537 27.3601 6.0409 27.3601 6.61999V39.37" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M49.1899 4.42999H42.6399V42.64H49.1899V4.42999Z" fill="#C0A5BD" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M38.27 7.70999V12.08" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M38.27 14.26V18.63" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M35.3399 26.53C32.8099 29.07 22.9199 38.74 22.9199 38.74L31.6399 50.13C32.7046 48.7314 33.3794 47.0755 33.5955 45.3311C33.8116 43.5866 33.5612 41.8161 32.8699 40.2C32.8699 40.2 37.2799 35.26 38.3599 33.77C40.7699 30.2 37.8799 24 35.3399 26.53Z" fill="white"/>
<path d="M22.9199 38.74C22.9199 38.74 32.8099 29.07 35.3399 26.53C37.8699 23.99 40.7699 30.2 38.3399 33.77C37.2599 35.26 32.8499 40.2 32.8499 40.2C33.5412 41.8161 33.7916 43.5866 33.5755 45.331C33.3594 47.0755 32.6846 48.7314 31.6199 50.13" fill="white"/>
<path d="M22.9199 38.74C22.9199 38.74 32.8099 29.07 35.3399 26.53C37.8699 23.99 40.7699 30.2 38.3399 33.77C37.2599 35.26 32.8499 40.2 32.8499 40.2C33.5412 41.8161 33.7916 43.5866 33.5755 45.331C33.3594 47.0755 32.6846 48.7314 31.6199 50.13" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_42_408">
<rect width="70" height="70" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -0,0 +1,16 @@
<svg width="70" height="70" viewBox="0 0 70 70" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_42_450)">
<path d="M8.46992 15.2L8.41992 18.65L22.9499 26.39L30.5799 34.02L42.3699 22.05L8.46992 15.2Z" fill="#C0A5BD" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M47.95 27.63L35.98 39.42L43.61 47.05L51.35 61.58L54.8 61.52L47.95 27.63Z" fill="#C0A5BD" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M27.0899 49.7L28.6599 61.79L25.3899 63.28L19.6599 53.73" fill="white"/>
<path d="M27.0899 49.7L28.6599 61.79L25.3899 63.28L19.6599 53.73" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M20.3 42.91L8.20997 41.34L6.71997 44.61L16.27 50.34" fill="white"/>
<path d="M20.3 42.91L8.20997 41.34L6.71997 44.61L16.27 50.34" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
<path d="M62.7301 7.27001C60.8701 5.40001 54.4201 8.79001 52.5501 10.66L22.3301 40.88C20.3301 42.88 11.5001 54.47 13.5101 56.49C15.5201 58.51 27.1001 49.69 29.1201 47.67L59.3401 17.45C61.2101 15.58 64.6001 9.13001 62.7301 7.27001Z" fill="white" stroke="#875B7B" stroke-width="2" stroke-miterlimit="10"/>
</g>
<defs>
<clipPath id="clip0_42_450">
<rect width="70" height="70" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,28 @@
/** @odoo-module */
import { useService } from '@web/core/utils/hooks';
import { formatMonetary } from "@web/views/fields/formatters";
const { Component, onWillStart, useState } = owl;
export class ExpenseDashboard extends Component {
setup() {
super.setup();
this.orm = useService('orm');
this.state = useState({
expenses: {}
});
onWillStart(async () => {
const expense_states = await this.orm.call("hr.expense", 'get_expense_dashboard', []);
this.state.expenses = expense_states;
});
}
renderMonetaryField(value, currency_id) {
return formatMonetary(value, { currencyId: currency_id});;
}
}
ExpenseDashboard.template = 'hr_expense.ExpenseDashboard';

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.ExpenseDashboard" owl="1">
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
<t t-foreach="Object.entries(state.expenses)" t-as="expense" t-key="expense[0]">
<t t-set="name" t-value="expense[0]"/>
<t t-set="data" t-value="expense[1]"/>
<div t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<span t-esc="renderMonetaryField(data['amount'], data['currency'])" class="h2 m-0 text-odoo"/>
<b class="mx-2" t-esc="data['description']"/>
</div>
<div t-if="name !== 'approved'" t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<i class="fa fa-angle-right fa-3x"/>
</div>
</t>
<div class="fa fa-question-circle-o flex-grow-0.5 d-flex flex-column p-3 border-bottom text-center" t-if="env.debug" data-tooltip="Numbers computed from your personal expenses."/>
</div>
</t>
</templates>

View file

@ -0,0 +1,18 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
const actionRegistry = registry.category("actions");
class QRModalComponent extends Component {
setup() {
this.url = _.str.sprintf(
"/report/barcode/?barcode_type=QR&value=%s&width=256&height=256&humanreadable=1",
this.props.action.params.url);
}
}
QRModalComponent.template = "hr_expense.QRModalComponent"
actionRegistry.add("expense_qr_code_modal", QRModalComponent);

View file

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<templates>
<t t-name="hr_expense.QRModalComponent" owl="1">
<div style="text-align:center;" class="o_expense_modal">
<t t-if="url">
<h3>Scan this QR code to get the Odoo app:</h3><br/><br/>
<img class="border border-dark rounded" t-att-src="url"/>
</t>
</div>
</t>
</templates>

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,82 @@
odoo.define('hr_expense.tour', function(require) {
"use strict";
const {_t} = require('web.core');
const {Markup} = require('web.utils');
var tour = require('web_tour.tour');
tour.register('hr_expense_tour' , {
url: "/web",
rainbowManMessage: _t("There you go - expense management in a nutshell!"),
}, [tour.stepUtils.showAppsMenuItem(), {
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
content: _t("Wasting time recording your receipts? Lets try a better way."),
position: 'right',
edition: 'community'
}, {
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
content: _t("Wasting time recording your receipts? Lets try a better way."),
position: 'bottom',
edition: 'enterprise'
}, {
trigger: '.o_list_button_add',
extra_trigger: '.o_button_upload_expense',
content: _t("It all begins here - let's go!"),
position: 'bottom',
mobile: false,
}, {
trigger: '.o-kanban-button-new',
extra_trigger: '.o_button_upload_expense',
content: _t("It all begins here - let's go!"),
position: 'bottom',
mobile: true,
}, {
trigger: '.o_field_widget[name="product_id"] .o_input_dropdown',
extra_trigger: '.o_expense_form',
content: _t("Enter a name then choose a category and configure the amount of your expense."),
position: 'bottom',
}, {
trigger: '.o_form_status_indicator_dirty .o_form_button_save',
extra_trigger: '.o_expense_form',
content: Markup(_t("Ready? You can save it manually or discard modifications from here. You don't <em>need to save</em> - Odoo will save eveyrthing for you when you navigate.")),
position: 'bottom',
}, ...tour.stepUtils.statusbarButtonsSteps(_t("Attach Receipt"), _t("Attach a receipt - usually an image or a PDF file.")),
...tour.stepUtils.statusbarButtonsSteps(_t("Create Report"), _t("Create a report to submit one or more expenses to your manager.")),
...tour.stepUtils.statusbarButtonsSteps(_t("Submit to Manager"), Markup(_t('Once your <b>Expense Report</b> is ready, you can submit it to your manager and wait for approval.'))),
...tour.stepUtils.goBackBreadcrumbsMobile(
_t("Use the breadcrumbs to go back to the list of expenses."),
undefined,
".o_expense_form",
),
{
trigger: '.breadcrumb > li.breadcrumb-item:first',
extra_trigger: ".o_expense_form",
content: _t("Let's go back to your expenses."),
position: 'bottom',
mobile: false,
}, {
trigger: '.o_expense_container',
content: _t("The status of all your current expenses is visible from here."),
position: 'bottom',
},
tour.stepUtils.openBuggerMenu(),
{
trigger: "[data-menu-xmlid='hr_expense.menu_hr_expense_report']",
extra_trigger: '.o_main_navbar',
content: _t("Let's check out where you can manage all your employees expenses"),
position: "bottom"
}, {
trigger: '.o_list_renderer tbody tr[data-id]',
content: _t('Managers can inspect all expenses from here.'),
position: 'bottom',
mobile: false,
}, {
trigger: '.o_kanban_renderer .oe_kanban_card',
content: _t('Managers can inspect all expenses from here.'),
position: 'bottom',
mobile: true,
},
...tour.stepUtils.statusbarButtonsSteps(_t("Approve"), _t("Managers can approve the report here, then an accountant can post the accounting entries.")),
]);
});

View file

@ -0,0 +1,116 @@
/** @odoo-module */
import { useBus, useService } from '@web/core/utils/hooks';
const { useRef, useEffect, useState } = owl;
export const ExpenseDocumentDropZone = {
setup() {
this._super();
this.dragState = useState({
showDragZone: false,
});
this.root = useRef("root");
useEffect(
(el) => {
if (!el) {
return;
}
const highlight = this.highlight.bind(this);
const unhighlight = this.unhighlight.bind(this);
const drop = this.onDrop.bind(this);
el.addEventListener("dragover", highlight);
el.addEventListener("dragleave", unhighlight);
el.addEventListener("drop", drop);
return () => {
el.removeEventListener("dragover", highlight);
el.removeEventListener("dragleave", unhighlight);
el.removeEventListener("drop", drop);
};
},
() => [document.querySelector('.o_content')]
);
},
highlight(ev) {
ev.stopPropagation();
ev.preventDefault();
this.dragState.showDragZone = true;
},
unhighlight(ev) {
ev.stopPropagation();
ev.preventDefault();
this.dragState.showDragZone = false;
},
async onDrop(ev) {
ev.preventDefault();
await this.env.bus.trigger("change_file_input", {
files: ev.dataTransfer.files,
});
},
};
export const ExpenseDocumentUpload = {
setup() {
this._super();
this.actionService = useService('action');
this.notification = useService('notification');
this.orm = useService('orm');
this.http = useService('http');
this.fileInput = useRef('fileInput');
this.root = useRef("root");
this.isExpense = this.model.rootParams.resModel === "hr.expense";
useBus(this.env.bus, "change_file_input", async (ev) => {
this.fileInput.el.files = ev.detail.files;
await this.onChangeFileInput();
});
},
displayCreateReport() {
return this.isExpense;
},
async onCreateReportClick() {
const records = this.model.root.selection;
const recordIds = records.map((a) => a.resId);
const action = await this.orm.call('hr.expense', 'get_expenses_to_submit', [recordIds]);
this.actionService.doAction(action);
},
uploadDocument() {
this.fileInput.el.click();
},
async onChangeFileInput() {
const params = {
csrf_token: odoo.csrf_token,
ufile: [...this.fileInput.el.files],
model: 'hr.expense',
id: 0,
};
const fileData = await this.http.post('/web/binary/upload_attachment', params, "text");
const attachments = JSON.parse(fileData);
if (attachments.error) {
throw new Error(attachments.error);
}
this.onUpload(attachments);
},
async onUpload(attachments) {
const attachmentIds = attachments.map((a) => a.id);
if (!attachmentIds.length) {
this.notification.add(
this.env._t('An error occurred during the upload')
);
return;
}
const action = await this.orm.call('hr.expense', 'create_expense_from_attachments', ["", attachmentIds]);
this.actionService.doAction(action);
},
};

View file

@ -0,0 +1,46 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
const { onMounted, onPatched, useRef } = owl;
export const ExpenseMobileQRCode = {
setup() {
this._super();
this.root = useRef('root');
this.actionService = useService('action');
onMounted(this.bindAppsIcons);
onPatched(this.bindAppsIcons);
},
bindAppsIcons() {
const apps = this.root.el.querySelectorAll('.o_expense_mobile_app');
if (!apps) {
return;
}
const handler = this.handleClick.bind(this);
for (const app of apps) {
app.addEventListener('click', handler);
}
},
handleClick(ev) {
ev.preventDefault();
ev.stopPropagation();
const url = ev.currentTarget && ev.currentTarget.href;
if (!this.env.isSmall) {
this.actionService.doAction({
name: this.env._t("Download our App"),
type: "ir.actions.client",
tag: 'expense_qr_code_modal',
target: "new",
params: { url },
});
} else {
this.actionService.doAction({ type: "ir.actions.act_url", url });
}
}
};

View file

@ -0,0 +1,50 @@
.hr_expense {
@include media-breakpoint-up(md) {
&.o_list_view, &.o_kanban_renderer {
min-height: auto;
}
}
.o_view_nocontent {
top: 10%;
.o_view_nocontent_expense_receipt:before {
@extend %o-nocontent-init-image;
width: 300px;
height: 230px;
background: transparent url(/hr_expense/static/img/nocontent.png) no-repeat center;
background-size: 300px 230px;
margin-bottom: 0.75rem;
}
}
}
.o_kanban_view .o_cp_bottom_left:has(.o_button_create_report) {
align-items: baseline;
}
.o_expense_container {
@include media-breakpoint-down(sm) {
overflow: auto visible;
}
}
.o_dropzone {
width: 100%;
height: 100%;
position: absolute;
background-color: #AAAA;
z-index: 2;
left: 0;
top: 0;
i {
justify-content: center;
display: flex;
align-items: center;
height: 100%;
}
}
.o_expense_categories td[name="description"] p:last-child {
margin-bottom: 0;
}

View file

@ -0,0 +1,47 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { FormController } from "@web/views/form/form_controller";
import { formView } from "@web/views/form/form_view";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
export class ExpenseFormController extends FormController {
setup() {
super.setup();
this.dialogService = useService("dialog");
this.orm = useService("orm");
}
/**
* @override
*/
async beforeExecuteActionButton(clickParams) {
const record = this.model.root;
if (
clickParams.name === "action_submit_expenses" &&
record.data.duplicate_expense_ids.count
) {
return new Promise((resolve) => {
this.dialogService.add(ConfirmationDialog, {
body: this.env._t("An expense of same category, amount and date already exists."),
confirm: async () => {
await this.orm.call("hr.expense", "action_approve_duplicates", [record.resId]);
resolve(true);
},
}, {
onClose: resolve.bind(null, false),
});
});
}
return super.beforeExecuteActionButton(...arguments);
}
}
export const ExpenseFormView = {
...formView,
Controller: ExpenseFormController,
};
registry.category("views").add("hr_expense_form_view", ExpenseFormView);

View file

@ -0,0 +1,38 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { patch } from '@web/core/utils/patch';
import { ExpenseDashboard } from '../components/expense_dashboard';
import { ExpenseMobileQRCode } from '../mixins/qrcode';
import { ExpenseDocumentUpload, ExpenseDocumentDropZone } from '../mixins/document_upload';
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanController } from '@web/views/kanban/kanban_controller';
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
export class ExpenseKanbanController extends KanbanController {}
patch(ExpenseKanbanController.prototype, 'expense_kanban_controller_upload', ExpenseDocumentUpload);
export class ExpenseKanbanRenderer extends KanbanRenderer {}
patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode', ExpenseMobileQRCode);
patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode_dzone', ExpenseDocumentDropZone);
ExpenseKanbanRenderer.template = 'hr_expense.KanbanRenderer';
export class ExpenseDashboardKanbanRenderer extends ExpenseKanbanRenderer {}
ExpenseDashboardKanbanRenderer.components = { ...ExpenseDashboardKanbanRenderer.components, ExpenseDashboard};
ExpenseDashboardKanbanRenderer.template = 'hr_expense.DashboardKanbanRenderer';
registry.category('views').add('hr_expense_kanban', {
...kanbanView,
buttonTemplate: 'hr_expense.KanbanButtons',
Controller: ExpenseKanbanController,
Renderer: ExpenseKanbanRenderer,
});
registry.category('views').add('hr_expense_dashboard_kanban', {
...kanbanView,
buttonTemplate: 'hr_expense.KanbanButtons',
Controller: ExpenseKanbanController,
Renderer: ExpenseDashboardKanbanRenderer,
});

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.KanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-upload fa-10x"></i>
</div>
</xpath>
</t>
<t t-name="hr_expense.DashboardKanbanRenderer" t-inherit="hr_expense.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<ExpenseDashboard/>
</xpath>
</t>
<t t-name="hr_expense.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<!-- Remove class 'align-items-baseline' to ensure consistency with list buttons when adding a third button
(Create Report) on mobile. Instead, align-items: baseline is added to parent div in css -->
<xpath expr="//div[@t-if='props.showButtons']" position="attributes">
<attribute name="class" remove="align-items-baseline" separator=" "/>
</xpath>
<xpath expr="//t[@t-if='canCreate']" position="after">
<button type="button" class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Upload
</button>
<button t-if="displayCreateReport()" class="btn btn-secondary o_button_create_report" t-on-click="onCreateReportClick">
Create Report
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput" />
</xpath>
</t>
</templates>

View file

@ -0,0 +1,90 @@
/** @odoo-module */
import { ExpenseDashboard } from '../components/expense_dashboard';
import { ExpenseMobileQRCode } from '../mixins/qrcode';
import { ExpenseDocumentUpload, ExpenseDocumentDropZone } from '../mixins/document_upload';
import { registry } from '@web/core/registry';
import { patch } from '@web/core/utils/patch';
import { useService } from '@web/core/utils/hooks';
import { listView } from "@web/views/list/list_view";
import { ListController } from "@web/views/list/list_controller";
import { ListRenderer } from "@web/views/list/list_renderer";
const { onWillStart } = owl;
export class ExpenseListController extends ListController {
setup() {
super.setup();
this.orm = useService('orm');
this.actionService = useService('action');
this.rpc = useService("rpc");
this.user = useService("user");
this.isExpenseSheet = this.model.rootParams.resModel === "hr.expense.sheet";
onWillStart(async () => {
this.is_expense_team_approver = await this.user.hasGroup("hr_expense.group_hr_expense_team_approver");
this.is_account_invoicing = await this.user.hasGroup("account.group_account_invoice");
});
}
displaySubmit() {
const records = this.model.root.selection;
return records.length && records.every(record => record.data.state === 'draft') && this.isExpenseSheet;
}
displayApprove() {
const records = this.model.root.selection;
return this.is_expense_team_approver && records.length && records.every(record => record.data.state === 'submit') && this.isExpenseSheet;
}
displayPost() {
const records = this.model.root.selection;
return this.is_account_invoicing && records.length && records.every(record => record.data.state === 'approve') && this.isExpenseSheet;
}
async onClick (action) {
const records = this.model.root.selection;
const recordIds = records.map((a) => a.resId);
const model = this.model.rootParams.resModel;
const context = {};
if (action === 'approve_expense_sheets') {
context['validate_analytic'] = true;
}
await this.orm.call(model, action, [recordIds], {context: context});
// sgv note: we tried this.model.notify(); and does not work
await this.model.root.load();
this.render(true);
}
}
patch(ExpenseListController.prototype, 'expense_list_controller_upload', ExpenseDocumentUpload);
export class ExpenseListRenderer extends ListRenderer {
setup() {
super.setup()
}
}
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode', ExpenseMobileQRCode);
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode_dzone', ExpenseDocumentDropZone);
ExpenseListRenderer.template = 'hr_expense.ListRenderer';
export class ExpenseDashboardListRenderer extends ExpenseListRenderer {}
ExpenseDashboardListRenderer.components = { ...ExpenseDashboardListRenderer.components, ExpenseDashboard};
ExpenseDashboardListRenderer.template = 'hr_expense.DashboardListRenderer';
registry.category('views').add('hr_expense_tree', {
...listView,
buttonTemplate: 'hr_expense.ListButtons',
Controller: ExpenseListController,
Renderer: ExpenseListRenderer,
});
registry.category('views').add('hr_expense_dashboard_tree', {
...listView,
buttonTemplate: 'hr_expense.ListButtons',
Controller: ExpenseListController,
Renderer: ExpenseDashboardListRenderer,
});

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.ListButtons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
<!-- hr.expense and hr.expense.sheet -->
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
<button type="button" class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Upload
</button>
<button t-if="displayCreateReport()" class="btn btn-secondary o_button_create_report" t-on-click="onCreateReportClick">
Create Report
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput"/>
</xpath>
<!-- hr.expense.sheet -->
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displaySubmit()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_submit_sheet')">
Submit
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('approve_expense_sheets')">
Approve Report
</button>
</xpath>
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
<button t-if="displayPost()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_sheet_move_create')">
Post Entries
</button>
</xpath>
</t>
<t t-name="hr_expense.ListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<div t-if="dragState.showDragZone" class="o_dropzone">
<i class="fa fa-upload fa-10x"></i>
</div>
</xpath>
</t>
<t t-name="hr_expense.DashboardListRenderer" t-inherit="hr_expense.ListRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
<ExpenseDashboard/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="hr.expense.DocumentsHiddenUploadForm">
<div class="d-none o_expense_documents_upload">
<t t-call="HiddenInputFile">
<t t-set="multi_upload" t-value="true"/>
<t t-set="fileupload_id" t-value="widget.fileUploadID"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" t-att-value="'hr.expense'"/>
<input type="hidden" name="id" t-att-value="0"/>
</t>
</div>
</t>
<t t-name="hr.expense.DocumentDropZone">
<div class="o_drop_area d-none">
<i class="fa fa-upload fa-10x"></i>
</div>
</t>
<t t-extend="ListView.buttons" t-name="ExpensesListView.buttons">
<t t-jquery="button.o_list_button_add" t-operation="after">
<button type="button" t-att-class="'d-none d-md-block btn' + (!widget.isMobile ? ' btn-secondary' : '') + ' o_button_upload_expense'">
Scan
</button>
</t>
<t t-jquery="button.o_list_button_add" t-operation="before">
<button type="button" t-att-class="'d-block d-md-none btn' + (widget.isMobile ? ' btn-primary' : '') + ' o_button_upload_expense'">
Scan
</button>
</t>
<!-- hr.expense buttons -->
<t t-jquery="button.o_list_button_add" t-operation="after">
<button type="button" t-att-class="'btn btn-secondary' + (widget.isExpense ? '' : ' d-none') + ' o_button_create_report'">
Create Report
</button>
</t>
<t t-jquery="button.o_list_button_add" t-operation="replace">
<button type="button" t-att-class="'btn' + (widget.isMobile ? ' btn-secondary' : ' btn-primary') + ' o_list_button_add'" title="Create record" accesskey="c">
Create
</button>
</t>
</t>
</templates>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template">
<t t-name="hr_expense.dashboard_list_header">
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
<t t-foreach="expenses" t-as="expense">
<div t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<span t-esc="render_monetary_field(expenses[expense]['amount'], expenses[expense]['currency'])" class="h2 m-0 text-odoo"/>
<b class="mx-2" t-esc="expenses[expense]['description']"/>
</div>
<div t-if="expense !== 'approved'" t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<i class="fa fa-angle-right fa-3x"/>
</div>
</t>
</div>
</t>
</templates>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_expense_qr_code">
<div style="text-align:center;" class="o_expense_modal">
<t t-if="widget.url">
<h3>Scan this QR code to get the Odoo app:</h3><br/><br/>
<img class="border border-dark rounded" t-att-src="widget.url"/>
</t>
</div>
</t>
</templates>

View file

@ -0,0 +1,84 @@
/** @odoo-module **/
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { getFixture, nextTick } from "@web/../tests/helpers/utils";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { makeFakeHTTPService } from "@web/../tests/helpers/mock_services";
const serviceRegistry = registry.category("services");
let target;
let serverData;
QUnit.module("Expense", (hooks) => {
hooks.beforeEach(() => {
serviceRegistry.add("http", makeFakeHTTPService());
target = getFixture();
serverData = {
models: {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
},
},
},
};
});
QUnit.test("expense dashboard can horizontally scroll", async function (assert) {
// for this test, we need the elements to be visible in the viewport
target = document.body;
target.classList.add("debug");
registerCleanup(() => target.classList.remove("debug"));
serverData.views = {
"partner,false,search": `<search/>`,
"partner,false,list": `
<tree js_class="hr_expense_dashboard_tree">
<field name="display_name"/>
</tree>
`,
};
const webclient = await createWebClient({
serverData,
target,
async mockRPC(_, { method }) {
if (method === "get_expense_dashboard") {
return {
draft: {
description: "to report",
amount: 1000000000.00,
currency: 2,
},
reported: {
description: "under validation",
amount: 1000000000.00,
currency: 2,
},
approved: {
description: "to be reimbursed",
amount: 1000000000.00,
currency: 2,
},
};
}
},
});
await doAction(webclient, {
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "list"]],
});
const statusBar = target.querySelector(".o_expense_container");
statusBar.scrollLeft = 20;
await nextTick();
assert.strictEqual(
statusBar.scrollLeft,
20,
"the o_content should be 20 due to the overflow auto"
);
});
});

View file

@ -0,0 +1,83 @@
odoo.define('hr_expense.tests.tours', function (require) {
"use strict";
var tour = require('web_tour.tour');
tour.register('hr_expense_test_tour', {
test: true,
url: "/web",
}, [tour.stepUtils.showAppsMenuItem(),
{
content: "Go to Expense",
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
},
{
content: "Check Upload Button",
trigger: '.o_button_upload_expense',
run() {
const button = document.querySelector('.o_button_upload_expense');
if(!button) {
console.error('Missing Upload button in My Expenses to Report > List View');
}
}
},
{
content: "Check Create Report Button, but not click on it",
trigger: "button.o_switch_view.o_list.active",
run() {
const button = Array.from(document.querySelectorAll('.btn-secondary'))
.filter(element => element.textContent.includes('Create Report'));
if(!button) {
console.error('Missing Create Report button in My Expenses to Report > List View');
}
}
},
{
content: "Go to kanban view",
trigger: "button.o_switch_view.o_kanban",
},
{
content: "Check Upload Button",
trigger: "button.o_switch_view.o_kanban.active",
run() {
const button = document.querySelector('.o_button_upload_expense');
if(!button) {
console.error('Missing Upload button in My Expenses to Report > Kanban View');
}
}
},
{
content: "Check Create Report Button, but not click on it",
trigger: "button.o_switch_view.o_kanban.active",
run() {
const button = Array.from(document.querySelectorAll('.btn-secondary'))
.filter(element => element.textContent.includes('Create Report'));
if(!button) {
console.error('Missing Create Report button in My Expenses to Report > Kanban View');
}
}
},
{
content: "Go to Reporting",
trigger: 'button[data-menu-xmlid="hr_expense.menu_hr_expense_reports"]',
},
{
content: "Go to Expenses Analysis",
trigger: 'a[data-menu-xmlid="hr_expense.menu_hr_expense_all_expenses"]',
},
{
content: "Go to list view",
trigger: "button.o_switch_view.o_list",
},
{
content: "Check Upload Button",
trigger: "button.o_switch_view.o_list.active",
run() {
const button = document.querySelector('.o_button_upload_expense');
if(!button) {
console.error('Missing Upload button in Expenses Analysis > List View');
}
}
},
]);
});