19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

@ -1,23 +1 @@
<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="100%">
<stop offset="0%" stop-color="#B06161"/>
<stop offset="45.785%" stop-color="#984E4E"/>
<stop offset="100%" stop-color="#7C3838"/>
</linearGradient>
</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="M4,59 C2,59 -7.10542736e-15,58.8521303 0,54.8596491 L0,35.1929825 L29.9218256,0 L35,0 L35,11.3859649 L49,13.4561404 L54,35.1929825 L37.7206839,59 L4,59 Z" opacity=".324" transform="translate(0 11)"/>
<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)"/>
<path fill="#000" d="M37.9166667,30.1041667 L37.9166667,42.0625 C37.9166667,42.3116313 37.8365885,42.516276 37.6764323,42.6764323 C37.516276,42.8365885 37.3116313,42.9166667 37.0625,42.9166667 L28.5208333,42.9166667 C28.2717019,42.9166667 28.0670573,42.8365885 27.906901,42.6764323 C27.7467448,42.516276 27.6666667,42.3116313 27.6666667,42.0625 L27.6666667,40.3541667 C27.6666667,40.1050353 27.7467448,39.9003906 27.906901,39.7402344 C28.0670573,39.5800781 28.2717019,39.5 28.5208333,39.5 L34.5,39.5 L34.5,30.1041667 C34.5,29.8550353 34.5800781,29.6503906 34.7402344,29.4902344 C34.9003906,29.3300781 35.1050353,29.25 35.3541667,29.25 L37.0625,29.25 C37.3116313,29.25 37.516276,29.3300781 37.6764323,29.4902344 C37.8365885,29.6503906 37.9166667,29.8550353 37.9166667,30.1041667 Z M49.0208333,39.5 C49.0208333,36.8663189 48.3713095,34.4372824 47.0722656,32.2128906 C45.7732215,29.9884988 44.0115027,28.2267792 41.7871094,26.9277344 C39.5627158,25.6286895 37.1336811,24.9791667 34.5,24.9791667 C31.8663189,24.9791667 29.4372824,25.6286895 27.2128906,26.9277344 C24.9884988,28.2267792 23.2267792,29.9884988 21.9277344,32.2128906 C20.6286895,34.4372824 19.9791667,36.8663189 19.9791667,39.5 C19.9791667,42.1336811 20.6286895,44.5627158 21.9277344,46.7871094 C23.2267792,49.0115027 24.9884988,50.7732215 27.2128906,52.0722656 C29.4372824,53.3713095 31.8663189,54.0208333 34.5,54.0208333 C37.1336811,54.0208333 39.5627158,53.3713095 41.7871094,52.0722656 C44.0115027,50.7732215 45.7732215,49.0115027 47.0722656,46.7871094 C48.3713095,44.5627158 49.0208333,42.1336811 49.0208333,39.5 Z M50.0369141,26.0953981 L52.6900045,24.0961511 C53.1310787,23.7637779 53.7580818,23.8518974 54.090455,24.2929716 L55.8959001,26.6888781 C56.2282733,27.1299524 56.1401538,27.7569554 55.6990796,28.0893287 L52.8223311,30.2571141 C54.2741095,33.1090334 55,36.1899942 55,39.5 C55,43.219185 54.0835491,46.649198 52.250651,49.7900391 C50.4177527,52.9308798 47.9308798,55.4177527 44.7900391,57.250651 C41.649198,59.0835491 38.219185,60 34.5,60 C30.7808149,60 27.3508035,59.0835491 24.2099609,57.250651 C21.0691184,55.4177527 18.5822488,52.9308798 16.749349,49.7900391 C14.9164495,46.649198 14,43.219185 14,39.5 C14,35.7808149 14.9164495,32.3508035 16.749349,29.2099609 C18.5822488,26.0691184 21.0691184,23.5822488 24.2099609,21.749349 C26.6343486,20.3345506 29.2310275,19.4657874 32,19.143059 L32,17 L30,17 C29.4477153,17 29,16.5522847 29,16 L29,14 C29,13.4477153 29.4477153,13 30,13 L40,13 C40.5522847,13 41,13.4477153 41,14 L41,16 C41,16.5522847 40.5522847,17 40,17 L38,17 L38,19.2845984 C40.3963771,19.684578 42.6597235,20.5061615 44.7900391,21.749349 C46.8093002,22.9277288 48.5582591,24.3764115 50.0369141,26.0953981 Z" opacity=".3"/>
<path fill="#FFF" d="M37.9166667,28.1041667 L37.9166667,40.0625 C37.9166667,40.3116313 37.8365885,40.516276 37.6764323,40.6764323 C37.516276,40.8365885 37.3116313,40.9166667 37.0625,40.9166667 L28.5208333,40.9166667 C28.2717019,40.9166667 28.0670573,40.8365885 27.906901,40.6764323 C27.7467448,40.516276 27.6666667,40.3116313 27.6666667,40.0625 L27.6666667,38.3541667 C27.6666667,38.1050353 27.7467448,37.9003906 27.906901,37.7402344 C28.0670573,37.5800781 28.2717019,37.5 28.5208333,37.5 L34.5,37.5 L34.5,28.1041667 C34.5,27.8550353 34.5800781,27.6503906 34.7402344,27.4902344 C34.9003906,27.3300781 35.1050353,27.25 35.3541667,27.25 L37.0625,27.25 C37.3116313,27.25 37.516276,27.3300781 37.6764323,27.4902344 C37.8365885,27.6503906 37.9166667,27.8550353 37.9166667,28.1041667 Z M49.0208333,37.5 C49.0208333,34.8663189 48.3713095,32.4372824 47.0722656,30.2128906 C45.7732215,27.9884988 44.0115027,26.2267792 41.7871094,24.9277344 C39.5627158,23.6286895 37.1336811,22.9791667 34.5,22.9791667 C31.8663189,22.9791667 29.4372824,23.6286895 27.2128906,24.9277344 C24.9884988,26.2267792 23.2267792,27.9884988 21.9277344,30.2128906 C20.6286895,32.4372824 19.9791667,34.8663189 19.9791667,37.5 C19.9791667,40.1336811 20.6286895,42.5627158 21.9277344,44.7871094 C23.2267792,47.0115027 24.9884988,48.7732215 27.2128906,50.0722656 C29.4372824,51.3713095 31.8663189,52.0208333 34.5,52.0208333 C37.1336811,52.0208333 39.5627158,51.3713095 41.7871094,50.0722656 C44.0115027,48.7732215 45.7732215,47.0115027 47.0722656,44.7871094 C48.3713095,42.5627158 49.0208333,40.1336811 49.0208333,37.5 Z M50.0369141,24.0953981 L52.6900045,22.0961511 C53.1310787,21.7637779 53.7580818,21.8518974 54.090455,22.2929716 L55.8959001,24.6888781 C56.2282733,25.1299524 56.1401538,25.7569554 55.6990796,26.0893287 L52.8223311,28.2571141 C54.2741095,31.1090334 55,34.1899942 55,37.5 C55,41.219185 54.0835491,44.649198 52.250651,47.7900391 C50.4177527,50.9308798 47.9308798,53.4177527 44.7900391,55.250651 C41.649198,57.0835491 38.219185,58 34.5,58 C30.7808149,58 27.3508035,57.0835491 24.2099609,55.250651 C21.0691184,53.4177527 18.5822488,50.9308798 16.749349,47.7900391 C14.9164495,44.649198 14,41.219185 14,37.5 C14,33.7808149 14.9164495,30.3508035 16.749349,27.2099609 C18.5822488,24.0691184 21.0691184,21.5822488 24.2099609,19.749349 C26.6343486,18.3345506 29.2310275,17.4657874 32,17.143059 L32,15 L30,15 C29.4477153,15 29,14.5522847 29,14 L29,12 C29,11.4477153 29.4477153,11 30,11 L40,11 C40.5522847,11 41,11.4477153 41,12 L41,14 C41,14.5522847 40.5522847,15 40,15 L38,15 L38,17.2845984 C40.3963771,17.684578 42.6597235,18.5061615 44.7900391,19.749349 C46.8093002,20.9277288 48.5582591,22.3764115 50.0369141,24.0953981 Z"/>
</g>
</g>
</svg>
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M45.445 23.222A22.222 22.222 0 1 0 12.11 42.467l11.111-19.245h22.223Z" fill="#FC868B"/><path d="M5.313 32.53A22.222 22.222 0 1 0 37.889 7.533L26.778 26.778 5.313 32.53Z" fill="#2EBCFA"/><path d="M23.221 45.444c12.274 0 22.223-9.95 22.223-22.223 0-5.23-1.807-10.039-4.832-13.835a22.128 22.128 0 0 0-13.835-4.831c-12.273 0-22.222 9.949-22.222 22.222 0 5.23 1.807 10.04 4.831 13.835a22.128 22.128 0 0 0 13.835 4.832Z" fill="#2D6388"/><path d="M7.719 7.303c.646-.63 1.33-1.22 2.05-1.768.227.056.445.161.639.316L26.58 18.796c1.82 1.456 1.946 4.192.297 5.84-1.649 1.647-4.387 1.521-5.845-.297L8.075 8.181a1.651 1.651 0 0 1-.356-.878Z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 741 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

@ -0,0 +1,13 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.376 45.5317C17.1026 33.5076 23.9747 21.4504 33.1645 13.7722C38.4136 9.4823 45.6679 6.72472 52.2221 10.3616C51.9341 10.1907 43.9663 5.15163 43.9745 5.2212C42.6941 4.60548 41.2935 4.26875 39.8761 4.17736C31.2311 3.84811 24.2539 10.041 19.251 16.5016C15.3733 21.5634 12.3934 27.347 10.6655 33.4905C8.28743 41.465 8.5365 52.7762 15.5077 56.6752C15.5075 56.6753 24.089 61.0976 24.089 61.0976C19.9257 58.925 15.8798 52.1542 16.376 45.5317Z" fill="#FBDBD0"/>
<path d="M36.8403 11.2289C25.5026 17.8293 16.3392 33.833 16.3762 46.9724C16.4131 60.1091 25.6363 65.4088 36.974 58.8084C48.3144 52.2066 57.4776 36.2028 57.4407 23.0662C57.4037 9.9268 48.1806 4.62689 36.8403 11.2289ZM36.809 55.0952C27.2055 60.6649 19.3665 56.1774 19.3352 45.0923C19.3039 34.0038 27.092 20.4531 36.6956 14.8834C46.3021 9.31192 54.1411 13.7993 54.1724 24.888C54.2037 35.9732 46.4155 49.5239 36.809 55.0952Z" fill="white"/>
<path d="M36.809 55.0952C27.2055 60.6649 19.3665 56.1774 19.3352 45.0923C19.3039 34.0038 27.092 20.4531 36.6956 14.8834C46.3021 9.31192 54.1411 13.7993 54.1724 24.888C54.2037 35.9732 46.4155 49.5239 36.809 55.0952Z" fill="white"/>
<path d="M48.2282 13.2289C50.3568 15.6328 51.9008 19.8191 51.9135 24.0517C51.9476 35.3917 43.4906 49.2021 33.026 54.8997C29.727 56.6958 26.2628 56.199 23.4779 55.7818C27.781 58.7178 31.8169 57.9904 36.809 55.0952C46.4156 49.5239 54.2037 35.9732 54.1724 24.888C54.1565 19.253 52.658 15.3425 48.2282 13.2289Z" fill="#C1DBF6"/>
<path d="M40.7406 35.8751L46.1874 38.6438L45.0732 40.4964L39.8762 37.7142L40.7406 35.8751Z" fill="#374874"/>
<path d="M35.1716 32.7948L31.8221 23.0592L32.8727 22.3906L36.2633 31.6678L35.1716 32.7948Z" fill="#374874"/>
<path d="M34.8618 37.2808C34.972 35.4566 36.0146 33.5669 37.4087 32.4021C38.205 31.7514 39.3785 31.1622 40.3728 31.7139C40.3291 31.688 39.0694 30.9524 39.0706 30.9629C38.8764 30.8695 38.6639 30.8184 38.4489 30.8046C37.1374 30.7546 35.9306 31.8146 35.1716 32.7948C34.5833 33.5626 34.1313 34.4401 33.8691 35.3721C33.5084 36.5818 33.5462 38.2978 34.6037 38.8893C34.6037 38.8893 35.799 39.5839 35.799 39.5839C35.1674 39.2543 34.7865 38.2854 34.8618 37.2808Z" fill="#374874"/>
<path d="M39.522 31.9525C40.3559 31.9525 40.8557 32.6377 40.8589 33.7854C40.8642 35.6538 39.4945 38.0257 37.8678 38.9648C37.4252 39.2204 36.9888 39.3555 36.6058 39.3555C35.7496 39.3555 35.2364 38.6846 35.2333 37.5609C35.228 35.6912 36.6342 33.2972 38.3039 32.3331C38.7352 32.0841 39.1564 31.9525 39.522 31.9525ZM39.522 31.4954C39.0861 31.4954 38.5947 31.6375 38.0754 31.9373C36.2942 32.9656 34.7703 35.5155 34.7761 37.5623C34.7802 38.9998 35.538 39.8126 36.6058 39.8126C37.0583 39.8126 37.5663 39.6667 38.0964 39.3607C39.8776 38.3324 41.3219 35.8308 41.3161 33.784C41.312 32.3339 40.5811 31.4954 39.522 31.4954Z" fill="#374874"/>
<path d="M38.0805 33.7538C37.1711 34.2788 36.4336 35.5561 36.4366 36.6012C36.4396 37.6461 37.1818 38.0692 38.0913 37.5441C39.0007 37.019 39.7382 35.7418 39.7352 34.6967C39.7322 33.6516 38.99 33.2287 38.0805 33.7538Z" fill="#374874"/>
<path d="M39.2165 4.16477C39.4349 4.16477 39.6556 4.16903 39.8762 4.17739C41.2935 4.2688 42.6941 4.60551 43.9745 5.22137C43.9745 5.22091 43.9748 5.22071 43.9755 5.22071C44.0484 5.22071 48.222 7.84772 50.6054 9.34727C54.7836 11.2764 57.421 16.0536 57.4407 23.0662C57.4776 36.2029 48.3144 52.2066 36.974 58.8084C33.6668 60.7338 30.5403 61.6451 27.7689 61.6451C26.3434 61.6451 25.0119 61.4039 23.7981 60.9357C23.895 60.9913 23.9919 61.0471 24.089 61.0977C24.0877 61.0971 23.9331 61.0175 23.6716 60.8826C23.1017 60.6536 22.5592 60.3733 22.0449 60.0443C19.5068 58.7363 15.5076 56.6753 15.5077 56.6753C8.53651 52.7763 8.28744 41.4651 10.6655 33.4906C12.3934 27.3472 15.3733 21.5634 19.251 16.5017C24.1262 10.2059 30.8751 4.1642 39.2165 4.16477ZM39.2152 3.25049C32.0094 3.25049 25.0493 7.52057 18.5281 15.9419C14.5215 21.1719 11.5 27.1508 9.78746 33.2357C8.46776 37.6646 8.06266 42.5881 8.67615 46.7447C9.42938 51.8483 11.6374 55.5582 15.0614 57.4733L15.0654 57.4661C15.1426 57.5157 15.214 57.5525 15.2656 57.5791L15.7534 57.8305L17.4057 58.6821L21.589 60.8379C22.129 61.1803 22.7008 61.4749 23.29 61.7145L23.6583 61.9043L23.6695 61.9101C23.8036 61.9792 23.9468 62.012 24.088 62.012C24.0938 62.012 24.0996 62.012 24.1053 62.0119C25.2531 62.3754 26.4823 62.5593 27.7689 62.5593C30.8074 62.5593 34.0592 61.5631 37.4339 59.5986C43.055 56.3263 48.3229 50.7305 52.2672 43.8423C56.2112 36.9543 58.3732 29.5751 58.3549 23.0637C58.335 15.9992 55.6712 10.711 51.0434 8.54272L50.9002 8.45266C49.6953 7.69451 48.1026 6.6925 46.8023 5.88C45.2748 4.92553 44.6738 4.54999 44.3685 4.40221L44.3708 4.39753C43.0224 3.74887 41.53 3.36795 39.935 3.26505C39.9269 3.2645 39.9189 3.26416 39.9109 3.26381C39.6806 3.25499 39.447 3.25053 39.2165 3.25053L39.2152 3.25049Z" fill="#374874"/>
<path d="M44.4976 12.4896C50.21 12.489 54.1502 17.0316 54.1723 24.888C54.2036 35.9732 46.4155 49.5239 36.809 55.0952C34.0095 56.7188 31.3597 57.4879 29.0098 57.4879C23.2984 57.4879 19.3573 52.946 19.3351 45.0923C19.3039 34.0038 27.0919 20.453 36.6955 14.8834C39.4959 13.2593 42.1472 12.4899 44.4976 12.4896ZM44.4988 12.0325V12.4896L44.4985 12.0325C41.9771 12.0327 39.275 12.8589 36.4662 14.4879C26.7364 20.1308 18.8463 33.8604 18.878 45.0936C18.889 48.9926 19.8589 52.2528 21.6827 54.5217C23.4831 56.7612 26.0167 57.945 29.0098 57.945C31.5291 57.945 34.2303 57.1192 37.0383 55.4906C46.7698 49.8469 54.6612 36.118 54.6295 24.8866C54.6185 20.9867 53.6488 17.7259 51.8254 15.4567C50.0252 13.2165 47.4917 12.0325 44.4988 12.0325Z" fill="#374874"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,25 @@
import { registry } from "@web/core/registry";
import { formatPercentage } from "@web/views/fields/formatters";
import { progressBarField, ProgressBarField } from "@web/views/fields/progress_bar/progress_bar_field";
export class ProjectTaskProgressBarField extends ProgressBarField {
get currentValue() {
return super.currentValue * 100;
}
get progressBarColorClass() {
if (this.currentValue > this.maxValue) {
return super.progressBarColorClass;
}
return this.currentValue < 80 ? "bg-success" : "bg-warning";
}
}
export const projectTaskProgressBarField = {
...progressBarField,
component: ProjectTaskProgressBarField,
};
registry.category("fields").add("project_task_progressbar", projectTaskProgressBarField);
registry.category("formatters").add("project_task_progressbar", formatPercentage);

View file

@ -1,51 +1,40 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
import { buildM2OFieldDescription, Many2OneField } from "@web/views/fields/many2one/many2one_field";
import { Component, onWillStart } from "@odoo/owl";
export class TaskWithHours extends Component {
static template = "hr_timesheet.TaskWithHours";
static components = { Many2One };
static props = { ...Many2OneField.props };
class TaskWithHours extends Many2OneField {
get canCreate() {
return Boolean(this.context.default_project_id);
setup() {
super.setup();
onWillStart(this.onWillStart);
}
/**
* @override
*/
get displayName() {
const displayName = super.displayName;
return displayName ? displayName.split('\u00A0')[0] : displayName;
async onWillStart() { }
canCreate() {
return Boolean(this.props.context.default_project_id);
}
/**
* @override
*/
get context() {
return {...super.context, hr_timesheet_display_remaining_hours: true};
get m2oProps() {
const props = computeM2OProps(this.props);
return {
...props,
canCreate: props.canCreate && this.canCreate(),
canCreateEdit: props.canCreateEdit && this.canCreate(),
canQuickCreate: props.canQuickCreate && this.canCreate(),
context: { ...props.context, hr_timesheet_display_remaining_hours: true },
value: props.value && {
...props.value,
display_name: props.value.display_name?.split("\u00A0")[0],
},
};
}
/**
* @override
*/
get Many2XAutocompleteProps() {
const props = super.Many2XAutocompleteProps;
if (!this.canCreate) {
props.quickCreate = null;
}
return props;
}
/**
* @override
*/
computeActiveActions(props) {
super.computeActiveActions(props);
const activeActions = this.state.activeActions;
activeActions.create = activeActions.create && this.canCreate;
activeActions.createEdit = activeActions.create;
}
}
registry.category("fields").add("task_with_hours", TaskWithHours);
registry.category("fields").add("task_with_hours", {
...buildM2OFieldDescription(TaskWithHours),
});

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="hr_timesheet.TaskWithHours">
<Many2One t-props="m2oProps"/>
</t>
</templates>

View file

@ -0,0 +1,18 @@
import { registry } from "@web/core/registry";
import {_t} from "@web/core/l10n/translation";
import { FloatTimeField } from "@web/views/fields/float_time/float_time_field";
export class TimeHourField extends FloatTimeField {
get formattedValue() {
const unitAmount = super.formattedValue;
const [hourStr, minuteStr] = unitAmount.split(":");
const hours = parseInt(hourStr, 10);
const minutes = parseInt(minuteStr, 10);
return minutes ? _t("%(hours)sh%(minutes)s", { hours, minutes }) : _t("%(hours)sh", { hours });
}
}
export const timeHourField = {
component: TimeHourField,
};
registry.category("fields").add("time_hour_uom", timeHourField);

View file

@ -0,0 +1,22 @@
import { registry } from "@web/core/registry";
import { TimesheetUOM } from "../timesheet_uom/timesheet_uom";
import { TimeHourField } from "../time_hour_field/time_hour_field";
export class TimesheetDurationUOM extends TimesheetUOM {
static components = {
...TimesheetUOM.components,
TimeHourField,
};
get timesheetComponent() {
if (this.timesheetUOMService.timesheetWidget === "float_time") {
return this.timesheetUOMService.getTimesheetComponent("time_hour_uom");
}
return super.timesheetComponent;
}
}
export const timesheetDurationUOM = {
component: TimesheetDurationUOM,
};
registry.category("fields").add("timesheet_duration_uom", timesheetDurationUOM);

View file

@ -1,56 +1,36 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { session } from "@web/session";
import { registry } from "@web/core/registry";
import { FloatFactorField } from "@web/views/fields/float_factor/float_factor_field";
import { FloatToggleField } from "@web/views/fields/float_toggle/float_toggle_field";
import { FloatTimeField } from "@web/views/fields/float_time/float_time_field";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
const { Component } = owl;
import { Component } from "@odoo/owl";
export class TimesheetUOM extends Component {
static props = {
...standardFieldProps,
};
static template = "hr_timesheet.TimesheetUOM";
static components = { FloatFactorField, FloatToggleField, FloatTimeField };
setup() {
this.companyService = useService("company");
}
get timesheetUOMId() {
return this.companyService.currentCompany.timesheet_uom_id;
}
get timesheetWidget() {
let timesheet_widget = "float_factor";
if (this.timesheetUOMId in session.uom_ids) {
timesheet_widget = session.uom_ids[this.timesheetUOMId].timesheet_widget;
}
return timesheet_widget;
this.timesheetUOMService = useService("timesheet_uom");
}
get timesheetComponent() {
return registry.category("fields").get(this.timesheetWidget, FloatFactorField);
return this.timesheetUOMService.getTimesheetComponent();
}
get timesheetComponentProps() {
const factorDependantComponents = ["float_toggle", "float_factor"];
return factorDependantComponents.includes(this.timesheetWidget) ? this.FactorCompanyDependentProps : this.props;
return this.timesheetUOMService.getTimesheetComponentProps(this.props);
}
get FactorCompanyDependentProps() {
const factor = this.companyService.currentCompany.timesheet_uom_factor || this.props.factor;
return { ...this.props, factor };
}
}
TimesheetUOM.props = {
...standardFieldProps,
export const timesheetUOM = {
component: TimesheetUOM,
};
TimesheetUOM.template = "hr_timesheet.TimesheetUOM";
TimesheetUOM.components = { FloatFactorField, FloatToggleField, FloatTimeField };
registry.category("fields").add("timesheet_uom", TimesheetUOM);
registry.category("fields").add("timesheet_uom", timesheetUOM);

View file

@ -0,0 +1,3 @@
div.o_field_timesheet_uom > button.o_field_float_toggle {
width: 50px !important;
}

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="hr_timesheet.TimesheetUOM" owl="1">
<t t-name="hr_timesheet.TimesheetUOM">
<t t-component="timesheetComponent" t-props="timesheetComponentProps"/>
</t>

View file

@ -1,20 +1,22 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { TimesheetUOM } from "../timesheet_uom/timesheet_uom";
import { TimesheetUOM, timesheetUOM } from "../timesheet_uom/timesheet_uom";
export class TimesheetUOMNoToggle extends TimesheetUOM {
get timesheetWidget() {
const timesheetWidget = super.timesheetWidget;
return timesheetWidget !== "float_toggle" ? timesheetWidget : "float_factor";
get timesheetComponent() {
if (this.timesheetUOMService.timesheetWidget === "float_toggle") {
return this.timesheetUOMService.getTimesheetComponent("float_factor");
}
return super.timesheetComponent;
}
}
// As FloatToggleField won't be used by TimesheetUOMNoToggle, we remove it from the components that we get from TimesheetUOM.
delete TimesheetUOMNoToggle.components.FloatToggleField;
registry.category("fields").add("timesheet_uom_no_toggle", TimesheetUOMNoToggle);
export const timesheetUOMNoToggle = {
...timesheetUOM,
component: TimesheetUOMNoToggle,
};
registry.category("fields").add("timesheet_uom_no_toggle", timesheetUOMNoToggle);

View file

@ -1,50 +0,0 @@
/** @odoo-module alias=hr_timesheet.task_with_hours **/
import field_registry from 'web.field_registry';
import TimesheetFieldMany2One from 'hr_timesheet.TimesheetFieldMany2one';
const TaskWithHours = TimesheetFieldMany2One.extend({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
this.additionalContext.hr_timesheet_display_remaining_hours = true;
// By default, we keep the no_quick_create value or we set to false.
this.nodeOptions.no_quick_create = this.nodeOptions.no_quick_create || false;
},
/**
* @override
*/
_getDisplayNameWithoutHours: function (value) {
return value && value.split('\u00A0')[0];
},
/**
* @override
* @private
*/
_onInputClick: function () {
const context = Object.assign(
this.record.getContext(this.recordParams),
this.additionalContext
);
// We don't want to quick create if no project is set in the timesheet
const canCreate = 'default_project_id' in context && context.default_project_id;
this.nodeOptions.no_quick_create =
this.nodeOptions.no_quick_create || !canCreate;
this.can_create = this.can_create && canCreate;
this._super.apply(this, arguments);
},
/**
* @override
* @private
*/
_renderEdit: function (){
this.m2o_value = this._getDisplayNameWithoutHours(this.m2o_value);
this._super.apply(this, arguments);
},
});
field_registry.add('task_with_hours', TaskWithHours);
export default TaskWithHours;

View file

@ -1,35 +0,0 @@
/** @odoo-module alias=hr_timesheet.TimesheetFieldMany2one **/
import FieldRegistry from 'web.field_registry';
import { FieldMany2One } from 'web.relational_fields';
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { Component } from "@odoo/owl";
const TimesheetFieldMany2one = FieldMany2One.extend({
/**
* @override
* @private
*/
_searchCreatePopup(view, ids, context, dynamicFilters) {
const options = this._getSearchCreatePopupOptions(view, ids, context, dynamicFilters);
Component.env.services.dialog.add(SelectCreateDialog, {
title: options.title,
resModel: options.res_model,
multiSelect: false,
domain: options.domain,
context: options.context,
noCreate: options.no_create,
onSelected: (resId) => {
return this.reinitialize(resId);
},
onClose: () => {
this.activate();
}
});
},
});
FieldRegistry.add('timesheet_field_many2one', TimesheetFieldMany2one);
export default TimesheetFieldMany2one;

View file

@ -1,196 +0,0 @@
odoo.define('hr_timesheet.timesheet_uom', function (require) {
'use strict';
const { registry } = require("@web/core/registry");
const basicFields = require('web.basic_fields');
const fieldUtils = require('web.field_utils');
const fieldRegistry = require('web.field_registry');
// We need the field registry to be populated, as we bind the
// timesheet_uom widget on existing field widgets.
require('web._field_registry');
const session = require('web.session');
const TimesheetUOMMultiCompanyMixin = {
init: function(parent, name, record, options) {
this._super(parent, name, record, options);
const currentCompanyId = session.user_context.allowed_company_ids[0];
const currentCompany = session.user_companies.allowed_companies[currentCompanyId];
this.currentCompanyTimesheetUOMFactor = currentCompany.timesheet_uom_factor || 1;
}
};
/**
* Extend the float factor widget to set default value for timesheet
* use case. The 'factor' is forced to be the UoM timesheet
* conversion from the session info.
**/
const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend(TimesheetUOMMultiCompanyMixin).extend({
formatType: 'float_factor',
/**
* Override init to tweak options depending on the session info
*
* @constructor
* @override
*/
init: function(parent, name, record, options) {
this._super(parent, name, record, options);
// force factor in format and parse options
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
this.parseOptions.factor = this.currentCompanyTimesheetUOMFactor;
},
});
/**
* Extend the float toggle widget to set default value for timesheet
* use case. The 'range' is different from the default one of the
* native widget, and the 'factor' is forced to be the UoM timesheet
* conversion.
**/
const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend(TimesheetUOMMultiCompanyMixin).extend({
formatType: 'float_factor',
/**
* Override init to tweak options depending on the session info
*
* @constructor
* @override
*/
init: function(parent, name, record, options) {
options = options || {};
var fieldsInfo = record.fieldsInfo[options.viewType || 'default'];
var attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {};
var hasRange = _.contains(_.keys(attrs.options || {}), 'range');
this._super(parent, name, record, options);
// Set the timesheet widget options: the range can be customized
// by setting the option on the field in the view. The factor
// is forced to be the UoM conversion factor.
if (!hasRange) {
this.nodeOptions.range = [0.00, 1.00, 0.50];
}
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
},
});
/**
* Extend float time widget
*/
const FieldTimesheetTime = basicFields.FieldFloatTime.extend(TimesheetUOMMultiCompanyMixin).extend({
init: function () {
this._super.apply(this, arguments);
this.nodeOptions.factor = this.currentCompanyTimesheetUOMFactor;
this.parseOptions.factor = this.currentCompanyTimesheetUOMFactor;
}
});
const timesheetUomService = {
dependencies: ["legacy_session"],
start() {
const timesheetUomInfo = {
widget: null,
factor: 1,
};
if (session.user_context &&
session.user_context.allowed_company_ids &&
session.user_context.allowed_company_ids.length) {
const currentCompanyId = session.user_context.allowed_company_ids[0];
const currentCompany = session.user_companies.allowed_companies[currentCompanyId];
const currentCompanyTimesheetUOMId = currentCompany.timesheet_uom_id || false;
timesheetUomInfo.factor = currentCompany.timesheet_uom_factor || 1;
if (currentCompanyTimesheetUOMId) {
timesheetUomInfo.widget = session.uom_ids[currentCompanyTimesheetUOMId].timesheet_widget;
}
}
/**
* Binding depending on Company Preference
*
* determine wich widget will be the timesheet one.
* Simply match the 'timesheet_uom' widget key with the correct
* implementation (float_time, float_toggle, ...). The default
* value will be 'float_factor'.
**/
const widgetName = timesheetUomInfo.widget || 'float_factor';
let FieldTimesheetUom = null;
if (widgetName === 'float_toggle') {
FieldTimesheetUom = FieldTimesheetToggle;
} else if (widgetName === 'float_time') {
FieldTimesheetUom = FieldTimesheetTime;
} else {
FieldTimesheetUom = (
fieldRegistry.get(widgetName) &&
fieldRegistry.get(widgetName).extend({ })
) || FieldTimesheetFactor;
}
fieldRegistry.add('timesheet_uom', FieldTimesheetUom);
// widget timesheet_uom_no_toggle is the same as timesheet_uom but without toggle.
// We can modify easly huge amount of days.
let FieldTimesheetUomWithoutToggle = null;
if (widgetName === 'float_toggle') {
FieldTimesheetUomWithoutToggle = FieldTimesheetFactor;
} else {
FieldTimesheetUomWithoutToggle = FieldTimesheetTime;
}
fieldRegistry.add('timesheet_uom_no_toggle', FieldTimesheetUomWithoutToggle);
// bind the formatter and parser method, and tweak the options
const _tweak_options = (options) => {
if (!_.contains(options, 'factor')) {
options.factor = timesheetUomInfo.factor;
}
return options;
};
fieldUtils.format.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || { });
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.parse.timesheet_uom = function(value, field, options) {
options = _tweak_options(options || { });
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
fieldUtils.format.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || { });
const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.parse.timesheet_uom_no_toggle = function(value, field, options) {
options = _tweak_options(options || { });
const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType];
return parser(value, field, options);
};
if (!registry.category("formatters").contains("timesheet_uom")) {
registry.category("formatters").add("timesheet_uom", fieldUtils.format.timesheet_uom);
}
if (!registry.category("formatters").contains("timesheet_uom_no_toggle")) {
registry.category("formatters").add("timesheet_uom_no_toggle", fieldUtils.format.timesheet_uom_no_toggle);
}
return timesheetUomInfo;
},
};
registry.category("services").add("timesheet_uom", timesheetUomService);
return {
FieldTimesheetFactor,
FieldTimesheetTime,
FieldTimesheetToggle,
timesheetUomService,
};
});

View file

@ -1,3 +1,3 @@
.o_project_kanban .oe_kanban_align.badge {
color: inherit;
.o_web_studio_form_view_editor .o_field_widget.o_web_studio_widget_empty.o_task_planned_hours {
max-width: 70ch;
}

View file

@ -0,0 +1,57 @@
import { session } from "@web/session";
import { registry } from "@web/core/registry";
import { formatFloatTime, formatFloatFactor } from "@web/views/fields/formatters";
import { formatFloat } from "@web/core/utils/numbers";
import { FloatFactorField } from "@web/views/fields/float_factor/float_factor_field";
import { user } from "@web/core/user";
export const timesheetUOMService = {
start() {
const service = {
get timesheetUOMId() {
return user.activeCompany.timesheet_uom_id;
},
get timesheetWidget() {
let timesheet_widget = "float_factor";
if (session.uom_ids && this.timesheetUOMId in session.uom_ids) {
timesheet_widget = session.uom_ids[this.timesheetUOMId].timesheet_widget;
}
return timesheet_widget;
},
getTimesheetComponent(widgetName = this.timesheetWidget) {
return registry.category("fields").get(widgetName, { component: FloatFactorField })
.component;
},
getTimesheetComponentProps(props) {
const factorDependantComponents = ["float_toggle", "float_factor"];
return factorDependantComponents.includes(this.timesheetWidget)
? this._getFactorCompanyDependentProps(props)
: props;
},
_getFactorCompanyDependentProps(props) {
const factor = user.activeCompany.timesheet_uom_factor || props.factor;
return { ...props, factor };
},
get formatter() {
if (this.timesheetWidget === "float_time") {
return formatFloatTime;
}
const factor = user.activeCompany.timesheet_uom_factor || 1;
if (this.timesheetWidget === "float_toggle") {
return (value, options = {}) => formatFloat(value * factor, options);
}
return (value, options = {}) =>
formatFloatFactor(value, Object.assign({ factor }, options));
},
};
if (!registry.category("formatters").contains("timesheet_uom")) {
registry.category("formatters").add("timesheet_uom", service.formatter);
}
if (!registry.category("formatters").contains("timesheet_uom_no_toggle")) {
registry.category("formatters").add("timesheet_uom_no_toggle", service.formatter);
}
return service;
},
};
registry.category("services").add("timesheet_uom", timesheetUOMService);

View file

@ -0,0 +1,26 @@
import { user } from "@web/core/user";
import { patch } from "@web/core/utils/patch";
const FIELDS = [
'unit_amount', 'effective_hours', 'allocated_hours', 'remaining_hours', 'total_hours_spent', 'subtask_effective_hours',
'overtime', 'number_hours', 'difference', 'timesheet_unit_amount'
];
export function patchGraphModel(Model) {
patch(Model.prototype, {
/**
* Override processDataPoints to take into account the analytic line uom.
* @override
*/
_getProcessedDataPoints() {
const factor = user.activeCompany.timesheet_uom_factor || 1;
if (factor !== 1 && FIELDS.includes(this.metaData.measure)) {
// recalculate the Duration values according to the timesheet_uom_factor
for (const dataPt of this.dataPoints) {
dataPt.value *= factor;
}
}
return super._getProcessedDataPoints(...arguments);
}
});
}

View file

@ -0,0 +1,5 @@
import { ProjectTaskAnalysisGraphModel } from "@project/views/project_task_analysis_graph/project_task_analysis_graph_model";
import { patchGraphModel } from "../graph_model_patch";
patchGraphModel(ProjectTaskAnalysisGraphModel);

View file

@ -0,0 +1,4 @@
import { ProjectTaskGraphModel } from "@project/views/project_task_graph/project_task_graph_model";
import { patchGraphModel } from "../graph_model_patch";
patchGraphModel(ProjectTaskGraphModel);

View file

@ -0,0 +1,21 @@
import { _t } from "@web/core/l10n/translation";
import { TimesheetCalendarMyTimesheetsModel } from "../timesheet_calendar_my_timesheets/timesheet_calendar_my_timesheets_model";
export class TimesheetCalendarModel extends TimesheetCalendarMyTimesheetsModel {
setup(params, services) {
super.setup(...arguments);
this.notification = services.notification;
}
async multiCreateRecords(multiCreateData, dates) {
const [section] = this.filterSections;
if (section.filters.filter((filter) => filter.active).length === 0) {
this.notification.add(_t("Choose an employee to create their timesheet."), {
type: "danger",
});
return;
}
return await super.multiCreateRecords(multiCreateData, dates);
}
}

View file

@ -0,0 +1,10 @@
import { registry } from "@web/core/registry";
import { timesheetCalendarMyTimesheetsView } from "../timesheet_calendar_my_timesheets/timesheet_calendar_my_timesheets_view";
import { TimesheetCalendarModel } from "./timesheet_calendar_model";
export const timesheetCalendarView = {
...timesheetCalendarMyTimesheetsView,
Model: TimesheetCalendarModel,
};
registry.category("views").add("timesheet_calendar", timesheetCalendarView);

View file

@ -0,0 +1,12 @@
import { CalendarModel } from "@web/views/calendar/calendar_model";
export class TimesheetCalendarMyTimesheetsModel extends CalendarModel {
/**
* @override
*/
async multiCreateRecords(multiCreateData, dates) {
this.meta.context = this.meta.context || {};
this.meta.context.timesheet_calendar = true;
return super.multiCreateRecords(multiCreateData, dates);
}
}

View file

@ -0,0 +1,12 @@
import { registry } from "@web/core/registry";
import { calendarView } from "@web/views/calendar/calendar_view";
import { TimesheetCalendarMyTimesheetsModel } from "./timesheet_calendar_my_timesheets_model";
export const timesheetCalendarMyTimesheetsView = {
...calendarView,
Model: TimesheetCalendarMyTimesheetsModel,
};
registry
.category("views")
.add("timesheet_calendar_my_timesheets", timesheetCalendarMyTimesheetsView);

View file

@ -1,39 +1,6 @@
/** @odoo-module **/
import { GraphModel } from "@web/views/graph/graph_model";
const FIELDS = [
'unit_amount', 'effective_hours', 'planned_hours', 'remaining_hours', 'total_hours_spent', 'subtask_effective_hours',
'overtime', 'number_hours', 'difference', 'hours_effective', 'hours_planned', 'timesheet_unit_amount'
];
import { patchGraphModel } from "../graph_model_patch";
export class hrTimesheetGraphModel extends GraphModel {
/**
* @override
*/
setup(params, services) {
super.setup(...arguments);
this.companyService = services.company;
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Override processDataPoints to take into account the analytic line uom.
* @override
*/
_getProcessedDataPoints() {
const currentCompany = this.companyService.currentCompany;
const factor = currentCompany.timesheet_uom_factor || 1;
if (factor !== 1 && FIELDS.includes(this.metaData.measure)) {
// recalculate the Duration values according to the timesheet_uom_factor
for (const dataPt of this.dataPoints) {
dataPt.value *= factor;
}
}
return super._getProcessedDataPoints(...arguments);
}
}
hrTimesheetGraphModel.services = [...GraphModel.services, "company"];
export class hrTimesheetGraphModel extends GraphModel {}
patchGraphModel(hrTimesheetGraphModel);

View file

@ -1,13 +1,11 @@
/** @odoo-module **/
import { projectGraphView } from "@project/js/project_graph_view";
import { hrTimesheetGraphModel } from "./timesheet_graph_model";
import { registry } from "@web/core/registry";
import { graphView } from "@web/views/graph/graph_view";
import { hrTimesheetGraphModel } from "./timesheet_graph_model";
const viewRegistry = registry.category("views");
export const hrTimesheetGraphView = {
...projectGraphView,
...graphView,
Model: hrTimesheetGraphModel,
};

View file

@ -1,130 +0,0 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { companyService } from "@web/webclient/company_service";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
export const getServerData = () => JSON.parse(JSON.stringify({
models: {
'account.analytic.line': {
fields: {
project_id: { string: "Project", type: "many2one", relation: "project.project" },
task_id: { string: "Task", type: "many2one", relation: "project.task" },
unit_amount: { string: "Unit Amount", type: "integer" },
},
records: [
{ id: 1, project_id: 1, task_id: 3, unit_amount: 1 },
{ id: 2, project_id: 1, task_id: false, unit_amount: 1 },
{ id: 3, project_id: false, task_id: false, unit_amount: 1 },
],
},
'project.project': {
fields: {
name: { string: "Name", type: "string" },
},
records: [
{ id: 1, name: "Project 1" },
],
},
'project.task': {
fields: {
name: { string: "Name", type: "string" },
project_id: { string: "Project", type: "many2one", relation: "project.project" },
},
records: [
{ id: 1, name: "Task 1\u00A0AdditionalInfo", project_id: 1 },
{ id: 2, name: "Task 2\u00A0AdditionalInfo", project_id: 1 },
{ id: 3, name: "Task 3\u00A0AdditionalInfo", project_id: 1 },
],
},
},
views: {
"account.analytic.line,false,form": `
<form>
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount"/>
</form>
`,
"account.analytic.line,false,list": `
<tree editable="bottom">
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount"/>
</tree>
`,
},
}));
export function updateArch(serverData, fieldNameWidgetNameMapping = {}, fieldNameContextMapping = {}) {
for (const viewKey in serverData.views) {
for (const [fieldName, widgetName] of Object.entries(fieldNameWidgetNameMapping)) {
serverData.views[viewKey] = serverData.views[viewKey].replace(
`name="${fieldName}"`,
`name="${fieldName}" widget="${widgetName}"`
);
}
for (const [fieldName, context] of Object.entries(fieldNameContextMapping)) {
serverData.views[viewKey] = serverData.views[viewKey].replace(
`name="${fieldName}"`,
`name="${fieldName}" context="${context}"`
);
}
}
}
export function addFieldsInArch(serverData, fields, beforeField) {
let fieldsArch = "";
for (const field of fields) {
fieldsArch += `<field name="${field}"/>
`;
}
for (const viewKey in serverData.views) {
serverData.views[viewKey] = serverData.views[viewKey].replace(
`<field name="${beforeField}"`,
`${fieldsArch}<field name="${beforeField}"`
);
}
}
export function setupTestEnv() {
setupViewRegistries();
patchWithCleanup(session, {
user_companies: {
current_company: 1,
allowed_companies: {
1: {
id: 1,
name: 'Company',
timesheet_uom_id: 1,
timesheet_uom_factor: 1,
},
},
},
user_context: {
allowed_company_ids: [1],
},
uom_ids: {
1: {
id: 1,
name: 'hour',
rounding: 0.01,
timesheet_widget: 'float_time',
},
2: {
id: 2,
name: 'day',
rounding: 0.01,
timesheet_widget: 'float_toggle',
},
},
});
const serviceRegistry = registry.category("services");
serviceRegistry.add("company", companyService, { force: true });
}

View file

@ -0,0 +1,132 @@
import { mockDate } from "@odoo/hoot-mock";
import { session } from "@web/session";
import { defineModels, fields, models, patchWithCleanup, serverState } from "@web/../tests/web_test_helpers";
import { defineProjectModels, projectModels } from "@project/../tests/project_models";
export class HRTimesheet extends models.Model {
_name = "account.analytic.line";
name = fields.Char();
project_id = fields.Many2one({ relation: "project.project", required: true });
task_id = fields.Many2one({ relation: "project.task" });
unit_amount = fields.Float();
_records = [
{
id: 1,
project_id: 1,
task_id: 3,
unit_amount: 1,
},
{
id: 2,
project_id: 1,
task_id: false,
unit_amount: 2,
},
{
id: 3,
project_id: false,
task_id: false,
unit_amount: 5,
},
];
_views = {
form: `
<form>
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount"/>
</form>
`,
list: `
<tree editable="bottom">
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount"/>
</tree>
`,
graph: `
<graph js_class="hr_timesheet_graphview">
<field name="unit_amount"/>
<field name="unit_amount" type="measure"/>
</graph>
`,
};
}
export class ProjectTask extends projectModels.ProjectTask {
progress = fields.Float();
_records = [
{
id: 1,
name: "Task 1\u00A0AdditionalInfo",
project_id: 1,
progress: 0.5,
},
{
id: 2,
name: "Task 2\u00A0AdditionalInfo",
project_id: 1,
progress: 0.8,
},
{
id: 3,
name: "Task 3\u00A0AdditionalInfo",
project_id: 1,
progress: 1.04,
},
];
}
export class ProjectProject extends projectModels.ProjectProject {
get_create_edit_project_ids() {
return [];
}
}
projectModels.ProjectTask = ProjectTask;
projectModels.ProjectProject = ProjectProject;
export const hrTimesheetModels = { HRTimesheet };
export function defineTimesheetModels() {
defineProjectModels();
defineModels(hrTimesheetModels);
}
export function patchSession() {
mockDate("2017-01-25 00:00:00");
serverState.companies = [
{
id: 1,
name: "Company",
timesheet_uom_id: 1,
timesheet_uom_factor: 1,
},
];
patchWithCleanup(session, {
uom_ids: {
1: {
id: 1,
name: "hour",
rounding: 0.01,
timesheet_widget: "float_time",
},
2: {
id: 2,
name: "day",
rounding: 0.01,
timesheet_widget: "float_toggle",
},
3: {
id: 3,
name: "foo",
rounding: 0.01,
timesheet_widget: "float_factor",
},
},
});
}

View file

@ -1,196 +0,0 @@
odoo.define("hr_timesheet.timesheet_uom_tests_env", function (require) {
"use strict";
const session = require('web.session');
const { createView } = require("web.test_utils");
const ListView = require('web.ListView');
const { timesheetUomService } = require('hr_timesheet.timesheet_uom');
const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
const { registry } = require("@web/core/registry");
/**
* Sets the timesheet related widgets testing environment up.
*/
function SetupTimesheetUOMWidgetsTestEnvironment () {
this.allowedCompanies = {
1: {
id: 1,
name: 'Company 1',
timesheet_uom_id: 1,
timesheet_uom_factor: 1,
},
2: {
id: 2,
name: 'Company 2',
timesheet_uom_id: 2,
timesheet_uom_factor: 0.125,
},
3: {
id: 3,
name: 'Company 3',
timesheet_uom_id: 2,
timesheet_uom_factor: 0.125,
},
};
this.uomIds = {
1: {
id: 1,
name: 'hour',
rounding: 0.01,
timesheet_widget: 'float_time',
},
2: {
id: 2,
name: 'day',
rounding: 0.01,
timesheet_widget: 'float_toggle',
},
};
this.singleCompanyHourUOMUser = {
allowed_company_ids: [this.allowedCompanies[1].id],
};
this.singleCompanyDayUOMUser = {
allowed_company_ids: [this.allowedCompanies[2].id],
};
this.multiCompanyHourUOMUser = {
allowed_company_ids: [
this.allowedCompanies[1].id,
this.allowedCompanies[3].id,
],
};
this.multiCompanyDayUOMUser = {
allowed_company_ids: [
this.allowedCompanies[3].id,
this.allowedCompanies[1].id,
],
};
this.session = {
uid: 0, // In order to avoid bbqState and cookies to be taken into account in AbstractWebClient.
user_companies: {
current_company: 1,
allowed_companies: this.allowedCompanies,
},
user_context: this.singleCompanyHourUOMUser,
uom_ids: this.uomIds,
};
this.data = {
'account.analytic.line': {
fields: {
project_id: {
string: "Project",
type: "many2one",
relation: "project.project",
},
task_id: {
string:"Task",
type: "many2one",
relation: "project.task",
},
date: {
string: "Date",
type: "date",
},
unit_amount: {
string: "Unit Amount",
type: "float",
},
},
records: [
{
id: 1,
project_id: 1,
task_id: 1,
date: "2021-01-12",
unit_amount: 8,
},
],
},
'project.project': {
fields: {
name: {
string: "Project Name",
type: "char",
},
},
records: [
{
id: 1,
display_name: "P1",
},
],
},
'project.task': {
fields: {
name: {
string: "Task Name",
type: "char",
},
project_id: {
string: "Project",
type: "many2one",
relation: "project.project",
},
},
records: [
{
id: 1,
display_name: "T1",
project_id: 1,
},
],
},
};
this.patchSessionAndStartServices = async function (sessionToApply, doNotUseEnvSession = false) {
/*
Adds the timesheet_uom to the fieldRegistry by setting the session and
starting the timesheet_uom service which registers the widget in the registry.
*/
session.user_companies = Object.assign(
{ },
!doNotUseEnvSession && this.session.user_companies || { },
sessionToApply && sessionToApply.user_companies);
if (Object.keys(session.user_companies).length === 0) {
delete session.user_companies;
}
session.user_context = Object.assign(
{ },
!doNotUseEnvSession && this.session.user_context || { },
sessionToApply && sessionToApply.user_context);
session.uom_ids = Object.assign(
{ },
!doNotUseEnvSession && this.session.uom_ids || { },
sessionToApply && sessionToApply.uom_ids);
if (!doNotUseEnvSession && 'uid' in this.session) {
session.uid = this.session.uid;
}
if (sessionToApply && 'uid' in sessionToApply) {
session.uid = sessionToApply.uid;
}
const serviceRegistry = registry.category("services");
if (!serviceRegistry.contains("timesheet_uom")) {
// Remove dependency on legacy_session since we're patching the session directly
serviceRegistry.add("timesheet_uom", Object.assign({}, timesheetUomService, { dependencies: [] }));
}
await makeTestEnv(); // Start services
};
this.createView = async function (options) {
const sessionToApply = options && options.session || { };
await this.patchSessionAndStartServices(sessionToApply);
return await createView(Object.assign(
{
View: ListView,
data: this.data,
model: 'account.analytic.line',
arch: `
<tree>
<field name="unit_amount" widget="timesheet_uom"/>
</tree>`,
},
options || { },
));
};
};
return SetupTimesheetUOMWidgetsTestEnvironment;
});

View file

@ -1,140 +0,0 @@
odoo.define("hr_timesheet.timesheet_uom_tests", function (require) {
"use strict";
const session = require('web.session');
const SetupTimesheetUOMWidgetsTestEnvironment = require('hr_timesheet.timesheet_uom_tests_env');
const fieldUtils = require('web.field_utils');
const TimesheetUOM = require('hr_timesheet.timesheet_uom');
QUnit.module('Timesheet UOM Widgets', function (hooks) {
let env;
let sessionUserCompaniesBackup;
let sessionUserContextBackup;
let sessionUOMIdsBackup;
let sessionUIDBackup;
hooks.beforeEach(async function (assert) {
env = new SetupTimesheetUOMWidgetsTestEnvironment();
// Backups session parts that this testing module will alter in order to restore it at the end.
sessionUserCompaniesBackup = session.user_companies || false;
sessionUserContextBackup = session.user_context || false;
sessionUOMIdsBackup = session.uom_ids || false;
sessionUIDBackup = session.uid || false;
});
hooks.afterEach(async function (assert) {
// Restores the session
const sessionToApply = Object.assign(
{ },
sessionUserCompaniesBackup && {
user_companies: sessionUserCompaniesBackup,
} || { },
sessionUserContextBackup && {
user_context: sessionUserContextBackup,
} || { },
sessionUOMIdsBackup && {
uom_ids: sessionUOMIdsBackup,
} || { },
sessionUIDBackup && {
uid: sessionUIDBackup,
} || { });
await env.patchSessionAndStartServices(sessionToApply, true);
});
QUnit.module('timesheet_uom', function (hooks) {
QUnit.module('fieldRegistry', function (hooks) {
let FieldTimesheetTimeBackup;
let FieldTimesheetToggleBackup;
hooks.beforeEach(function (assert) {
// Backups the FieldTimesheetTime widget as it will be altered in this testing module
// in order to to ease testing.
FieldTimesheetTimeBackup = TimesheetUOM.FieldTimesheetTime;
TimesheetUOM.FieldTimesheetTime.include({
_render: function () {
const $widgetIdentification = $('<div>').addClass('i_am_a_timesheet_time_widget');
this.$el.append($widgetIdentification);
},
});
FieldTimesheetToggleBackup = TimesheetUOM.FieldTimesheetToggle;
TimesheetUOM.FieldTimesheetToggle.include({
_render: function () {
const $widgetIdentification = $('<div>').addClass('i_am_a_timesheet_toggle_widget');
this.$el.append($widgetIdentification);
},
});
});
hooks.afterEach(async function (hooks) {
// Restores the widgets and trigger reload in FieldRegistry.
TimesheetUOM.FieldTimesheetTime = FieldTimesheetTimeBackup;
TimesheetUOM.FieldTimesheetToggle = FieldTimesheetToggleBackup;
await env.patchSessionAndStartServices({ }, true);
});
QUnit.test('the timesheet_uom widget added to the fieldRegistry is company related', async function (assert) {
assert.expect(2);
let view = await env.createView();
assert.ok(view.$('.i_am_a_timesheet_time_widget').length, 'FieldTimesheetTime is rendered when company uom is hour');
view.destroy();
let option = {
session: {
user_context: env.singleCompanyDayUOMUser,
},
};
view = await env.createView(option);
assert.ok(view.$('.i_am_a_timesheet_toggle_widget').length, 'FieldTimesheetToggle is rendered when company uom is day');
view.destroy();
});
QUnit.test('the timesheet_uom widget added to the fieldRegistry in a multi company environment is the current company', async function (assert) {
assert.expect(2);
let option = {
session: {
user_context: env.multiCompanyHourUOMUser,
},
};
let view = await env.createView(option);
assert.ok(view.$('.i_am_a_timesheet_time_widget').length, 'FieldTimesheetTime is rendered when current company uom is hour');
view.destroy();
option = {
session: {
user_context: env.multiCompanyDayUOMUser,
},
};
view = await env.createView(option);
assert.ok(view.$('.i_am_a_timesheet_toggle_widget').length, 'FieldTimesheetToggle is rendered when current company uom is day');
view.destroy();
});
});
QUnit.module('timesheet_uom_factor', function (hooks) {
QUnit.test('the timesheet_uom_factor usage in formatters and parsers is company related', async function (assert) {
assert.expect(4);
await env.patchSessionAndStartServices();
assert.strictEqual(fieldUtils.format.timesheet_uom(1), '01:00', 'The format is taking the timesheet_uom_factor into account');
assert.strictEqual(fieldUtils.parse.timesheet_uom('01:00'), 1, 'The parsing is taking the timesheet_uom_factor into account');
const sessionToApply = {
user_context: env.singleCompanyDayUOMUser,
};
await env.patchSessionAndStartServices(sessionToApply);
assert.strictEqual(fieldUtils.format.timesheet_uom(8), '1.00', 'The format is taking the timesheet_uom_factor into account');
assert.strictEqual(fieldUtils.parse.timesheet_uom('1.00'), 8, 'The parsing is taking the timesheet_uom_factor into account');
});
QUnit.test('the timesheet_uom_factor taken into account in a multi company environment is the current company', async function (assert) {
assert.expect(4);
let sessionToApply = {
user_context: env.multiCompanyHourUOMUser,
};
await env.patchSessionAndStartServices(sessionToApply);
assert.strictEqual(fieldUtils.format.timesheet_uom(1), '01:00', 'The format is taking the timesheet_uom_factor into account');
assert.strictEqual(fieldUtils.parse.timesheet_uom('01:00'), 1, 'The parsing is taking the timesheet_uom_factor into account');
sessionToApply.user_context = env.singleCompanyDayUOMUser;
await env.patchSessionAndStartServices(sessionToApply);
assert.strictEqual(fieldUtils.format.timesheet_uom(8), '1.00', 'The format is taking the timesheet_uom_factor into account');
assert.strictEqual(fieldUtils.parse.timesheet_uom('1.00'), 8, 'The parsing is taking the timesheet_uom_factor into account');
});
});
});
});
});

View file

@ -1,125 +0,0 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
import { session } from "@web/session";
import { companyService } from "@web/webclient/company_service";
import { uiService } from "@web/core/ui/ui_service";
import { makeView, setupViewRegistries} from "@web/../tests/views/helpers";
import { click, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
const serviceRegistry = registry.category("services");
QUnit.module("Timesheet UOM Widgets", (hooks) => {
let serverData;
let target;
hooks.beforeEach(async function (assert) {
setupViewRegistries();
target = getFixture();
serverData = {
models: {
'account.analytic.line': {
fields: {
unit_amount: { string: "Unit Amount", type: "float" },
},
records: [
{ id: 1, unit_amount: 8 }
],
},
},
views: {
"account.analytic.line,false,list": `
<tree>
<field name="unit_amount" widget="timesheet_uom"/>
</tree>
`,
},
};
serviceRegistry.add("ui", uiService);
serviceRegistry.add("company", companyService, { force: true });
patchWithCleanup(session, {
user_companies: {
current_company: 1,
allowed_companies: {
1: {
id: 1,
name: 'Company',
timesheet_uom_id: 2,
timesheet_uom_factor: 0.125,
},
},
},
user_context: {
allowed_company_ids: [1],
},
uom_ids: {
1: {
id: 1,
name: 'hour',
rounding: 0.01,
timesheet_widget: 'float_time',
},
2: {
id: 2,
name: 'day',
rounding: 0.01,
timesheet_widget: 'float_toggle',
},
}
});
});
QUnit.module("TimesheetFloatToggleField");
QUnit.test("factor is applied in TimesheetFloatToggleField", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
assert.containsOnce(target, 'div[name="unit_amount"]:contains("1")', "TimesheetFloatToggleField should take `timesheet_uom_factor` into account");
});
QUnit.test("ranges are working properly in TimesheetFloatToggleField", async function (assert) {
serverData.models["account.analytic.line"].records[0].unit_amount = 1;
serverData.views["account.analytic.line,false,list"] = serverData.views["account.analytic.line,false,list"].replace('<tree', '<tree editable="bottom"')
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
// Enter edit mode
await click(target, 'div[name="unit_amount"]');
await click(target, 'div[name="unit_amount"] .o_field_float_toggle');
assert.containsOnce(target, 'div[name="unit_amount"]:contains("0.00")', "ranges are working properly in TimesheetFloatToggleField");
await click(target, 'div[name="unit_amount"] .o_field_float_toggle');
assert.containsOnce(target, 'div[name="unit_amount"]:contains("0.50")', "ranges are working properly in TimesheetFloatToggleField");
await click(target, 'div[name="unit_amount"] .o_field_float_toggle');
assert.containsOnce(target, 'div[name="unit_amount"]:contains("1.00")', "ranges are working properly in TimesheetFloatToggleField");
});
QUnit.module("TimesheetFloatTimeField");
QUnit.test("factor is applied in TimesheetFloatTimeField", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], {timesheet_uom_id: 1});
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
assert.containsOnce(target, 'div[name="unit_amount"]:contains("08:00")', "TimesheetFloatTimeField should not take `timesheet_uom_factor` into account");
});
QUnit.module("TimesheetFloatFactorField");
QUnit.test("factor is applied in TimesheetFloatFactorField", async function (assert) {
patchWithCleanup(session.uom_ids[2], {timesheet_widget: 'float_factor'});
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
assert.containsOnce(target, 'div[name="unit_amount"]:contains("1")', "TimesheetFloatFactorField should take `timesheet_uom_factor` into account");
});
});

View file

@ -0,0 +1,82 @@
import { describe, expect, test } from "@odoo/hoot";
import { click, edit } from "@odoo/hoot-dom";
import { animationFrame } from "@odoo/hoot-mock";
import { mountView } from "@web/../tests/web_test_helpers";
import { HRTimesheet, defineTimesheetModels } from "./hr_timesheet_models";
HRTimesheet._views.list = `
<list editable="bottom">
<field name="project_id"/>
<field name="task_id" widget="task_with_hours" context="{ 'default_project_id': project_id }"/>
<field name="unit_amount"/>
</list>
`;
defineTimesheetModels();
describe.current.tags("desktop");
async function _expectCreateAndEdit(rowN) {
const taskField = `.o_list_table .o_data_row:nth-of-type(${rowN}) .o_list_many2one[name=task_id]`;
await click(taskField);
await animationFrame();
await edit("NonExistingTask", { confirm: false });
await click(`${taskField} input`);
await animationFrame();
return expect(
'.o_list_many2one[name=task_id] .dropdown ul li:contains(Create "NonExistingTask")'
);
}
test("hr.timesheet (tree): quick create is enabled when project_id is set", async () => {
await mountView({
resModel: "account.analytic.line",
type: "list",
});
(await _expectCreateAndEdit(2)).toBeVisible();
});
test("hr.timesheet (tree): quick create is no enabled when project_id is not set", async () => {
await mountView({
resModel: "account.analytic.line",
type: "list",
});
(await _expectCreateAndEdit(3)).not.toHaveCount();
});
test("hr.timesheet (tree): the text of the task includes hours in the drop down but not in the line", async () => {
await mountView({
resModel: "account.analytic.line",
type: "list",
});
const taskField = ".o_list_table .o_data_row:first-of-type .o_list_many2one[name=task_id]";
expect(taskField).toHaveText("Task 3");
await click(taskField);
await animationFrame();
await click(`${taskField} input`);
await animationFrame();
expect(`${taskField} .dropdown ul li:contains("AdditionalInfo")`).toHaveCount(3);
});
test("project.task (tree): progress bar color", async () => {
await mountView({
resModel: "project.task",
type: "list",
arch: `
<list>
<field name="name"/>
<field name="project_id"/>
<field name="progress" widget="project_task_progressbar" options="{'overflow_class': 'bg-danger'}"/>
</list>
`,
});
expect("div.o_progressbar .bg-success").toHaveCount(1, {
message: "Task 1 having progress = 50 < 80 => green color",
});
expect("div.o_progressbar .bg-warning").toHaveCount(1, {
message: "Task 2 having progress = 80 >= 80 => orange color",
});
expect("div.o_progressbar .bg-success").toHaveCount(1, {
message: "Task 3 having progress = 101 > 100 => red color",
});
});

View file

@ -1,67 +0,0 @@
/** @odoo-module */
import { makeView } from "@web/../tests/views/helpers";
import { click, clickDropdown, editInput, getFixture } from "@web/../tests/helpers/utils";
import { getServerData, updateArch, setupTestEnv } from "./hr_timesheet_common_tests";
QUnit.module("hr_timesheet", (hooks) => {
let target;
let serverData;
hooks.beforeEach(async function (assert) {
setupTestEnv();
serverData = getServerData();
updateArch(
serverData,
{ task_id: "task_with_hours" },
{ task_id: "{ 'default_project_id': project_id }" });
target = getFixture();
});
QUnit.module("task_with_hours");
async function _testCreateAndEdit(target, visible, assert) {
await click(target, ".o_list_many2one[name=task_id]");
await click(target, ".o_list_many2one[name=task_id] input");
await editInput(target, ".o_list_many2one[name=task_id] input", "NonExistingTask");
await click(target, ".o_list_many2one[name=task_id] input");
await clickDropdown(target, "task_id");
const testFunction = visible ? assert.containsOnce : assert.containsNone;
testFunction(target, '.o_list_many2one[name=task_id] .dropdown ul li:contains("Create and edit...")');
}
QUnit.test("quick create is enabled when project_id is set", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
const secondRow = target.querySelector(".o_list_table .o_data_row:nth-of-type(2)");
await _testCreateAndEdit(secondRow, true, assert);
});
QUnit.test("quick create is no enabled when project_id is not set", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
const thirdRow = target.querySelector(".o_list_table .o_data_row:nth-of-type(3)");
await _testCreateAndEdit(thirdRow, false, assert);
});
QUnit.test("the text of the task includes hours in the drop down but not in the line", async function (assert) {
await makeView({
serverData,
type: "list",
resModel: "account.analytic.line",
});
const firstRow = target.querySelector(".o_list_table .o_data_row:first-of-type");
assert.containsNone(firstRow, '.o_list_many2one[name=task_id]:contains("AdditionalInfo")');
await click(firstRow, ".o_list_many2one[name=task_id]");
await clickDropdown(firstRow, "task_id");
assert.containsN(firstRow, '.o_list_many2one[name=task_id] .dropdown ul li:contains("AdditionalInfo")', 3);
});
});

View file

@ -0,0 +1,29 @@
import { test } from "@odoo/hoot";
import { mountView, serverState } from "@web/../tests/web_test_helpers";
import { defineTimesheetModels } from "./hr_timesheet_models";
import { checkDatasets } from "@web/../tests/views/graph/graph_test_helpers";
defineTimesheetModels();
test("hr.timesheet (graph): data are not multiplied by a company related factor (factor === 1)", async () => {
serverState.companies[0].timesheet_uom_factor = 1;
const graph = await mountView({
resModel: "account.analytic.line",
type: "graph",
});
checkDatasets(graph, "data", { data: [8] });
});
test("hr.timesheet (graph): data are multiplied by a company related factor (factor !== 1)", async () => {
serverState.companies[0].timesheet_uom_factor = 0.125;
const graph = await mountView({
resModel: "account.analytic.line",
type: "graph",
});
checkDatasets(graph, "data", { data: [1] });
});

View file

@ -1,76 +0,0 @@
/** @odoo-module **/
import { companyService } from "@web/webclient/company_service";
import { getGraphRenderer } from "@web/../tests/views/graph_view_tests";
import { makeView } from "@web/../tests/views/helpers";
import { patchWithCleanup } from "@web/../tests/helpers/utils";
import { session } from "@web/session";
import { registry } from "@web/core/registry";
import { setupControlPanelServiceRegistry } from "@web/../tests/search/helpers";
const serviceRegistry = registry.category("services");
QUnit.module('hr_timesheet', function (hooks) {
let serverData;
hooks.beforeEach(() => {
serverData = {
models: {
'account.analytic.line': {
fields: {
unit_amount: { string: "Unit Amount", type: "float", group_operator: "sum", store: true },
},
records: [
{ id: 1, unit_amount: 8 }
],
},
},
views: {
// unit_amount is used as group_by and measure
"account.analytic.line,false,graph": `
<graph>
<field name="unit_amount"/>
<field name="unit_amount" type="measure"/>
</graph>
`,
}
}
setupControlPanelServiceRegistry();
serviceRegistry.add("company", companyService, { force: true });
});
QUnit.module("hr_timesheet_graphview");
QUnit.test('the timesheet graph view data are not multiplied by a factor that is company related (factor = 1)', async function (assert) {
assert.expect(1);
patchWithCleanup(session.user_companies.allowed_companies[1], {
timesheet_uom_factor: 1,
});
const graph = await makeView({
serverData,
resModel: "account.analytic.line",
type: "hr_timesheet_graphview",
});
const renderedData = getGraphRenderer(graph).chart.data.datasets[0].data;
assert.deepEqual(renderedData, [8], 'The timesheet graph view is taking the timesheet_uom_factor into account (factor === 1)');
});
QUnit.test('the timesheet graph view data are multiplied by a factor that is company related (factor !== 1)', async function (assert) {
assert.expect(1);
patchWithCleanup(session.user_companies.allowed_companies[1], {
timesheet_uom_factor: 0.125,
});
const graph = await makeView({
serverData,
resModel: "account.analytic.line",
type: "hr_timesheet_graphview",
});
const renderedData = getGraphRenderer(graph).chart.data.datasets[0].data;
assert.deepEqual(renderedData, [1], 'The timesheet graph view is taking the timesheet_uom_factor into account (factor !== 1)');
});
});

View file

@ -0,0 +1,94 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { mountView, serverState } from "@web/../tests/web_test_helpers";
import { HRTimesheet, defineTimesheetModels, patchSession } from "./hr_timesheet_models";
HRTimesheet._views.form = `
<form>
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount" widget="timesheet_uom"/>
</form>
`;
defineTimesheetModels();
beforeEach(patchSession);
test("hr.timesheet (form): FloatTimeField is used when current company uom uses float_time widget", async () => {
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("01:00", {
message: "unit_amount should be displayed as time",
});
});
test("hr.timesheet (form): FloatTimeField is not dependent of timesheet_uom_factor of the current company when current company uom uses float_time widget", async () => {
serverState.companies[0].timesheet_uom_factor = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("01:00", {
message: "timesheet_uom_factor shouldn't be taken into account",
});
});
test("hr.timesheet (form): FloatToggleField is used when current company uom uses float_toggle widget", async () => {
serverState.companies[0].timesheet_uom_id = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] .o_field_float_toggle').toBeVisible({
message: "unit_amount should be displayed as float toggle",
});
});
test("hr.timesheet (form): FloatToggleField is dependent on timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async () => {
serverState.companies[0].timesheet_uom_id = 2;
serverState.companies[0].timesheet_uom_factor = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] .o_field_float_toggle').toBeVisible({
message: "timesheet_uom_factor should be taken into account",
});
});
test("hr.timesheet (form): FloatFactorField is used when the current_company uom is not part of the session uom", async () => {
serverState.companies[0].timesheet_uom_id = "dummy";
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input[inputmode="decimal"]').toBeVisible({
message: "unit_amount is displayed as float",
});
expect('div[name="unit_amount"] input').toHaveValue("1.00", {
message: "unit_amount is not displayed as float and not as time",
});
expect('div[name="unit_amount"].o_field_float_toggle').not.toHaveCount(null, {
message: "unit_amount is not displayed as float toggle",
});
});
test("hr.timesheet (form): FloatFactorField is dependent on timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async () => {
serverState.companies[0].timesheet_uom_id = "dummy";
serverState.companies[0].timesheet_uom_factor = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("2.00", {
message: "timesheet_uom_factor is taken into account",
});
});

View file

@ -0,0 +1,87 @@
import { beforeEach, expect, test } from "@odoo/hoot";
import { mountView, serverState } from "@web/../tests/web_test_helpers";
import { HRTimesheet, defineTimesheetModels, patchSession } from "./hr_timesheet_models";
HRTimesheet._views.form = `
<form>
<field name="project_id"/>
<field name="task_id"/>
<field name="unit_amount" widget="timesheet_uom_no_toggle"/>
</form>
`;
defineTimesheetModels();
beforeEach(patchSession);
test("hr.timesheet (form): FloatTimeField is used when current company uom uses float_time widget", async () => {
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("01:00", {
message: "unit_amount should be displayed as time",
});
});
test("hr.timesheet (form): FloatTimeField is not dependent of timesheet_uom_factor of the current company when current company uom uses float_time widget", async () => {
serverState.companies[0].timesheet_uom_factor = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("01:00", {
message: "timesheet_uom_factor shouldn't be taken into account",
});
});
test("hr.timesheet (form): FloatToggleField is not used when current company uom uses float_toggle widget, FloatFactorField is used instead", async () => {
serverState.companies[0].timesheet_uom_id = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] .o_field_float_toggle').not.toHaveCount(null, {
message: "unit_amount shouldn't be displayed as float toggle",
});
expect('div[name="unit_amount"] input[inputmode="decimal"]').toBeVisible({
message: "unit_amount should be displayed as float",
});
expect('div[name="unit_amount"] input').toHaveValue("1.00", {
message: "unit_amount shouldn't be displayed as float and not as time",
});
});
test("FloatFactorField is used when the current_company uom is not part of the session uom", async () => {
serverState.companies[0].timesheet_uom_id = "dummy";
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input[inputmode="decimal"]').toBeVisible({
message: "unit_amount should be displayed as float",
});
expect('div[name="unit_amount"] input').toHaveValue("1.00", {
message: "unit_amount shouldn't be displayed as float and not as time",
});
expect('div[name="unit_amount"].o_field_float_toggle').not.toHaveCount(null, {
message: "unit_amount shouldn't be displayed as float toggle",
});
});
test("FloatFactorField is dependent of timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async () => {
serverState.companies[0].timesheet_uom_id = "dummy";
serverState.companies[0].timesheet_uom_factor = 2;
await mountView({
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
expect('div[name="unit_amount"] input').toHaveValue("2.00", {
message: "timesheet_uom_factor should be taken into account",
});
});

View file

@ -1,86 +0,0 @@
/** @odoo-module */
import { session } from "@web/session";
import { makeView } from "@web/../tests/views/helpers";
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getServerData, updateArch, setupTestEnv } from "./hr_timesheet_common_tests";
QUnit.module("hr_timesheet", (hooks) => {
let target;
let serverData;
hooks.beforeEach(async function (assert) {
setupTestEnv();
serverData = getServerData();
updateArch(serverData, { unit_amount: "timesheet_uom_no_toggle" });
target = getFixture();
});
QUnit.module("timesheet_uom_no_toggle");
QUnit.test("FloatTimeField is used when current company uom uses float_time widget", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "01:00", "unit_amount is displayed as time");
});
QUnit.test("FloatTimeField is not dependent of timesheet_uom_factor of the current company when current company uom uses float_time widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_factor: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "01:00", "timesheet_uom_factor is not taken into account");
});
QUnit.test("FloatToggleField is not used when current company uom uses float_toggle widget, FloatFactorField is used instead", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
assert.containsNone(target, 'div[name="unit_amount"] .o_field_float_toggle', "unit_amount is not displayed as float toggle");
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.containsOnce(target, 'div[name="unit_amount"] input[inputmode="decimal"]', "unit_amount is displayed as float");
assert.equal(unitAmountInput.value, "1.00", "unit_amount is not displayed as float and not as time");
});
QUnit.test("FloatFactorField is used when the current_company uom is not part of the session uom", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 'dummy' });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.containsOnce(target, 'div[name="unit_amount"] input[inputmode="decimal"]', "unit_amount is displayed as float");
assert.equal(unitAmountInput.value, "1.00", "unit_amount is not displayed as float and not as time");
assert.containsNone(target, 'div[name="unit_amount"].o_field_float_toggle', "unit_amount is not displayed as float toggle");
});
QUnit.test("FloatFactorField is dependent of timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 'dummy', timesheet_uom_factor: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "2.00", "timesheet_uom_factor is taken into account");
});
});

View file

@ -1,94 +0,0 @@
/** @odoo-module */
import { session } from "@web/session";
import { makeView } from "@web/../tests/views/helpers";
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import { getServerData, updateArch, setupTestEnv } from "./hr_timesheet_common_tests";
QUnit.module("hr_timesheet", (hooks) => {
let target;
let serverData;
hooks.beforeEach(async function (assert) {
setupTestEnv();
serverData = getServerData();
updateArch(serverData, { unit_amount: "timesheet_uom" });
target = getFixture();
});
QUnit.module("timesheet_uom");
QUnit.test("FloatTimeField is used when current company uom uses float_time widget", async function (assert) {
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "01:00", "unit_amount is displayed as time");
});
QUnit.test("FloatTimeField is not dependent of timesheet_uom_factor of the current company when current company uom uses float_time widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_factor: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "01:00", "timesheet_uom_factor is not taken into account");
});
QUnit.test("FloatToggleField is used when current company uom uses float_toggle widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
assert.containsOnce(target, 'div[name="unit_amount"] .o_field_float_toggle', "unit_amount is displayed as float toggle");
});
QUnit.test("FloatToggleField is dependent of timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 2, timesheet_uom_factor: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
assert.containsOnce(target, 'div[name="unit_amount"] .o_field_float_toggle:contains("2.00")', "timesheet_uom_factor is taken into account");
});
QUnit.test("FloatFactorField is used when the current_company uom is not part of the session uom", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 'dummy' });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.containsOnce(target, 'div[name="unit_amount"] input[inputmode="decimal"]', "unit_amount is displayed as float");
assert.equal(unitAmountInput.value, "1.00", "unit_amount is not displayed as float and not as time");
assert.containsNone(target, 'div[name="unit_amount"].o_field_float_toggle', "unit_amount is not displayed as float toggle");
});
QUnit.test("FloatFactorField is dependent of timesheet_uom_factor of the current company when current company uom uses float_toggle widget", async function (assert) {
patchWithCleanup(session.user_companies.allowed_companies[1], { timesheet_uom_id: 'dummy', timesheet_uom_factor: 2 });
await makeView({
serverData,
type: "form",
resModel: "account.analytic.line",
resId: 1,
});
const unitAmountInput = target.querySelector('div[name="unit_amount"] input');
assert.equal(unitAmountInput.value, "2.00", "timesheet_uom_factor is taken into account");
});
});