Eliminate Python dependency: embed frontend assets in odoo-go

- Copy all OWL frontend assets (JS/CSS/XML/fonts/images) into frontend/
  directory (2925 files, 43MB) — no more runtime reads from Python Odoo
- Replace OdooAddonsPath config with FrontendDir pointing to local frontend/
- Rewire bundle.go, static.go, templates.go, webclient.go to read from
  frontend/ instead of external Python Odoo addons directory
- Auto-detect frontend/ and build/ dirs relative to binary in main.go
- Delete obsolete Python helper scripts (tools/*.py)

The Go server is now fully self-contained: single binary + frontend/ folder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-03-31 23:09:12 +02:00
parent 0ed29fe2fd
commit 8741282322
2933 changed files with 280644 additions and 264 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="m25 4 6.857 4L15.5 19v16l.5 5.75-9-5.25v-21L25 4Z" fill="#FBB945"/><path d="m18.087 41.967-6.138-3.58V17.5L30.046 6.943l6.283 3.665L22.001 23.5l-3.915 18.467Z" fill="#F86126"/><path d="M36.328 10.61 43 14.5v21L25 46l-6.914-4.032V21.25l18.242-10.642Z" fill="#985184"/></svg>

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -0,0 +1,256 @@
<svg width="890" height="529" viewBox="0 0 890 529" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_2)">
<path d="M37.7 238.8L77.3 288.4H101.9L127.3 390H163.9V39H183.2L230.7 106.3L267.9 106.9L296.2 145.7L341.9 149L365.9 181.2L395.6 200.6L419.3 202.9L444.4 259L482.9 267L511.9 300.6L557.8 317.6L580.5 345L624.5 361L653 380L669.5 388L669.6 478.2H37.4L37.7 279.7" fill="url(#paint0_linear_1_2)"/>
<path d="M37.4 239.4L76.8498 287.847C77.1347 288.197 77.5618 288.4 78.013 288.4H100.729C101.417 288.4 102.017 288.868 102.184 289.536L127.016 388.864C127.183 389.532 127.783 390 128.471 390H162.4C163.228 390 163.9 389.328 163.9 388.5V40.8C163.9 39.9716 164.572 39.3 165.4 39.3H182.425C182.911 39.3 183.367 39.5357 183.648 39.9325L230.26 105.68C230.537 106.07 230.982 106.305 231.46 106.312L267.153 106.888C267.623 106.896 268.063 107.124 268.34 107.504L295.79 145.138C296.05 145.494 296.454 145.718 296.894 145.75L341.215 148.951C341.65 148.982 342.05 149.201 342.31 149.55L365.741 180.986C365.846 181.128 365.976 181.249 366.123 181.346L395.291 200.398C395.494 200.531 395.726 200.612 395.967 200.636L418.432 202.816C418.969 202.868 419.436 203.204 419.656 203.696L444.087 258.3C444.283 258.74 444.679 259.058 445.151 259.156L482.402 266.896C482.725 266.964 483.017 267.135 483.232 267.385L511.648 300.308C511.813 300.499 512.025 300.646 512.262 300.734L557.422 317.46C557.67 317.552 557.889 317.707 558.057 317.911L580.438 344.983C580.61 345.191 580.834 345.348 581.087 345.439L623.932 360.839C624.043 360.88 624.15 360.933 624.249 360.998L653.1 380L670 388" stroke="#875A7B" stroke-width="3.5" stroke-miterlimit="10"/>
<path d="M37.4 8.10001V478.2H766.9" stroke="#827882" stroke-width="2" stroke-miterlimit="10"/>
<path d="M33.6913 10C32.9307 10 32.4485 9.18453 32.8151 8.51808L36.6238 1.59312C37.0037 0.902344 37.9963 0.902345 38.3762 1.59312L42.1849 8.51808C42.5515 9.18454 42.0693 10 41.3087 10H33.6913Z" fill="#827882"/>
<path d="M44 34.5C44 35.2941 43.3824 36 42.5 36C41.7059 36 41 35.3824 41 34.5C41 33.7059 41.6176 33 42.5 33C43.2941 33 44 33.7059 44 34.5Z" fill="#00A09D"/>
<path d="M51.5 36C52.3284 36 53 35.3284 53 34.5C53 33.6716 52.3284 33 51.5 33C50.6716 33 50 33.6716 50 34.5C50 35.3284 50.6716 36 51.5 36Z" fill="#00A09D"/>
<path d="M62 34.5C62 35.2941 61.3824 36 60.5 36C59.7059 36 59 35.3824 59 34.5C59 33.7059 59.6176 33 60.5 33C61.2941 33 62 33.7059 62 34.5Z" fill="#00A09D"/>
<path d="M71 34.5C71 35.2941 70.3824 36 69.5 36C68.7059 36 68 35.3824 68 34.5C68 33.7059 68.6176 33 69.5 33C70.2941 33 71 33.7059 71 34.5Z" fill="#00A09D"/>
<path d="M80 34.5C80 35.2941 79.3824 36 78.5 36C77.6176 36 77 35.3824 77 34.5C77 33.7059 77.6176 33 78.5 33C79.3824 33 80 33.7059 80 34.5Z" fill="#00A09D"/>
<path d="M89 34.5C89 35.2941 88.3823 36 87.5 36C86.7059 36 86 35.3824 86 34.5C86 33.7059 86.6176 33 87.5 33C88.2941 33 89 33.7059 89 34.5Z" fill="#00A09D"/>
<path d="M98 34.5C98 35.2941 97.3824 36 96.5 36C95.7059 36 95 35.3824 95 34.5C95 33.7059 95.6177 33 96.5 33C97.2941 33 98 33.7059 98 34.5Z" fill="#00A09D"/>
<path d="M107 34.5C107 35.2941 106.382 36 105.5 36C104.618 36 104 35.3824 104 34.5C104 33.7059 104.618 33 105.5 33C106.382 33 107 33.7059 107 34.5Z" fill="#00A09D"/>
<path d="M116 34.5C116 35.2941 115.382 36 114.5 36C113.706 36 113 35.3824 113 34.5C113 33.7059 113.618 33 114.5 33C115.294 33 116 33.7059 116 34.5Z" fill="#00A09D"/>
<path d="M125 34.5C125 35.2941 124.382 36 123.5 36C122.706 36 122 35.3824 122 34.5C122 33.7059 122.618 33 123.5 33C124.294 33 125 33.7059 125 34.5Z" fill="#00A09D"/>
<path d="M134 34.5C134 35.2941 133.382 36 132.5 36C131.618 36 131 35.3824 131 34.5C131 33.7059 131.618 33 132.5 33C133.382 33 134 33.7059 134 34.5Z" fill="#00A09D"/>
<path d="M143 34.5C143 35.2941 142.382 36 141.5 36C140.706 36 140 35.3824 140 34.5C140 33.7059 140.618 33 141.5 33C142.294 33 143 33.7059 143 34.5Z" fill="#00A09D"/>
<path d="M152 34.5C152 35.2941 151.382 36 150.5 36C149.706 36 149 35.3824 149 34.5C149 33.7059 149.618 33 150.5 33C151.294 33 152 33.7059 152 34.5Z" fill="#00A09D"/>
<path d="M161 34.5C161 35.2941 160.382 36 159.5 36C158.618 36 158 35.3824 158 34.5C158 33.7059 158.618 33 159.5 33C160.382 33 161 33.7059 161 34.5Z" fill="#00A09D"/>
<path d="M170 34.5C170 35.2941 169.382 36 168.5 36C167.706 36 167 35.3824 167 34.5C167 33.7059 167.618 33 168.5 33C169.294 33 170 33.7059 170 34.5Z" fill="#00A09D"/>
<path d="M177.5 36C178.328 36 179 35.3284 179 34.5C179 33.6716 178.328 33 177.5 33C176.672 33 176 33.6716 176 34.5C176 35.3284 176.672 36 177.5 36Z" fill="#00A09D"/>
<path d="M188 34.5C188 35.2941 187.382 36 186.5 36C185.618 36 185 35.3824 185 34.5C185 33.7059 185.618 33 186.5 33C187.382 33 188 33.7059 188 34.5Z" fill="#00A09D"/>
<path d="M197 34.5C197 35.2941 196.382 36 195.5 36C194.706 36 194 35.3824 194 34.5C194 33.7059 194.618 33 195.5 33C196.294 33 197 33.7059 197 34.5Z" fill="#00A09D"/>
<path d="M206 34.5C206 35.2941 205.382 36 204.5 36C203.706 36 203 35.3824 203 34.5C203 33.7059 203.618 33 204.5 33C205.294 33 206 33.7059 206 34.5Z" fill="#00A09D"/>
<path d="M215 34.5C215 35.2941 214.382 36 213.5 36C212.618 36 212 35.3824 212 34.5C212 33.7059 212.618 33 213.5 33C214.382 33 215 33.7059 215 34.5Z" fill="#00A09D"/>
<path d="M224 34.5C224 35.2941 223.382 36 222.5 36C221.706 36 221 35.3824 221 34.5C221 33.7059 221.618 33 222.5 33C223.294 33 224 33.7059 224 34.5Z" fill="#00A09D"/>
<path d="M231.5 36C232.328 36 233 35.3284 233 34.5C233 33.6716 232.328 33 231.5 33C230.672 33 230 33.6716 230 34.5C230 35.3284 230.672 36 231.5 36Z" fill="#00A09D"/>
<path d="M240.5 36C241.328 36 242 35.3284 242 34.5C242 33.6716 241.328 33 240.5 33C239.672 33 239 33.6716 239 34.5C239 35.3284 239.672 36 240.5 36Z" fill="#00A09D"/>
<path d="M249.5 36C250.328 36 251 35.3284 251 34.5C251 33.6716 250.328 33 249.5 33C248.672 33 248 33.6716 248 34.5C248 35.3284 248.672 36 249.5 36Z" fill="#00A09D"/>
<path d="M258.5 36C259.328 36 260 35.3284 260 34.5C260 33.6716 259.328 33 258.5 33C257.672 33 257 33.6716 257 34.5C257 35.3284 257.672 36 258.5 36Z" fill="#00A09D"/>
<path d="M267.5 36C268.328 36 269 35.3284 269 34.5C269 33.6716 268.328 33 267.5 33C266.672 33 266 33.6716 266 34.5C266 35.3284 266.672 36 267.5 36Z" fill="#00A09D"/>
<path d="M276.5 36C277.328 36 278 35.3284 278 34.5C278 33.6716 277.328 33 276.5 33C275.672 33 275 33.6716 275 34.5C275 35.3284 275.672 36 276.5 36Z" fill="#00A09D"/>
<path d="M285.5 36C286.328 36 287 35.3284 287 34.5C287 33.6716 286.328 33 285.5 33C284.672 33 284 33.6716 284 34.5C284 35.3284 284.672 36 285.5 36Z" fill="#00A09D"/>
<path d="M296 34.5C296 35.2941 295.382 36 294.5 36C293.618 36 293 35.3824 293 34.5C293 33.7059 293.618 33 294.5 33C295.382 33 296 33.7059 296 34.5Z" fill="#00A09D"/>
<path d="M305 34.5C305 35.2941 304.382 36 303.5 36C302.706 36 302 35.3824 302 34.5C302 33.7059 302.618 33 303.5 33C304.294 33 305 33.7059 305 34.5Z" fill="#00A09D"/>
<path d="M314 34.5C314 35.2941 313.382 36 312.5 36C311.706 36 311 35.3824 311 34.5C311 33.7059 311.618 33 312.5 33C313.294 33 314 33.7059 314 34.5Z" fill="#00A09D"/>
<path d="M323 34.5C323 35.2941 322.382 36 321.5 36C320.618 36 320 35.3824 320 34.5C320 33.7059 320.618 33 321.5 33C322.382 33 323 33.7059 323 34.5Z" fill="#00A09D"/>
<path d="M330.5 36C331.328 36 332 35.3284 332 34.5C332 33.6716 331.328 33 330.5 33C329.672 33 329 33.6716 329 34.5C329 35.3284 329.672 36 330.5 36Z" fill="#00A09D"/>
<path d="M339.5 36C340.328 36 341 35.3284 341 34.5C341 33.6716 340.328 33 339.5 33C338.672 33 338 33.6716 338 34.5C338 35.3284 338.672 36 339.5 36Z" fill="#00A09D"/>
<path d="M348.5 36C349.328 36 350 35.3284 350 34.5C350 33.6716 349.328 33 348.5 33C347.672 33 347 33.6716 347 34.5C347 35.3284 347.672 36 348.5 36Z" fill="#00A09D"/>
<path d="M357.5 36C358.328 36 359 35.3284 359 34.5C359 33.6716 358.328 33 357.5 33C356.672 33 356 33.6716 356 34.5C356 35.3284 356.672 36 357.5 36Z" fill="#00A09D"/>
<path d="M366.5 36C367.328 36 368 35.3284 368 34.5C368 33.6716 367.328 33 366.5 33C365.672 33 365 33.6716 365 34.5C365 35.3284 365.672 36 366.5 36Z" fill="#00A09D"/>
<path d="M377 34.5C377 35.2941 376.382 36 375.5 36C374.706 36 374 35.3824 374 34.5C374 33.7059 374.618 33 375.5 33C376.382 33 377 33.7059 377 34.5Z" fill="#00A09D"/>
<path d="M386 34.5C386 35.2941 385.382 36 384.5 36C383.706 36 383 35.3824 383 34.5C383 33.7059 383.618 33 384.5 33C385.382 33 386 33.7059 386 34.5Z" fill="#00A09D"/>
<path d="M395 34.5C395 35.2941 394.382 36 393.5 36C392.706 36 392 35.3824 392 34.5C392 33.7059 392.618 33 393.5 33C394.382 33 395 33.7059 395 34.5Z" fill="#00A09D"/>
<path d="M404 34.5C404 35.2941 403.382 36 402.5 36C401.706 36 401 35.3824 401 34.5C401 33.7059 401.618 33 402.5 33C403.382 33 404 33.7059 404 34.5Z" fill="#00A09D"/>
<path d="M411.5 36C412.328 36 413 35.3284 413 34.5C413 33.6716 412.328 33 411.5 33C410.672 33 410 33.6716 410 34.5C410 35.3284 410.672 36 411.5 36Z" fill="#00A09D"/>
<path d="M420.5 36C421.328 36 422 35.3284 422 34.5C422 33.6716 421.328 33 420.5 33C419.672 33 419 33.6716 419 34.5C419 35.3284 419.672 36 420.5 36Z" fill="#00A09D"/>
<path d="M429.5 36C430.328 36 431 35.3284 431 34.5C431 33.6716 430.328 33 429.5 33C428.672 33 428 33.6716 428 34.5C428 35.3284 428.672 36 429.5 36Z" fill="#00A09D"/>
<path d="M438.5 36C439.328 36 440 35.3284 440 34.5C440 33.6716 439.328 33 438.5 33C437.672 33 437 33.6716 437 34.5C437 35.3284 437.672 36 438.5 36Z" fill="#00A09D"/>
<path d="M447.5 36C448.328 36 449 35.3284 449 34.5C449 33.6716 448.328 33 447.5 33C446.672 33 446 33.6716 446 34.5C446 35.3284 446.672 36 447.5 36Z" fill="#00A09D"/>
<path d="M456.5 36C457.328 36 458 35.3284 458 34.5C458 33.6716 457.328 33 456.5 33C455.672 33 455 33.6716 455 34.5C455 35.3284 455.672 36 456.5 36Z" fill="#00A09D"/>
<path d="M467 34.5C467 35.2941 466.382 36 465.5 36C464.706 36 464 35.3824 464 34.5C464 33.7059 464.618 33 465.5 33C466.382 33 467 33.7059 467 34.5Z" fill="#00A09D"/>
<path d="M476 34.5C476 35.2941 475.382 36 474.5 36C473.706 36 473 35.3824 473 34.5C473 33.7059 473.618 33 474.5 33C475.382 33 476 33.7059 476 34.5Z" fill="#00A09D"/>
<path d="M485 34.5C485 35.2941 484.382 36 483.5 36C482.706 36 482 35.3824 482 34.5C482 33.7059 482.618 33 483.5 33C484.382 33 485 33.7059 485 34.5Z" fill="#00A09D"/>
<path d="M494 34.5C494 35.2941 493.382 36 492.5 36C491.706 36 491 35.3824 491 34.5C491 33.7059 491.618 33 492.5 33C493.382 33 494 33.7059 494 34.5Z" fill="#00A09D"/>
<path d="M501.5 36C502.328 36 503 35.3284 503 34.5C503 33.6716 502.328 33 501.5 33C500.672 33 500 33.6716 500 34.5C500 35.3284 500.672 36 501.5 36Z" fill="#00A09D"/>
<path d="M510.5 36C511.328 36 512 35.3284 512 34.5C512 33.6716 511.328 33 510.5 33C509.672 33 509 33.6716 509 34.5C509 35.3284 509.672 36 510.5 36Z" fill="#00A09D"/>
<path d="M519.5 36C520.328 36 521 35.3284 521 34.5C521 33.6716 520.328 33 519.5 33C518.672 33 518 33.6716 518 34.5C518 35.3284 518.672 36 519.5 36Z" fill="#00A09D"/>
<path d="M528.5 36C529.328 36 530 35.3284 530 34.5C530 33.6716 529.328 33 528.5 33C527.672 33 527 33.6716 527 34.5C527 35.3284 527.672 36 528.5 36Z" fill="#00A09D"/>
<path d="M537.5 36C538.328 36 539 35.3284 539 34.5C539 33.6716 538.328 33 537.5 33C536.672 33 536 33.6716 536 34.5C536 35.3284 536.672 36 537.5 36Z" fill="#00A09D"/>
<path d="M548 34.5C548 35.2941 547.382 36 546.5 36C545.706 36 545 35.3824 545 34.5C545 33.7059 545.618 33 546.5 33C547.382 33 548 33.7059 548 34.5Z" fill="#00A09D"/>
<path d="M557 34.5C557 35.2941 556.382 36 555.5 36C554.706 36 554 35.3824 554 34.5C554 33.7059 554.618 33 555.5 33C556.382 33 557 33.7059 557 34.5Z" fill="#00A09D"/>
<path d="M566 34.5C566 35.2941 565.382 36 564.5 36C563.706 36 563 35.3824 563 34.5C563 33.7059 563.618 33 564.5 33C565.382 33 566 33.7059 566 34.5Z" fill="#00A09D"/>
<path d="M575 34.5C575 35.2941 574.382 36 573.5 36C572.706 36 572 35.3824 572 34.5C572 33.7059 572.618 33 573.5 33C574.382 33 575 33.7059 575 34.5Z" fill="#00A09D"/>
<path d="M582.5 36C583.328 36 584 35.3284 584 34.5C584 33.6716 583.328 33 582.5 33C581.672 33 581 33.6716 581 34.5C581 35.3284 581.672 36 582.5 36Z" fill="#00A09D"/>
<path d="M591.5 36C592.328 36 593 35.3284 593 34.5C593 33.6716 592.328 33 591.5 33C590.672 33 590 33.6716 590 34.5C590 35.3284 590.672 36 591.5 36Z" fill="#00A09D"/>
<path d="M600.5 36C601.328 36 602 35.3284 602 34.5C602 33.6716 601.328 33 600.5 33C599.672 33 599 33.6716 599 34.5C599 35.3284 599.672 36 600.5 36Z" fill="#00A09D"/>
<path d="M609.5 36C610.328 36 611 35.3284 611 34.5C611 33.6716 610.328 33 609.5 33C608.672 33 608 33.6716 608 34.5C608 35.3284 608.672 36 609.5 36Z" fill="#00A09D"/>
<path d="M618.5 36C619.328 36 620 35.3284 620 34.5C620 33.6716 619.328 33 618.5 33C617.672 33 617 33.6716 617 34.5C617 35.3284 617.672 36 618.5 36Z" fill="#00A09D"/>
<path d="M629 34.5C629 35.2941 628.382 36 627.5 36C626.706 36 626 35.3824 626 34.5C626 33.7059 626.618 33 627.5 33C628.382 33 629 33.7059 629 34.5Z" fill="#00A09D"/>
<path d="M638 34.5C638 35.2941 637.382 36 636.5 36C635.706 36 635 35.3824 635 34.5C635 33.7059 635.618 33 636.5 33C637.294 33 638 33.7059 638 34.5Z" fill="#00A09D"/>
<path d="M647 34.5C647 35.2941 646.382 36 645.5 36C644.706 36 644 35.3824 644 34.5C644 33.7059 644.618 33 645.5 33C646.382 33 647 33.7059 647 34.5Z" fill="#00A09D"/>
<path d="M656 34.5C656 35.2941 655.382 36 654.5 36C653.706 36 653 35.3824 653 34.5C653 33.7059 653.618 33 654.5 33C655.382 33 656 33.7059 656 34.5Z" fill="#00A09D"/>
<path d="M665 34.5C665 35.2941 664.382 36 663.5 36C662.706 36 662 35.3824 662 34.5C662 33.7059 662.618 33 663.5 33C664.294 33 665 33.7059 665 34.5Z" fill="#00A09D"/>
<path d="M672.5 36C673.328 36 674 35.3284 674 34.5C674 33.6716 673.328 33 672.5 33C671.672 33 671 33.6716 671 34.5C671 35.3284 671.672 36 672.5 36Z" fill="#00A09D"/>
<path d="M681.5 36C682.328 36 683 35.3284 683 34.5C683 33.6716 682.328 33 681.5 33C680.672 33 680 33.6716 680 34.5C680 35.3284 680.672 36 681.5 36Z" fill="#00A09D"/>
<path d="M690.5 36C691.328 36 692 35.3284 692 34.5C692 33.6716 691.328 33 690.5 33C689.672 33 689 33.6716 689 34.5C689 35.3284 689.672 36 690.5 36Z" fill="#00A09D"/>
<path d="M699.5 36C700.328 36 701 35.3284 701 34.5C701 33.6716 700.328 33 699.5 33C698.672 33 698 33.6716 698 34.5C698 35.3284 698.672 36 699.5 36Z" fill="#00A09D"/>
<path d="M708.5 36C709.328 36 710 35.3284 710 34.5C710 33.6716 709.328 33 708.5 33C707.672 33 707 33.6716 707 34.5C707 35.3284 707.672 36 708.5 36Z" fill="#00A09D"/>
<path d="M719 34.5C719 35.2941 718.382 36 717.5 36C716.706 36 716 35.3824 716 34.5C716 33.7059 716.618 33 717.5 33C718.294 33 719 33.7059 719 34.5Z" fill="#00A09D"/>
<path d="M728 34.5C728 35.2941 727.382 36 726.5 36C725.706 36 725 35.3824 725 34.5C725 33.7059 725.618 33 726.5 33C727.382 33 728 33.7059 728 34.5Z" fill="#00A09D"/>
<path d="M737 34.5C737 35.2941 736.382 36 735.5 36C734.706 36 734 35.3824 734 34.5C734 33.7059 734.618 33 735.5 33C736.382 33 737 33.7059 737 34.5Z" fill="#00A09D"/>
<path d="M44 394.5C44 395.294 43.3824 396 42.5 396C41.7059 396 41 395.382 41 394.5C41 393.706 41.6176 393 42.5 393C43.2941 393 44 393.706 44 394.5Z" fill="#00A09D"/>
<path d="M51.5 396C52.3284 396 53 395.328 53 394.5C53 393.672 52.3284 393 51.5 393C50.6716 393 50 393.672 50 394.5C50 395.328 50.6716 396 51.5 396Z" fill="#00A09D"/>
<path d="M62 394.5C62 395.294 61.3824 396 60.5 396C59.7059 396 59 395.382 59 394.5C59 393.706 59.6176 393 60.5 393C61.2941 393 62 393.706 62 394.5Z" fill="#00A09D"/>
<path d="M71 394.5C71 395.294 70.3824 396 69.5 396C68.7059 396 68 395.382 68 394.5C68 393.706 68.6176 393 69.5 393C70.2941 393 71 393.706 71 394.5Z" fill="#00A09D"/>
<path d="M80 394.5C80 395.294 79.3824 396 78.5 396C77.6176 396 77 395.382 77 394.5C77 393.706 77.6176 393 78.5 393C79.3824 393 80 393.706 80 394.5Z" fill="#00A09D"/>
<path d="M89 394.5C89 395.294 88.3823 396 87.5 396C86.7059 396 86 395.382 86 394.5C86 393.706 86.6176 393 87.5 393C88.2941 393 89 393.706 89 394.5Z" fill="#00A09D"/>
<path d="M98 394.5C98 395.294 97.3824 396 96.5 396C95.7059 396 95 395.382 95 394.5C95 393.706 95.6177 393 96.5 393C97.2941 393 98 393.706 98 394.5Z" fill="#00A09D"/>
<path d="M107 394.5C107 395.294 106.382 396 105.5 396C104.618 396 104 395.382 104 394.5C104 393.706 104.618 393 105.5 393C106.382 393 107 393.706 107 394.5Z" fill="#00A09D"/>
<path d="M116 394.5C116 395.294 115.382 396 114.5 396C113.706 396 113 395.382 113 394.5C113 393.706 113.618 393 114.5 393C115.294 393 116 393.706 116 394.5Z" fill="#00A09D"/>
<path d="M125 394.5C125 395.294 124.382 396 123.5 396C122.706 396 122 395.382 122 394.5C122 393.706 122.618 393 123.5 393C124.294 393 125 393.706 125 394.5Z" fill="#00A09D"/>
<path d="M134 394.5C134 395.294 133.382 396 132.5 396C131.618 396 131 395.382 131 394.5C131 393.706 131.618 393 132.5 393C133.382 393 134 393.706 134 394.5Z" fill="#00A09D"/>
<path d="M143 394.5C143 395.294 142.382 396 141.5 396C140.706 396 140 395.382 140 394.5C140 393.706 140.618 393 141.5 393C142.294 393 143 393.706 143 394.5Z" fill="#00A09D"/>
<path d="M152 394.5C152 395.294 151.382 396 150.5 396C149.706 396 149 395.382 149 394.5C149 393.706 149.618 393 150.5 393C151.294 393 152 393.706 152 394.5Z" fill="#00A09D"/>
<path d="M161 394.5C161 395.294 160.382 396 159.5 396C158.618 396 158 395.382 158 394.5C158 393.706 158.618 393 159.5 393C160.382 393 161 393.706 161 394.5Z" fill="#00A09D"/>
<path d="M170 394.5C170 395.294 169.382 396 168.5 396C167.706 396 167 395.382 167 394.5C167 393.706 167.618 393 168.5 393C169.294 393 170 393.706 170 394.5Z" fill="#00A09D"/>
<path d="M177.5 396C178.328 396 179 395.328 179 394.5C179 393.672 178.328 393 177.5 393C176.672 393 176 393.672 176 394.5C176 395.328 176.672 396 177.5 396Z" fill="#00A09D"/>
<path d="M188 394.5C188 395.294 187.382 396 186.5 396C185.618 396 185 395.382 185 394.5C185 393.706 185.618 393 186.5 393C187.382 393 188 393.706 188 394.5Z" fill="#00A09D"/>
<path d="M197 394.5C197 395.294 196.382 396 195.5 396C194.706 396 194 395.382 194 394.5C194 393.706 194.618 393 195.5 393C196.294 393 197 393.706 197 394.5Z" fill="#00A09D"/>
<path d="M206 394.5C206 395.294 205.382 396 204.5 396C203.706 396 203 395.382 203 394.5C203 393.706 203.618 393 204.5 393C205.294 393 206 393.706 206 394.5Z" fill="#00A09D"/>
<path d="M215 394.5C215 395.294 214.382 396 213.5 396C212.618 396 212 395.382 212 394.5C212 393.706 212.618 393 213.5 393C214.382 393 215 393.706 215 394.5Z" fill="#00A09D"/>
<path d="M224 394.5C224 395.294 223.382 396 222.5 396C221.706 396 221 395.382 221 394.5C221 393.706 221.618 393 222.5 393C223.294 393 224 393.706 224 394.5Z" fill="#00A09D"/>
<path d="M231.5 396C232.328 396 233 395.328 233 394.5C233 393.672 232.328 393 231.5 393C230.672 393 230 393.672 230 394.5C230 395.328 230.672 396 231.5 396Z" fill="#00A09D"/>
<path d="M240.5 396C241.328 396 242 395.328 242 394.5C242 393.672 241.328 393 240.5 393C239.672 393 239 393.672 239 394.5C239 395.328 239.672 396 240.5 396Z" fill="#00A09D"/>
<path d="M249.5 396C250.328 396 251 395.328 251 394.5C251 393.672 250.328 393 249.5 393C248.672 393 248 393.672 248 394.5C248 395.328 248.672 396 249.5 396Z" fill="#00A09D"/>
<path d="M258.5 396C259.328 396 260 395.328 260 394.5C260 393.672 259.328 393 258.5 393C257.672 393 257 393.672 257 394.5C257 395.328 257.672 396 258.5 396Z" fill="#00A09D"/>
<path d="M267.5 396C268.328 396 269 395.328 269 394.5C269 393.672 268.328 393 267.5 393C266.672 393 266 393.672 266 394.5C266 395.328 266.672 396 267.5 396Z" fill="#00A09D"/>
<path d="M276.5 396C277.328 396 278 395.328 278 394.5C278 393.672 277.328 393 276.5 393C275.672 393 275 393.672 275 394.5C275 395.328 275.672 396 276.5 396Z" fill="#00A09D"/>
<path d="M285.5 396C286.328 396 287 395.328 287 394.5C287 393.672 286.328 393 285.5 393C284.672 393 284 393.672 284 394.5C284 395.328 284.672 396 285.5 396Z" fill="#00A09D"/>
<path d="M296 394.5C296 395.294 295.382 396 294.5 396C293.618 396 293 395.382 293 394.5C293 393.706 293.618 393 294.5 393C295.382 393 296 393.706 296 394.5Z" fill="#00A09D"/>
<path d="M305 394.5C305 395.294 304.382 396 303.5 396C302.706 396 302 395.382 302 394.5C302 393.706 302.618 393 303.5 393C304.294 393 305 393.706 305 394.5Z" fill="#00A09D"/>
<path d="M314 394.5C314 395.294 313.382 396 312.5 396C311.706 396 311 395.382 311 394.5C311 393.706 311.618 393 312.5 393C313.294 393 314 393.706 314 394.5Z" fill="#00A09D"/>
<path d="M323 394.5C323 395.294 322.382 396 321.5 396C320.618 396 320 395.382 320 394.5C320 393.706 320.618 393 321.5 393C322.382 393 323 393.706 323 394.5Z" fill="#00A09D"/>
<path d="M330.5 396C331.328 396 332 395.328 332 394.5C332 393.672 331.328 393 330.5 393C329.672 393 329 393.672 329 394.5C329 395.328 329.672 396 330.5 396Z" fill="#00A09D"/>
<path d="M339.5 396C340.328 396 341 395.328 341 394.5C341 393.672 340.328 393 339.5 393C338.672 393 338 393.672 338 394.5C338 395.328 338.672 396 339.5 396Z" fill="#00A09D"/>
<path d="M348.5 396C349.328 396 350 395.328 350 394.5C350 393.672 349.328 393 348.5 393C347.672 393 347 393.672 347 394.5C347 395.328 347.672 396 348.5 396Z" fill="#00A09D"/>
<path d="M357.5 396C358.328 396 359 395.328 359 394.5C359 393.672 358.328 393 357.5 393C356.672 393 356 393.672 356 394.5C356 395.328 356.672 396 357.5 396Z" fill="#00A09D"/>
<path d="M366.5 396C367.328 396 368 395.328 368 394.5C368 393.672 367.328 393 366.5 393C365.672 393 365 393.672 365 394.5C365 395.328 365.672 396 366.5 396Z" fill="#00A09D"/>
<path d="M377 394.5C377 395.294 376.382 396 375.5 396C374.706 396 374 395.382 374 394.5C374 393.706 374.618 393 375.5 393C376.382 393 377 393.706 377 394.5Z" fill="#00A09D"/>
<path d="M386 394.5C386 395.294 385.382 396 384.5 396C383.706 396 383 395.382 383 394.5C383 393.706 383.618 393 384.5 393C385.382 393 386 393.706 386 394.5Z" fill="#00A09D"/>
<path d="M395 394.5C395 395.294 394.382 396 393.5 396C392.706 396 392 395.382 392 394.5C392 393.706 392.618 393 393.5 393C394.382 393 395 393.706 395 394.5Z" fill="#00A09D"/>
<path d="M404 394.5C404 395.294 403.382 396 402.5 396C401.706 396 401 395.382 401 394.5C401 393.706 401.618 393 402.5 393C403.382 393 404 393.706 404 394.5Z" fill="#00A09D"/>
<path d="M411.5 396C412.328 396 413 395.328 413 394.5C413 393.672 412.328 393 411.5 393C410.672 393 410 393.672 410 394.5C410 395.328 410.672 396 411.5 396Z" fill="#00A09D"/>
<path d="M420.5 396C421.328 396 422 395.328 422 394.5C422 393.672 421.328 393 420.5 393C419.672 393 419 393.672 419 394.5C419 395.328 419.672 396 420.5 396Z" fill="#00A09D"/>
<path d="M429.5 396C430.328 396 431 395.328 431 394.5C431 393.672 430.328 393 429.5 393C428.672 393 428 393.672 428 394.5C428 395.328 428.672 396 429.5 396Z" fill="#00A09D"/>
<path d="M438.5 396C439.328 396 440 395.328 440 394.5C440 393.672 439.328 393 438.5 393C437.672 393 437 393.672 437 394.5C437 395.328 437.672 396 438.5 396Z" fill="#00A09D"/>
<path d="M447.5 396C448.328 396 449 395.328 449 394.5C449 393.672 448.328 393 447.5 393C446.672 393 446 393.672 446 394.5C446 395.328 446.672 396 447.5 396Z" fill="#00A09D"/>
<path d="M456.5 396C457.328 396 458 395.328 458 394.5C458 393.672 457.328 393 456.5 393C455.672 393 455 393.672 455 394.5C455 395.328 455.672 396 456.5 396Z" fill="#00A09D"/>
<path d="M467 394.5C467 395.294 466.382 396 465.5 396C464.706 396 464 395.382 464 394.5C464 393.706 464.618 393 465.5 393C466.382 393 467 393.706 467 394.5Z" fill="#00A09D"/>
<path d="M476 394.5C476 395.294 475.382 396 474.5 396C473.706 396 473 395.382 473 394.5C473 393.706 473.618 393 474.5 393C475.382 393 476 393.706 476 394.5Z" fill="#00A09D"/>
<path d="M485 394.5C485 395.294 484.382 396 483.5 396C482.706 396 482 395.382 482 394.5C482 393.706 482.618 393 483.5 393C484.382 393 485 393.706 485 394.5Z" fill="#00A09D"/>
<path d="M494 394.5C494 395.294 493.382 396 492.5 396C491.706 396 491 395.382 491 394.5C491 393.706 491.618 393 492.5 393C493.382 393 494 393.706 494 394.5Z" fill="#00A09D"/>
<path d="M501.5 396C502.328 396 503 395.328 503 394.5C503 393.672 502.328 393 501.5 393C500.672 393 500 393.672 500 394.5C500 395.328 500.672 396 501.5 396Z" fill="#00A09D"/>
<path d="M510.5 396C511.328 396 512 395.328 512 394.5C512 393.672 511.328 393 510.5 393C509.672 393 509 393.672 509 394.5C509 395.328 509.672 396 510.5 396Z" fill="#00A09D"/>
<path d="M519.5 396C520.328 396 521 395.328 521 394.5C521 393.672 520.328 393 519.5 393C518.672 393 518 393.672 518 394.5C518 395.328 518.672 396 519.5 396Z" fill="#00A09D"/>
<path d="M528.5 396C529.328 396 530 395.328 530 394.5C530 393.672 529.328 393 528.5 393C527.672 393 527 393.672 527 394.5C527 395.328 527.672 396 528.5 396Z" fill="#00A09D"/>
<path d="M537.5 396C538.328 396 539 395.328 539 394.5C539 393.672 538.328 393 537.5 393C536.672 393 536 393.672 536 394.5C536 395.328 536.672 396 537.5 396Z" fill="#00A09D"/>
<path d="M548 394.5C548 395.294 547.382 396 546.5 396C545.706 396 545 395.382 545 394.5C545 393.706 545.618 393 546.5 393C547.382 393 548 393.706 548 394.5Z" fill="#00A09D"/>
<path d="M557 394.5C557 395.294 556.382 396 555.5 396C554.706 396 554 395.382 554 394.5C554 393.706 554.618 393 555.5 393C556.382 393 557 393.706 557 394.5Z" fill="#00A09D"/>
<path d="M566 394.5C566 395.294 565.382 396 564.5 396C563.706 396 563 395.382 563 394.5C563 393.706 563.618 393 564.5 393C565.382 393 566 393.706 566 394.5Z" fill="#00A09D"/>
<path d="M575 394.5C575 395.294 574.382 396 573.5 396C572.706 396 572 395.382 572 394.5C572 393.706 572.618 393 573.5 393C574.382 393 575 393.706 575 394.5Z" fill="#00A09D"/>
<path d="M582.5 396C583.328 396 584 395.328 584 394.5C584 393.672 583.328 393 582.5 393C581.672 393 581 393.672 581 394.5C581 395.328 581.672 396 582.5 396Z" fill="#00A09D"/>
<path d="M591.5 396C592.328 396 593 395.328 593 394.5C593 393.672 592.328 393 591.5 393C590.672 393 590 393.672 590 394.5C590 395.328 590.672 396 591.5 396Z" fill="#00A09D"/>
<path d="M600.5 396C601.328 396 602 395.328 602 394.5C602 393.672 601.328 393 600.5 393C599.672 393 599 393.672 599 394.5C599 395.328 599.672 396 600.5 396Z" fill="#00A09D"/>
<path d="M609.5 396C610.328 396 611 395.328 611 394.5C611 393.672 610.328 393 609.5 393C608.672 393 608 393.672 608 394.5C608 395.328 608.672 396 609.5 396Z" fill="#00A09D"/>
<path d="M618.5 396C619.328 396 620 395.328 620 394.5C620 393.672 619.328 393 618.5 393C617.672 393 617 393.672 617 394.5C617 395.328 617.672 396 618.5 396Z" fill="#00A09D"/>
<path d="M629 394.5C629 395.294 628.382 396 627.5 396C626.706 396 626 395.382 626 394.5C626 393.706 626.618 393 627.5 393C628.382 393 629 393.706 629 394.5Z" fill="#00A09D"/>
<path d="M638 394.5C638 395.294 637.382 396 636.5 396C635.706 396 635 395.382 635 394.5C635 393.706 635.618 393 636.5 393C637.294 393 638 393.706 638 394.5Z" fill="#00A09D"/>
<path d="M647 394.5C647 395.294 646.382 396 645.5 396C644.706 396 644 395.382 644 394.5C644 393.706 644.618 393 645.5 393C646.382 393 647 393.706 647 394.5Z" fill="#00A09D"/>
<path d="M656 394.5C656 395.294 655.382 396 654.5 396C653.706 396 653 395.382 653 394.5C653 393.706 653.618 393 654.5 393C655.382 393 656 393.706 656 394.5Z" fill="#00A09D"/>
<path d="M665 394.5C665 395.294 664.382 396 663.5 396C662.706 396 662 395.382 662 394.5C662 393.706 662.618 393 663.5 393C664.294 393 665 393.706 665 394.5Z" fill="#00A09D"/>
<path d="M672.5 396C673.328 396 674 395.328 674 394.5C674 393.672 673.328 393 672.5 393C671.672 393 671 393.672 671 394.5C671 395.328 671.672 396 672.5 396Z" fill="#00A09D"/>
<path d="M681.5 396C682.328 396 683 395.328 683 394.5C683 393.672 682.328 393 681.5 393C680.672 393 680 393.672 680 394.5C680 395.328 680.672 396 681.5 396Z" fill="#00A09D"/>
<path d="M690.5 396C691.328 396 692 395.328 692 394.5C692 393.672 691.328 393 690.5 393C689.672 393 689 393.672 689 394.5C689 395.328 689.672 396 690.5 396Z" fill="#00A09D"/>
<path d="M699.5 396C700.328 396 701 395.328 701 394.5C701 393.672 700.328 393 699.5 393C698.672 393 698 393.672 698 394.5C698 395.328 698.672 396 699.5 396Z" fill="#00A09D"/>
<path d="M708.5 396C709.328 396 710 395.328 710 394.5C710 393.672 709.328 393 708.5 393C707.672 393 707 393.672 707 394.5C707 395.328 707.672 396 708.5 396Z" fill="#00A09D"/>
<path d="M719 394.5C719 395.294 718.382 396 717.5 396C716.706 396 716 395.382 716 394.5C716 393.706 716.618 393 717.5 393C718.294 393 719 393.706 719 394.5Z" fill="#00A09D"/>
<path d="M728 394.5C728 395.294 727.382 396 726.5 396C725.706 396 725 395.382 725 394.5C725 393.706 725.618 393 726.5 393C727.382 393 728 393.706 728 394.5Z" fill="#00A09D"/>
<path d="M737 394.5C737 395.294 736.382 396 735.5 396C734.706 396 734 395.382 734 394.5C734 393.706 734.618 393 735.5 393C736.382 393 737 393.706 737 394.5Z" fill="#00A09D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M564.608 439.25C565.646 439.25 566.578 439.447 567.406 439.84C568.243 440.224 568.954 440.766 569.536 441.468C570.128 442.16 570.578 442.98 570.888 443.925C571.197 444.861 571.352 445.876 571.352 446.971V447.293C571.352 448.388 571.197 449.404 570.888 450.34C570.578 451.276 570.128 452.094 569.536 452.796C568.954 453.488 568.248 454.032 567.42 454.425C566.601 454.808 565.673 455 564.636 455C563.598 455 562.665 454.808 561.837 454.425C561.009 454.032 560.3 453.488 559.708 452.796C559.126 452.094 558.68 451.276 558.37 450.34C558.061 449.404 557.906 448.388 557.906 447.293V446.971C557.906 445.876 558.061 444.861 558.37 443.925C558.68 442.98 559.126 442.16 559.708 441.468C560.29 440.766 560.995 440.223 561.823 439.84C562.651 439.447 563.58 439.25 564.608 439.25ZM564.608 441.384C563.917 441.384 563.307 441.534 562.779 441.833C562.261 442.132 561.824 442.544 561.469 443.068C561.123 443.583 560.863 444.177 560.69 444.851C560.518 445.515 560.431 446.222 560.431 446.971V447.293C560.431 448.051 560.518 448.768 560.69 449.441C560.863 450.106 561.123 450.695 561.469 451.21C561.824 451.725 562.265 452.131 562.793 452.431C563.321 452.721 563.935 452.866 564.636 452.866C565.327 452.866 565.933 452.721 566.451 452.431C566.979 452.131 567.416 451.725 567.762 451.21C568.107 450.695 568.366 450.106 568.539 449.441C568.721 448.768 568.812 448.051 568.812 447.293V446.971C568.812 446.222 568.721 445.515 568.539 444.851C568.366 444.177 568.103 443.583 567.748 443.068C567.402 442.544 566.965 442.132 566.438 441.833C565.919 441.534 565.309 441.384 564.608 441.384Z" fill="#005E7A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M610.474 439.25C611.511 439.25 612.443 439.447 613.271 439.84C614.109 440.224 614.819 440.766 615.401 441.468C615.993 442.16 616.444 442.98 616.753 443.925C617.062 444.861 617.217 445.876 617.217 446.971V447.293C617.217 448.388 617.062 449.404 616.753 450.34C616.444 451.276 615.993 452.094 615.401 452.796C614.819 453.488 614.113 454.032 613.285 454.425C612.466 454.808 611.538 455 610.501 455C609.464 455 608.53 454.808 607.702 454.425C606.874 454.032 606.165 453.488 605.573 452.796C604.991 452.094 604.545 451.276 604.235 450.34C603.926 449.404 603.771 448.388 603.771 447.293V446.971C603.771 445.876 603.926 444.861 604.235 443.925C604.545 442.98 604.991 442.16 605.573 441.468C606.156 440.766 606.861 440.223 607.688 439.84C608.517 439.447 609.445 439.25 610.474 439.25ZM610.474 441.384C609.782 441.384 609.172 441.534 608.645 441.833C608.126 442.132 607.689 442.544 607.334 443.068C606.988 443.583 606.729 444.177 606.556 444.851C606.383 445.515 606.296 446.222 606.296 446.971V447.293C606.296 448.051 606.383 448.768 606.556 449.441C606.729 450.106 606.988 450.695 607.334 451.21C607.689 451.725 608.13 452.131 608.658 452.431C609.186 452.721 609.8 452.866 610.501 452.866C611.192 452.866 611.798 452.721 612.316 452.431C612.844 452.131 613.281 451.725 613.627 451.21C613.973 450.695 614.231 450.106 614.404 449.441C614.586 448.768 614.678 448.051 614.678 447.293V446.971C614.678 446.222 614.586 445.515 614.404 444.851C614.231 444.177 613.968 443.583 613.613 443.068C613.268 442.544 612.83 442.132 612.303 441.833C611.784 441.534 611.174 441.384 610.474 441.384Z" fill="#005E7A"/>
<path d="M541.635 443.068H551.654V434.28H554.274V454.72H551.654V445.272H541.635V454.72H539V434.28H541.635V443.068Z" fill="#005E7A"/>
<path d="M580.538 439.25C580.72 439.25 580.929 439.274 581.166 439.32C581.402 439.358 581.566 439.4 581.657 439.446L581.644 441.861C581.434 441.815 581.234 441.786 581.043 441.776C580.861 441.758 580.652 441.749 580.415 441.749C579.833 441.749 579.319 441.842 578.873 442.029C578.427 442.216 578.05 442.479 577.74 442.815C577.453 443.128 577.221 443.497 577.044 443.923V454.72H574.519V439.53H576.976L577.025 441.274C577.032 441.264 577.038 441.253 577.044 441.243C577.417 440.616 577.89 440.129 578.463 439.783C579.045 439.428 579.737 439.25 580.538 439.25Z" fill="#005E7A"/>
<path d="M586.735 454.72H584.196V439.53H586.735V454.72Z" fill="#005E7A"/>
<path d="M601.396 441.37L593.185 452.586H601.792V454.72H590.08V452.811L598.215 441.679H590.188V439.53H601.396V441.37Z" fill="#005E7A"/>
<path d="M627.264 439.25C627.991 439.25 628.646 439.353 629.229 439.559C629.811 439.755 630.307 440.074 630.717 440.514C631.135 440.953 631.454 441.524 631.673 442.226C631.891 442.918 632 443.765 632 444.767V454.72H629.461V444.738C629.461 443.943 629.348 443.306 629.12 442.829C628.893 442.343 628.56 441.992 628.123 441.776C627.686 441.552 627.149 441.439 626.513 441.439C625.885 441.439 625.312 441.575 624.793 441.847C624.283 442.118 623.842 442.493 623.469 442.97C623.255 443.249 623.069 443.554 622.909 443.882V454.72H620.384V439.53H622.772L622.852 441.423C622.88 441.386 622.908 441.349 622.937 441.313C623.464 440.658 624.092 440.153 624.82 439.798C625.557 439.433 626.372 439.25 627.264 439.25Z" fill="#005E7A"/>
<path d="M585.493 434C585.975 434 586.344 434.145 586.599 434.436C586.862 434.726 586.995 435.081 586.995 435.502C586.995 435.904 586.863 436.251 586.599 436.541C586.344 436.822 585.975 436.962 585.493 436.962C585.002 436.962 584.629 436.822 584.374 436.541C584.128 436.251 584.005 435.904 584.005 435.502C584.005 435.081 584.128 434.726 584.374 434.436C584.629 434.145 585.002 434 585.493 434Z" fill="#005E7A"/>
<path d="M452.242 455C451.212 455 450.277 454.829 449.438 454.487C448.609 454.136 447.893 453.646 447.291 453.016C446.698 452.386 446.243 451.639 445.923 450.776C445.604 449.912 445.445 448.967 445.445 447.942V447.375C445.445 446.187 445.623 445.13 445.978 444.203C446.334 443.267 446.817 442.476 447.428 441.828C448.039 441.18 448.732 440.69 449.507 440.357C450.282 440.024 451.084 439.857 451.914 439.857C452.972 439.857 453.883 440.037 454.649 440.397C455.424 440.757 456.058 441.261 456.55 441.909C457.043 442.548 457.407 443.303 457.644 444.176C457.881 445.04 458 445.985 458 447.01V448.13H446.949V446.093H455.47V445.904C455.433 445.256 455.297 444.626 455.059 444.014C454.832 443.402 454.467 442.898 453.965 442.503C453.464 442.107 452.78 441.909 451.914 441.909C451.339 441.909 450.811 442.03 450.327 442.273C449.844 442.507 449.429 442.858 449.083 443.326C448.736 443.794 448.467 444.365 448.276 445.04C448.084 445.715 447.989 446.493 447.989 447.375V447.942C447.989 448.634 448.084 449.287 448.276 449.898C448.476 450.501 448.764 451.032 449.137 451.491C449.52 451.95 449.981 452.31 450.519 452.571C451.066 452.832 451.686 452.962 452.379 452.962C453.272 452.962 454.029 452.782 454.649 452.422C455.269 452.062 455.812 451.581 456.277 450.978L457.809 452.179C457.489 452.656 457.084 453.111 456.591 453.542C456.099 453.974 455.493 454.325 454.772 454.595C454.061 454.865 453.218 455 452.242 455Z" fill="#B79CB0"/>
<path d="M424.054 443.029V454.73H421.51V440.127H423.917L424.054 443.029ZM423.535 446.875L422.358 446.835C422.367 445.836 422.5 444.914 422.755 444.068C423.01 443.213 423.389 442.471 423.89 441.841C424.392 441.211 425.016 440.726 425.764 440.384C426.512 440.033 427.378 439.857 428.362 439.857C429.055 439.857 429.694 439.956 430.277 440.154C430.861 440.343 431.367 440.645 431.795 441.058C432.224 441.472 432.557 442.003 432.794 442.651C433.031 443.299 433.149 444.082 433.149 444.999V454.73H430.619V445.121C430.619 444.356 430.487 443.744 430.222 443.285C429.967 442.826 429.602 442.494 429.128 442.287C428.654 442.071 428.098 441.963 427.46 441.963C426.712 441.963 426.088 442.093 425.586 442.354C425.085 442.615 424.683 442.975 424.382 443.434C424.082 443.893 423.863 444.419 423.726 445.013C423.598 445.598 423.535 446.219 423.535 446.875ZM433.122 445.499L431.426 446.012C431.435 445.211 431.567 444.442 431.823 443.704C432.087 442.966 432.465 442.309 432.958 441.733C433.459 441.157 434.075 440.703 434.804 440.37C435.534 440.028 436.368 439.857 437.307 439.857C438.1 439.857 438.802 439.961 439.413 440.168C440.033 440.375 440.553 440.694 440.972 441.126C441.401 441.549 441.725 442.093 441.943 442.759C442.162 443.425 442.272 444.217 442.272 445.134V454.73H439.728V445.107C439.728 444.289 439.596 443.654 439.331 443.204C439.076 442.746 438.711 442.426 438.237 442.246C437.772 442.057 437.216 441.963 436.568 441.963C436.012 441.963 435.52 442.057 435.091 442.246C434.663 442.435 434.303 442.696 434.011 443.029C433.719 443.353 433.496 443.726 433.341 444.149C433.195 444.572 433.122 445.022 433.122 445.499Z" fill="#B79CB0"/>
<path d="M417.476 440.127V454.73H414.932V440.127H417.476ZM414.74 436.254C414.74 435.849 414.863 435.507 415.11 435.228C415.365 434.949 415.739 434.81 416.231 434.81C416.714 434.81 417.084 434.949 417.339 435.228C417.603 435.507 417.736 435.849 417.736 436.254C417.736 436.641 417.603 436.974 417.339 437.253C417.084 437.522 416.714 437.657 416.231 437.657C415.739 437.657 415.365 437.522 415.11 437.253C414.863 436.974 414.74 436.641 414.74 436.254Z" fill="#B79CB0"/>
<path d="M411.759 440.127V442.044H403.758V440.127H411.759ZM406.466 436.578H408.996V451.113C408.996 451.608 409.074 451.981 409.229 452.233C409.384 452.485 409.584 452.652 409.83 452.733C410.077 452.814 410.341 452.854 410.624 452.854C410.833 452.854 411.052 452.836 411.28 452.8C411.517 452.755 411.695 452.719 411.814 452.692L411.827 454.73C411.627 454.793 411.362 454.852 411.034 454.906C410.715 454.969 410.327 455 409.871 455C409.251 455 408.682 454.879 408.162 454.636C407.642 454.393 407.227 453.988 406.917 453.421C406.616 452.845 406.466 452.071 406.466 451.1V436.578Z" fill="#B79CB0"/>
<path d="M392.133 451.896V434H394.677V454.73H392.351L392.133 451.896ZM382.176 447.591V447.307C382.176 446.192 382.313 445.179 382.586 444.271C382.869 443.353 383.266 442.566 383.776 441.909C384.296 441.252 384.911 440.748 385.623 440.397C386.343 440.037 387.145 439.857 388.03 439.857C388.96 439.857 389.771 440.019 390.464 440.343C391.166 440.658 391.759 441.121 392.242 441.733C392.734 442.336 393.122 443.065 393.405 443.92C393.687 444.774 393.883 445.742 393.993 446.821V448.063C393.892 449.134 393.696 450.096 393.405 450.951C393.122 451.806 392.734 452.535 392.242 453.138C391.759 453.74 391.166 454.204 390.464 454.528C389.762 454.843 388.941 455 388.002 455C387.136 455 386.343 454.816 385.623 454.447C384.911 454.078 384.296 453.56 383.776 452.895C383.266 452.229 382.869 451.446 382.586 450.546C382.313 449.638 382.176 448.652 382.176 447.591ZM384.72 447.307V447.591C384.72 448.319 384.793 449.003 384.939 449.642C385.094 450.281 385.331 450.843 385.65 451.329C385.969 451.815 386.375 452.197 386.867 452.476C387.359 452.746 387.948 452.881 388.631 452.881C389.47 452.881 390.159 452.706 390.697 452.355C391.244 452.004 391.681 451.54 392.01 450.965C392.338 450.389 392.593 449.763 392.775 449.089V445.836C392.666 445.341 392.506 444.864 392.297 444.406C392.096 443.938 391.832 443.524 391.504 443.164C391.184 442.795 390.788 442.503 390.314 442.287C389.849 442.071 389.297 441.963 388.659 441.963C387.966 441.963 387.369 442.107 386.867 442.395C386.375 442.674 385.969 443.06 385.65 443.555C385.331 444.041 385.094 444.608 384.939 445.256C384.793 445.895 384.72 446.578 384.72 447.307Z" fill="#B79CB0"/>
<path d="M376.295 452.233V444.716C376.295 444.14 376.176 443.641 375.939 443.218C375.711 442.786 375.365 442.453 374.9 442.219C374.435 441.985 373.86 441.868 373.177 441.868C372.538 441.868 371.978 441.976 371.494 442.192C371.02 442.408 370.646 442.692 370.373 443.042C370.109 443.393 369.976 443.771 369.976 444.176H367.446C367.446 443.654 367.583 443.137 367.856 442.624C368.13 442.111 368.522 441.648 369.033 441.234C369.552 440.811 370.172 440.478 370.893 440.235C371.622 439.983 372.434 439.857 373.327 439.857C374.403 439.857 375.351 440.037 376.172 440.397C377.002 440.757 377.649 441.301 378.114 442.03C378.588 442.75 378.825 443.654 378.825 444.743V451.545C378.825 452.031 378.866 452.548 378.948 453.097C379.039 453.646 379.172 454.118 379.345 454.514V454.73H376.705C376.578 454.442 376.477 454.06 376.404 453.583C376.331 453.097 376.295 452.647 376.295 452.233ZM376.733 445.877L376.76 447.631H374.202C373.482 447.631 372.839 447.69 372.274 447.807C371.709 447.915 371.235 448.081 370.852 448.306C370.469 448.531 370.177 448.814 369.976 449.156C369.776 449.489 369.675 449.88 369.675 450.33C369.675 450.789 369.78 451.208 369.99 451.585C370.2 451.963 370.514 452.265 370.934 452.49C371.362 452.706 371.886 452.814 372.506 452.814C373.282 452.814 373.965 452.652 374.558 452.328C375.151 452.004 375.62 451.608 375.967 451.14C376.322 450.672 376.514 450.218 376.541 449.777L377.622 450.978C377.558 451.356 377.385 451.774 377.102 452.233C376.819 452.692 376.441 453.133 375.967 453.556C375.502 453.97 374.946 454.316 374.298 454.595C373.66 454.865 372.94 455 372.137 455C371.134 455 370.254 454.807 369.498 454.42C368.75 454.033 368.166 453.515 367.747 452.868C367.337 452.211 367.132 451.478 367.132 450.668C367.132 449.885 367.287 449.197 367.597 448.603C367.907 448 368.353 447.501 368.937 447.105C369.52 446.7 370.222 446.394 371.043 446.187C371.864 445.98 372.78 445.877 373.792 445.877H376.733Z" fill="#B79CB0"/>
<path d="M358.857 455C357.827 455 356.892 454.829 356.053 454.487C355.224 454.136 354.508 453.646 353.906 453.016C353.313 452.386 352.858 451.639 352.538 450.776C352.219 449.912 352.06 448.967 352.06 447.942V447.375C352.06 446.187 352.238 445.13 352.593 444.203C352.949 443.267 353.432 442.476 354.043 441.828C354.654 441.18 355.347 440.69 356.122 440.357C356.897 440.024 357.699 439.857 358.529 439.857C359.587 439.857 360.498 440.037 361.264 440.397C362.039 440.757 362.673 441.261 363.165 441.909C363.658 442.548 364.022 443.303 364.259 444.176C364.496 445.04 364.615 445.985 364.615 447.01V448.13H353.564V446.093H362.085V445.904C362.048 445.256 361.912 444.626 361.674 444.014C361.447 443.402 361.082 442.898 360.58 442.503C360.079 442.107 359.395 441.909 358.529 441.909C357.954 441.909 357.426 442.03 356.942 442.273C356.459 442.507 356.044 442.858 355.698 443.326C355.351 443.794 355.082 444.365 354.891 445.04C354.699 445.715 354.604 446.493 354.604 447.375V447.942C354.604 448.634 354.699 449.287 354.891 449.898C355.091 450.501 355.379 451.032 355.752 451.491C356.135 451.95 356.596 452.31 357.134 452.571C357.681 452.832 358.301 452.962 358.994 452.962C359.887 452.962 360.644 452.782 361.264 452.422C361.884 452.062 362.427 451.581 362.892 450.978L364.424 452.179C364.104 452.656 363.699 453.111 363.206 453.542C362.714 453.974 362.108 454.325 361.387 454.595C360.676 454.865 359.833 455 358.857 455Z" fill="#B79CB0"/>
<path d="M350.077 452.611V454.73H340.12V452.611H350.077ZM340.64 435.08V454.73H338V435.08H340.64Z" fill="#B79CB0"/>
<path d="M670 387L672 32" stroke="#00A09D" stroke-width="4" stroke-miterlimit="10"/>
<path d="M675 392.002C675 394.779 672.778 397 670 397C667.222 397 665 394.779 665 392.002C665 389.225 667.222 387.004 670 387.004C672.778 386.893 675 389.225 675 392.002Z" fill="#00A09D"/>
<path d="M677 35.0549C677 32.3077 674.802 30.1099 672.055 30C669.308 30 667.11 32.1978 667 34.9451C667 37.6923 669.198 39.8901 671.945 40H672.055C674.802 39.8901 677 37.6923 677 35.0549Z" fill="#00A09D"/>
<path d="M503 415H292" stroke="#B79CB0" stroke-width="4" stroke-miterlimit="10"/>
<path d="M668 415L503 415" stroke="#005E7A" stroke-width="4" stroke-miterlimit="10"/>
<path d="M290 440L290 492" stroke="#00A09D" stroke-width="2.26" stroke-miterlimit="10"/>
<path d="M336 490H243V529H336V490Z" fill="#00A09D"/>
<path d="M765 474.2C765 473.437 765.819 472.955 766.486 473.325L773.427 477.181C774.112 477.562 774.112 478.549 773.427 478.93L766.486 482.786C765.819 483.156 765 482.674 765 481.912V474.2Z" fill="#827882"/>
<path d="M852.966 32.9313V43.749H849.982V30.1704H852.792L852.966 32.9313ZM852.483 36.4577L851.467 36.4452C851.467 35.5081 851.583 34.6422 851.814 33.8474C852.045 33.0526 852.384 32.3624 852.829 31.7767C853.275 31.1827 853.828 30.7268 854.489 30.4088C855.157 30.0825 855.929 29.9194 856.804 29.9194C857.415 29.9194 857.972 30.0114 858.475 30.1955C858.987 30.3712 859.429 30.6515 859.8 31.0363C860.18 31.4212 860.469 31.9148 860.667 32.5172C860.873 33.1195 860.977 33.8474 860.977 34.7008V43.749H857.993V34.9643C857.993 34.3034 857.894 33.7847 857.695 33.4082C857.506 33.0317 857.229 32.764 856.866 32.605C856.511 32.4377 856.086 32.354 855.59 32.354C855.029 32.354 854.55 32.4628 854.154 32.6803C853.766 32.8978 853.448 33.1948 853.201 33.5713C852.953 33.9478 852.772 34.3829 852.656 34.8765C852.54 35.3701 852.483 35.8972 852.483 36.4577ZM860.791 35.6545L859.392 35.9683C859.392 35.1484 859.503 34.3745 859.726 33.6466C859.957 32.9104 860.291 32.2662 860.729 31.714C861.175 31.1534 861.724 30.7142 862.376 30.3963C863.028 30.0784 863.775 29.9194 864.617 29.9194C865.302 29.9194 865.913 30.0156 866.449 30.208C866.994 30.3921 867.456 30.6849 867.836 31.0865C868.216 31.4881 868.505 32.011 868.703 32.6552C868.901 33.291 869 34.0607 869 34.9643V43.749H866.004V34.9518C866.004 34.2657 865.905 33.7345 865.706 33.358C865.517 32.9815 865.244 32.7221 864.889 32.5799C864.534 32.4293 864.109 32.354 863.614 32.354C863.152 32.354 862.743 32.4419 862.388 32.6176C862.041 32.7849 861.748 33.0233 861.509 33.3329C861.27 33.6341 861.088 33.9813 860.964 34.3745C860.849 34.7677 860.791 35.1944 860.791 35.6545Z" fill="#00A09D"/>
<path d="M843.927 40.5489V30.1704H846.923V43.749H844.1L843.927 40.5489ZM844.348 37.7252L845.351 37.7001C845.351 38.6121 845.252 39.4529 845.054 40.2226C844.855 40.9839 844.55 41.649 844.137 42.218C843.725 42.7785 843.196 43.2177 842.552 43.5357C841.909 43.8452 841.137 44 840.237 44C839.585 44 838.986 43.9038 838.442 43.7113C837.897 43.5189 837.426 43.2219 837.03 42.8203C836.642 42.4187 836.341 41.8959 836.126 41.2516C835.912 40.6074 835.804 39.8377 835.804 38.9425V30.1704H838.788V38.9676C838.788 39.4612 838.846 39.8754 838.962 40.21C839.077 40.5363 839.234 40.7999 839.432 41.0007C839.63 41.2014 839.861 41.3437 840.126 41.4273C840.39 41.511 840.67 41.5528 840.968 41.5528C841.818 41.5528 842.486 41.3855 842.973 41.0508C843.469 40.7078 843.819 40.2477 844.026 39.6704C844.24 39.0931 844.348 38.4447 844.348 37.7252Z" fill="#00A09D"/>
<path d="M816.761 32.9313V43.749H813.777V30.1704H816.588L816.761 32.9313ZM816.278 36.4577L815.263 36.4452C815.263 35.5081 815.379 34.6422 815.61 33.8474C815.841 33.0526 816.179 32.3624 816.625 31.7767C817.071 31.1827 817.624 30.7268 818.284 30.4088C818.953 30.0825 819.725 29.9194 820.6 29.9194C821.21 29.9194 821.768 30.0114 822.271 30.1955C822.783 30.3712 823.224 30.6515 823.596 31.0363C823.976 31.4212 824.265 31.9148 824.463 32.5172C824.669 33.1195 824.772 33.8474 824.772 34.7008V43.749H821.788V34.9643C821.788 34.3034 821.689 33.7847 821.491 33.4082C821.301 33.0317 821.025 32.764 820.661 32.605C820.307 32.4377 819.881 32.354 819.386 32.354C818.825 32.354 818.346 32.4628 817.95 32.6803C817.562 32.8978 817.244 33.1948 816.996 33.5713C816.749 33.9478 816.567 34.3829 816.452 34.8765C816.336 35.3701 816.278 35.8972 816.278 36.4577ZM824.586 35.6545L823.187 35.9683C823.187 35.1484 823.299 34.3745 823.522 33.6466C823.753 32.9104 824.087 32.2662 824.525 31.714C824.97 31.1534 825.519 30.7142 826.171 30.3963C826.823 30.0784 827.57 29.9194 828.412 29.9194C829.098 29.9194 829.708 30.0156 830.245 30.208C830.79 30.3921 831.252 30.6849 831.632 31.0865C832.011 31.4881 832.3 32.011 832.498 32.6552C832.697 33.291 832.796 34.0607 832.796 34.9643V43.749H829.799V34.9518C829.799 34.2657 829.7 33.7345 829.502 33.358C829.312 32.9815 829.04 32.7221 828.685 32.5799C828.33 32.4293 827.905 32.354 827.41 32.354C826.947 32.354 826.539 32.4419 826.184 32.6176C825.837 32.7849 825.544 33.0233 825.305 33.3329C825.065 33.6341 824.884 33.9813 824.76 34.3745C824.644 34.7677 824.586 35.1944 824.586 35.6545Z" fill="#00A09D"/>
<path d="M810.508 30.1704V43.749H807.512V30.1704H810.508ZM807.314 26.6063C807.314 26.1462 807.462 25.7655 807.76 25.4643C808.065 25.1548 808.486 25 809.023 25C809.551 25 809.968 25.1548 810.273 25.4643C810.578 25.7655 810.731 26.1462 810.731 26.6063C810.731 27.0581 810.578 27.4346 810.273 27.7358C809.968 28.037 809.551 28.1876 809.023 28.1876C808.486 28.1876 808.065 28.037 807.76 27.7358C807.462 27.4346 807.314 27.0581 807.314 26.6063Z" fill="#00A09D"/>
<path d="M796.839 30.1704L799.389 34.6757L801.99 30.1704H805.271L801.21 36.8342L805.432 43.749H802.151L799.427 39.0555L796.703 43.749H793.409L797.619 36.8342L793.57 30.1704H796.839Z" fill="#00A09D"/>
<path d="M788.444 41.0257V34.5502C788.444 34.0649 788.357 33.6466 788.184 33.2952C788.011 32.9438 787.746 32.6719 787.391 32.4795C787.045 32.2871 786.607 32.1909 786.079 32.1909C785.592 32.1909 785.171 32.2745 784.816 32.4419C784.461 32.6092 784.185 32.8351 783.986 33.1195C783.788 33.404 783.689 33.7261 783.689 34.0858H780.718C780.718 33.5504 780.846 33.0317 781.102 32.5297C781.357 32.0277 781.729 31.5801 782.216 31.1869C782.703 30.7937 783.285 30.4841 783.962 30.2582C784.639 30.0323 785.398 29.9194 786.24 29.9194C787.247 29.9194 788.139 30.0909 788.914 30.4339C789.699 30.777 790.314 31.2957 790.759 31.9901C791.213 32.6761 791.44 33.5378 791.44 34.5753V40.6116C791.44 41.2307 791.482 41.7871 791.564 42.2807C791.655 42.7659 791.783 43.1884 791.948 43.5482V43.749H788.89C788.749 43.4227 788.638 43.0086 788.555 42.5066C788.481 41.9962 788.444 41.5026 788.444 41.0257ZM788.877 35.4914L788.902 37.3613H786.76C786.207 37.3613 785.72 37.4157 785.299 37.5244C784.878 37.6248 784.527 37.7754 784.247 37.9762C783.966 38.177 783.755 38.4196 783.615 38.7041C783.475 38.9885 783.405 39.3106 783.405 39.6704C783.405 40.0301 783.487 40.3606 783.652 40.6618C783.817 40.9546 784.057 41.1847 784.37 41.352C784.692 41.5193 785.08 41.603 785.534 41.603C786.145 41.603 786.677 41.4775 787.131 41.2265C787.594 40.9672 787.957 40.6534 788.221 40.2853C788.485 39.9088 788.626 39.5533 788.642 39.2186L789.608 40.5614C789.509 40.9044 789.34 41.2725 789.1 41.6658C788.861 42.059 788.547 42.4355 788.159 42.7952C787.779 43.1466 787.321 43.4352 786.785 43.6611C786.256 43.887 785.646 44 784.952 44C784.077 44 783.297 43.8243 782.612 43.4729C781.927 43.1131 781.39 42.6321 781.002 42.0297C780.615 41.419 780.421 40.7287 780.421 39.959C780.421 39.2395 780.553 38.6037 780.817 38.0515C781.089 37.4909 781.485 37.0224 782.005 36.6459C782.534 36.2695 783.178 35.985 783.937 35.7926C784.696 35.5918 785.563 35.4914 786.537 35.4914H788.877Z" fill="#00A09D"/>
<path d="M760.263 25.4769H763.036L768.249 39.57L773.45 25.4769H776.223L769.339 43.749H767.135L760.263 25.4769ZM759 25.4769H761.637L762.095 37.675V43.749H759V25.4769ZM774.849 25.4769H777.498V43.749H774.391V37.675L774.849 25.4769Z" fill="#00A09D"/>
<path d="M818.555 354.498L821.893 344.979H824.473L818.871 360.14L818.869 360.145C818.754 360.444 818.607 360.772 818.433 361.125L818.434 361.126C818.251 361.512 818.008 361.876 817.713 362.22L817.714 362.221C817.4 362.603 817.007 362.908 816.543 363.137L816.542 363.136C816.058 363.386 815.491 363.5 814.861 363.5C814.679 363.5 814.463 363.476 814.222 363.437C814.22 363.436 814.218 363.437 814.217 363.437C814.214 363.436 814.211 363.436 814.208 363.436C813.984 363.405 813.792 363.37 813.652 363.328L813.302 363.222L813.279 361.03L813.916 361.202C813.961 361.214 814.052 361.23 814.207 361.244C814.372 361.26 814.463 361.266 814.499 361.266C814.952 361.266 815.296 361.189 815.552 361.063L815.556 361.062C815.824 360.932 816.051 360.737 816.238 360.466L816.241 360.462C816.442 360.178 816.63 359.805 816.798 359.336L816.8 359.33L817.348 357.858L812.38 344.979H814.974L818.555 354.498Z" fill="#827882"/>
<path d="M765.252 340.435C766.505 340.435 767.615 340.669 768.564 341.158C769.508 341.641 770.253 342.302 770.78 343.145L770.969 343.464C771.38 344.219 771.584 345.052 771.584 345.952V346.452H769.146V345.952C769.146 345.386 769.029 344.888 768.8 344.449L768.69 344.258C768.401 343.772 767.979 343.386 767.403 343.102L767.396 343.099C766.838 342.813 766.13 342.657 765.252 342.657C764.365 342.657 763.652 342.793 763.095 343.041L763.093 343.042C762.529 343.289 762.129 343.61 761.864 343.989C761.605 344.365 761.475 344.78 761.475 345.248C761.475 345.579 761.539 345.876 761.661 346.143L761.766 346.331C761.885 346.518 762.053 346.708 762.279 346.898L762.536 347.085C762.816 347.271 763.163 347.457 763.582 347.639C763.998 347.813 764.491 347.985 765.066 348.151L765.67 348.317L765.676 348.318C766.597 348.569 767.422 348.849 768.148 349.16C768.881 349.473 769.513 349.841 770.038 350.267C770.575 350.694 770.992 351.202 771.276 351.79C771.564 352.385 771.7 353.062 771.7 353.808C771.7 354.566 771.536 355.265 771.196 355.891C770.87 356.499 770.408 357.012 769.821 357.43L769.822 357.431C769.246 357.847 768.57 358.158 767.806 358.371L767.807 358.372C767.048 358.586 766.226 358.69 765.346 358.69C764.547 358.69 763.753 358.589 762.966 358.387C762.169 358.182 761.434 357.864 760.764 357.435L760.758 357.431C760.079 356.981 759.531 356.402 759.118 355.698L759.115 355.694C758.698 354.966 758.5 354.115 758.5 353.161V352.661H760.926V353.161C760.926 353.801 761.056 354.304 761.288 354.693C761.536 355.109 761.861 355.444 762.268 355.701L762.433 355.799C762.821 356.019 763.244 356.184 763.702 356.295L764.113 356.376C764.525 356.445 764.936 356.479 765.346 356.479C766.195 356.479 766.905 356.355 767.486 356.123L767.487 356.122C768.082 355.887 768.512 355.571 768.805 355.188L768.806 355.187C769.101 354.802 769.252 354.356 769.252 353.83C769.252 353.425 769.183 353.083 769.059 352.794C768.94 352.52 768.74 352.26 768.436 352.019L768.427 352.012C768.128 351.763 767.702 351.516 767.131 351.282L767.124 351.279C766.559 351.037 765.842 350.794 764.967 350.552H764.965C764.1 350.309 763.313 350.04 762.606 349.747C761.89 349.45 761.264 349.104 760.733 348.705L760.729 348.702C760.194 348.293 759.774 347.808 759.48 347.245L759.479 347.242C759.179 346.66 759.037 345.998 759.037 345.271C759.037 344.551 759.195 343.881 759.517 343.273C759.833 342.675 760.275 342.162 760.836 341.736L760.839 341.733C761.406 341.31 762.064 340.989 762.807 340.767C763.56 340.544 764.377 340.435 765.252 340.435Z" fill="#827882"/>
<path d="M809.506 344.979H812.124V347.109H809.506V354.926C809.506 355.473 809.583 355.81 809.682 355.994C809.795 356.207 809.918 356.307 810.034 356.354C810.22 356.428 810.425 356.468 810.654 356.468C810.844 356.468 811.025 356.457 811.197 356.437C811.381 356.407 811.546 356.379 811.69 356.351L812.258 356.238L812.365 358.367L811.996 358.482C811.786 358.548 811.533 358.594 811.249 358.627C810.954 358.669 810.658 358.69 810.362 358.69C809.765 358.69 809.215 358.583 808.726 358.355L808.719 358.352C808.19 358.096 807.792 357.667 807.518 357.106L807.516 357.104C807.236 356.525 807.115 355.783 807.115 354.914V347.109H804.79V344.979H807.115V341.772H809.506V344.979Z" fill="#827882"/>
<path d="M837.24 340.435C838.493 340.435 839.603 340.669 840.553 341.158C841.496 341.641 842.241 342.302 842.769 343.145L842.957 343.464C843.368 344.219 843.572 345.052 843.572 345.952V346.452H841.135V345.952C841.135 345.386 841.017 344.888 840.788 344.449L840.679 344.258C840.39 343.772 839.967 343.386 839.392 343.102L839.385 343.099C838.826 342.813 838.118 342.657 837.24 342.657C836.353 342.657 835.64 342.793 835.083 343.041L835.081 343.042C834.517 343.289 834.117 343.61 833.853 343.989C833.593 344.365 833.463 344.78 833.463 345.248C833.463 345.579 833.527 345.876 833.649 346.143L833.754 346.331C833.873 346.518 834.041 346.708 834.268 346.898L834.524 347.085C834.804 347.271 835.151 347.457 835.57 347.639C835.986 347.813 836.48 347.985 837.055 348.151L837.658 348.317L837.664 348.318C838.585 348.569 839.41 348.849 840.137 349.16C840.869 349.473 841.501 349.841 842.026 350.267C842.563 350.694 842.98 351.202 843.265 351.79C843.553 352.385 843.688 353.062 843.688 353.808C843.688 354.566 843.524 355.265 843.185 355.891C842.858 356.499 842.396 357.012 841.81 357.43L841.811 357.431C841.234 357.847 840.559 358.158 839.794 358.371L839.795 358.372C839.036 358.586 838.215 358.69 837.334 358.69C836.535 358.69 835.742 358.589 834.954 358.387C834.157 358.182 833.422 357.864 832.752 357.435L832.746 357.431C832.067 356.981 831.519 356.402 831.106 355.698L831.104 355.694C830.686 354.966 830.488 354.115 830.488 353.161V352.661H832.914V353.161C832.914 353.801 833.045 354.304 833.276 354.693C833.524 355.109 833.849 355.444 834.256 355.701L834.421 355.799C834.81 356.019 835.232 356.184 835.69 356.295L836.102 356.376C836.513 356.445 836.924 356.479 837.334 356.479C838.183 356.479 838.893 356.355 839.475 356.123L839.476 356.122C840.071 355.887 840.5 355.571 840.793 355.188L840.794 355.187C841.09 354.802 841.24 354.356 841.24 353.83C841.24 353.425 841.171 353.083 841.047 352.794C840.929 352.52 840.728 352.26 840.424 352.019L840.415 352.012C840.116 351.763 839.69 351.516 839.119 351.282L839.112 351.279C838.548 351.037 837.83 350.794 836.955 350.552H836.953C836.088 350.309 835.302 350.04 834.595 349.747C833.878 349.45 833.252 349.104 832.722 348.705L832.718 348.702C832.183 348.293 831.763 347.808 831.469 347.245L831.467 347.242C831.167 346.66 831.025 345.998 831.025 345.271C831.025 344.551 831.184 343.881 831.505 343.273C831.821 342.675 832.264 342.162 832.824 341.736L832.827 341.733C833.394 341.31 834.053 340.989 834.795 340.767C835.548 340.544 836.365 340.435 837.24 340.435Z" fill="#827882"/>
<path d="M848.632 344.979H851.25V347.109H848.632V354.926C848.632 355.473 848.709 355.81 848.808 355.994C848.921 356.207 849.044 356.307 849.16 356.354C849.346 356.428 849.551 356.468 849.78 356.468C849.97 356.468 850.151 356.457 850.323 356.437C850.507 356.407 850.672 356.379 850.816 356.351L851.384 356.238L851.491 358.367L851.122 358.482C850.912 358.548 850.659 358.594 850.375 358.627C850.08 358.669 849.784 358.69 849.488 358.69C848.891 358.69 848.341 358.583 847.852 358.355L847.845 358.352C847.316 358.096 846.918 357.667 846.644 357.106L846.642 357.104C846.362 356.525 846.241 355.783 846.241 354.914V347.109H843.916V344.979H846.241V341.772H848.632V344.979Z" fill="#827882"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M778.469 344.748C779.427 344.748 780.292 344.907 781.052 345.239L781.336 345.373C781.982 345.706 782.506 346.174 782.896 346.775C783.351 347.475 783.561 348.33 783.562 349.309V355.341C783.562 355.747 783.592 356.171 783.651 356.612C783.718 357.045 783.806 357.381 783.909 357.629L783.947 357.721V358.459H781.604L781.489 358.119C781.41 357.885 781.347 357.617 781.296 357.319C781.266 357.345 781.238 357.374 781.207 357.399L781.202 357.402C780.728 357.785 780.174 358.095 779.547 358.334L779.544 358.335C778.902 358.575 778.186 358.689 777.405 358.689C776.518 358.689 775.718 358.524 775.021 358.176L775.018 358.175C774.337 357.83 773.793 357.353 773.398 356.742L773.394 356.735C773.006 356.112 772.815 355.412 772.815 354.648C772.816 353.959 772.965 353.326 773.274 352.76C773.542 352.264 773.911 351.839 774.373 351.486L774.577 351.34C775.141 350.955 775.805 350.673 776.561 350.484C777.324 350.288 778.169 350.192 779.089 350.192H781.171V349.285C781.171 348.764 781.062 348.343 780.864 348.002C780.669 347.665 780.383 347.401 779.983 347.21C779.586 347.02 779.068 346.913 778.411 346.913C777.816 346.913 777.302 347.017 776.862 347.215L776.859 347.217C776.419 347.411 776.089 347.665 775.851 347.974C775.627 348.266 775.522 348.58 775.521 348.928V349.432L773.119 349.412V348.916C773.119 348.36 773.258 347.826 773.528 347.319C773.797 346.816 774.174 346.373 774.649 345.989C775.129 345.602 775.694 345.302 776.334 345.086C776.992 344.859 777.705 344.748 778.469 344.748ZM779.275 352.254C778.588 352.254 777.984 352.316 777.459 352.437L777.454 352.438C776.937 352.552 776.518 352.717 776.188 352.926L776.183 352.93C775.85 353.133 775.61 353.37 775.446 353.633C775.297 353.885 775.218 354.176 775.218 354.521C775.218 354.875 775.306 355.19 775.479 355.477L775.552 355.583C775.727 355.822 775.96 356.024 776.262 356.186C776.607 356.359 777.035 356.456 777.558 356.456C778.275 356.456 778.884 356.324 779.397 356.078L779.4 356.077C779.942 355.82 780.378 355.489 780.717 355.088C780.904 354.866 781.054 354.635 781.171 354.396V352.254H779.275Z" fill="#827882"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M799.085 344.748C799.947 344.748 800.732 344.901 801.431 345.22L801.43 345.221C802.118 345.527 802.701 345.967 803.171 346.54C803.646 347.103 803.999 347.767 804.235 348.523C804.471 349.271 804.587 350.088 804.587 350.969V352.312H795.581C795.602 352.857 795.692 353.364 795.851 353.835C796.042 354.372 796.308 354.842 796.647 355.246L796.78 355.393C797.095 355.724 797.456 355.986 797.863 356.182C798.32 356.4 798.83 356.514 799.4 356.514C800.002 356.514 800.527 356.419 800.983 356.239L801.176 356.156C801.669 355.918 802.127 355.539 802.546 355L802.851 354.607L803.244 354.909L804.498 355.865L804.23 356.257C803.928 356.7 803.552 357.104 803.106 357.47L803.105 357.469C802.649 357.849 802.11 358.145 801.494 358.361C800.861 358.584 800.14 358.689 799.342 358.689C798.454 358.689 797.626 358.529 796.864 358.199L796.862 358.198C796.115 357.871 795.46 357.409 794.899 356.814L794.896 356.812C794.343 356.215 793.917 355.511 793.615 354.709L793.612 354.703C793.318 353.889 793.174 353.009 793.174 352.065V351.569C793.174 350.558 793.326 349.629 793.638 348.788C793.946 347.957 794.376 347.235 794.93 346.629C795.478 346.029 796.109 345.566 796.819 345.244C797.539 344.914 798.296 344.748 799.085 344.748ZM799.085 346.937C798.565 346.937 798.095 347.044 797.67 347.254L797.669 347.253C797.251 347.463 796.884 347.766 796.569 348.171L796.57 348.172C796.262 348.571 796.015 349.056 795.835 349.636C795.787 349.796 795.747 349.963 795.712 350.135H802.175C802.128 349.631 802.006 349.159 801.808 348.717L801.804 348.71C801.574 348.176 801.232 347.748 800.777 347.418C800.35 347.108 799.797 346.937 799.085 346.937Z" fill="#827882"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M858.312 344.748C859.229 344.748 860.074 344.917 860.839 345.264C861.597 345.6 862.252 346.077 862.799 346.692C863.345 347.307 863.758 348.031 864.043 348.857C864.328 349.684 864.468 350.578 864.468 351.534V351.915C864.468 352.871 864.328 353.766 864.043 354.593C863.759 355.41 863.344 356.128 862.801 356.741L862.802 356.742C862.263 357.358 861.612 357.84 860.855 358.184L860.852 358.186C860.087 358.525 859.246 358.689 858.335 358.689C857.417 358.689 856.572 358.525 855.807 358.186L855.803 358.184C855.047 357.84 854.393 357.359 853.848 356.745C853.303 356.131 852.888 355.412 852.604 354.594C852.319 353.767 852.179 352.871 852.179 351.915V351.534C852.179 350.578 852.319 349.684 852.604 348.857C852.888 348.033 853.296 347.309 853.833 346.695L853.836 346.692C854.314 346.154 854.875 345.721 855.516 345.396L855.795 345.264C856.559 344.917 857.401 344.748 858.312 344.748ZM858.312 346.937C857.695 346.937 857.168 347.061 856.72 347.295L856.719 347.294C856.263 347.536 855.875 347.866 855.553 348.286C855.233 348.713 854.987 349.201 854.816 349.755C854.653 350.312 854.569 350.904 854.569 351.534V351.915C854.569 352.553 854.657 353.153 854.828 353.718L854.896 353.923C855.061 354.394 855.285 354.814 855.565 355.188L855.686 355.337C855.975 355.675 856.321 355.946 856.727 356.152C857.175 356.38 857.707 356.502 858.335 356.502C858.954 356.502 859.477 356.38 859.917 356.153L859.92 356.152C860.383 355.917 860.769 355.596 861.081 355.188C861.4 354.762 861.643 354.275 861.806 353.723L861.807 353.719C861.978 353.154 862.065 352.553 862.065 351.915V351.534C862.065 350.904 861.978 350.312 861.807 349.755L861.806 349.75C861.643 349.198 861.397 348.711 861.07 348.286L861.067 348.282C860.754 347.865 860.367 347.537 859.903 347.295C859.455 347.061 858.928 346.937 858.312 346.937Z" fill="#827882"/>
<path d="M871.518 344.748C872.488 344.748 873.377 344.943 874.171 345.343L874.465 345.501C875.132 345.888 875.679 346.407 876.098 347.053C876.589 347.799 876.848 348.673 876.891 349.656L876.912 350.178H874.587L874.56 349.706C874.525 349.111 874.367 348.623 874.104 348.223C873.833 347.812 873.479 347.498 873.035 347.275C872.589 347.052 872.086 346.937 871.518 346.937C870.865 346.937 870.34 347.062 869.923 347.29L869.917 347.293C869.484 347.523 869.133 347.839 868.859 348.246C868.583 348.662 868.376 349.146 868.24 349.703V349.705C868.103 350.263 868.032 350.853 868.032 351.477V351.961C868.032 352.592 868.098 353.19 868.228 353.756C868.363 354.303 868.57 354.783 868.846 355.198L868.952 355.345C869.209 355.679 869.528 355.95 869.911 356.159C870.337 356.38 870.87 356.502 871.529 356.502C872.058 356.502 872.544 356.406 872.992 356.219C873.439 356.028 873.798 355.752 874.079 355.392L874.08 355.391C874.354 355.041 874.519 354.604 874.56 354.059L874.594 353.595H876.916L876.89 354.12C876.846 355.005 876.568 355.805 876.059 356.509L876.06 356.51C875.564 357.196 874.915 357.733 874.121 358.12L874.119 358.121C873.329 358.503 872.463 358.689 871.529 358.689C870.589 358.689 869.735 358.521 868.98 358.17L868.979 358.169C868.242 357.822 867.62 357.338 867.119 356.716L867.117 356.713C866.685 356.169 866.348 355.552 866.103 354.865L866.001 354.561C865.76 353.749 865.642 352.881 865.642 351.961V351.477C865.642 350.557 865.761 349.693 866.002 348.888L866.003 348.885C866.253 348.069 866.624 347.35 867.119 346.734C867.619 346.105 868.241 345.616 868.979 345.269C869.726 344.917 870.577 344.748 871.518 344.748Z" fill="#827882"/>
<path d="M791.547 339.5C791.804 339.5 792.068 339.517 792.338 339.549L792.339 339.548C792.632 339.574 792.917 339.632 793.194 339.72L793.576 339.842L793.436 341.406L793.381 342.003L792.804 341.843C792.641 341.798 792.474 341.769 792.304 341.756L792.282 341.754C792.112 341.734 791.903 341.723 791.651 341.723C791.217 341.723 790.871 341.814 790.596 341.977L790.59 341.98C790.317 342.135 790.104 342.362 789.951 342.676C789.798 342.989 789.709 343.402 789.709 343.934V344.979H792.725V347.109H789.709V358.459H787.318V347.109H785.227V344.979H787.318V343.934C787.318 343.025 787.476 342.223 787.816 341.551L787.819 341.546C788.167 340.877 788.665 340.361 789.31 340.011L789.312 340.009C789.96 339.663 790.711 339.5 791.547 339.5Z" fill="#827882"/>
<path d="M881.098 350.306L881.554 349.82L881.562 349.811L881.571 349.803L886.566 344.979H889.758L883.84 350.773L890.047 358.459H887.08L886.93 358.275L882.149 352.409L881.098 353.413V358.459H878.707V339.742H881.098V350.306Z" fill="#827882"/>
<path d="M847.836 392.931V403.749H844.827V390.17H847.661L847.836 392.931ZM847.349 396.458L846.325 396.445C846.325 395.508 846.442 394.642 846.675 393.847C846.908 393.053 847.249 392.362 847.698 391.777C848.148 391.183 848.705 390.727 849.371 390.409C850.045 390.083 850.823 389.919 851.705 389.919C852.321 389.919 852.883 390.011 853.39 390.196C853.906 390.371 854.351 390.651 854.726 391.036C855.109 391.421 855.4 391.915 855.6 392.517C855.808 393.12 855.912 393.847 855.912 394.701V403.749H852.903V394.964C852.903 394.303 852.803 393.785 852.604 393.408C852.412 393.032 852.134 392.764 851.767 392.605C851.41 392.438 850.981 392.354 850.482 392.354C849.916 392.354 849.433 392.463 849.034 392.68C848.643 392.898 848.322 393.195 848.073 393.571C847.823 393.948 847.64 394.383 847.524 394.876C847.407 395.37 847.349 395.897 847.349 396.458ZM855.724 395.655L854.314 395.968C854.314 395.148 854.426 394.375 854.651 393.647C854.884 392.91 855.221 392.266 855.662 391.714C856.111 391.153 856.665 390.714 857.322 390.396C857.979 390.078 858.733 389.919 859.581 389.919C860.272 389.919 860.888 390.016 861.429 390.208C861.978 390.392 862.444 390.685 862.827 391.087C863.209 391.488 863.501 392.011 863.7 392.655C863.9 393.291 864 394.061 864 394.964V403.749H860.979V394.952C860.979 394.266 860.879 393.734 860.68 393.358C860.488 392.982 860.214 392.722 859.856 392.58C859.498 392.429 859.07 392.354 858.57 392.354C858.104 392.354 857.692 392.442 857.335 392.618C856.985 392.785 856.69 393.023 856.448 393.333C856.207 393.634 856.024 393.981 855.899 394.375C855.783 394.768 855.724 395.194 855.724 395.655Z" fill="#00A09D"/>
<path d="M838.724 400.549V390.17H841.744V403.749H838.898L838.724 400.549ZM839.148 397.725L840.159 397.7C840.159 398.612 840.059 399.453 839.859 400.223C839.66 400.984 839.352 401.649 838.936 402.218C838.52 402.779 837.987 403.218 837.338 403.536C836.689 403.845 835.911 404 835.004 404C834.346 404 833.743 403.904 833.194 403.711C832.645 403.519 832.17 403.222 831.771 402.82C831.38 402.419 831.076 401.896 830.86 401.252C830.643 400.607 830.535 399.838 830.535 398.943V390.17H833.543V398.968C833.543 399.461 833.602 399.875 833.718 400.21C833.835 400.536 833.993 400.8 834.193 401.001C834.392 401.201 834.625 401.344 834.892 401.427C835.158 401.511 835.441 401.553 835.74 401.553C836.597 401.553 837.271 401.386 837.762 401.051C838.262 400.708 838.615 400.248 838.823 399.67C839.04 399.093 839.148 398.445 839.148 397.725Z" fill="#00A09D"/>
<path d="M811.338 392.931V403.749H808.33V390.17H811.163L811.338 392.931ZM810.851 396.458L809.827 396.445C809.827 395.508 809.944 394.642 810.177 393.847C810.41 393.053 810.751 392.362 811.2 391.777C811.65 391.183 812.207 390.727 812.873 390.409C813.547 390.083 814.325 389.919 815.207 389.919C815.823 389.919 816.385 390.011 816.892 390.196C817.408 390.371 817.853 390.651 818.228 391.036C818.611 391.421 818.902 391.915 819.102 392.517C819.31 393.12 819.414 393.847 819.414 394.701V403.749H816.405V394.964C816.405 394.303 816.306 393.785 816.106 393.408C815.915 393.032 815.636 392.764 815.27 392.605C814.912 392.438 814.483 392.354 813.984 392.354C813.418 392.354 812.935 392.463 812.536 392.68C812.145 392.898 811.825 393.195 811.575 393.571C811.325 393.948 811.142 394.383 811.026 394.876C810.909 395.37 810.851 395.897 810.851 396.458ZM819.226 395.655L817.816 395.968C817.816 395.148 817.928 394.375 818.153 393.647C818.386 392.91 818.723 392.266 819.164 391.714C819.613 391.153 820.167 390.714 820.824 390.396C821.482 390.078 822.235 389.919 823.083 389.919C823.774 389.919 824.39 390.016 824.931 390.208C825.48 390.392 825.946 390.685 826.329 391.087C826.712 391.488 827.003 392.011 827.203 392.655C827.402 393.291 827.502 394.061 827.502 394.964V403.749H824.481V394.952C824.481 394.266 824.382 393.734 824.182 393.358C823.99 392.982 823.716 392.722 823.358 392.58C823 392.429 822.572 392.354 822.072 392.354C821.606 392.354 821.194 392.442 820.837 392.618C820.487 392.785 820.192 393.023 819.95 393.333C819.709 393.634 819.526 393.981 819.401 394.375C819.285 394.768 819.226 395.194 819.226 395.655Z" fill="#00A09D"/>
<path d="M805.034 390.17V403.749H802.014V390.17H805.034ZM801.814 386.606C801.814 386.146 801.964 385.766 802.263 385.464C802.571 385.155 802.995 385 803.536 385C804.069 385 804.489 385.155 804.797 385.464C805.105 385.766 805.259 386.146 805.259 386.606C805.259 387.058 805.105 387.435 804.797 387.736C804.489 388.037 804.069 388.188 803.536 388.188C802.995 388.188 802.571 388.037 802.263 387.736C801.964 387.435 801.814 387.058 801.814 386.606Z" fill="#00A09D"/>
<path d="M790.567 393.069V403.749H787.559V390.17H790.393L790.567 393.069ZM790.031 396.458L789.057 396.445C789.065 395.483 789.199 394.6 789.456 393.797C789.723 392.994 790.089 392.304 790.555 391.727C791.029 391.149 791.595 390.706 792.252 390.396C792.91 390.078 793.642 389.919 794.449 389.919C795.098 389.919 795.685 390.011 796.209 390.196C796.742 390.371 797.195 390.66 797.57 391.061C797.953 391.463 798.244 391.986 798.444 392.63C798.643 393.266 798.743 394.048 798.743 394.977V403.749H795.723V394.964C795.723 394.312 795.627 393.797 795.435 393.421C795.252 393.036 794.982 392.764 794.624 392.605C794.275 392.438 793.838 392.354 793.313 392.354C792.798 392.354 792.336 392.463 791.928 392.68C791.52 392.898 791.175 393.195 790.892 393.571C790.617 393.948 790.405 394.383 790.255 394.876C790.106 395.37 790.031 395.897 790.031 396.458Z" fill="#00A09D"/>
<path d="M784.289 390.17V403.749H781.268V390.17H784.289ZM781.068 386.606C781.068 386.146 781.218 385.766 781.518 385.464C781.826 385.155 782.25 385 782.791 385C783.324 385 783.744 385.155 784.052 385.464C784.36 385.766 784.514 386.146 784.514 386.606C784.514 387.058 784.36 387.435 784.052 387.736C783.744 388.037 783.324 388.188 782.791 388.188C782.25 388.188 781.826 388.037 781.518 387.736C781.218 387.435 781.068 387.058 781.068 386.606Z" fill="#00A09D"/>
<path d="M760.273 385.477H763.069L768.324 399.57L773.567 385.477H776.363L769.423 403.749H767.201L760.273 385.477ZM759 385.477H761.659L762.121 397.675V403.749H759V385.477ZM774.977 385.477H777.648V403.749H774.515V397.675L774.977 385.477Z" fill="#00A09D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M756.37 497.357C757.436 497.357 758.404 497.549 759.261 497.945C760.108 498.327 760.822 498.875 761.397 499.588C761.981 500.288 762.416 501.115 762.707 502.06L762.809 502.414C763.03 503.25 763.14 504.154 763.14 505.123V506.692H751.849C751.865 507.455 751.983 508.162 752.201 508.817C752.446 509.512 752.786 510.12 753.222 510.645L753.392 510.836C753.797 511.269 754.262 511.611 754.787 511.865C755.377 512.152 756.034 512.299 756.766 512.299C757.642 512.299 758.397 512.139 759.042 511.834C759.681 511.522 760.272 511.027 760.808 510.327L761.112 509.928L762.979 511.371L762.718 511.76C762.348 512.31 761.885 512.811 761.336 513.268C760.776 513.741 760.112 514.111 759.351 514.383C758.572 514.66 757.683 514.793 756.692 514.793C755.595 514.793 754.574 514.591 753.636 514.18L753.634 514.179C752.712 513.77 751.904 513.194 751.214 512.451L751.211 512.448C750.529 511.704 750.004 510.826 749.632 509.823L749.63 509.817C749.267 508.801 749.088 507.698 749.088 506.515V505.885C749.088 504.615 749.276 503.452 749.66 502.402C750.041 501.362 750.571 500.461 751.253 499.705C751.846 499.048 752.515 498.523 753.26 498.133L753.584 497.974C754.469 497.563 755.399 497.357 756.37 497.357ZM756.37 499.866C755.701 499.866 755.094 500.007 754.543 500.282L754.542 500.281C754 500.557 753.525 500.956 753.119 501.485L753.12 501.486C752.722 502.008 752.405 502.641 752.176 503.394C752.1 503.65 752.037 503.919 751.987 504.198H760.383C760.334 503.488 760.172 502.825 759.897 502.205L759.895 502.198C759.596 501.497 759.154 500.934 758.564 500.501C758.003 500.088 757.282 499.866 756.37 499.866Z" fill="#827882"/>
<path d="M714.668 494.725H707.358V514.5H704.571V494.725H697.276V492.172H714.668V494.725Z" fill="#827882"/>
<path d="M719.678 514.5H716.935V497.65H719.678V514.5Z" fill="#827882"/>
<path d="M740.916 497.357C741.779 497.357 742.565 497.476 743.271 497.721C743.988 497.96 744.601 498.349 745.1 498.889C745.61 499.422 745.988 500.092 746.241 500.884C746.496 501.679 746.616 502.612 746.616 503.673V514.5H743.858V503.658C743.858 502.573 743.696 501.799 743.42 501.288L743.416 501.28C743.14 500.745 742.773 500.4 742.317 500.207C741.816 499.995 741.213 499.881 740.495 499.881C739.793 499.89 739.198 500.021 738.699 500.258L738.692 500.261C738.239 500.468 737.857 500.732 737.54 501.052L737.408 501.192C737.064 501.58 736.802 501.994 736.62 502.434C736.468 502.823 736.381 503.199 736.348 503.562V514.5H733.604V503.512C733.604 502.5 733.444 501.771 733.17 501.279L733.167 501.274C732.895 500.771 732.522 500.427 732.045 500.221L732.043 500.22C731.542 499.999 730.944 499.881 730.237 499.881C729.408 499.881 728.738 500.052 728.207 500.367L728.205 500.368C727.654 500.692 727.207 501.114 726.859 501.636C726.539 502.121 726.294 502.64 726.123 503.193V514.5H723.365V497.65H726.039L726.075 499.284C726.613 498.718 727.243 498.267 727.96 497.932C728.788 497.545 729.716 497.357 730.735 497.357C731.549 497.357 732.301 497.471 732.983 497.704C733.687 497.931 734.293 498.297 734.791 498.804C735.108 499.114 735.374 499.478 735.595 499.893C735.753 499.69 735.924 499.495 736.106 499.309C736.691 498.704 737.384 498.228 738.183 497.882L738.185 497.881C739.007 497.528 739.921 497.357 740.916 497.357Z" fill="#827882"/>
<path d="M718.313 491.879C718.792 491.879 719.221 492.034 719.545 492.378L719.546 492.377C719.549 492.38 719.552 492.384 719.556 492.388C719.557 492.389 719.559 492.391 719.561 492.393C719.874 492.713 720.029 493.114 720.029 493.565C720.029 493.961 719.905 494.318 719.664 494.62L719.554 494.746C719.226 495.088 718.792 495.237 718.313 495.237C717.835 495.237 717.4 495.088 717.073 494.746L717.065 494.738L717.059 494.729C716.771 494.401 716.627 494.004 716.627 493.565C716.627 493.121 716.768 492.719 717.065 492.394L717.066 492.393C717.392 492.038 717.828 491.879 718.313 491.879Z" fill="#827882"/>
<path d="M646.781 59.1875L660 59.1875L660 63.6875L643.094 63.6875L643.094 59.4531L646.781 59.1875ZM642.984 54.0937L647.156 54.1719C647.125 54.3906 647.099 54.6562 647.078 54.9687C647.047 55.2708 647.031 55.5469 647.031 55.7969C647.031 56.4323 647.115 56.9844 647.281 57.4531C647.437 57.9115 647.672 58.2969 647.984 58.6094C648.297 58.9115 648.677 59.1406 649.125 59.2969C649.573 59.4427 650.083 59.526 650.656 59.5469L650.375 60.4531C649.281 60.4531 648.276 60.3437 647.359 60.125C646.432 59.9062 645.625 59.5885 644.937 59.1719C644.25 58.7448 643.719 58.224 643.344 57.6094C642.969 56.9948 642.781 56.2917 642.781 55.5C642.781 55.25 642.802 54.9948 642.844 54.7344C642.875 54.474 642.922 54.2604 642.984 54.0937Z" fill="#00A09D"/>
<path d="M660.312 73.1875C660.312 74.5 660.104 75.6771 659.687 76.7187C659.26 77.7604 658.672 78.6458 657.922 79.375C657.172 80.0937 656.302 80.6458 655.312 81.0312C654.312 81.4167 653.25 81.6094 652.125 81.6094L651.5 81.6094C650.219 81.6094 649.047 81.4271 647.984 81.0625C646.922 80.6979 646 80.1771 645.219 79.5C644.437 78.8125 643.839 77.9792 643.422 77C642.995 76.0208 642.781 74.9167 642.781 73.6875C642.781 72.4896 642.979 71.4271 643.375 70.5C643.771 69.5729 644.333 68.7969 645.062 68.1719C645.792 67.5365 646.667 67.0573 647.687 66.7344C648.698 66.4115 649.823 66.25 651.062 66.25L652.937 66.25L652.937 79.6875L649.937 79.6875L649.937 70.6719L649.594 70.6719C648.969 70.6719 648.411 70.7865 647.922 71.0156C647.422 71.2344 647.026 71.5677 646.734 72.0156C646.443 72.4635 646.297 73.0365 646.297 73.7344C646.297 74.3281 646.427 74.8385 646.687 75.2656C646.948 75.6927 647.312 76.0417 647.781 76.3125C648.25 76.5729 648.802 76.7708 649.437 76.9062C650.062 77.0312 650.75 77.0937 651.5 77.0937L652.125 77.0937C652.802 77.0937 653.427 77 654 76.8125C654.573 76.6146 655.068 76.3385 655.484 75.9844C655.901 75.6198 656.224 75.1823 656.453 74.6719C656.682 74.151 656.797 73.5625 656.797 72.9062C656.797 72.0937 656.641 71.3385 656.328 70.6406C656.005 69.9323 655.521 69.3229 654.875 68.8125L657.25 66.625C657.76 66.9792 658.25 67.4635 658.719 68.0781C659.187 68.6823 659.573 69.4115 659.875 70.2656C660.167 71.1198 660.312 72.0937 660.312 73.1875Z" fill="#00A09D"/>
<path d="M656.312 89.0156L636 89.0156L636 84.4844L660 84.4844L660 88.5625L656.312 89.0156ZM651.75 99.7031L651.422 99.7031C650.13 99.7031 648.958 99.5573 647.906 99.2656C646.844 98.974 645.932 98.5469 645.172 97.9844C644.411 97.4219 643.823 96.7292 643.406 95.9062C642.99 95.0833 642.781 94.1458 642.781 93.0937C642.781 92.1042 642.99 91.2396 643.406 90.5C643.823 89.75 644.417 89.1146 645.187 88.5937C645.948 88.0625 646.849 87.6354 647.891 87.3125C648.922 86.9896 650.052 86.7552 651.281 86.6094L652 86.6094C653.177 86.7552 654.271 86.9896 655.281 87.3125C656.292 87.6354 657.177 88.0625 657.937 88.5937C658.687 89.1146 659.271 89.75 659.687 90.5C660.104 91.25 660.312 92.125 660.312 93.125C660.312 94.1771 660.099 95.1146 659.672 95.9375C659.245 96.75 658.646 97.4375 657.875 98C657.104 98.5521 656.198 98.974 655.156 99.2656C654.115 99.5573 652.979 99.7031 651.75 99.7031ZM651.422 95.2031L651.75 95.2031C652.448 95.2031 653.099 95.151 653.703 95.0469C654.307 94.9323 654.844 94.75 655.312 94.5C655.771 94.2396 656.13 93.9062 656.391 93.5C656.641 93.0833 656.766 92.5781 656.766 91.9844C656.766 91.2135 656.594 90.5781 656.25 90.0781C655.896 89.5781 655.411 89.1979 654.797 88.9375C654.182 88.6667 653.474 88.5104 652.672 88.4687L650.625 88.4687C649.969 88.5 649.38 88.5937 648.859 88.75C648.328 88.8958 647.875 89.1146 647.5 89.4062C647.125 89.6875 646.833 90.0417 646.625 90.4687C646.417 90.8854 646.312 91.3802 646.312 91.9531C646.312 92.5365 646.448 93.0365 646.719 93.4531C646.979 93.8698 647.339 94.2083 647.797 94.4687C648.255 94.7187 648.797 94.9062 649.422 95.0312C650.036 95.1458 650.703 95.2031 651.422 95.2031Z" fill="#00A09D"/>
<path d="M646.781 105.906L660 105.906L660 110.406L643.094 110.406L643.094 106.172L646.781 105.906ZM642.984 100.812L647.156 100.891C647.125 101.109 647.099 101.375 647.078 101.687C647.047 101.99 647.031 102.266 647.031 102.516C647.031 103.151 647.115 103.703 647.281 104.172C647.437 104.63 647.672 105.016 647.984 105.328C648.297 105.63 648.677 105.859 649.125 106.016C649.573 106.161 650.083 106.245 650.656 106.266L650.375 107.172C649.281 107.172 648.276 107.062 647.359 106.844C646.432 106.625 645.625 106.307 644.937 105.891C644.25 105.464 643.719 104.943 643.344 104.328C642.969 103.714 642.781 103.01 642.781 102.219C642.781 101.969 642.802 101.714 642.844 101.453C642.875 101.193 642.922 100.979 642.984 100.812Z" fill="#00A09D"/>
<path d="M651.719 129.187L651.391 129.187C650.151 129.187 649.01 129.01 647.969 128.656C646.917 128.302 646.005 127.786 645.234 127.109C644.464 126.432 643.865 125.599 643.437 124.609C643 123.62 642.781 122.484 642.781 121.203C642.781 119.922 643 118.781 643.437 117.781C643.865 116.781 644.464 115.943 645.234 115.266C646.005 114.578 646.917 114.057 647.969 113.703C649.01 113.349 650.151 113.172 651.391 113.172L651.719 113.172C652.948 113.172 654.089 113.349 655.141 113.703C656.182 114.057 657.094 114.578 657.875 115.266C658.646 115.943 659.245 116.776 659.672 117.766C660.099 118.755 660.312 119.891 660.312 121.172C660.312 122.453 660.099 123.594 659.672 124.594C659.245 125.583 658.646 126.422 657.875 127.109C657.094 127.786 656.182 128.302 655.141 128.656C654.089 129.01 652.948 129.187 651.719 129.187ZM651.391 124.687L651.719 124.687C652.427 124.687 653.089 124.625 653.703 124.5C654.318 124.375 654.859 124.177 655.328 123.906C655.786 123.625 656.146 123.26 656.406 122.812C656.667 122.365 656.797 121.818 656.797 121.172C656.797 120.547 656.667 120.01 656.406 119.562C656.146 119.115 655.786 118.755 655.328 118.484C654.859 118.214 654.318 118.016 653.703 117.891C653.089 117.755 652.427 117.687 651.719 117.687L651.391 117.687C650.703 117.687 650.057 117.755 649.453 117.891C648.839 118.016 648.297 118.219 647.828 118.5C647.349 118.771 646.974 119.13 646.703 119.578C646.432 120.026 646.297 120.568 646.297 121.203C646.297 121.839 646.432 122.38 646.703 122.828C646.974 123.266 647.349 123.625 647.828 123.906C648.297 124.177 648.839 124.375 649.453 124.5C650.057 124.625 650.703 124.687 651.391 124.687Z" fill="#00A09D"/>
<path d="M651.719 155.219L651.391 155.219C650.151 155.219 649.01 155.042 647.969 154.687C646.917 154.333 646.005 153.818 645.234 153.141C644.464 152.464 643.865 151.63 643.437 150.641C643 149.651 642.781 148.516 642.781 147.234C642.781 145.953 643 144.812 643.437 143.812C643.865 142.812 644.464 141.974 645.234 141.297C646.005 140.609 646.917 140.089 647.969 139.734C649.01 139.38 650.151 139.203 651.391 139.203L651.719 139.203C652.948 139.203 654.089 139.38 655.141 139.734C656.182 140.089 657.094 140.609 657.875 141.297C658.646 141.974 659.245 142.807 659.672 143.797C660.099 144.786 660.312 145.922 660.312 147.203C660.312 148.484 660.099 149.625 659.672 150.625C659.245 151.615 658.646 152.453 657.875 153.141C657.094 153.818 656.182 154.333 655.141 154.687C654.089 155.042 652.948 155.219 651.719 155.219ZM651.391 150.719L651.719 150.719C652.427 150.719 653.089 150.656 653.703 150.531C654.318 150.406 654.859 150.208 655.328 149.937C655.786 149.656 656.146 149.292 656.406 148.844C656.667 148.396 656.797 147.849 656.797 147.203C656.797 146.578 656.667 146.042 656.406 145.594C656.146 145.146 655.786 144.786 655.328 144.516C654.859 144.245 654.318 144.047 653.703 143.922C653.089 143.786 652.427 143.719 651.719 143.719L651.391 143.719C650.703 143.719 650.057 143.786 649.453 143.922C648.839 144.047 648.297 144.25 647.828 144.531C647.349 144.802 646.974 145.161 646.703 145.609C646.432 146.057 646.297 146.599 646.297 147.234C646.297 147.87 646.432 148.411 646.703 148.859C646.974 149.297 647.349 149.656 647.828 149.937C648.297 150.208 648.839 150.406 649.453 150.531C650.057 150.656 650.703 150.719 651.391 150.719Z" fill="#00A09D"/>
<path d="M643.094 156.656L646.281 156.656L646.281 166.5L643.094 166.5L643.094 156.656ZM638.922 164.062L638.922 159.562L654.906 159.562C655.396 159.562 655.771 159.5 656.031 159.375C656.292 159.24 656.474 159.042 656.578 158.781C656.672 158.521 656.719 158.193 656.719 157.797C656.719 157.516 656.708 157.266 656.687 157.047C656.656 156.818 656.625 156.625 656.594 156.469L659.906 156.453C660.031 156.839 660.13 157.255 660.203 157.703C660.276 158.151 660.312 158.646 660.312 159.187C660.312 160.177 660.151 161.042 659.828 161.781C659.495 162.51 658.964 163.073 658.234 163.469C657.505 163.865 656.547 164.062 655.359 164.062L638.922 164.062Z" fill="#00A09D"/>
<path d="M658.094 184.016L643.094 179.531L643.094 174.703L662.547 181.5C662.974 181.646 663.432 181.844 663.922 182.094C664.411 182.333 664.875 182.661 665.312 183.078C665.76 183.484 666.125 184 666.406 184.625C666.687 185.24 666.828 185.995 666.828 186.891C666.828 187.318 666.802 187.667 666.75 187.937C666.698 188.208 666.625 188.531 666.531 188.906L663.234 188.906C663.234 188.792 663.234 188.672 663.234 188.547C663.245 188.422 663.25 188.302 663.25 188.187C663.25 187.594 663.182 187.109 663.047 186.734C662.911 186.359 662.703 186.057 662.422 185.828C662.151 185.599 661.797 185.417 661.359 185.281L658.094 184.016ZM643.094 185.891L655.344 182.219L660.109 181.578L660.437 184.641L643.094 190.719L643.094 185.891Z" fill="#00A09D"/>
<path d="M643.094 191.594L646.281 191.594L646.281 201.437L643.094 201.437L643.094 191.594ZM638.922 199L638.922 194.5L654.906 194.5C655.396 194.5 655.771 194.437 656.031 194.312C656.292 194.177 656.474 193.979 656.578 193.719C656.672 193.458 656.719 193.13 656.719 192.734C656.719 192.453 656.708 192.203 656.687 191.984C656.656 191.755 656.625 191.562 656.594 191.406L659.906 191.391C660.031 191.776 660.13 192.193 660.203 192.641C660.276 193.089 660.312 193.583 660.312 194.125C660.312 195.115 660.151 195.979 659.828 196.719C659.495 197.448 658.964 198.01 658.234 198.406C657.505 198.802 656.547 199 655.359 199L638.922 199Z" fill="#00A09D"/>
<path d="M643.094 203.594L660 203.594L660 208.109L643.094 208.109L643.094 203.594ZM638.688 208.391C638.031 208.391 637.49 208.161 637.062 207.703C636.635 207.245 636.422 206.63 636.422 205.859C636.422 205.099 636.635 204.49 637.062 204.031C637.49 203.562 638.031 203.328 638.687 203.328C639.344 203.328 639.885 203.562 640.312 204.031C640.74 204.49 640.953 205.099 640.953 205.859C640.953 206.63 640.74 207.245 640.312 207.703C639.885 208.161 639.344 208.391 638.688 208.391Z" fill="#00A09D"/>
<path d="M643.094 210.906L646.281 210.906L646.281 220.75L643.094 220.75L643.094 210.906ZM638.922 218.312L638.922 213.812L654.906 213.812C655.396 213.812 655.771 213.75 656.031 213.625C656.292 213.49 656.474 213.292 656.578 213.031C656.672 212.771 656.719 212.443 656.719 212.047C656.719 211.766 656.708 211.516 656.687 211.297C656.656 211.068 656.625 210.875 656.594 210.719L659.906 210.703C660.031 211.089 660.13 211.505 660.203 211.953C660.276 212.401 660.312 212.896 660.312 213.437C660.312 214.427 660.151 215.292 659.828 216.031C659.495 216.76 658.964 217.323 658.234 217.719C657.505 218.115 656.547 218.312 655.359 218.312L638.922 218.312Z" fill="#00A09D"/>
<path d="M646.703 232.719L660 232.719L660 237.219L643.094 237.219L643.094 233L646.703 232.719ZM650.953 233.375L650.953 234.594C649.703 234.594 648.578 234.432 647.578 234.109C646.568 233.786 645.708 233.333 645 232.75C644.281 232.167 643.734 231.474 643.359 230.672C642.974 229.859 642.781 228.953 642.781 227.953C642.781 227.161 642.896 226.437 643.125 225.781C643.354 225.125 643.719 224.562 644.219 224.094C644.719 223.615 645.38 223.25 646.203 223C647.026 222.74 648.031 222.609 649.219 222.609L660 222.609L660 227.141L649.203 227.141C648.453 227.141 647.87 227.245 647.453 227.453C647.036 227.661 646.745 227.969 646.578 228.375C646.401 228.771 646.312 229.26 646.312 229.844C646.312 230.448 646.432 230.974 646.672 231.422C646.911 231.859 647.245 232.224 647.672 232.516C648.089 232.797 648.578 233.01 649.141 233.156C649.703 233.302 650.307 233.375 650.953 233.375Z" fill="#00A09D"/>
<path d="M656.187 245.125L648.656 245.125C648.115 245.125 647.651 245.214 647.266 245.391C646.87 245.568 646.562 245.844 646.344 246.219C646.125 246.583 646.016 247.057 646.016 247.641C646.016 248.141 646.104 248.573 646.281 248.937C646.448 249.302 646.693 249.583 647.016 249.781C647.328 249.979 647.698 250.078 648.125 250.078L648.125 254.578C647.406 254.578 646.724 254.411 646.078 254.078C645.432 253.745 644.865 253.26 644.375 252.625C643.875 251.99 643.484 251.234 643.203 250.359C642.922 249.474 642.781 248.484 642.781 247.391C642.781 246.078 643 244.911 643.437 243.891C643.875 242.87 644.531 242.068 645.406 241.484C646.281 240.891 647.375 240.594 648.687 240.594L655.922 240.594C656.849 240.594 657.609 240.536 658.203 240.422C658.786 240.307 659.297 240.141 659.734 239.922L660 239.922L660 244.469C659.542 244.687 658.969 244.854 658.281 244.969C657.583 245.073 656.885 245.125 656.187 245.125ZM649.703 244.531L652.25 244.5L652.25 247.016C652.25 247.609 652.318 248.125 652.453 248.562C652.589 249 652.781 249.359 653.031 249.641C653.271 249.922 653.552 250.13 653.875 250.266C654.198 250.391 654.552 250.453 654.937 250.453C655.323 250.453 655.672 250.365 655.984 250.187C656.286 250.01 656.526 249.755 656.703 249.422C656.87 249.089 656.953 248.698 656.953 248.25C656.953 247.573 656.818 246.984 656.547 246.484C656.276 245.984 655.943 245.599 655.547 245.328C655.151 245.047 654.776 244.901 654.422 244.891L656.328 243.703C656.755 243.87 657.198 244.099 657.656 244.391C658.115 244.672 658.547 245.031 658.953 245.469C659.349 245.906 659.677 246.432 659.937 247.047C660.187 247.661 660.312 248.391 660.312 249.234C660.312 250.307 660.099 251.281 659.672 252.156C659.234 253.021 658.635 253.708 657.875 254.219C657.104 254.719 656.229 254.969 655.25 254.969C654.365 254.969 653.578 254.802 652.891 254.469C652.203 254.135 651.625 253.646 651.156 253C650.677 252.344 650.318 251.526 650.078 250.547C649.828 249.568 649.703 248.432 649.703 247.141L649.703 244.531Z" fill="#00A09D"/>
<path d="M655.953 262.187L643.094 262.188L643.094 257.688L660 257.687L660 261.922L655.953 262.187ZM652.484 261.687L652.453 260.359C653.578 260.359 654.625 260.49 655.594 260.75C656.552 261.01 657.385 261.401 658.094 261.922C658.792 262.443 659.339 263.099 659.734 263.891C660.12 264.682 660.312 265.615 660.312 266.687C660.312 267.51 660.198 268.271 659.969 268.969C659.729 269.656 659.359 270.25 658.859 270.75C658.349 271.24 657.698 271.625 656.906 271.906C656.104 272.177 655.141 272.312 654.016 272.312L643.094 272.312L643.094 267.812L654.047 267.812C654.547 267.812 654.969 267.755 655.312 267.641C655.656 267.516 655.937 267.344 656.156 267.125C656.375 266.906 656.531 266.651 656.625 266.359C656.719 266.057 656.766 265.724 656.766 265.359C656.766 264.432 656.578 263.703 656.203 263.172C655.828 262.63 655.318 262.25 654.672 262.031C654.016 261.802 653.286 261.687 652.484 261.687Z" fill="#00A09D"/>
<path d="M656.625 281.609L661.5 275.453L664.062 278.406L659.187 284.469L656.625 281.609ZM648.094 275.297L649.172 275.297C650.901 275.297 652.453 275.531 653.828 276C655.203 276.458 656.375 277.12 657.344 277.984C658.302 278.849 659.036 279.87 659.547 281.047C660.057 282.224 660.312 283.531 660.312 284.969C660.312 286.396 660.057 287.703 659.547 288.891C659.036 290.078 658.302 291.104 657.344 291.969C656.375 292.833 655.203 293.505 653.828 293.984C652.453 294.453 650.901 294.687 649.172 294.687L648.094 294.687C646.354 294.687 644.802 294.453 643.437 293.984C642.062 293.505 640.891 292.839 639.922 291.984C638.953 291.13 638.214 290.109 637.703 288.922C637.193 287.734 636.937 286.427 636.937 285C636.937 283.562 637.193 282.255 637.703 281.078C638.214 279.891 638.953 278.865 639.922 278C640.891 277.135 642.062 276.469 643.437 276C644.802 275.531 646.354 275.297 648.094 275.297ZM649.172 280.031L648.062 280.031C646.854 280.031 645.792 280.146 644.875 280.375C643.958 280.594 643.187 280.917 642.562 281.344C641.937 281.76 641.469 282.276 641.156 282.891C640.833 283.505 640.672 284.208 640.672 285C640.672 285.792 640.833 286.495 641.156 287.109C641.469 287.724 641.937 288.24 642.562 288.656C643.187 289.073 643.958 289.391 644.875 289.609C645.792 289.828 646.854 289.937 648.062 289.937L649.172 289.937C650.37 289.937 651.432 289.828 652.359 289.609C653.276 289.391 654.052 289.073 654.687 288.656C655.312 288.229 655.786 287.708 656.109 287.094C656.432 286.469 656.594 285.76 656.594 284.969C656.594 284.177 656.432 283.479 656.109 282.875C655.786 282.26 655.312 281.745 654.687 281.328C654.052 280.901 653.276 280.578 652.359 280.359C651.432 280.141 650.37 280.031 649.172 280.031Z" fill="#00A09D"/>
<path d="M316.884 516.718L320.61 504.503H323.879L318.38 520.303C318.253 520.641 318.088 521.009 317.885 521.406C317.683 521.803 317.417 522.179 317.087 522.534C316.766 522.897 316.365 523.188 315.883 523.408C315.402 523.636 314.819 523.75 314.135 523.75C313.865 523.75 313.603 523.725 313.349 523.674C313.104 523.632 312.872 523.585 312.652 523.535L312.64 521.203C312.724 521.212 312.825 521.22 312.944 521.228C313.07 521.237 313.172 521.241 313.248 521.241C313.755 521.241 314.177 521.178 314.515 521.051C314.853 520.933 315.127 520.739 315.339 520.468C315.558 520.198 315.744 519.835 315.896 519.378L316.884 516.718ZM314.781 504.503L318.038 514.766L318.582 517.985L316.466 518.53L311.487 504.503H314.781Z" fill="white"/>
<path d="M306.849 515.463V508.925C306.849 508.435 306.76 508.013 306.583 507.658C306.405 507.303 306.135 507.028 305.772 506.834C305.417 506.64 304.969 506.543 304.429 506.543C303.93 506.543 303.5 506.627 303.136 506.796C302.773 506.965 302.49 507.193 302.287 507.48C302.085 507.768 301.983 508.093 301.983 508.456H298.942C298.942 507.915 299.073 507.392 299.335 506.885C299.597 506.378 299.977 505.926 300.475 505.529C300.974 505.132 301.569 504.819 302.262 504.591C302.955 504.363 303.732 504.249 304.594 504.249C305.624 504.249 306.536 504.422 307.33 504.769C308.133 505.115 308.762 505.639 309.218 506.34C309.683 507.033 309.915 507.903 309.915 508.95V515.045C309.915 515.67 309.958 516.232 310.042 516.73C310.135 517.22 310.266 517.647 310.435 518.01V518.213H307.305C307.162 517.883 307.047 517.465 306.963 516.958C306.887 516.443 306.849 515.945 306.849 515.463ZM307.292 509.875L307.318 511.763H305.126C304.56 511.763 304.061 511.818 303.631 511.928C303.2 512.029 302.841 512.181 302.553 512.384C302.266 512.587 302.051 512.832 301.907 513.119C301.764 513.406 301.692 513.731 301.692 514.095C301.692 514.458 301.776 514.792 301.945 515.096C302.114 515.391 302.359 515.624 302.68 515.793C303.01 515.962 303.407 516.046 303.871 516.046C304.496 516.046 305.041 515.919 305.506 515.666C305.979 515.404 306.351 515.087 306.621 514.716C306.891 514.335 307.035 513.976 307.052 513.638L308.04 514.994C307.939 515.341 307.766 515.712 307.521 516.109C307.276 516.506 306.955 516.887 306.558 517.25C306.169 517.605 305.7 517.896 305.151 518.124C304.61 518.352 303.985 518.466 303.276 518.466C302.38 518.466 301.582 518.289 300.881 517.934C300.18 517.571 299.631 517.085 299.234 516.477C298.837 515.86 298.638 515.163 298.638 514.386C298.638 513.66 298.773 513.018 299.044 512.46C299.322 511.894 299.728 511.421 300.26 511.041C300.801 510.661 301.46 510.374 302.237 510.179C303.014 509.977 303.901 509.875 304.898 509.875H307.292Z" fill="white"/>
<path d="M292.86 515.374V498.75H295.927V518.213H293.152L292.86 515.374ZM283.94 511.51V511.244C283.94 510.205 284.062 509.259 284.307 508.405C284.552 507.544 284.907 506.805 285.372 506.188C285.836 505.563 286.402 505.086 287.069 504.756C287.737 504.418 288.489 504.249 289.325 504.249C290.153 504.249 290.879 504.41 291.504 504.731C292.129 505.052 292.662 505.512 293.101 506.112C293.54 506.703 293.891 507.413 294.153 508.241C294.414 509.06 294.6 509.972 294.71 510.978V511.827C294.6 512.806 294.414 513.702 294.153 514.513C293.891 515.324 293.54 516.025 293.101 516.616C292.662 517.208 292.125 517.664 291.492 517.985C290.867 518.306 290.136 518.466 289.3 518.466C288.472 518.466 287.724 518.293 287.057 517.947C286.398 517.6 285.836 517.115 285.372 516.489C284.907 515.864 284.552 515.129 284.307 514.285C284.062 513.432 283.94 512.507 283.94 511.51ZM286.993 511.244V511.51C286.993 512.135 287.048 512.718 287.158 513.258C287.276 513.799 287.458 514.276 287.703 514.69C287.948 515.096 288.265 515.417 288.653 515.653C289.05 515.881 289.523 515.995 290.072 515.995C290.765 515.995 291.335 515.843 291.783 515.539C292.231 515.235 292.581 514.825 292.835 514.31C293.097 513.786 293.274 513.203 293.367 512.561V510.268C293.316 509.77 293.211 509.305 293.05 508.874C292.898 508.443 292.691 508.067 292.429 507.746C292.167 507.417 291.842 507.164 291.454 506.986C291.074 506.8 290.622 506.707 290.098 506.707C289.54 506.707 289.067 506.826 288.679 507.062C288.29 507.299 287.969 507.624 287.716 508.038C287.471 508.452 287.289 508.933 287.171 509.482C287.053 510.031 286.993 510.619 286.993 511.244Z" fill="white"/>
<path d="M269.191 511.51V511.218C269.191 510.23 269.334 509.313 269.621 508.469C269.909 507.616 270.323 506.876 270.863 506.251C271.412 505.618 272.08 505.128 272.865 504.781C273.659 504.427 274.555 504.249 275.551 504.249C276.557 504.249 277.452 504.427 278.238 504.781C279.032 505.128 279.703 505.618 280.252 506.251C280.801 506.876 281.22 507.616 281.507 508.469C281.794 509.313 281.938 510.23 281.938 511.218V511.51C281.938 512.498 281.794 513.415 281.507 514.259C281.22 515.104 280.801 515.843 280.252 516.477C279.703 517.102 279.036 517.592 278.25 517.947C277.465 518.293 276.574 518.466 275.577 518.466C274.572 518.466 273.672 518.293 272.878 517.947C272.092 517.592 271.425 517.102 270.876 516.477C270.327 515.843 269.909 515.104 269.621 514.259C269.334 513.415 269.191 512.498 269.191 511.51ZM272.244 511.218V511.51C272.244 512.126 272.308 512.709 272.434 513.258C272.561 513.807 272.76 514.289 273.03 514.703C273.3 515.117 273.647 515.442 274.069 515.679C274.491 515.915 274.994 516.033 275.577 516.033C276.143 516.033 276.633 515.915 277.047 515.679C277.469 515.442 277.815 515.117 278.086 514.703C278.356 514.289 278.554 513.807 278.681 513.258C278.816 512.709 278.884 512.126 278.884 511.51V511.218C278.884 510.61 278.816 510.036 278.681 509.495C278.554 508.946 278.352 508.46 278.073 508.038C277.803 507.616 277.456 507.286 277.034 507.05C276.62 506.805 276.126 506.682 275.551 506.682C274.977 506.682 274.479 506.805 274.056 507.05C273.642 507.286 273.3 507.616 273.03 508.038C272.76 508.46 272.561 508.946 272.434 509.495C272.308 510.036 272.244 510.61 272.244 511.218Z" fill="white"/>
<path d="M263.628 499.764V518.213H260.473V499.764H263.628ZM269.419 499.764V502.298H254.733V499.764H269.419Z" fill="white"/>
<path d="M16.9131 28.3018L5.39648 24.3008L5.06055 24.1836V21.2842L23.46 28.0215L23.4648 28.0234C23.8282 28.1615 24.2259 28.3378 24.6562 28.5488C25.1238 28.7679 25.5674 29.0576 25.9844 29.415L25.9834 29.416C26.4333 29.7842 26.7945 30.2451 27.0664 30.791L27.0654 30.792C27.3626 31.3608 27.5 32.0321 27.5 32.7852C27.5 33 27.4707 33.257 27.4229 33.5479H27.4248C27.3867 33.8252 27.3441 34.0578 27.2939 34.2217L27.1875 34.5713L24.7354 34.5957L24.9082 33.958C24.9252 33.8955 24.9459 33.7773 24.9639 33.5879C24.9828 33.3884 24.9893 33.2734 24.9893 33.2246C24.9892 32.664 24.8947 32.2302 24.7314 31.9023L24.7295 31.8984C24.5628 31.5562 24.3117 31.2668 23.9648 31.0293L23.9609 31.0264C23.6027 30.7755 23.1371 30.5435 22.5557 30.3369L22.5498 30.335L20.708 29.6553L5.06055 35.6377V32.7217L16.9131 28.3018Z" fill="#827882"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-0.499997 117.979C-0.499997 116.698 -0.267784 115.526 0.207034 114.471C0.679527 113.411 1.35931 112.502 2.24317 111.745C3.1268 110.989 4.18434 110.416 5.40528 110.021C6.62083 109.624 7.97319 109.428 9.45606 109.428H11.6582C13.1412 109.428 14.4974 109.623 15.7217 110.02C16.9327 110.415 17.9852 110.983 18.8682 111.729C19.3495 112.135 19.7681 112.59 20.1279 113.09L23.3193 109.436L24.7988 111.034L25.1025 111.363L24.8086 111.702L21.3418 115.688C21.5241 116.399 21.6143 117.154 21.6143 117.95C21.6142 119.213 21.3818 120.379 20.9082 121.441L20.9072 121.443C20.4347 122.493 19.7549 123.402 18.8731 124.167L18.8711 124.169C17.9887 124.924 16.9376 125.502 15.7266 125.907L15.7217 125.909C14.4975 126.306 13.1412 126.5 11.6582 126.5H9.45606C7.97319 126.5 6.62083 126.305 5.40528 125.908L5.40332 125.907C4.18301 125.502 3.12625 124.925 2.24317 124.169C1.35999 123.413 0.679801 122.508 0.207034 121.457C-0.267225 120.403 -0.499958 119.241 -0.499997 117.979ZM2.03809 117.979C2.03813 118.887 2.20988 119.687 2.54102 120.386C2.87453 121.09 3.35261 121.692 3.98145 122.193C4.61162 122.696 5.38053 123.09 6.29785 123.367C7.21228 123.634 8.254 123.77 9.42774 123.771H11.6582C12.8417 123.771 13.8927 123.629 14.8164 123.353L15.1533 123.243C15.9252 122.975 16.5888 122.619 17.1494 122.178C17.7766 121.677 18.2538 121.077 18.5869 120.374L18.7041 120.104C18.9594 119.461 19.0898 118.746 19.0898 117.95C19.0898 117.01 18.9183 116.197 18.5879 115.499L18.5869 115.497C18.2535 114.785 17.7761 114.185 17.1504 113.694C16.5121 113.201 15.7401 112.822 14.8252 112.563L14.8213 112.562C13.896 112.295 12.8433 112.157 11.6582 112.157H9.42774C8.25234 112.157 7.20931 112.295 6.29395 112.562C5.37805 112.83 4.61046 113.219 3.98145 113.721L3.97852 113.723C3.35289 114.213 2.87545 114.813 2.542 115.525L2.54102 115.527C2.21065 116.225 2.03809 117.039 2.03809 117.979Z" fill="#827882"/>
<path d="M5.06152 55.2598L5.06152 52.085H7.44434L7.44434 55.2598H17.1211C17.7972 55.2597 18.2322 55.1664 18.4824 55.0342C18.761 54.8868 18.9035 54.7203 18.9717 54.5518C19.0681 54.3133 19.1191 54.0515 19.1191 53.7617C19.1191 53.527 19.1054 53.3024 19.0801 53.0889C19.044 52.8635 19.009 52.6619 18.9736 52.4844L18.8604 51.916L19.4385 51.8867L20.8643 51.8164L21.249 51.7969L21.3662 52.165C21.4454 52.4151 21.5015 52.7198 21.541 53.0674L21.54 53.0684C21.5889 53.417 21.6152 53.7659 21.6152 54.1152C21.6152 54.8276 21.4864 55.4795 21.2148 56.0576L21.2109 56.0645C20.9095 56.6812 20.4046 57.1468 19.7383 57.4707L19.7354 57.4717C19.0486 57.8 18.1602 57.9462 17.1064 57.9463H7.44434V60.7666H5.06152V57.9463H1.1377L1.1377 55.2598H5.06152Z" fill="#827882"/>
<path d="M5.06152 39.3857V36.2109H7.44434V39.3857H17.1211C17.7972 39.3857 18.2322 39.2924 18.4824 39.1602C18.7609 39.0128 18.9035 38.8462 18.9717 38.6777C19.068 38.4394 19.1191 38.1773 19.1191 37.8877C19.1191 37.653 19.1054 37.4283 19.0801 37.2148C19.044 36.9895 19.009 36.7879 18.9736 36.6104L18.8604 36.042L19.4385 36.0127L20.8643 35.9424L21.249 35.9229L21.3662 36.291C21.4454 36.541 21.5014 36.8458 21.541 37.1934L21.54 37.1943C21.5889 37.5429 21.6152 37.892 21.6152 38.2412C21.6152 38.9534 21.4862 39.6056 21.2148 40.1836L21.2109 40.1904C20.9096 40.8071 20.4045 41.2728 19.7383 41.5967L19.7354 41.5977C19.0486 41.926 18.1602 42.0722 17.1064 42.0723H7.44434L7.44434 44.8926H5.06152L5.06152 42.0723H1.1377L1.13769 39.3857H5.06152Z" fill="#827882"/>
<path d="M15.3555 103.73C16.1374 103.73 16.7604 103.644 17.2383 103.488L17.418 103.422C17.8195 103.263 18.1291 103.067 18.3604 102.843L18.46 102.737C18.6842 102.486 18.845 102.209 18.9463 101.904C19.0689 101.535 19.1318 101.13 19.1318 100.688C19.1318 99.4978 18.9024 98.6244 18.4971 98.0176L18.4951 98.0146C18.0701 97.3703 17.5205 96.9244 16.8408 96.6611L16.8379 96.6592C16.7257 96.6149 16.611 96.5756 16.4961 96.5381H5.06055V93.8379H21.332V96.458L20.1797 96.4814C20.545 96.9002 20.8455 97.382 21.0791 97.9248C21.4418 98.7679 21.6142 99.7599 21.6143 100.886C21.6143 101.678 21.5043 102.418 21.2783 103.099L21.2754 103.106C21.0335 103.793 20.652 104.389 20.1338 104.888C19.6093 105.393 18.9431 105.773 18.1533 106.037L18.1484 106.039C17.351 106.296 16.4063 106.417 15.3272 106.417H5.06055V103.73H15.3555Z" fill="#827882"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.77832 84.46C4.77832 83.3093 4.97054 82.2744 5.37012 81.3682L5.53223 81.0303C5.93162 80.2618 6.49068 79.6402 7.20899 79.1777C8.04279 78.641 9.06601 78.3897 10.2461 78.3896H17.6279C18.1282 78.3896 18.6493 78.3543 19.1904 78.2822C19.7305 78.2009 20.1543 78.0906 20.4717 77.96L20.5635 77.9219H21.332V80.582L20.9902 80.6963C20.6632 80.8057 20.2777 80.8905 19.8398 80.9561C19.9133 81.0374 19.987 81.12 20.0586 81.2061L20.0625 81.21C20.5233 81.7755 20.8962 82.4371 21.1855 83.1885L21.1865 83.1924C21.4747 83.9573 21.6142 84.812 21.6143 85.749C21.6142 86.8133 21.414 87.768 20.9961 88.5967L20.9951 88.5986C20.5816 89.4083 20.0104 90.0545 19.2813 90.5215L19.2734 90.5264C18.5305 90.9845 17.6949 91.21 16.7813 91.21C15.9553 91.2099 15.1985 91.0333 14.5244 90.668C13.9321 90.3514 13.4246 89.9141 13.002 89.3643L12.8262 89.1221C12.3635 88.4507 12.0221 87.6565 11.7939 86.75C11.5563 85.8337 11.4405 84.8186 11.4404 83.709V81.0762H10.2178C9.56405 81.0763 9.02748 81.2128 8.58985 81.4639C8.15702 81.7124 7.81975 82.0778 7.57813 82.5791C7.33735 83.0789 7.20414 83.7228 7.2041 84.5303C7.20413 85.264 7.33441 85.901 7.58301 86.4502L7.58399 86.4531C7.82745 87.0011 8.15079 87.4172 8.5459 87.7188C8.92178 88.0033 9.32853 88.1405 9.78028 88.1406H10.2852L10.2803 88.6445L10.2627 90.8418H9.7666C9.10666 90.8417 8.4712 90.6776 7.86719 90.3584C7.26578 90.0405 6.73465 89.5934 6.27344 89.0273C5.80814 88.4562 5.44776 87.7832 5.18653 87.0166C4.91254 86.2307 4.77837 85.3769 4.77832 84.46ZM13.7383 83.4814C13.7383 84.3207 13.8162 85.0617 13.9658 85.707L13.9668 85.7119C14.1077 86.3484 14.3151 86.8675 14.5781 87.2803L14.5811 87.2871C14.8375 87.7024 15.1379 88.0061 15.4766 88.2148C15.8045 88.4077 16.1833 88.5087 16.626 88.5088C17.0827 88.5088 17.491 88.3961 17.8613 88.1719C18.2296 87.9395 18.5316 87.6119 18.7656 87.1758C18.9848 86.7409 19.1035 86.2092 19.1035 85.5654C19.1035 84.6824 18.9414 83.9277 18.6318 83.2891L18.6309 83.2871C18.3106 82.6179 17.8962 82.0766 17.3916 81.6543C17.106 81.4153 16.8078 81.2242 16.498 81.0762H13.7383V83.4814Z" fill="#827882"/>
<path d="M4.77832 67.6787C4.77835 66.869 4.88829 66.1278 5.11621 65.4619C5.34874 64.7827 5.7217 64.1975 6.23438 63.7148C6.75 63.2295 7.3977 62.8742 8.16016 62.6387L8.45508 62.5527C9.1603 62.3647 9.97656 62.2744 10.8955 62.2744H21.332V64.9756H10.8818C9.86424 64.9756 9.12765 65.1188 8.63281 65.3633L8.62988 65.3643C8.11703 65.6138 7.77846 65.9526 7.5791 66.375C7.36315 66.8328 7.24616 67.3951 7.24609 68.0752C7.24609 68.8121 7.40796 69.4366 7.71484 69.9639L7.71582 69.9658C8.02898 70.5098 8.43783 70.969 8.94629 71.3438L8.94727 71.3428C9.41728 71.6867 9.91893 71.9563 10.4521 72.1533H21.332V74.8398H5.06055V72.2354L6.70801 72.2002C6.13135 71.6506 5.67163 71.0174 5.33399 70.3018L5.33301 70.2988C4.96087 69.4993 4.77833 68.6231 4.77832 67.6787Z" fill="#827882"/>
<path d="M21.332 46.6426V49.3291H5.06055V46.6426H21.332Z" fill="#827882"/>
<path d="M-0.5 47.9785C-0.499906 47.5131 -0.349748 47.0934 -0.0146485 46.7764C-0.0111037 46.7728 -0.00747147 46.7691 -0.00390631 46.7656C-0.00243281 46.7643 -0.00145118 46.7621 -5.96046e-08 46.7607C0.312277 46.4546 0.704069 46.3027 1.14355 46.3027C1.58427 46.3028 1.97613 46.462 2.29395 46.7676C2.62735 47.0879 2.77237 47.5124 2.77246 47.9785C2.77242 48.4447 2.62737 48.8691 2.29395 49.1895L2.28613 49.1973L2.27734 49.2051C1.95719 49.4862 1.57083 49.6269 1.14355 49.627C0.710882 49.627 0.318151 49.4889 0.000976622 49.1982L5.96046e-08 49.1973C-0.346262 48.8785 -0.499954 48.4513 -0.5 47.9785Z" fill="#827882"/>
</g>
<defs>
<linearGradient id="paint0_linear_1_2" x1="352.009" y1="451.098" x2="352.009" y2="39.7177" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0"/>
<stop offset="0.48" stop-color="#875A7B" stop-opacity="0.27"/>
</linearGradient>
<clipPath id="clip0_1_2">
<rect width="890" height="529" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,32 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
async function doMultiPrint(env, action) {
for (const report of action.params.reports) {
if (report.type != "ir.actions.report") {
env.services.notification.add(_t("Incorrect type of action submitted as a report, skipping action"), {
title: _t("Report Printing Error"),
});
continue
} else if (report.report_type === "qweb-html") {
env.services.notification.add(
_t("HTML reports cannot be auto-printed, skipping report: %s", report.name),
{ title: _t("Report Printing Error") }
);
continue
}
// WARNING: potential issue if pdf generation fails, then action_service defaults
// to HTML and rest of the action chain will break w/potentially never resolving promise
await env.services.action.doAction({ type: "ir.actions.report", ...report });
}
if (action.params.anotherAction) {
return env.services.action.doAction(action.params.anotherAction);
} else if (action.params.onClose) {
// handle special cases such as barcode
action.params.onClose()
} else {
return env.services.action.doAction("reload_context");
}
}
registry.category("actions").add("do_multi_print", doMultiPrint);

View File

@@ -0,0 +1,157 @@
import { _t } from "@web/core/l10n/translation";
import { Component, onWillStart, useState } from "@odoo/owl";
import { download } from "@web/core/network/download";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useSetupAction } from "@web/search/action_hook";
import { Layout } from "@web/search/layout";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
function processLine(line) {
return { ...line, lines: [], isFolded: true };
}
function extractPrintData(lines) {
const data = [];
for (const line of lines) {
const { id, model_id, model, unfoldable, level } = line;
data.push({
id: id,
model_id: model_id,
model_name: model,
unfoldable,
level: level || 1,
});
if (!line.isFolded) {
data.push(...extractPrintData(line.lines));
}
}
return data;
}
export class TraceabilityReport extends Component {
static template = "stock.TraceabilityReport";
static components = { Layout };
static props = { ...standardActionServiceProps };
setup() {
this.actionService = useService("action");
this.orm = useService("orm");
onWillStart(this.onWillStart);
useSetupAction({
getLocalState: () => ({
lines: [...this.state.lines],
}),
});
this.state = useState({
lines: this.props.state?.lines || [],
});
const { active_id, active_model, auto_unfold, context, lot_name, ttype, url, lang } =
this.props.action.context;
this.controllerUrl = url;
this.context = context || {};
Object.assign(this.context, {
active_id: active_id || this.props.action.params.active_id,
auto_unfold: auto_unfold || false,
model: active_model || this.props.action.context.params?.active_model || false,
lot_name: lot_name || false,
ttype: ttype || false,
lang: lang || false,
});
if (this.context.model) {
this.props.updateActionState({ active_model: this.context.model });
}
this.display = {
controlPanel: {},
searchPanel: false,
};
}
async onWillStart() {
if (!this.state.lines.length) {
const mainLines = await this.orm.call("stock.traceability.report", "get_main_lines", [
this.context,
]);
this.state.lines = mainLines.map(processLine);
}
}
onClickBoundLink(line) {
this.actionService.doAction({
type: "ir.actions.act_window",
res_model: line.res_model,
res_id: line.res_id,
views: [[false, "form"]],
target: "current",
});
}
onClickPartner(line) {
this.actionService.doAction({
type: "ir.actions.act_window",
res_model: "res.partner",
res_id: line.partner_id,
views: [[false, "form"]],
target: "current",
});
}
onClickOpenLot(line) {
this.actionService.doAction({
type: 'ir.actions.act_window',
res_model: 'stock.lot',
res_id: line.lot_id,
views: [[false, 'form']],
target: 'current',
});
}
onClickUpDownStream(line) {
this.actionService.doAction({
type: "ir.actions.client",
tag: "stock_report_generic",
name: _t("Traceability Report"),
context: {
active_id: line.model_id,
active_model: line.model,
auto_unfold: true,
lot_name: line.lot_name !== undefined && line.lot_name,
url: "/stock/output_format/stock/active_id",
},
});
}
onClickPrint() {
const data = JSON.stringify(extractPrintData(this.state.lines));
const url = this.controllerUrl
.replace(":active_id", this.context.active_id)
.replace(":active_model", this.context.model)
.replace("output_format", "pdf");
download({
data: { data },
url,
});
}
async toggleLine(line) {
line.isFolded = !line.isFolded;
if (!line.lines.length) {
line.lines = (
await this.orm.call("stock.traceability.report", "get_lines", [line.id], {
model_id: line.model_id,
model_name: line.model,
level: line.level + 30 || 1,
})
).map(processLine);
}
}
}
registry.category("actions").add("stock_report_generic", TraceabilityReport);

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="stock.TraceabilityReport">
<div class="o_action">
<Layout display="display">
<t t-set-slot="layout-buttons">
<div class="o_cp_buttons" role="toolbar" aria-label="Control panel buttons" t-ref="buttons">
<button type="button" class="btn btn-primary" t-on-click="() => this.onClickPrint()">Print</button>
</div>
</t>
<div class="container-fluid o_stock_reports_page o_stock_reports_no_print">
<t t-if="state.lines.length">
<h1 class="o_report_heading text-start">Traceability Report</h1>
<div class="o_stock_reports_table table-responsive">
<table class="table">
<thead>
<tr class="o_report_header">
<th class="o_report_line_header">Reference</th>
<th class="o_report_line_header">Product</th>
<th class="o_report_line_header">Date</th>
<th class="o_report_line_header">Lot/Serial #</th>
<th class="o_report_line_header">From</th>
<th class="o_report_line_header">To</th>
<th class="o_report_line_header">Quantity</th>
</tr>
</thead>
<tbody>
<t t-call="stock.ReportMRPLines">
<t t-set="lines" t-value="state.lines"/>
<t t-set="hasUpDown" t-value="true"/>
</t>
</tbody>
</table>
</div>
</t>
<h1 t-else="" class="text-center">No operation made on this lot.</h1>
</div>
</Layout>
</div>
</t>
<t t-name="stock.ReportMRPLines">
<t t-foreach="lines" t-as="line" t-key="line.id">
<t t-set="column" t-value="0" />
<tr t-att-class="line.model === 'stock.move.line' ? 'o_stock_reports_level0' : 'o_stock_reports_default_style'">
<t t-foreach="line.columns" t-as="col" t-key="col_index">
<td t-att-class="line.unfoldable ? 'o_stock_reports_unfoldable' : ''">
<t t-if="col_first">
<span t-attf-style="margin-left: {{line.level}}px"/>
<t t-if="hasUpDown and line.is_used ">
<span role="img" title="Traceability Report" aria-label="Traceability Report" t-on-click.prevent="() => this.onClickUpDownStream(line)">
<i class="fa fa-fw fa-level-up fa-rotate-270"/>
</span>
</t>
<t t-elif="line.unfoldable">
<span class="o_stock_reports_unfoldable o_stock_reports_caret_icon" t-on-click="() => this.toggleLine(line)">
<i class="fa fa-fw" t-att-class="line.isFolded ? 'fa-caret-right' : 'fa-caret-down'" role="img" aria-label="Unfold" title="Unfold"/>
</span>
</t>
</t>
<span t-if="col and line.reference === col" t-att-class="!line.unfoldable ? 'o_stock_reports_nofoldable' : ''">
<a class="o_stock_reports_web_action" href="#" t-on-click.prevent="() => this.onClickBoundLink(line)" t-esc="col"/>
</span>
<span t-elif="col and ((line.picking_type_code == 'incoming' and line.location_source === col) or (line.picking_type_code == 'outgoing' and line.location_destination === col))">
<a class="o_stock_report_partner_action" href="#" t-on-click.prevent="() => this.onClickPartner(line)" t-esc="col"/>
</span>
<span t-elif="col and line.lot_name === col">
<a class="o_stock_report_lot_action" href="#" t-on-click.prevent="() => this.onClickOpenLot(line)" t-esc="col"/>
</span>
<t t-elif="col" t-esc="col"/>
</td>
</t>
</tr>
<t t-if="!line.isFolded and line.lines.length" t-call="stock.ReportMRPLines">
<t t-set="lines" t-value="line.lines"/>
<t t-set="hasUpDown" t-value="false"/>
</t>
</t>
</t>
</templates>

View File

@@ -0,0 +1,72 @@
import { useService } from "@web/core/utils/hooks";
import { formatFloat } from "@web/views/fields/formatters";
import { Component } from "@odoo/owl";
export class ReceptionReportLine extends Component {
static template = "stock.ReceptionReportLine";
static props = {
data: Object,
labelReport: Object,
parentIndex: String,
showUom: Boolean,
precision: Number,
};
setup() {
this.ormService = useService("orm");
this.actionService = useService("action");
this.formatFloat = (val) => formatFloat(val, { digits: [false, this.props.precision] });
}
//---- Handlers ----
async onClickForecast() {
const action = await this.ormService.call(
"stock.move",
"action_product_forecast_report",
[[this.data.move_out_id]],
);
return this.actionService.doAction(action);
}
async onClickPrint() {
if (!this.data.move_out_id) {
return;
}
const modelIds = [this.data.move_out_id];
const productQtys = [Math.ceil(this.data.quantity) || '1'];
return this.actionService.doAction({
...this.props.labelReport,
context: { active_ids: modelIds },
data: { docids: modelIds, quantity: productQtys.join(",") },
});
}
async onClickAssign() {
await this.ormService.call(
"report.stock.report_reception",
"action_assign",
[false, [this.data.move_out_id], [this.data.quantity], [this.data.move_ins]],
);
this.env.bus.trigger("update-assign-state", { isAssigned: true, tableIndex: this.props.parentIndex, lineIndex: this.data.index });
}
async onClickUnassign() {
const done = await this.ormService.call(
"report.stock.report_reception",
"action_unassign",
[false, this.data.move_out_id, this.data.quantity, this.data.move_ins]
)
if (done) {
this.env.bus.trigger("update-assign-state", { isAssigned: false, tableIndex: this.props.parentIndex, lineIndex: this.data.index });
}
}
//---- Getters ----
get data() {
return this.props.data;
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<tr t-name="stock.ReceptionReportLine" class="align-middle">
<td t-esc="data.product.display_name"/>
<td>
<t t-esc="formatFloat(data.quantity)"/> <t t-if="props.showUom" t-esc="data.uom"/>
<button class="btn btn-link fa fa-area-chart" t-on-click="onClickForecast" name="forecasted_report_link"/>
</td>
<td t-if="data.is_qty_assignable">
<button t-if="!data.is_assigned"
t-on-click="onClickAssign"
class="btn btn-sm btn-primary"
name="assign_link">
Assign
</button>
<button t-else=""
t-on-click="onClickUnassign"
class="btn btn-sm btn-primary"
name="unassign_link">
Unassign
</button>
</td>
<td>
<button t-if="data.is_qty_assignable &amp;&amp; data.source"
class="btn btn-sm btn-primary"
t-attf-disabled="{{ !data.is_assigned }}"
name="print_label"
t-on-click="onClickPrint">
<span class="d-sm-block">Print Label</span>
</button>
</td>
</tr>
</templates>

View File

@@ -0,0 +1,173 @@
import { registry } from "@web/core/registry";
import { useBus, useService } from "@web/core/utils/hooks";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { ReceptionReportTable } from "../reception_report_table/stock_reception_report_table";
import { Component, onWillStart, useState } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
export class ReceptionReportMain extends Component {
static template = "stock.ReceptionReportMain";
static components = {
ControlPanel,
ReceptionReportTable,
};
static props = { ...standardActionServiceProps };
setup() {
this.controlPanelDisplay = {};
this.ormService = useService("orm");
this.actionService = useService("action");
this.reportName = "stock.report_reception";
this.labelReportName = "stock.report_reception_report_label";
this.state = useState({
sourcesToLines: {},
});
useBus(this.env.bus, "update-assign-state", (ev) => this._changeAssignedState(ev.detail));
onWillStart(async () => {
// Check the URL if report was alreadu loaded.
let defaultDocIds;
const { rfield, rids } = this.props.action.context.params || {};
if (rfield && rids) {
const parsedIds = JSON.parse(rids);
defaultDocIds = [rfield, parsedIds instanceof Array ? parsedIds : [parsedIds]];
} else {
defaultDocIds = Object.entries(this.context).find(([k,v]) => k.startsWith("default_"));
if (!defaultDocIds) {
// If nothing could be found, just ask for empty data.
defaultDocIds = [false, [0]];
}
}
this.contextDefaultDoc = { field: defaultDocIds[0], ids: defaultDocIds[1] };
if (this.contextDefaultDoc.field) {
// Add the fields/ids to the URL, so we can properly reload them after a page refresh.
this.props.updateActionState({ rfield: this.contextDefaultDoc.field, rids: JSON.stringify(this.contextDefaultDoc.ids) });
}
this.data = await this.getReportData();
this.state.sourcesToLines = this.data.sources_to_lines;
const matchingReports = await this.ormService.searchRead("ir.actions.report", [
["report_name", "in", [this.reportName, this.labelReportName]],
]);
this.receptionReportAction = matchingReports.find(
(report) => report.report_name === this.reportName
);
this.receptionReportLabelAction = matchingReports.find(
(report) => report.report_name === this.labelReportName
);
});
}
async getReportData() {
const context = { ...this.context, [this.contextDefaultDoc.field]: this.contextDefaultDoc.ids };
const args = [
this.contextDefaultDoc.ids,
{ context, report_type: "html" },
];
return this.ormService.call(
"report.stock.report_reception",
"get_report_data",
args,
{ context },
);
}
//---- Handlers ----
async onClickAssignAll() {
const moveIds = [];
const quantities = [];
const inIds = [];
for (const lines of Object.values(this.state.sourcesToLines)) {
for (const line of lines) {
if (line.is_assigned) continue;
moveIds.push(line.move_out_id);
quantities.push(line.quantity);
inIds.push(line.move_ins);
}
}
await this.ormService.call(
"report.stock.report_reception",
"action_assign",
[false, moveIds, quantities, inIds],
);
this._changeAssignedState({ isAssigned: true });
}
async onClickTitle(docId) {
return this.actionService.doAction({
type: "ir.actions.act_window",
res_model: this.data.doc_model,
res_id: docId,
views: [[false, "form"]],
target: "current",
});
}
onClickPrint() {
return this.actionService.doAction({
...this.receptionReportAction,
context: { [this.contextDefaultDoc.field]: this.contextDefaultDoc.ids },
});
}
onClickPrintLabels() {
const modelIds = [];
const quantities = [];
for (const lines of Object.values(this.state.sourcesToLines)) {
for (const line of lines) {
if (!line.is_assigned) continue;
modelIds.push(line.move_out_id);
quantities.push(Math.ceil(line.quantity) || 1);
}
}
if (!modelIds.length) {
return;
}
return this.actionService.doAction({
...this.receptionReportLabelAction,
context: { active_ids: modelIds },
data: { docids: modelIds, quantity: quantities.join(",") },
});
}
//---- Utils ----
_changeAssignedState(options) {
const { isAssigned, tableIndex, lineIndex } = options;
for (const [tabIndex, lines] of Object.entries(this.state.sourcesToLines)) {
if (tableIndex && tableIndex != tabIndex) continue;
lines.forEach(line => {
if (isNaN(lineIndex) || lineIndex == line.index) {
line.is_assigned = isAssigned;
}
});
}
}
//---- Getters ----
get context() {
return this.props.action.context;
}
get hasContent() {
return this.data.sources_to_lines && Object.keys(this.data.sources_to_lines).length > 0;
}
get isAssignAllDisabled() {
return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => line.is_assigned || !line.is_qty_assignable));
}
get isPrintLabelDisabled() {
return Object.values(this.state.sourcesToLines).every(lines => lines.every(line => !line.is_assigned));
}
}
registry.category("actions").add("reception_report", ReceptionReportMain);

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<div t-name="stock.ReceptionReportMain" class="o_action">
<ControlPanel display="controlPanelDisplay">
<t t-set-slot="control-panel-always-buttons">
<button t-on-click="onClickPrint" type="button" class="btn btn-primary" title="Print">Print</button>
<button t-on-click="onClickAssignAll" class="btn btn-secondary" t-att-disabled="isAssignAllDisabled">Assign All</button>
<button t-on-click="onClickPrintLabels" class="btn btn-secondary" t-att-disabled="isPrintLabelDisabled">Print Labels</button>
</t>
</ControlPanel>
<div class="o_report_reception o_report_reception_no_print container-fluid justify-content-between">
<div class="o_report_reception_header my-4">
<h1>
<t t-if="data.docs">
<div t-foreach="data.docs" t-as="doc" t-key="doc.id">
<a href="#" t-on-click.prevent="() => this.onClickTitle(doc.id)" view-type="form" t-esc="doc.name"/>
<span t-esc="doc.display_state" t-attf-class="ms-1 align-text-top badge rounded-pill bg-opacity-50 {{ doc.state == 'done' ? 'bg-success' : 'bg-info' }}"/>
</div>
</t>
<t t-else="">
<span t-esc="data.reason"/>
</t>
</h1>
</div>
<t t-if="hasContent">
<table class="table table-sm">
<ReceptionReportTable
t-foreach="state.sourcesToLines" t-as="source" t-key="source"
index="source"
scheduledDate="data.sources_to_formatted_scheduled_date[source]"
lines="state.sourcesToLines[source]"
source="data.sources_info[source]"
labelReport="receptionReportLabelAction"
showUom="data.show_uom"
precision="data.precision"/>
</table>
</t>
<p t-else="">
No allocation need found.
</p>
</div>
</div>
</templates>

View File

@@ -0,0 +1,92 @@
import { useService } from "@web/core/utils/hooks";
import { ReceptionReportLine } from "../reception_report_line/stock_reception_report_line";
import { Component } from "@odoo/owl";
export class ReceptionReportTable extends Component {
static template = "stock.ReceptionReportTable";
static components = {
ReceptionReportLine,
};
static props = {
index: String,
scheduledDate: { type: String, optional: true },
lines: Array,
source: Array,
labelReport: Object,
showUom: Boolean,
precision: Number,
};
setup() {
this.actionService = useService("action");
this.ormService = useService("orm");
}
//---- Handlers ----
async onClickAssignAll() {
const moveIds = [];
const quantities = [];
const inIds = [];
for (const line of this.props.lines) {
if (line.is_assigned) continue;
moveIds.push(line.move_out_id);
quantities.push(line.quantity);
inIds.push(line.move_ins);
}
await this.ormService.call(
"report.stock.report_reception",
"action_assign",
[false, moveIds, quantities, inIds],
);
this.env.bus.trigger("update-assign-state", { isAssigned: true, tableIndex: this.props.index });
}
async onClickLink(resModel, resId, viewType) {
return this.actionService.doAction({
type: "ir.actions.act_window",
res_model: resModel,
res_id: resId,
views: [[false, viewType]],
target: "current",
});
}
async onClickPrintLabels() {
const modelIds = [];
const quantities = [];
for (const line of this.props.lines) {
if (!line.is_assigned) continue;
modelIds.push(line.move_out_id);
quantities.push(Math.ceil(line.quantity) || 1);
}
if (!modelIds.length) {
return;
}
return this.actionService.doAction({
...this.props.labelReport,
context: { active_ids: modelIds },
data: { docids: modelIds, quantity: quantities.join(",") },
});
}
//---- Getters ----
get hasMovesIn() {
return this.props.lines.some(line => line.move_ins && line.move_ins.length > 0);
}
get hasAssignAllButton() {
return this.props.lines.some(line => line.is_qty_assignable);
}
get isAssignAllDisabled() {
return this.props.lines.every(line => line.is_assigned);
}
get isPrintLabelDisabled() {
return this.props.lines.every(line => !line.is_assigned);
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="stock.ReceptionReportTable">
<thead>
<tr class="bg-light">
<th>
<i t-if="props.source[0].priority == '1'" class="o_priority o_priority_star fa fa-star"/>
<a href="#" t-on-click.prevent="() => this.onClickLink(props.source[0].model, props.source[0].id, 'form')" t-out="props.source[0].name"/>
<span t-if="props.source.length > 1">
(<a href="#" t-on-click.prevent="() => this.onClickLink(props.source[1].model, props.source[1].id, 'form')" t-out="props.source[1].name"/>)
</span>
<span t-if="props.source[0].model == 'stock.picking' and props.source[0].partner_id">:
<a href="#" t-on-click.prevent="() => this.onClickLink('res.partner', props.source[0].partner_id, 'form')" t-out="props.source[0].partner_name"/>
</span>
</th>
<th>Expected Delivery: <t t-esc="props.scheduledDate"/></th>
<th t-if="hasMovesIn">
<button t-if="hasAssignAllButton" t-on-click="onClickAssignAll" class="btn btn-sm btn-primary" t-att-disabled="isAssignAllDisabled" name="assign_source_link">
Assign All
</button>
</th>
<th>
<button t-if="hasMovesIn" t-on-click="onClickPrintLabels" class="btn btn-sm btn-primary" t-att-disabled="isPrintLabelDisabled" name="print_labels">
<span class="d-none d-sm-block">Print Labels</span>
<span class="d-block d-sm-none fa fa-print"/>
</button>
</th>
</tr>
</thead>
<tbody>
<t t-foreach="props.lines" t-as="line" t-key="line.index">
<ReceptionReportLine
data="line"
labelReport="props.labelReport"
parentIndex="props.index"
showUom="props.showUom"
precision="props.precision"/>
</t>
</tbody>
</t>
</templates>

View File

@@ -0,0 +1,46 @@
import { registry } from "@web/core/registry";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
import { DynamicRecordList } from "@web/model/relational_model/dynamic_record_list";
import { DynamicGroupList } from "@web/model/relational_model/dynamic_group_list";
export class StockKanbanRenderer extends KanbanRenderer {
setup() {
super.setup();
}
// If all Inventory Overview graphs are empty, we use random sample data
getGroupsOrRecords() {
const { list } = this.props;
let records = [];
if (list instanceof DynamicRecordList) {
records.push(...list.records);
} else if (list instanceof DynamicGroupList) {
list.groups.forEach(g => {
records.push(...g.list.records);
});
}
// Data type "sample" is assigned in Python to empty graph data
let allEmpty = records.every(r => {
return r.data.kanban_dashboard_graph.includes('"type": "sample"');
});
if (allEmpty) {
records.forEach(r => {
let parsedDashboardData = JSON.parse(r.data.kanban_dashboard_graph);
parsedDashboardData[0].values.forEach(d => {
d.value = Math.floor(Math.random() * 9 + 1);
});
r.data.kanban_dashboard_graph = JSON.stringify(parsedDashboardData);
});
}
return super.getGroupsOrRecords();
}
}
export const StockKanbanView = {
...kanbanView,
Renderer: StockKanbanRenderer,
};
registry.category("views").add("stock_dashboard_kanban", StockKanbanView);

View File

@@ -0,0 +1,95 @@
import { Component } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr } from "@web/core/py_js/py";
import { floatField, FloatField } from "@web/views/fields/float/float_field";
import { monetaryField, MonetaryField } from "@web/views/fields/monetary/monetary_field";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
const fieldRegistry = registry.category("fields");
class StockActionField extends Component {
static props = {
...FloatField.props,
...MonetaryField.props,
actionName: { type: String, optional: false },
actionContext: { type: String, optional: true },
disabled: { type: String, optional: true },
};
static components = {
FloatField,
MonetaryField,
}
static template = "stock.actionField";
setup() {
super.setup();
this.actionService = useService("action");
this.orm = useService("orm");
this.fieldType = this.props.record.fields[this.props.name].type;
}
extractProps () {
const keysToRemove = ["actionName", "actionContext", "disabled"];
return Object.fromEntries(
Object.entries(this.props).filter(([prop]) => !keysToRemove.includes(prop))
);
}
get disabled() {
return this.props.disabled ? evaluateExpr(this.props.disabled, this.props.record.evalContext) : false;
}
_onClick(ev) {
ev.stopPropagation();
ev.preventDefault();
// Get the action name from props.options
const actionName = this.props.actionName;
const actionContext = evaluateExpr(this.props.actionContext, this.props.record.evalContext);
// const action = this.orm.call(this.props.record.resModel, actionName, this.props.record.resId);
// Use the action service to perform the action
this.actionService.doAction(actionName, {
additionalContext: { ...actionContext, ...this.props.record.context },
});
}
}
const stockActionField = {
...floatField,
...monetaryField,
component: StockActionField,
supportedOptions: [
Object.values(
Object.fromEntries(
[...floatField.supportedOptions, ...monetaryField.supportedOptions].map(
(option) => [option.name, option]
)
)
),
{
label: _t("Action Name"),
name: "action_name",
type: "string",
},
],
extractProps: (...args) => {
const [{ context, fieldType, options }] = args;
const action_props = {
actionName: options.action_name,
disabled: options.disabled,
actionContext: context,
}
let props = {...action_props}
if (fieldType === "monetary") {
props = { ...action_props, ...monetaryField.extractProps(...args) };
} else if (fieldType === "float") {
props = { ...action_props, ...floatField.extractProps(...args) };
};
return props;
},
};
fieldRegistry.add("stock_action_field", stockActionField);

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="stock.actionField" owl="1">
<a t-if="! disabled" href="#" t-on-click="_onClick" class="btn-link">
<t t-call="stock.actionFieldContent"/>
</a>
<t t-else="">
<t t-call="stock.actionFieldContent"/>
</t>
</t>
<t t-name="stock.actionFieldContent" owl="1">
<t t-if="fieldType === 'monetary'">
<MonetaryField t-props="extractProps()"/>
</t>
<t t-else="">
<FloatField t-props="extractProps()"/>
</t>
</t>
</templates>

View File

@@ -0,0 +1,190 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
import { useSelectCreate, useOpenMany2XRecord} from "@web/views/fields/relational_utils";
import { useService } from "@web/core/utils/hooks";
import { Domain } from "@web/core/domain";
export class SMLX2ManyField extends X2ManyField {
setup() {
super.setup();
this.orm = useService("orm");
this.dirtyQuantsData = new Map();
const selectCreate = useSelectCreate({
resModel: "stock.quant",
activeActions: this.activeActions,
onSelected: (resIds) => this.selectRecord(resIds),
onCreateEdit: () => this.createOpenRecord(),
});
this.selectCreate = (params) => {
return selectCreate(params);
};
this.openQuantRecord = useOpenMany2XRecord({
resModel: "stock.quant",
activeActions: this.activeActions,
onRecordSaved: (record) => this.selectRecord([record.resId]),
fieldString: this.props.string,
is2Many: true,
});
}
get quantListViewShowOnHandOnly(){
return true; // To override in mrp_subcontracting
}
async onAdd({ context, editable } = {}) {
if (!this.props.record.data.show_quant) {
return super.onAdd(...arguments);
}
// Compute the quant offset from move lines quantity changes that were not saved yet.
// Hence, did not yet affect quant's quantity in DB.
await this.updateDirtyQuantsData();
context = {
...context,
single_product: true,
list_view_ref: "stock.view_stock_quant_tree_simple",
};
const productName = this.props.record.data.product_id.display_name;
const title = _t("Add line: %s", productName);
let domain = [
["product_id", "=", this.props.record.data.product_id.id],
["location_id", "child_of", this.props.context.default_location_id],
["quantity", ">", 0.0],
];
if (this.quantListViewShowOnHandOnly) {
domain.push(["on_hand", "=", true]);
}
if (this.dirtyQuantsData.size) {
const notFullyUsed = [];
const fullyUsed = [];
for (const [quantId, quantData] of this.dirtyQuantsData.entries()) {
if (quantData.available_quantity > 0) {
notFullyUsed.push(quantId);
} else {
fullyUsed.push(quantId);
}
}
if (fullyUsed.length) {
domain = Domain.and([domain, [["id", "not in", fullyUsed]]]).toList();
}
if (notFullyUsed.length) {
domain = Domain.or([domain, [["id", "in", notFullyUsed]]]).toList();
}
}
return this.selectCreate({ domain, context, title });
}
async updateDirtyQuantsData() {
// Since changes of move line quantities will not affect the available quantity of the quant before
// the record has been saved, it is necessary to determine the offset of the DB quant data.
this.dirtyQuantsData.clear();
const dirtyQuantityMoveLines = this._move_line_ids.filter(
(ml) => !ml.data.quant_id && ml._values.quantity - ml._changes.quantity
);
const dirtyQuantMoveLines = this._move_line_ids.filter(
(ml) => ml.data.quant_id.id
);
const dirtyMoveLines = [...dirtyQuantityMoveLines, ...dirtyQuantMoveLines];
if (!dirtyMoveLines.length) {
return;
}
const match = await this.orm.call(
"stock.move.line",
"get_move_line_quant_match",
[
this._move_line_ids
.filter((rec) => rec.resId)
.map((rec) => rec.resId),
this.props.record.resId,
dirtyMoveLines.filter((rec) => rec.resId).map((rec) => rec.resId),
dirtyQuantMoveLines.map((ml) => ml.data.quant_id.id),
],
{}
);
const quants = match[0];
if (!quants.length) {
return;
}
const dbMoveLinesData = new Map();
for (const data of match[1]) {
dbMoveLinesData.set(data[0], { quantity: data[1].quantity, quantId: data[1].quant_id });
}
const offsetByQuant = new Map();
for (const ml of dirtyQuantMoveLines) {
const quantId = ml.data.quant_id.id;
offsetByQuant.set(quantId, (offsetByQuant.get(quantId) || 0) - ml.data.quantity);
const dbQuantId = dbMoveLinesData.get(ml.resId)?.quantId;
if (dbQuantId && quantId != dbQuantId) {
offsetByQuant.set(
dbQuantId,
(offsetByQuant.get(dbQuantId) || 0) + dbMoveLinesData.get(ml.resId).quantity
);
}
}
const offsetByQuantity = new Map();
for (const ml of dirtyQuantityMoveLines) {
offsetByQuantity.set(ml.resId, ml._values.quantity - ml._changes.quantity);
}
for (const quant of quants) {
const quantityOffest = quant[1].move_line_ids
.map((ml) => offsetByQuantity.get(ml) || 0)
.reduce((val, sum) => val + sum, 0);
const quantOffest = offsetByQuant.get(quant[0]) || 0;
this.dirtyQuantsData.set(quant[0], {
available_quantity: quant[1].available_quantity + quantityOffest + quantOffest,
});
}
}
async selectRecord(res_ids) {
const demand =
this.props.record.data.product_uom_qty -
this._move_line_ids
.map((ml) => ml.data.quantity)
.reduce((val, sum) => val + sum, 0);
const params = {
context: { default_quant_id: res_ids[0] },
};
if (demand <= 0) {
params.context.default_quantity = 0;
} else if (this.dirtyQuantsData.has(res_ids[0])) {
params.context.default_quantity = Math.min(
this.dirtyQuantsData.get(res_ids[0]).available_quantity,
demand
);
}
this.list.addNewRecord(params).then((record) => {
// Make it dirty to force the save of the record. addNewRecord make
// the new record dirty === False by default to remove them at unfocus event
record.dirty = true;
});
}
createOpenRecord() {
const activeElement = document.activeElement;
this.openQuantRecord({
context: {
...this.props.context,
form_view_ref: "stock.view_stock_quant_form",
},
immediate: true,
onClose: () => {
if (activeElement) {
activeElement.focus();
}
},
});
}
get _move_line_ids() {
return this.props.record.data.move_line_ids.records;
}
}
export const smlX2ManyField = {
...x2ManyField,
component: SMLX2ManyField,
};
registry.category("fields").add("sml_x2_many", smlX2ManyField);

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,110 @@
import { cookie } from "@web/core/browser/cookie";
import { getColor, getCustomColor } from "@web/core/colors/colors";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { JournalDashboardGraphField } from "@web/views/fields/journal_dashboard_graph/journal_dashboard_graph_field";
export class PickingTypeDashboardGraphField extends JournalDashboardGraphField {
setup() {
super.setup();
this.actionService = useService("action");
}
getBarChartConfig() {
// Only bar chart is available for picking types
const data = [];
const labels = [];
const backgroundColor = [];
const colorPast = getColor(8, cookie.get("color_scheme"));
const colorPresent = getColor(16, cookie.get("color_scheme"));
const colorFuture = getColor(12, cookie.get("color_scheme"));
this.data[0].values.forEach((pt) => {
data.push(pt.value);
labels.push(pt.label);
if (pt.type === "past") {
backgroundColor.push(colorPast);
} else if (pt.type === "present") {
backgroundColor.push(colorPresent);
} else if (pt.type === "future") {
backgroundColor.push(colorFuture);
} else {
backgroundColor.push(getCustomColor(cookie.get("color_scheme"), "#ebebeb", "#3C3E4B"));
}
});
return {
type: "bar",
data: {
labels,
datasets: [
{
backgroundColor,
data,
fill: "start",
label: this.data[0].key,
},
],
},
options: {
onClick: (e) => {
const pickingTypeId = e.chart.config._config.options.pickingTypeId;
// If no picking type ID was provided, than this is sample data
if (!pickingTypeId) {
return;
}
const columnIndex = e.chart.tooltip.dataPoints[0].parsed.x;
const dateCategories = {
0: "before",
1: "yesterday",
2: "today",
3: "day_1",
4: "day_2",
5: "after",
};
const dateCategory = dateCategories[columnIndex];
const additionalContext = {
picking_type_id: pickingTypeId,
search_default_picking_type_id: [pickingTypeId],
};
// Add a filter for the given date category
additionalContext["search_default_".concat(dateCategory)] = true;
this.actionService.doAction("stock.click_dashboard_graph", {
additionalContext: additionalContext
});
},
plugins: {
legend: { display: false },
tooltip: {
intersect: false,
position: "nearest",
caretSize: 0,
},
},
scales: {
y: {
display: false,
},
x: {
display: false,
},
},
pickingTypeId: this.data[0].picking_type_id,
maintainAspectRatio: false,
elements: {
line: {
tension: 0.000001,
},
},
},
};
}
}
export const pickingTypeDashboardGraphField = {
component: PickingTypeDashboardGraphField,
supportedTypes: ["text"],
extractProps: ({ attrs }) => ({
graphType: attrs.graph_type,
}),
};
registry.category("fields").add("picking_type_dashboard_graph", pickingTypeDashboardGraphField);

View File

@@ -0,0 +1,5 @@
.o_field_picking_type_dashboard_graph .o_dashboard_graph {
margin-bottom: 0;
margin-left: -16px;
margin-right: -16px;
}

View File

@@ -0,0 +1,3 @@
.o_forecast_widget_cell {
text-align: left !important;
}

View File

@@ -0,0 +1,27 @@
.o_stock_forecast {
button[data-bs-toggle="collapse"] i::before {
content: "\f078"; /* fa-chevron-down */
}
button[data-bs-toggle="collapse"].collapsed i::before {
content: "\f054"; /* fa-chevron-right */
}
thead.o_forecasted_details_main_header tr th {
border: none !important;
}
thead.o_forecasted_details_main_header tr th:first-child {
width:1%;
white-space:nowrap;
}
table.o_forecasted_details_table tr td:first-child {
border-right: none !important;
}
table.o_forecasted_details_table tr td:nth-child(2):not(.o_forecasted_details_line_button) {
border-left: none !important;
padding-left: 0px !important;
}
}

View File

@@ -0,0 +1,8 @@
.hover-show .btn-show {
opacity: 0;
transition: opacity 0.1s;
}
.hover-show:hover .btn-show {
opacity: 1;
}

View File

@@ -0,0 +1,58 @@
.o_report_reception {
.o_priority {
&.o_priority_star {
font-size: 1.35em;
&.fa-star {
color: gold;
}
}
}
& .btn {
&.btn-primary {
height: 31px;
background-color: $o-brand-primary;
border-color: $o-brand-primary;
&:hover:not([disabled]) {
background-color: darken($o-brand-primary, 10%);
}
}
}
& .badge {
line-height: .75;
}
@each $-name, $-bg-color in $theme-colors {
$-safe-text-color: color-contrast(mix($-bg-color, $o-view-background-color));
@include bg-variant(".bg-#{$-name}-light", rgba(map-get($theme-colors, $-name), 0.5), $-safe-text-color);
}
& thead{
display: table-row-group;
}
}
.o_report_reception_no_print {
overflow-y: auto;
}
.o_label_page {
margin-left: -3mm;
margin-right: -3mm;
overflow: hidden;
page-break-before: always;
padding: 1mm 0mm 0mm;
&.o_label_dymo {
font-size:80%;
width: 57mm;
height: 32mm;
& span, div {
line-height: 1;
white-space: nowrap;
}
}
span[itemprop="name"] {
font-weight: bold;
}
}

View File

@@ -0,0 +1,134 @@
.o_report_stock_rule {
.table > :not(:first-child) {
border-top: $border-width * 2 solid currentColor;
}
.table {
--table-border-color: #{$o-gray-300};
}
.o_report_stock_rule_rule {
display: flex;
flex-flow: row nowrap;
}
.o_report_stock_rule_legend {
display: flex;
flex-flow: row wrap;
max-width: 1000px;
}
.o_report_stock_rule_legend_line {
flex: 0 1 auto;
display: flex;
flex-flow: row nowrap;
width: 29%;
margin-right: 20px;
margin-left: 20px;
margin-top: 15px;
min-width: 200px;
>.o_report_stock_rule_legend_label {
flex: 1 1 auto;
width: 30%;
min-width: 100px;
}
>.o_report_stock_rule_legend_symbol {
flex: 1 1 auto;
width: 70%;
}
}
.o_report_stock_rule_putaway {
>p {
text-align: center;
color: black;
font-weight: normal;
font-size: 12px
}
}
.o_report_stock_rule_line {
flex: 1 1 auto;
height: 20px;
>line {
stroke: black;
stroke-width: 1;
}
}
.o_report_stock_rule_arrow {
flex: 0 0 auto;
height: 20px;
width: 20px;
>svg {
>line {
stroke: black;
stroke-width: 1;
}
>polygon {
fill: black;
fill-opacity: 0.5;
stroke: black;
stroke-width: 1;
}
}
}
.o_report_stock_rule_vertical_bar {
flex: 0 0 auto;
height: 20px;
width: 2px;
>svg {
>line {
stroke: black;
stroke-width: 2;
}
}
}
.o_report_stock_rule_rule_name {
text-align: center;
}
.o_report_stock_rule_symbol_cell {
border: none !important;
>div {
max-width: 200px;
height: 20px;
}
}
.o_report_stock_rule_rule_main {
height: 100%;
padding-top: 2px;
}
.o_report_stock_rule_location_header {
text-align: center;
>a {
display: block;
&:hover {
text-decoration: none;
cursor: pointer;
background-color: #efefef;
}
>div {
color: black;
}
}
}
.o_report_stock_rule_rule_cell {
padding:0 !important;
>a {
display: block;
&:hover {
text-decoration: none;
cursor: pointer;
background-color: #efefef;
}
}
}
.o_report_stock_rule_rtl {
transform: scaleX(-1);
}
}

View File

@@ -0,0 +1,32 @@
.o_report_stockpicking_operations table {
thead, tbody, td, th, tr {
border: 0;
}
}
/*
* Before this PR, col-auto was the closest thing to flex box support.
* However, an issue arised at the time of testing: when one of the
* fields has long words in it, the spacing fails miserably
* (it becomes very elongated on the vertical axis). The only way
* to remove this effect and also with wkhtmltopdf outdated CSS support
* is to add min-width so that it forces the fields to have a readable
* width for this specific report. It can also be seen that this solution is
* suggested in base styling for reports.
*/
.o_stock_report_header_row {
display: -webkit-box;
display: flex;
flex-wrap: wrap;
-webkit-box-pack: center;
justify-content: center;
flex-direction: row;
-webkit-flex-direction: row;
-ms-flex-wrap: wrap;
> div {
flex: 1;
-webkit-flex: 1;
-webkit-box-flex: 1;
min-width: 150px;
}
}

View File

@@ -0,0 +1,29 @@
.o_view_nocontent {
&_barcode_scanner:before {
@extend %o-nocontent-init-image;
width: 250px;
height: 250px;
background: transparent url(/stock/static/img/barcode_scanner.png) no-repeat center;
background-size: 250px 250px;
}
&_replenishment:before {
@extend %o-nocontent-init-image;
width: 100%;
height: 300px;
max-width: 500px;
margin-bottom: 20px;
background: transparent url(/stock/static/img/replenishment.svg) no-repeat center / contain;;
}
}
.o_nocontent_help {
.o_view_nocontent_smiling_face.o_view_nocontent_stock:before {
@extend %o-nocontent-init-image;
width: 200px;
height: 200px;
background: transparent url(/stock/static/img/empty_list.png) no-repeat center;
background-size: 200px 200px;
resize: both;
}
}

View File

@@ -0,0 +1,32 @@
.o_stock_forecasted_page {
.o_priority {
display: inline-block;
padding: 0;
border: 0;
&.o_priority_star {
background-color: transparent;
font-size: 1.35em;
&.fa-star-o {
color: #a8a8a8;
&:hover {
color: gold;
&:before{
content: "\f005";
}
}
}
&.fa-star {
color: gold;
&:hover {
color: #a8a8a8;
&:before{
content: "\f006";
}
}
}
}
}
.table td {
vertical-align: middle;
}
}

View File

@@ -0,0 +1,3 @@
.btn[name="action_show_details"] {
border-width: 0;
}

View File

@@ -0,0 +1,19 @@
.o_stock_kanban .o_kanban_renderer {
--KanbanRecord-width: 480px;
@include media-only(screen) {
--KanbanGroup-width: 480px;
}
@include media-only(print) {
--KanbanGroup-width: 400px;
}
padding: 4px;
.o_kanban_record {
margin-left: 4px;
margin-right: 4px;
}
}
.stock-overview-links {
height: 5.5rem;
}

View File

@@ -0,0 +1,8 @@
.o_stock_replenishment_info {
.o_field_widget.o_small {
display: inline;
input {
width: 40px;
}
}
}

View File

@@ -0,0 +1,89 @@
@mixin o-stock-reports-lines($border-width: 5px, $font-weight: inherit, $border-top-style: initial, $border-bottom-style: initial) {
border-width: $border-width;
border-left-style: hidden;
border-right-style: hidden;
font-weight: $font-weight;
border-top-style: $border-top-style;
border-bottom-style: $border-bottom-style;
}
.o_stock_reports_body_print {
background-color: white;
color: black;
.o_stock_reports_level0 {
@include o-stock-reports-lines($border-width: 1px, $font-weight: bold, $border-top-style: solid, $border-bottom-style: groove);
}
}
.o_main_content {
.o_stock_reports_page {
position: absolute;
}
}
.o_stock_reports_page {
background-color: $o-view-background-color;
&.o_stock_reports_no_print {
margin: $o-horizontal-padding auto;
@include o-webclient-padding($top: $o-sheet-vpadding, $bottom: $o-sheet-vpadding);
.o_stock_reports_level0 {
@include o-stock-reports-lines($border-width: 1px, $font-weight: normal, $border-top-style: solid, $border-bottom-style: groove);
}
.o_stock_reports_table {
thead {
display: table-row-group;
}
white-space: nowrap;
margin-top: 30px;
}
.o_report_line_header {
text-align: left;
padding-left: 10px;
}
.o_report_header {
border-top-style: solid;
border-top-style: groove;
border-bottom-style: groove;
border-width: 2px;
}
}
.o_stock_reports_unfolded {
display: inline-block;
}
.o_stock_reports_nofoldable {
margin-left: 17px;
}
a.o_stock_report_lot_action {
cursor: pointer;
}
a.o_stock_report_partner_action {
cursor: pointer;
}
.o_stock_reports_unfolded td + td {
visibility: hidden;
}
div.o_stock_reports_web_action,
span.o_stock_reports_web_action, i.fa,
span.o_stock_reports_unfoldable, span.o_stock_reports_foldable, a.o_stock_reports_web_action {
cursor: pointer;
}
.o_stock_reports_caret_icon {
margin-left: -3px;
}
th {
border-bottom: thin groove;
}
.o_stock_reports_level1 {
@include o-stock-reports-lines($border-width: 2px, $border-top-style: hidden, $border-bottom-style: solid);
}
.o_stock_reports_level2 {
@include o-stock-reports-lines($border-width: 1px, $border-top-style: solid, $border-bottom-style: solid);
> td > span:last-child {
margin-left: 25px;
}
}
.o_stock_reports_default_style {
@include o-stock-reports-lines($border-width: 0px, $border-top-style: solid, $border-bottom-style: solid);
> td > span:last-child {
margin-left: 50px;
}
}
}

View File

@@ -0,0 +1,60 @@
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { Component, markup } from "@odoo/owl";
export class ForecastedButtons extends Component {
static template = "stock.ForecastedButtons";
static props = {
action: Object,
resModel: { type: String, optional: true },
reloadReport: Function,
};
setup() {
this.actionService = useService("action");
this.orm = useService("orm");
this.context = this.props.action.context;
this.productId = this.context.active_id;
this.resModel = this.props.resModel || this.context.active_model || this.context.params?.active_model || 'product.template';
}
/**
* Called when an action open a wizard. If the wizard is discarded, this
* method does nothing, otherwise it reloads the report.
* @param {Object | undefined} res
*/
_onClose(res) {
return res?.special || !res?.noReload || this.props.reloadReport();
}
async _onClickReplenish() {
const context = { ...this.context };
if (this.resModel === 'product.product') {
context.default_product_id = this.productId;
} else if (this.resModel === 'product.template') {
context.default_product_tmpl_id = this.productId;
}
context.default_warehouse_id = this.context.warehouse_id;
const action = {
res_model: 'product.replenish',
name: _t('Product Replenish'),
type: 'ir.actions.act_window',
views: [[false, 'form']],
target: 'new',
context: context,
};
return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });
}
async _onClickUpdateQuantity() {
const action = await this.orm.call(this.resModel, "action_open_quants", [[this.productId]]);
if (action.res_model === "stock.quant") { // Quant view in inventory mode.
action.views = [[false, "list"]];
}
if (action.help) {
action.help = markup(action.help);
}
return this.actionService.doAction(action, { onClose: this._onClose.bind(this) });
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="stock.ForecastedButtons">
<button title="Replenish" t-on-click="_onClickReplenish"
class="o_forecasted_replenish_btn btn btn-primary">
Replenish
</button>
<button title="Update Quantity" t-on-click="_onClickUpdateQuantity"
class="o_forecasted_update_qty_btn btn btn-secondary">
Update Quantity
</button>
</t>
</templates>

View File

@@ -0,0 +1,242 @@
import { formatFloat } from "@web/views/fields/formatters";
import { useService } from "@web/core/utils/hooks";
import { Component } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
export class ForecastedDetails extends Component {
static template = "stock.ForecastedDetails";
static props = { docs: Object, openView: Function, reloadReport: Function };
setup() {
this.orm = useService("orm");
this._groupLines();
this._prepareLines();
this._prepareData();
this._mergeLines();
this._formatFloat = (num) => {
return formatFloat(num, { digits: this.props.docs.precision });
};
}
async _reserve(move_id){
await this.orm.call(
'stock.forecasted_product_product',
'action_reserve_linked_picks',
[move_id],
);
this.props.reloadReport();
}
async _unreserve(move_id){
await this.orm.call(
'stock.forecasted_product_product',
'action_unreserve_linked_picks',
[move_id],
);
this.props.reloadReport();
}
async _onClickChangePriority(modelName, record) {
const value = record.priority == "0" ? "1" : "0";
await this.orm.call(modelName, "write", [[record.id], { priority: value }]);
this.props.reloadReport();
}
_onHandCondition(line){
return !line.document_in && !line.in_transit && line.replenishment_filled && line.document_out;
}
_reconciledCondition(line){
return line.document_in && !line.in_transit && line.replenishment_filled && line.document_out;
}
_freeStockCondition(line){
return !line.document_in && !line.in_transit && line.replenishment_filled && !line.document_out;
}
_notAvailableCondition(line){
return !line.document_in && !line.in_transit && !line.replenishment_filled && line.document_out;
}
//Extend this to add new lines grouping
_groupLines(){
this._groupLinesByProduct();
this._groupOnHandLinesByProduct();
this._groupReconciledLinesByProduct();
this._groupFreeStockLinesByProduct();
this._groupNotAvailableLinesByProduct();
}
_groupLinesByProduct() {
this.LinesPerProduct = {};
for (const line of this.props.docs.lines) {
const key = line.product.id;
(this.LinesPerProduct[key] ??= []).push(line);
}
}
_groupOnHandLinesByProduct() {
this.OnHandLinesPerProduct = {};
for (const line of this.props.docs.lines) {
if (this._onHandCondition(line)) {
const key = line.product.id;
(this.OnHandLinesPerProduct[key] ??= []).push(line);
}
}
}
_groupReconciledLinesByProduct() {
this.ReconciledLinesPerProduct = {};
for (const line of this.props.docs.lines) {
if (this._onHandCondition(line)) {
const key = line.product.id;
(this.ReconciledLinesPerProduct[key] ??= []).push(line);
}
}
}
_groupNotAvailableLinesByProduct() {
this.NotAvailableLinesPerProduct = {};
for (const line of this.props.docs.lines) {
if (this._notAvailableCondition(line)) {
const key = line.product.id;
(this.NotAvailableLinesPerProduct[key] ??= []).push(line);
}
}
}
_groupFreeStockLinesByProduct() {
this.FreeStockLinesPerProduct = {};
for (const line of this.props.docs.lines) {
if (this._freeStockCondition(line) && line?.removal_date !== -1) {
const key = line.product.id;
(this.FreeStockLinesPerProduct[key] ??= []).push(line);
}
}
}
_prepareLines(){
if (this.multipleProducts) {
this.props.docs.lines.sort((a, b) => (a.product.id || 0) - (b.product.id || 0));
}
}
_prepareData(){
this.OnHandTotalQty = Object.fromEntries(
Object.entries(this.OnHandLinesPerProduct).map(([id, lines]) => [
id,
lines.reduce((sum, line) => sum + line.quantity, 0),
])
);
this.AvailableOnHandTotalQty = Object.fromEntries(
Object.entries(this.OnHandLinesPerProduct).map(([id, lines]) => [
id,
lines.reduce((sum, line) => sum + (line.reservation ? 0 : line.quantity), 0),
])
);
for (const productId of this.productIds){
if (!(productId in this.FreeStockLinesPerProduct) || !(productId in this.LinesPerProduct)){
continue;
}
const lines = this.FreeStockLinesPerProduct[productId]
if (this.LinesPerProduct[productId].length > 1 && lines.length == 1 && lines[0]?.quantity === 0 ){
const removeIndex = this.lines.indexOf(lines[0]);
this.lines.splice(removeIndex,1);
}
}
}
_mergeLines(){
let lines = this.lines;
this.mergesLinesData = {};
let lastIndex = 0;
for(let i = 0; i < lines.length-1; i++){
const line = lines[i];
const nextLine = lines[i + 1];
if (line.product.id != nextLine.product.id || !this._sameLineRule(line, nextLine)) {
lastIndex = i+1;
continue;
}
if (!this.mergesLinesData[lastIndex]){
this.mergesLinesData[lastIndex] = {
rowcount: 1,
tot_qty: line.quantity,
};
}
this.mergesLinesData[lastIndex].rowcount += 1;
this.mergesLinesData[lastIndex].tot_qty += nextLine.quantity;
}
}
_sameLineRule(line, nextLine){
const OnHand = this.OnHandLinesPerProduct[line.product.id] || [];
const NotAvailable = this.NotAvailableLinesPerProduct[line.product.id] || [];
return this.sameDocumentIn(line, nextLine) || (OnHand.includes(line) && OnHand.includes(nextLine)) || (NotAvailable.includes(line) && NotAvailable.includes(nextLine));
}
displayReserve(line){
let splittedLine = true;
if(this.line_index - 1 >= 0){
const previousLine = this.lines[this.line_index - 1];
const sameProduct = this.line.product.id == previousLine.product.id;
const isOnHandSplittedLine = this.OnHandLinesPerProduct[line.product.id] && this.OnHandLinesPerProduct[line.product.id].some(l => this.sameDocumentOut(l, line))
const isReconciledSplittedLine = this.ReconciledLinesPerProduct[line.product.id] && !this.isReconciled(line) && this.ReconciledLinesPerProduct[line.product.id].some(l => this.sameDocumentOut(l, line))
splittedLine = sameProduct && (this.sameDocumentOut(line, previousLine) || isOnHandSplittedLine || isReconciledSplittedLine);
}
const hasFreeStock = this.props.docs.product[line.product.id].free_qty > 0;
return this.props.docs.user_can_edit_pickings && !line.in_transit && this.canReserveOperation(line) &&
(this.isOnHand(line) || (hasFreeStock && !splittedLine));
}
canReserveOperation(line){
return line.move_out?.picking_id;
}
futureVirtualAvailable(line) {
const product = this.props.docs.product[line.product.id]
return product.virtual_available + product.qty.in - product.qty.out;
}
sameDocumentIn(line1, line2){
return this._sameDocument(line1, line2, 'document_in');
}
sameDocumentOut(line1, line2){
return this._sameDocument(line1, line2, 'document_out');
}
_sameDocument(line1, line2, docField) {
return (
line1[docField] && line2[docField] &&
line1[docField].id === line2[docField].id &&
line1[docField]._name === line2[docField]._name &&
line1[docField].name === line2[docField].name
);
}
isOnHand(line){
return this.OnHandLinesPerProduct[line.product.id] && this.OnHandLinesPerProduct[line.product.id].includes(this.lines[this.line_index]);
}
isReconciled(line){
return this.ReconciledLinesPerProduct[line.product.id] && this.ReconciledLinesPerProduct[line.product.id].includes(this.lines[this.line_index]);
}
get freeStockLabel() {
return _t('Free Stock');
}
get lines() {
return this.props.docs.lines;
}
get multipleProducts() {
return this.props.docs.multiple_product;
}
get productIds(){
return Object.keys(this.props.docs.product).map(Number);
}
}

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.ForecastedDetails">
<table class="table table-bordered bg-view o_forecasted_details_table">
<thead class="o_forecasted_details_main_header">
<tr class="bg-light border-top-0">
<th/>
<th scope="col">Available</th>
<th scope="col" class="text-end">Outgoing</th>
<th scope="col">Used by</th>
<th scope="col"></th> <!-- Action Button -->
<th scope="col">Delivery Date</th>
</tr>
</thead>
<tbody>
<t t-set="last_line_index" t-value="lines.length - 1"/>
<t t-foreach="lines" t-as="line" t-key="line_index">
<t t-set="currentProduct" t-value="props.docs.product[line.product.id]"/>
<t t-if="multipleProducts and (line_index === 0 or line.product.id !== lines[line_index - 1].product.id)">
<tr class="o_forecasted_product_header">
<td class="border-0 p-0">
<button class="btn btn-sm border-0"
data-bs-toggle="collapse"
t-attf-data-bs-target=".collapseGroup_#{line.product.id}"
aria-expanded="true" t-attf-aria-controls=".collapseGroup_#{line.product.id}">
<i class="icon fa"/>
</button>
</td>
<td colspan="5" class="fw-bold text-uppercase border-0">
<span t-out="line.product.display_name"/>
</td>
</tr>
</t>
<tr t-attf-class="#{line.is_late and 'table-warning' or ''} #{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}" >
<t t-if="skip_row > 0" t-set="skip_row" t-value="skip_row - 1"/>
<t t-if="!skip_row or skip_row == 0">
<t t-if="mergesLinesData[line_index]" t-set="skip_row" t-value="mergesLinesData[line_index]['rowcount']"/>
<td t-attf-rowspan="#{skip_row > 0 and skip_row}"/>
<td t-attf-rowspan="#{skip_row > 0 and skip_row}">
<t t-if="line.document_in">
<a t-if="line.document_in"
href="#"
t-on-click.prevent="() => this.props.openView(line.document_in._name, 'form', line.document_in.id)"
t-out="line.document_in.name"
class="fw-bold"/>
<span t-if="line.receipt_date">:
<t t-out="_formatFloat(skip_row > 0 ? mergesLinesData[line_index].tot_qty : line.quantity)"/> <t t-out="line.uom_id.display_name"/> expected on <t t-out="line.receipt_date"/>
</span>
</t>
<t t-elif="line.in_transit">
<t t-if="line.move_out">
<span>Stock In Transit</span>
</t>
<t t-else="">
<span>Free Stock in Transit</span>
</t>
</t>
<t t-elif="line.replenishment_filled">
<t name="onHand_cell" t-if="line.document_out">
Stock To Reserve: <t t-out="_formatFloat(OnHandTotalQty[line.product.id])"/> <t t-out="line.uom_id.display_name"/>
</t>
<t name="freeStock_cell" t-else="" t-out="freeStockLabel"/>
</t>
<span t-else="" class="text-muted">Not Available</span>
</td>
</t>
<td name="quantity_cell" class="text-end"><t t-attf-class="#{(!line.replenishment_filled and 'text-danger')" t-out="_formatFloat(line.quantity)"/> <t t-out="line.uom_id.display_name"/></td>
<td class="border-end-0 o_forecasted_details_line_button" t-attf-class="#{(!line.replenishment_filled and 'table-danger') or (line.is_matched and 'table-info')}" name="usedby_cell">
<button t-if="line.move_out and line.move_out.picking_id"
t-attf-class="o_priority o_priority_star me-1 fa fa-star#{line.move_out.picking_id.priority=='1' ? '' : '-o'}"
t-on-click="() => this._onClickChangePriority('stock.picking', line.move_out.picking_id)"
name="change_priority_link"/>
<a t-if="line.document_out"
href="#"
t-attf-class="#{line.is_matched and 'fst-italic'}"
t-on-click.prevent="() => this.props.openView(line.document_out._name, 'form', line.document_out.id)"
t-out="line.document_out.name"
class="fw-bold"/>
</td>
<td class="p-0 text-center border-start-0">
<t t-if="displayReserve(line)">
<a t-if="line.reservation"
href="#"
name="unreserve_link"
t-on-click="() => this._unreserve(line.move_out.id)">
Unreserve
</a>
<button t-elif="line.replenishment_filled or AvailableOnHandTotalQty[line.product.id] &gt; 0"
class="btn btn-sm btn-secondary"
name="reserve_link"
t-on-click="() => this._reserve(line.move_out.id)">
Reserve
<t t-if="AvailableOnHandTotalQty[line.product.id] &gt; 0 and !isOnHand(line)">
(<t t-out="_formatFloat(Math.min(AvailableOnHandTotalQty[line.product.id], line.quantity))"/> <t t-out="line.uom_id.display_name"/>)
</t>
</button>
</t>
</td>
<td t-out="line.delivery_date or ''"
t-attf-class="#{line.delivery_late and 'text-danger'}"/>
</tr>
<t t-if="line_index === last_line_index or line.product.id !== lines[line_index + 1].product.id">
<!-- Check in progress with DALA
<tr t-if="! multipleProducts and this.props.docs.qty_to_order" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
<td/>
<td>To Order</td>
<td class="text-end">
<span class="text-muted" t-out="this.props.docs.lead_horizon_date"/>
<span ><t t-out="_formatFloat(this.props.docs.qty_to_order)"/> <t t-out="line.uom_id.display_name"/></span>
</td>
</tr> -->
<tr class="o_forecasted_row fw-bold table-secondary" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
<td/>
<td class="border-end-0">Forecasted Inventory</td>
<td class="text-end border-start-0 border-end-0" t-attf-class="#{currentProduct.virtual_available &lt;= 0 and 'text-danger' or 'text-success'}">
<t t-out="_formatFloat(currentProduct.virtual_available)"/> <t t-out="line.uom_id.display_name"/>
</td>
<td class="border-start-0" colspan="3"/>
</tr>
<tr t-if="currentProduct.draft_picking_qty.in" name="draft_picking_in" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
<td/>
<td>Incoming Draft Transfer</td>
<td class="text-end">
<t t-out="_formatFloat(currentProduct.draft_picking_qty.in)"/> <t t-out="line.uom_id.display_name"/>
</td>
</tr>
<tr t-if="currentProduct.draft_picking_qty.out" name="draft_picking_out" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
<td/>
<td>Outgoing Draft Transfer</td>
<td class="text-end">
<t t-out="_formatFloat(-currentProduct.draft_picking_qty.out)"/> <t t-out="line.uom_id.display_name"/>
</td>
</tr>
<tr class="o_forecasted_row fw-bold table-secondary" t-attf-class="#{multipleProducts and 'collapse show' or ''} collapseGroup_#{line.product.id}">
<td/>
<td class="border-end-0">Forecasted with Pending</td>
<td class="text-end border-start-0 border-end-0" t-attf-class="#{futureVirtualAvailable(line) &lt;= 0 and 'text-danger' or 'text-success'}">
<t t-out="_formatFloat(futureVirtualAvailable(line))"/> <t t-out="line.uom_id.display_name"/>
</td>
<td class="border-start-0" colspan="3"/>
</tr>
</t>
</t>
</tbody>
</table>
</t>
</templates>

View File

@@ -0,0 +1,35 @@
import { registry } from "@web/core/registry";
import { GraphRenderer } from "@web/views/graph/graph_renderer";
import { graphView } from "@web/views/graph/graph_view";
export class StockForecastedGraphRenderer extends GraphRenderer {
static template = "stock.ForecastedGraphRenderer";
getLineChartData() {
const data = super.getLineChartData();
// Ensure the line chart is stepped
data.datasets.forEach((dataset) => {
dataset.stepped = true;
dataset.spanGaps = true;
});
if (data.datasets.length) {
const dataset_length = data.datasets[0].data.length;
for(let i = dataset_length-2; i > 0; i--) { // i=0 and i=last are always preserved
let skipData = data.datasets.every(d => d.data[i] == d.data[i-1]);
if (skipData){
data.datasets.forEach((dataset) => {
dataset.data[i] = null; // Mark as null to indicate it can be skipped
});
}
}
}
return data;
}
};
export const StockForecastedGraphView = {
...graphView,
Renderer: StockForecastedGraphRenderer,
};
registry.category("views").add("stock_forecasted_graph", StockForecastedGraphView);

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.ForecastedGraphRenderer" t-inherit="web.GraphRenderer" t-inherit-mode="primary">
<xpath expr="//canvas[@t-ref='canvas']" position="attributes">
<attribute name="height">300</attribute>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,87 @@
import { useService } from "@web/core/utils/hooks";
import { formatFloat } from "@web/views/fields/formatters";
import { Component, markup } from "@odoo/owl";
export class ForecastedHeader extends Component {
static template = "stock.ForecastedHeader";
static props = { docs: Object, openView: Function };
setup(){
this.orm = useService("orm");
this.action = useService("action");
this.tooltip = useService("tooltip");
this._formatFloat = (num) => formatFloat(num, { digits: this.props.docs.precision });
}
async _onClickInventory(){
const productIds = this.props.docs.product_variants_ids;
const action = await this.orm.call('product.product', 'action_open_quants', [productIds]);
if (action.help) {
action.help = markup(action.help);
}
return this.action.doAction(action);
}
get products() {
return this.props.docs.product;
}
get leadTime() {
if (!this.products || this.products.length === 0) {
return null;
}
const productsArray = Object.values(this.products || {});
const product = productsArray.reduce((minProduct, p) => {
if (
!minProduct ||
(p.leadtime && p.leadtime.total_delay < (minProduct.leadtime?.total_delay ?? Infinity))
) {
return p;
}
return minProduct;
}, null);
const today = new Date(Date.now());
product.leadtime["today"] = today.toLocaleDateString();
product.leadtime["earliestPossibleArrival"] = this.addDays(today, product.leadtime.total_delay);
return product.leadtime;
}
get leadTimeShort() {
let short = " " + (this.leadTime.total_delay) + " day(s)";
if (this.leadTime.total_delay != 0) {
short += " (" + this.leadTime.earliestPossibleArrival + ")";
}
return short;
}
get quantityOnHand() {
return Object.values(this.products).reduce((sum, product) => sum + product.quantity_on_hand, 0);
}
get incomingQty() {
return Object.values(this.products).reduce((sum, product) => sum + product.incoming_qty, 0);
}
get outgoingQty() {
return Object.values(this.products).reduce((sum, product) => sum + product.outgoing_qty, 0);
}
get virtualAvailable() {
return Object.values(this.products).reduce((sum, product) => sum + product.virtual_available, 0);
}
get uom() {
return Object.values(this.products)[0].uom;
}
addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result.toLocaleDateString();
}
toJsonString(obj) {
return JSON.stringify(obj);
}
}

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="stock.ForecastedHeader">
<div class="d-flex flex-wrap pb-1 justify-content-between">
<div class="o_product_name">
<h3 name="product_name">
<t t-if="props.docs.product_templates">
<t t-foreach="props.docs.product_templates" t-as="product_template" t-key='product_template.id'>
<a href="#"
t-on-click.prevent="() => this.props.openView('product.template', 'form', product_template.id)"
t-out="product_template.display_name"/>
</t>
</t>
<t t-elif="props.docs.product_variants">
<t t-foreach="props.docs.product_variants" t-as="product_variant" t-key="product_variant.id">
<a href="#"
t-on-click.prevent="() => this.props.openView('product.product', 'form', product_variant.id)"
t-out="product_variant.display_name"/>
</t>
</t>
</h3>
</div>
<div class="row">
<div class="col-md-auto text-center" name="on_hand">
<div class="h3">
<a href="#"
t-on-click.prevent="() => this._onClickInventory()"
t-out="_formatFloat(this.quantityOnHand)"/>
</div>
<div>On Hand</div>
</div>
<div class="h3 col-md-auto text-center">+</div>
<div t-attf-class="col-md-auto text-center #{props.docs.incoming_qty}">
<div class="h3">
<t t-out="_formatFloat(this.incomingQty)"/>
</div>
<div>Incoming</div>
</div>
<div class="h3 col-md-auto text-center">-</div>
<div t-attf-class="col-md-auto text-center #{props.docs.outgoing_qty}">
<div class="h3">
<t t-out="_formatFloat(this.outgoingQty)"/>
</div>
<div>Outgoing</div>
</div>
<div class="h3 col-md-auto text-center">=</div>
<div t-attf-class="col-md-auto text-center #{props.docs.virtual_available &lt; 0 and 'text-danger'}" name="forecasted_value">
<div class="h3">
<span t-out="_formatFloat(this.virtualAvailable)"/>
<span t-out="' ' + this.uom" groups="uom.group_uom"/>
</div>
<div>Forecasted</div>
</div>
</div>
</div>
<div class="d-flex flex-wrap pb-1 pt-2 justify-content-end">
<div class="row">
<h6>Time to replenish:
<span data-tooltip-template="LeadTimeTooltip" t-att-data-tooltip-info="this.toJsonString(this.leadTime)">
<span class="text-info" t-out="this.leadTimeShort"/>
</span>
</h6>
</div>
</div>
</t>
<t t-name="LeadTimeTooltip">
<h6>Lead Time Information</h6>
<table>
<tr>
<td>Today</td>
<td><span t-out="today"/></td>
</tr>
<t t-foreach="details" t-as="detail" t-key="detail[0]">
<tr>
<td><span t-out="detail[0]"/></td>
<td><span t-out="detail[1]"/></td>
</tr>
</t>
<tr>
<td>Earliest Possible Arrival</td>
<td><span t-out="earliestPossibleArrival"/></td>
</tr>
</table>
</t>
</templates>

View File

@@ -0,0 +1,37 @@
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart } from "@odoo/owl";
export class ForecastedWarehouseFilter extends Component {
static template = "stock.ForecastedWarehouseFilter";
static components = { Dropdown, DropdownItem };
static props = { action: Object, setWarehouseInContext: Function, warehouses: Array };
setup() {
this.orm = useService("orm");
this.context = this.props.action.context;
this.warehouses = this.props.warehouses;
onWillStart(this.onWillStart)
}
async onWillStart() {
this.displayWarehouseFilter = (this.warehouses.length > 1);
}
_onSelected(id){
this.props.setWarehouseInContext(Number(id));
}
get activeWarehouse(){
return this.context.warehouse_id ? this.warehouses.find((w) => w.id == this.context.warehouse_id) : this.warehouses[0];
}
get warehousesItems() {
return this.warehouses.map(warehouse => ({
id: warehouse.id,
label: warehouse.name,
onSelected: () => this._onSelected(warehouse.id),
}));
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="stock.ForecastedWarehouseFilter">
<div t-if="displayWarehouseFilter" class="btn-group">
<Dropdown menuClass="o_filter_menu" items="warehousesItems">
<button class="btn btn-secondary">
<span class="fa fa-home"/> Warehouse: <t t-out="activeWarehouse.name"/>
</button>
</Dropdown>
</div>
</t>
</templates>

View File

@@ -0,0 +1,137 @@
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { View } from "@web/views/view";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { ForecastedButtons } from "./forecasted_buttons";
import { ForecastedDetails } from "./forecasted_details";
import { ForecastedHeader } from "./forecasted_header";
import { ForecastedWarehouseFilter } from "./forecasted_warehouse_filter";
import { Component, onWillStart, useState } from "@odoo/owl";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
export class StockForecasted extends Component {
static template = "stock.Forecasted";
static components = {
ControlPanel,
ForecastedButtons,
ForecastedWarehouseFilter,
ForecastedHeader,
View,
ForecastedDetails,
};
static props = { ...standardActionServiceProps };
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.context = useState(this.props.action.context);
this.productId = this.context.active_id;
this.resModel = this.context.active_model;
this.title = this.props.action.name || _t("Forecasted Report");
if(!this.context.active_id){
this.context.active_id = this.props.action.params.active_id;
this.reloadReport();
}
this.warehouses = useState([]);
onWillStart(this._getReportValues);
}
async _getReportValues() {
await this._getResModel();
const isTemplate = !this.resModel || this.resModel === 'product.template';
this.reportModelName = `stock.forecasted_product_${isTemplate ? "template" : "product"}`;
this.warehouses.splice(0, this.warehouses.length);
this.warehouses.push(...await this.orm.searchRead('stock.warehouse', [],['id', 'name', 'code']));
if (!this.context.warehouse_id) {
this.updateWarehouse(this.warehouses[0].id);
}
const reportValues = await this.orm.call(this.reportModelName, "get_report_values", [], {
context: this.context,
docids: [this.productId],
});
this.docs = {
...reportValues.docs,
...reportValues.precision,
lead_horizon_date: this.context.lead_horizon_date,
qty_to_order: this.context.qty_to_order,
};
}
async _getResModel(){
this.resModel = this.context.active_model || this.context.params?.active_model;
//Following is used as a fallback when the forecast is not called by an action but through browser's history
if (!this.resModel) {
let resModel = this.props.action.res_model;
if (resModel) {
if (/^\d+$/.test(resModel)) {
// legacy action definition where res_model is the model id instead of name
const actionModel = await this.orm.read('ir.model', [Number(resModel)], ['model']);
resModel = actionModel[0]?.model;
}
this.resModel = resModel;
} else if (this.props.action._originalAction) {
const originalContextAction = JSON.parse(this.props.action._originalAction).context;
if (typeof originalContextAction === "string") {
this.resModel = JSON.parse(originalContextAction.replace(/'/g, '"')).active_model;
} else if (originalContextAction) {
this.resModel = originalContextAction.active_model;
}
}
this.context.active_model = this.resModel;
}
}
async updateWarehouse(id) {
const hasPreviousValue = this.context.warehouse_id !== undefined;
this.context.warehouse_id = id;
if (hasPreviousValue) {
await this.reloadReport();
}
}
async reloadReport() {
const actionRequest = {
id: this.props.action.id,
type: "ir.actions.client",
tag: "stock_forecasted",
context: this.context,
name: this.title,
};
const options = { stackPosition: "replaceCurrentAction" };
return this.action.doAction(actionRequest, options);
}
get graphDomain() {
const domain = [
["state", "=", "forecast"],
["warehouse_id", "=", this.context.warehouse_id],
];
if (this.resModel === "product.template") {
domain.push(["product_tmpl_id", "=", this.productId]);
} else if (this.resModel === "product.product") {
domain.push(["product_id", "=", this.productId]);
}
return domain;
}
get graphInfo() {
return { noContentHelp: _t("Try to add some incoming or outgoing transfers.") };
}
async openView(resModel, view, resId=false, domain = false) {
const action = {
type: "ir.actions.act_window",
res_model: resModel,
views: [[false, view]],
view_mode: view,
res_id: resId,
domain: domain,
};
return this.action.doAction(action);
}
}
registry.category("actions").add("stock_forecasted", StockForecasted);

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<div t-name="stock.Forecasted" class="o_action o_stock_forecast">
<ControlPanel>
<t t-set-slot="layout-buttons">
<ForecastedButtons action="props.action" reloadReport.bind="reloadReport" resModel="resModel"/>
</t>
<t t-set-slot="layout-actions">
<div class="btn-group o_search_options position-static" role="search">
<ForecastedWarehouseFilter action="props.action" warehouses="warehouses" setWarehouseInContext.bind="updateWarehouse"/>
</div>
</t>
</ControlPanel>
<div class="o-content pt-3 container-fluid overflow-auto o_stock_forecasted_page">
<ForecastedHeader docs="docs" openView.bind="openView"/>
<t t-if="context.warehouse_id">
<View type="'graph'"
viewId="stock_report_view_graph"
resModel="'report.stock.quantity'"
domain="graphDomain"
display="{controlPanel: false}"
context="{fill_temporal: false}"
info="graphInfo"
useSampleModel="true"
/>
</t>
<ForecastedDetails docs="docs" openView.bind="openView" reloadReport.bind="reloadReport"/>
</div>
</div>
</templates>

View File

@@ -0,0 +1,19 @@
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { UPDATE_METHODS } from "@web/core/orm_service";
import { rpcBus } from "@web/core/network/rpc";
registry.category("services").add("stock_warehouse", {
dependencies: ["action"],
start(env, { action }) {
rpcBus.addEventListener("RPC:RESPONSE", (ev) => {
const { data, error } = ev.detail;
const { model, method } = data.params;
if (!error && model === "stock.warehouse") {
if (UPDATE_METHODS.includes(method) && !browser.localStorage.getItem("running_tour")) {
action.doAction("reload_context");
}
}
});
},
});

View File

@@ -0,0 +1,69 @@
import { _t } from "@web/core/l10n/translation";
import { DynamicRecordList } from "@web/model/relational_model/dynamic_record_list";
import { RelationalModel } from "@web/model/relational_model/relational_model";
export class InventoryReportListModel extends RelationalModel {
/**
* Override
*/
setup(params, { action, dialog, notification, rpc, user, view, company }) {
// model has not created any record yet
this._lastCreatedRecordId;
return super.setup(...arguments);
}
/**
* Function called when a record has been _load (after saved).
* We need to detect when the user added to the list a quant which already exists
* (see stock.quant.create), either already loaded or not, to warn the user
* the quant was updated.
* This is done by checking :
* - the record id against the '_lastCreatedRecordId' on model
* - the create_date against the write_date (both are equal for newly created records).
*
*/
async _updateSimilarRecords(reloadedRecord, serverValues) {
if (this.config.isMonoRecord) {
return;
}
const justCreated = reloadedRecord.id == this._lastCreatedRecordId;
if (justCreated && serverValues.create_date !== serverValues.write_date) {
this.notification.add(
_t(
"You tried to create a record that already exists. The existing record was modified instead."
),
{ title: _t("This record already exists") }
);
const duplicateRecords = this.root.records.filter(
(record) => record.resId === reloadedRecord.resId && record.id !== reloadedRecord.id
);
if (duplicateRecords.length > 0) {
/* more than 1 'resId' record loaded in view (user added an already loaded record) :
* - both have been updated
* - remove the current record (the added one)
*/
await this.root._removeRecords([reloadedRecord.id]);
for (const record of duplicateRecords) {
record._applyValues(serverValues);
}
}
} else {
super._updateSimilarRecords(...arguments)
}
}
}
export class InventoryReportListDynamicRecordList extends DynamicRecordList {
/**
* Override
*/
async addNewRecord() {
const record = await super.addNewRecord(...arguments);
// keep created record id on model
record.model._lastCreatedRecordId = record.id;
return record;
}
}
InventoryReportListModel.DynamicRecordList = InventoryReportListDynamicRecordList;

View File

@@ -0,0 +1,10 @@
import { listView } from "@web/views/list/list_view";
import { InventoryReportListModel } from "./inventory_report_list_model";
import { registry } from "@web/core/registry";
export const InventoryReportListView = {
...listView,
Model: InventoryReportListModel,
};
registry.category("views").add('inventory_report_list', InventoryReportListView);

View File

@@ -0,0 +1,64 @@
import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view";
import { ListRenderer } from "@web/views/list/list_renderer";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { _t } from "@web/core/l10n/translation";
export class AddPackageListRenderer extends ListRenderer {
setup() {
super.setup();
this.orm = useService("orm");
this.actionService = useService("action");
this.addDialog = useOwnedDialogs();
this.pickingId = this.props.list.context.picking_ids?.length
? this.props.list.context.picking_ids[0]
: 0;
this.locationId = this.props.list.context.location_id || 0;
this.canAddEntirePacks = this.props.list.context?.can_add_entire_packs;
}
get displayRowCreates() {
return this.canAddEntirePacks;
}
async add(params) {
await this.onClickAdd();
}
async onClickAdd() {
const domain = [];
if (this.locationId) {
domain.push(["location_id", "child_of", this.locationId]);
}
this.addDialog(SelectCreateDialog, {
title: _t("Select Packages to Move"),
noCreate: true,
multiSelect: true,
resModel: "stock.package",
domain,
context: {
list_view_ref: "stock.stock_package_view_add_list",
},
onSelected: async (resIds) => {
if (resIds.length) {
const done = await this.orm.call("stock.picking", "action_add_entire_packs", [
[this.pickingId],
resIds,
]);
if (done) {
await this.actionService.doAction({
type: "ir.actions.client",
tag: "soft_reload",
});
}
}
},
});
}
}
registry.category("views").add("stock_add_package_list_view", {
...listView,
Renderer: AddPackageListRenderer,
});

View File

@@ -0,0 +1,13 @@
import { listView } from '@web/views/list/list_view';
import { registry } from "@web/core/registry";
import { StockReportSearchModel } from "../search/stock_report_search_model";
import { StockReportSearchPanel } from '../search/stock_report_search_panel';
export const StockReportListView = {
...listView,
SearchModel: StockReportSearchModel,
SearchPanel: StockReportSearchPanel,
};
registry.category("views").add("stock_report_list_view", StockReportListView);

View File

@@ -0,0 +1,93 @@
import { registry } from "@web/core/registry";
import { ListRenderer } from "@web/views/list/list_renderer";
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
import { ProductNameAndDescriptionListRendererMixin } from "@product/product_name_and_description/product_name_and_description";
import { user } from "@web/core/user";
import { patch } from "@web/core/utils/patch";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { onWillStart } from "@odoo/owl";
import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
import { _t } from "@web/core/l10n/translation";
export class MovesListRenderer extends ListRenderer {
static rowsTemplate = "stock.AddPackageListRendererRows";
setup() {
super.setup();
this.addDialog = useOwnedDialogs();
this.orm = useService("orm");
this.actionService = useService("action");
this.descriptionColumn = "description_picking";
this.productColumns = ["product_id", "product_template_id"];
onWillStart(async () => {
this.hasPackageActive = await user.hasGroup("stock.group_tracking_lot");
});
}
async onClickMovePackage() {
// If picking doesn't exist yet or location is outdated, it will lead to incorrect results
const canOpenDialog = await this.forceSave();
if (!canOpenDialog) {
return;
}
const domain = [];
if (this.locationId) {
domain.push(["location_id", "child_of", this.locationId]);
}
this.addDialog(SelectCreateDialog, {
title: _t("Select Packages to Move"),
noCreate: true,
multiSelect: true,
resModel: "stock.package",
domain,
context: {
list_view_ref: "stock.stock_package_view_add_list",
},
onSelected: async (resIds) => {
if (resIds.length) {
const done = await this.orm.call("stock.picking", "action_add_entire_packs", [
[this.pickingId],
resIds,
]);
if (done) {
await this.actionService.doAction({
type: "ir.actions.client",
tag: "soft_reload",
});
}
}
},
});
}
get canAddPackage() {
return (
this.hasPackageActive &&
!["done", "cancel"].includes(this.props.list.context.picking_state) &&
this.props.list.context.picking_type_code !== "incoming"
);
}
async forceSave() {
// This means the record hasn't been saved once, but we need the picking id to know for which picking we create the move lines.
const record = this.env.model.root;
const result = await record.save();
this.pickingId = record.data.id;
this.locationId = record.data.location_id?.id;
return result;
}
}
patch(MovesListRenderer.prototype, ProductNameAndDescriptionListRendererMixin);
export class StockMoveX2ManyField extends X2ManyField {
static components = { ...X2ManyField.components, ListRenderer: MovesListRenderer };
}
export const stockMoveX2ManyField = {
...x2ManyField,
component: StockMoveX2ManyField,
};
registry.category("fields").add("stock_move_one2many", stockMoveX2ManyField);

View File

@@ -0,0 +1,33 @@
import { registry } from "@web/core/registry";
import { ProductNameAndDescriptionField } from "@product/product_name_and_description/product_name_and_description";
import { many2OneField } from "@web/views/fields/many2one/many2one_field";
export class MoveProductLabelField extends ProductNameAndDescriptionField {
static template = "stock.MoveProductLabelField";
static descriptionColumn = "description_picking";
get label() {
const record = this.props.record.data;
let label = record[this.descriptionColumn];
const productName = record.product_id.display_name;
if (label === productName) {
label = "";
}
return label.trim();
}
get isDescriptionReadonly() {
return this.props.readonly && ["done", "cancel"].includes(this.props.record.evalContext.parent.state);
}
get showLabelVisibilityToggler() {
return !this.isDescriptionReadonly && this.columnIsProductAndLabel.value && !this.label;
}
parseLabel(value) {
return value;
}
}
export const moveProductLabelField = {
...many2OneField,
component: MoveProductLabelField,
};
registry.category("fields").add("move_product_label_field", moveProductLabelField);

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="stock.MoveProductLabelField">
<div class="d-flex align-items-center gap-1">
<Many2One t-props="this.m2oProps" cssClass="'w-100'" t-on-keydown="onM2oInputKeydown"/>
<t t-if="showLabelVisibilityToggler">
<button
class="btn fa fa-bars text-start o_external_button px-1"
type="button"
id="labelVisibilityButtonId"
data-tooltip="Click or press enter to add a description"
t-on-click="() => this.switchLabelVisibility()"
/>
</t>
</div>
<textarea
class="o_input d-print-none border-0 fst-italic"
placeholder="Enter a description"
rows="1"
type="text"
t-att-class="{ 'd-none': !(columnIsProductAndLabel.value and (label or labelVisibility.value)) }"
t-att-readonly="isDescriptionReadonly"
t-att-value="label"
t-ref="labelNodeRef"
/>
</t>
<t t-name="stock.AddPackageListRendererRows" t-inherit="web.ListRenderer.Rows" t-inherit-mode="primary">
<td class="o_field_x2many_list_row_add" position="inside">
<a t-if="canAddPackage" href="#" class="px-3" tabindex="0" t-on-click.stop.prevent="() => this.onClickMovePackage()">Move a Pack</a>
</td>
</t>
</templates>

View File

@@ -0,0 +1,35 @@
import { useService } from "@web/core/utils/hooks";
import { SearchModel } from "@web/search/search_model";
import { debounce } from "@web/core/utils/timing";
export class StockOrderpointSearchModel extends SearchModel {
static DEBOUNCE_DELAY = 500;
setup(services) {
super.setup(services);
this.ui = useService("ui");
this.applyGlobalHorizonDays = debounce(
this.applyGlobalHorizonDays.bind(this),
StockOrderpointSearchModel.DEBOUNCE_DELAY
);
}
async applyGlobalHorizonDays(globalHorizonDays) {
this.ui.block();
this.globalContext = {
...this.globalContext,
global_horizon_days: globalHorizonDays,
};
this._context = false; // Force rebuild of this.context to take into account the updated this.globalContext
await this.orm.call("stock.warehouse.orderpoint", "action_open_orderpoints", [], {
context: {
...this.context,
force_orderpoint_recompute: true,
}
});
await this._fetchSections(this.categories, this.filters);
this._notify();
this.ui.unblock();
}
}

View File

@@ -0,0 +1,25 @@
import { useService } from "@web/core/utils/hooks";
import { onWillStart, useState } from '@odoo/owl';
import { SearchPanel } from "@web/search/search_panel/search_panel";
export class StockOrderpointSearchPanel extends SearchPanel {
static template = "stock.StockOrderpointSearchPanel";
setup() {
this.orm = useService("orm");
super.setup(...arguments);
this.globalHorizonDays = useState({value: 0});
onWillStart(this.getHorizonParameter);
}
async getHorizonParameter() {
let res = await this.orm.call("stock.warehouse.orderpoint", "get_horizon_days", [0]);
this.globalHorizonDays.value = Math.abs(parseInt(res)) || 0;
}
async applyGlobalHorizonDays(ev) {
this.globalHorizonDays.value = Math.max(parseInt(ev.target.value || 0), 0);
await this.env.searchModel.applyGlobalHorizonDays(this.globalHorizonDays.value);
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.StockOrderpointSearchPanel.Regular" t-inherit="web.SearchPanel.Regular">
<xpath expr="//div[contains(@class, 'o_search_panel')]" position="inside">
<section class="o_search_panel_section o_search_panel_filter">
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase cursor-default">
<i t-attf-class="fa fa-clock-o o_search_panel_section_icon text-warning me-2" />
<b>Horizon</b>
</header>
<div class="input-group">
<input type="number" min="0" t-att-value="globalHorizonDays.value" class="form-control" aria-describedby="days-label" t-on-change="applyGlobalHorizonDays"/>
<span class="input-group-text" id="days-label">days</span>
</div>
</section>
</xpath>
</t>
<t t-name="stock.StockOrderpointSearchPanel" t-inherit="web.SearchPanel" t-inherit-mode="primary">
<xpath expr="//t[@t-call='web.SearchPanel.Regular']" position="attributes">
<attribute name="t-call">stock.StockOrderpointSearchPanel.Regular</attribute>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,46 @@
import { SearchModel } from "@web/search/search_model";
export class StockReportSearchModel extends SearchModel {
async load() {
await super.load(...arguments);
await this._loadWarehouses();
}
//---------------------------------------------------------------------
// Actions / Getters
//---------------------------------------------------------------------
getWarehouses() {
return this.warehouses;
}
async _loadWarehouses() {
this.warehouses = await this.orm.call(
'stock.warehouse',
'get_current_warehouses',
[[]],
{ context: this.context },
);
}
/**
* Clears the context of a warehouse so values calculate based on all possible
* warehouses
*/
clearWarehouseContext() {
delete this.globalContext.warehouse_id;
this._notify();
}
/**
* @param {number} warehouse_id
* Sets the context to the selected warehouse so values that take this into account
* will recalculate based on this without filtering out any records
*/
applyWarehouseContext(warehouse_id) {
this.globalContext['warehouse_id'] = warehouse_id;
this._notify();
}
}

View File

@@ -0,0 +1,27 @@
import { SearchPanel } from "@web/search/search_panel/search_panel";
export class StockReportSearchPanel extends SearchPanel {
static template = "stock.StockReportSearchPanel";
setup() {
super.setup(...arguments);
this.selectedWarehouse = false;
}
//---------------------------------------------------------------------
// Actions / Getters
//---------------------------------------------------------------------
get warehouses() {
return this.env.searchModel.getWarehouses();
}
clearWarehouseContext() {
this.env.searchModel.clearWarehouseContext();
this.selectedWarehouse = null;
}
applyWarehouseContext(warehouse_id) {
this.env.searchModel.applyWarehouseContext(warehouse_id);
this.selectedWarehouse = warehouse_id;
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.StockReportSearchPanel.Regular" t-inherit="web.SearchPanel.Regular">
<xpath expr="//div/section[1]" position="before">
<section t-if="warehouses.length > 1" class="o_search_panel_section o_search_panel_warehouse">
<header class="o_search_panel_section_header pt-4 pb-2 text-uppercase o_cursor_default">
<i t-attf-class="fa fa-filter o_search_panel_section_icon text-warning me-2"/>
<b>Warehouses</b>
</header>
<ul class="list-group d-block o_search_panel_field">
<li class="o_search_panel_filter_value list-group-item p-0 mb-1 border-0 o_cursor_pointer">
<span t-on-click="ev => this.clearWarehouseContext(ev)" t-att-class="{'fw-bolder': !selectedWarehouse, 'o_search_panel_label_title':true}">All Warehouses</span>
</li>
<li class="o_search_panel_filter_value list-group-item p-0 mb-1 border-0 o_cursor_pointer"
t-foreach="warehouses" t-as="warehouse" t-key="warehouse.id">
<div t-out="warehouse.name"
t-on-click="ev => this.applyWarehouseContext(warehouse.id, ev)"
t-att-class="{'fw-bolder': selectedWarehouse === warehouse.id, 'o_search_panel_label_title':true,
'bg-info-subtle': selectedWarehouse === warehouse.id}"/>
</li>
</ul>
</section>
</xpath>
</t>
<t t-name="stock.StockReportSearchPanel" t-inherit="web.SearchPanel" t-inherit-mode="primary">
<xpath expr="//t[@t-call='web.SearchPanel.Regular']" position="attributes">
<attribute name="t-call">stock.StockReportSearchPanel.Regular</attribute>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,29 @@
import { registry } from "@web/core/registry";
import { listView } from "@web/views/list/list_view";
import { ListRenderer } from "@web/views/list/list_renderer";
import { Component } from "@odoo/owl";
import { useActionLinks } from "@web/views/view_hook";
export class StockActionHelper extends Component {
static template = "stock.StockActionHelper";
static props = ["noContentHelp"];
setup() {
const resModel = "searchModel" in this.env ? this.env.searchModel.resModel : undefined;
this.handler = useActionLinks(resModel);
}
}
export class StockListRenderer extends ListRenderer {
static template = "stock.StockListRenderer";
static components = {
...StockListRenderer.components,
StockActionHelper,
};
}
export const StockListView = {
...listView,
Renderer: StockListRenderer,
};
registry.category("views").add("stock_list_view", StockListView);

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.StockListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
<ActionHelper position="replace">
<t t-if="showNoContentHelper">
<StockActionHelper noContentHelp="props.noContentHelp"/>
</t>
</ActionHelper>
</t>
<t t-name="stock.StockActionHelper">
<div class="o_view_nocontent">
<div t-on-click="handler" class="o_nocontent_help">
<t t-out="props.noContentHelp"/>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,37 @@
import { ListController } from '@web/views/list/list_controller';
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
export class StockOrderpointListController extends ListController {
static template = "stock.StockOrderpoint.listView";
static components = {
...super.components,
Dropdown,
DropdownItem,
}
get nbSelected() {
return this.model.root.selection.length;
}
async onClickOrder(force_to_max) {
const resIds = await this.model.root.getResIds(true);
const action = await this.model.orm.call(this.props.resModel, 'action_replenish', [resIds], {
context: this.props.context,
force_to_max: force_to_max,
});
if (action) {
await this.actionService.doAction(action);
}
return this.actionService.doAction({type: 'ir.actions.client', tag: 'reload'});
}
async onClickSnooze() {
const resIds = await this.model.root.getResIds(true);
return this.actionService.doAction('stock.action_orderpoint_snooze', {
additionalContext: { default_orderpoint_ids: resIds },
onClose: () => { this.actionService.doAction({type: 'ir.actions.client', tag: 'reload'}); },
});
}
}

View File

@@ -0,0 +1,14 @@
import { listView } from '@web/views/list/list_view';
import { registry } from "@web/core/registry";
import { StockOrderpointListController as Controller } from './stock_orderpoint_list_controller';
import { StockOrderpointSearchPanel } from './search/stock_orderpoint_search_panel';
import { StockOrderpointSearchModel } from './search/stock_orderpoint_search_model';
export const StockOrderpointListView = {
...listView,
Controller,
SearchPanel: StockOrderpointSearchPanel,
SearchModel: StockOrderpointSearchModel,
};
registry.category("views").add("stock_orderpoint_list", StockOrderpointListView);

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.StockOrderpoint.listView" t-inherit="web.ListView">
<xpath expr="//SelectionBox" position="after">
<Dropdown>
<button class="btn btn-secondary">
<span class="o_dropdown_title">Order</span>
<i class="fa fa-caret-down ms-1"/>
</button>
<t t-set-slot="content">
<DropdownItem t-if="hasSelectedRecords" onSelected="() => this.onClickOrder(false)">
Order
</DropdownItem>
<DropdownItem t-if="hasSelectedRecords" onSelected="() => this.onClickOrder(true)">
Order To Max
</DropdownItem>
</t>
</Dropdown>
<button t-if="hasSelectedRecords" type="button" t-on-click="onClickSnooze"
class="o_button_snooze btn btn-secondary me-1">
Snooze
</button>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,72 @@
import { FloatField, floatField } from "@web/views/fields/float/float_field";
import { registry } from "@web/core/registry";
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { useEffect, useRef } from "@odoo/owl";
export class CountedQuantityWidgetField extends FloatField {
setup() {
// Need to adapt useInputField to overide onInput and onChange
super.setup();
const inputRef = useRef("numpadDecimal");
useEffect(
(inputEl) => {
if (inputEl) {
const boundOnInput = this.onInput.bind(this);
const boundOnKeydown = this.onKeydown.bind(this);
const boundOnBlur = this.onBlur.bind(this);
inputEl.addEventListener("input", boundOnInput);
inputEl.addEventListener("keydown", boundOnKeydown);
inputEl.addEventListener("blur", boundOnBlur);
return () => {
inputEl.removeEventListener("input", boundOnInput);
inputEl.removeEventListener("keydown", boundOnKeydown);
inputEl.removeEventListener("blur", boundOnBlur);
};
}
},
() => [inputRef.el]
);
}
onInput(ev) {
//TODO remove in master
}
updateValue(ev){
try {
const val = this.parse(ev.target.value);
this.props.record.update({ [this.props.name]: val, inventory_quantity_set: true });
} catch {} // ignore since it will be handled later
}
onBlur(ev) {
this.updateValue(ev);
}
onKeydown(ev) {
const hotkey = getActiveHotkey(ev);
if (["enter", "tab", "shift+tab"].includes(hotkey)) {
this.updateValue(ev);
this.onInput(ev);
}
}
get formattedValue() {
if (
this.props.readonly &&
!this.props.record.data[this.props.name] & !this.props.record.data.inventory_quantity_set
) {
return "";
}
return super.formattedValue;
}
}
export const countedQuantityWidgetField = {
...floatField,
component: CountedQuantityWidgetField,
};
registry.category("fields").add("counted_quantity_widget", countedQuantityWidgetField);

View File

@@ -0,0 +1,28 @@
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
import { buildM2OFieldDescription, Many2OneField } from "@web/views/fields/many2one/many2one_field";
export class ForcedPlaceholder extends Many2One {
static template = "stock.ForcedPlaceholder";
static components = { ...Many2One.components };
static props = { ...Many2One.props };
}
export class ForcedPlaceholderField extends Component {
static template = "stock.ForcedPlaceholderField";
static components = { ForcedPlaceholder };
static props = { ...Many2OneField.props };
get m2oProps() {
const props = computeM2OProps(this.props);
return {
...props,
canOpen: !props.readonly && props.canOpen, // to remove the wrong link and the hand cursor on hover
}
}
}
registry.category("fields").add("stock.forced_placeholder", {
...buildM2OFieldDescription(ForcedPlaceholderField),
});

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--This field displays a placeholder even if the field is currently not selected.-->
<templates xml:space="preserve">
<t t-name="stock.ForcedPlaceholderField">
<ForcedPlaceholder t-props="m2oProps"/>
</t>
<t t-name="stock.ForcedPlaceholder" t-inherit="web.Many2One">
<xpath expr="//t[@t-if='props.value']" position="after">
<t t-else="">
<span t-esc="props.placeholder"/>
</t>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,66 @@
import { FloatField, floatField } from "@web/views/fields/float/float_field";
import { formatDate } from "@web/core/l10n/dates";
import { formatFloat } from "@web/views/fields/formatters";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
export class ForecastWidgetField extends FloatField {
static template = "stock.ForecastWidget";
setup() {
const { data, fields, resId } = this.props.record;
this.actionService = useService("action");
this.orm = useService("orm");
this.resId = resId;
this.forecastExpectedDate = formatDate(
data.forecast_expected_date,
fields.forecast_expected_date
);
if (data.forecast_expected_date && data.date_deadline) {
this.forecastIsLate = data.forecast_expected_date > data.date_deadline;
}
const digits = fields.forecast_availability.digits;
const options = { digits, thousandsSep: "", decimalPoint: "." };
const forecast_availability = parseFloat(formatFloat(data.forecast_availability, options));
const product_qty = parseFloat(formatFloat(data.product_qty, options));
this.willBeFulfilled = forecast_availability >= product_qty;
this.state = data.state;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Opens the Forecast Report for the `stock.move` product.
*/
async _openReport(ev) {
ev.preventDefault();
ev.stopPropagation();
if (!this.resId || !this.props.record.data.is_storable) {
return;
}
const action = await this.orm.call("stock.move", "action_product_forecast_report", [
this.resId,
]);
this.actionService.doAction(action);
}
get decoration() {
if (!this.forecastExpectedDate && this.willBeFulfilled){
return "text-bg-success"
} else if (this.forecastExpectedDate && this.willBeFulfilled){
return this.forecastIsLate ? 'text-bg-danger' : 'text-bg-warning'
} else {
return 'text-bg-danger'
}
}
}
export const forecastWidgetField = {
...floatField,
component: ForecastWidgetField,
};
registry.category("fields").add("forecast_widget", forecastWidgetField);

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.ForecastWidget">
<span title="Forecasted Report"
t-on-click="_openReport" t-att="resId ? {} : {'disabled': ''}"
class="badge rounded-pill align-middle"
t-att-class="decoration"
>
<t t-if="!forecastExpectedDate and willBeFulfilled">
Available
</t>
<t t-elif="forecastExpectedDate and willBeFulfilled">
Exp <t t-out="forecastExpectedDate"/>
</t>
<t t-else="">Not Available</t>
</span>
</t>
</templates>

View File

@@ -0,0 +1,142 @@
import { _t } from "@web/core/l10n/translation";
import { x2ManyCommands } from "@web/core/orm_service";
import { Dialog } from '@web/core/dialog/dialog';
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { parseInteger } from "@web/views/fields/parsers";
import { getId } from "@web/model/relational_model/utils";
import { Component, useRef, onMounted, onWillStart } from "@odoo/owl";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { user } from "@web/core/user";
export class GenerateDialog extends Component {
static template = "stock.generate_serial_dialog";
static components = { Dialog };
static props = {
mode: { type: String },
move: { type: Object },
close: { type: Function },
};
setup() {
this.size = 'md';
if (this.props.mode === 'generate') {
this.title = this.props.move.data.has_tracking === 'lot'
? _t("Generate Lot numbers")
: _t("Generate Serial numbers");
} else {
this.title = this.props.move.data.has_tracking === 'lot' ? _t("Import Lots") : _t("Import Serials");
}
this.nextSerial = useRef('nextSerial');
this.nextSerialCount = useRef('nextSerialCount');
this.totalReceived = useRef('totalReceived');
this.keepLines = useRef('keepLines');
this.lots = useRef('lots');
this.orm = useService("orm");
onWillStart(async () => {
this.displayUOM = await user.hasGroup("uom.group_uom");
});
onMounted(() => {
if (this.props.mode === 'generate') {
this.nextSerialCount.el.value = this.props.move.data.product_uom_qty || 2;
if (this.props.move.data.has_tracking === 'lot') {
this.totalReceived.el.value = this.props.move.data.quantity;
}
}
});
}
async _onGenerateCustomSerial() {
const product = (await this.orm.searchRead("product.product", [["id", "=", this.props.move.data.product_id.id]], ["lot_sequence_id"]))[0];
this.sequence = product.lot_sequence_id;
if (product.lot_sequence_id) {
this.sequence = (await this.orm.searchRead("ir.sequence", [["id", "=", this.sequence[0]]], ["number_next_actual"]))[0];
this.nextCustomSerialNumber = await this.orm.call("ir.sequence", "next_by_id", [this.sequence.id]);
this.nextSerial.el.value = this.nextCustomSerialNumber;
}
}
async _onGenerate() {
let count;
let qtyToProcess;
if (this.props.move.data.has_tracking === 'lot'){
count = parseFloat(this.nextSerialCount.el?.value || '0');
qtyToProcess = parseFloat(this.totalReceived.el?.value || this.props.move.data.product_qty);
} else {
count = parseInteger(this.nextSerialCount.el?.value || '0');
qtyToProcess = this.props.move.data.product_qty;
}
const move_line_vals = await this.orm.call("stock.move", "action_generate_lot_line_vals", [{
...this.props.move.context,
default_product_id: this.props.move.data.product_id.id,
default_location_dest_id: this.props.move.data.location_dest_id.id,
default_location_id: this.props.move.data.location_id.id,
default_tracking: this.props.move.data.has_tracking,
default_quantity: qtyToProcess,
},
this.props.mode,
this.nextSerial.el?.value,
count,
this.lots.el?.value,
]);
const newlines = [];
let lines = []
lines = this.props.move.data.move_line_ids;
// create records directly from values to bypass onchanges
for (const values of move_line_vals) {
newlines.push(
lines._createRecordDatapoint(values, {
mode: 'readonly',
virtualId: getId("virtual"),
manuallyAdded: false,
})
);
}
if (!this.keepLines.el.checked) {
await lines._applyCommands(lines._currentIds.map((currentId) => [
x2ManyCommands.DELETE,
currentId,
]));
}
lines.records.push(...newlines);
lines._commands.push(...newlines.map((record) => [
x2ManyCommands.CREATE,
record._virtualId,
]));
lines._currentIds.push(...newlines.map((record) => record._virtualId));
await lines._onUpdate();
this.props.close();
}
}
class GenerateSerials extends Component {
static template = "stock.GenerateSerials";
static props = {...standardWidgetProps};
setup(){
this.dialog = useService("dialog");
}
openDialog(ev){
this.dialog.add(GenerateDialog, {
move: this.props.record,
mode: 'generate',
});
}
}
class ImportLots extends Component {
static template = "stock.ImportLots";
static props = {...standardWidgetProps};
setup(){
this.dialog = useService("dialog");
}
openDialog(ev){
this.dialog.add(GenerateDialog, {
move: this.props.record,
mode: 'import',
});
}
}
registry.category("view_widgets").add("import_lots", {component: ImportLots});
registry.category("view_widgets").add("generate_serials", {component: GenerateSerials});

View File

@@ -0,0 +1,166 @@
import { loadBundle } from "@web/core/assets";
import { cookie } from "@web/core/browser/cookie";
import { getColor } from "@web/core/colors/colors";
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation";
import { user } from "@web/core/user";
import { Component, onWillStart, useEffect, useRef } from "@odoo/owl";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
export class JsonPopOver extends Component {
static template = "";
static props = {...standardFieldProps};
get jsonValue() {
return JSON.parse(this.props.record.data[this.props.name]);
}
}
export const jsonPopOver = {
component: JsonPopOver,
displayName: _t("Json Popup"),
supportedTypes: ["char"],
};
// --------------------------------------------------------------------------
// Lead Days
// --------------------------------------------------------------------------
export class PopOverLeadDays extends JsonPopOver {
static template = "stock.leadDays";
}
export const popOverLeadDays = {
...jsonPopOver,
component: PopOverLeadDays,
};
registry.category("fields").add("lead_days_widget", popOverLeadDays);
// --------------------------------------------------------------------------
// Forecast Graph
// --------------------------------------------------------------------------
export class ReplenishmentGraphWidget extends JsonPopOver {
static template = "stock.replenishmentGraph";
setup() {
super.setup();
this.chart = null;
this.canvasRef = useRef("canvas");
onWillStart(async () => {
this.displayUOM = await user.hasGroup("uom.group_uom");
await loadBundle("web.chartjs_lib");
});
useEffect(() => {
this.renderChart();
return () => {
if (this.chart) {
this.chart.destroy();
}
};
});
}
get productUomName(){
return this.jsonValue["product_uom_name"];
}
get qtyOnHand(){
return this.jsonValue["qty_on_hand"];
}
get productMaxQty() {
return this.jsonValue["product_max_qty"];
}
get productMinQty() {
return this.jsonValue["product_min_qty"];
}
get dailyDemand() {
return this.jsonValue["daily_demand"];
}
get averageStock() {
return this.jsonValue["average_stock"];
}
get orderingPeriod() {
return this.jsonValue["ordering_period"];
}
get qtiesAreTheSame() {
return this.productMinQty === this.productMaxQty;
}
get leadTime() {
return this.jsonValue["lead_time"];
}
renderChart() {
if (this.chart) {
this.chart.destroy();
}
const config = this.getScatterGraphConfig();
this.chart = new Chart(this.canvasRef.el, config);
}
getScatterGraphConfig() {
const dashLine = (ctx, value) => ctx.p1.raw.x === this.jsonValue['x_axis_vals'].slice(-1)[0] ? value : undefined;
const pushYLabels = (ticks) => ticks.push({value: this.productMinQty}, {value: this.productMaxQty});
const showYLabel = (tick) => tick === this.productMinQty || tick === this.productMaxQty ? tick : '';
const labels = this.jsonValue['x_axis_vals'];
const maxLineColor = getColor(1, cookie.get("color_scheme"), "odoo");
const minLineColor = getColor(2, cookie.get("color_scheme"), "odoo");
const curveLineColor = getColor(3, cookie.get("color_scheme"), "odoo");
return {
type: "scatter",
data: {
labels,
datasets: [{
type: "line",
data: this.jsonValue["max_line_vals"],
fill: false,
pointStyle: false,
borderColor: maxLineColor,
}, {
type: "line",
data: this.jsonValue["min_line_vals"],
fill: false,
pointStyle: false,
borderColor: minLineColor,
}, {
type: "line",
data: this.jsonValue["curve_line_vals"],
fill: false,
pointStyle: false,
borderColor: curveLineColor,
segment: {
borderDash: ctx => dashLine(ctx, [6, 6]),
}
}],
},
options: {
maintainAspectRatio: false,
showLine: true,
plugins: {
legend: { display: false },
tooltip: { enabled: false },
},
scales: {
y: {
grid: {display: false},
beforeTickToLabelConversion: data => pushYLabels(data.ticks),
ticks: {
autoSkip: false,
callback: tick => showYLabel(tick),
},
suggestedMax: this.productMaxQty * 1.1,
suggestedMin: this.productMinQty * 0.9,
},
x: {
type: 'category',
grid: {display: false},
},
},
},
}
}
}
export const replenishmentGraphWidget = {
...jsonPopOver,
component: ReplenishmentGraphWidget,
};
registry.category("fields").add("replenishment_graph_widget", replenishmentGraphWidget);

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<div t-name="stock.leadDays">
<h2>Forecasted Date</h2>
<hr/>
<table t-if="jsonValue.lead_days_description" class="table table-borderless table-sm">
<tbody>
<tr class="table-secondary">
<td>Today</td>
<td class="text-end" t-out="jsonValue.today"/>
</tr>
<tr t-foreach="jsonValue.lead_days_description" t-key="descr_index" t-as="descr"
t-attf-class="{{ descr[2] ? 'table-secondary' : '' }}">
<td t-out="descr[0]"/>
<td class="text-end" t-out="descr[1]"/>
</tr>
<tr class="table-info">
<td>Forecasted Date</td>
<td class="text-end text-nowrap">
= <t t-out="jsonValue.lead_horizon_date"/>
</td>
</tr>
</tbody>
</table>
</div>
<div t-name="stock.replenishmentGraph" class="col-11">
<div class="o_replenishment_graph">
<canvas t-ref="canvas"/>
</div>
<div class="d-flex pt-3">
<h6 class="row text-muted">
<span>Daily Demand: <span t-out="dailyDemand"/> <span t-out="productUomName"/>/day</span>
<span class="pt-2">Average Stock: <span t-out="averageStock"/> <span t-out="productUomName"/></span>
</h6>
<h6 class="row text-muted">
<span t-if="!qtiesAreTheSame">Ordering Frequency: <span t-out="orderingPeriod"/> day(s)</span>
<span t-if="qtiesAreTheSame">Ordering Frequency: On demand</span>
<span class="pt-2">Lead Time: <span t-out="leadTime"/> day(s)</span>
</h6>
</div>
</div>
</templates>

View File

@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.GenerateSerials">
<button class="btn btn-link" t-on-click="openDialog">Generate Serials/Lots</button>
</t>
<t t-name="stock.ImportLots">
<button class="btn btn-link" t-on-click="openDialog">Import Serials/Lots</button>
</t>
<t t-name="stock.generate_serial_dialog">
<Dialog size="size" title="title" withBodyPadding="false">
<t t-set-slot="footer">
<button class="btn btn-primary" t-on-click="_onGenerate">Generate</button>
<button class="btn btn-secondary" t-on-click="() => this.props.close()">
Discard
</button>
</t>
<div class="o_form_view o_form_nosheet">
<t t-if="props.mode === 'generate'">
<div class="container">
<div class="row mb-2">
<div class="col-3">
<label class="o_form_label" for="next_serial_0">
<t t-if="props.move.data.has_tracking === 'lot'">First Lot Number</t>
<t t-else="">First Serial Number</t>
</label>
</div>
<div class="col-7">
<div name="next_serial" class="o_field_widget o_field_char">
<input placeholder="e.g. LOT-PR-00012" class="o_input" t-ref="nextSerial" id="next_serial_0" type="text"/>
</div>
</div>
<div class="col">
<button class="btn btn-primary py-1" t-on-click="_onGenerateCustomSerial">New</button>
</div>
</div>
<div class="row mb-2">
<div class="col">
<label class="o_form_label" for="next_serial_count_0">
<t t-if="props.move.data.has_tracking === 'lot'">Quantity per Lot</t>
<t t-else="">Number of SN</t>
</label>
</div>
<div class="col-9">
<div name="next_serial_count" class="o_field_widget o_field_integer">
<input inputmode="numeric" t-ref="nextSerialCount" class="o_input" id="next_serial_count_0" type="number"
t-on-keydown="(ev) => props.move.data.has_tracking != 'lot' and ev.key === '.' and ev.preventDefault()"/>
</div>
<span t-if="props.move.data.has_tracking === 'lot' &amp;&amp; displayUOM" t-esc="props.move.data.product_uom.display_name"/>
</div>
</div>
<div t-if="props.move.data.has_tracking === 'lot'" class="row mb-2">
<div class="col">
<label class="o_form_label" for="total_received_0">Quantity Received</label>
</div>
<div class="col-9">
<div name="total_received" class="o_field_widget o_field_integer">
<input inputmode="numeric" t-ref="totalReceived" class="o_input" id="total_received_0" type="number"/>
</div>
<span t-if="displayUOM" t-esc="props.move.data.product_uom.display_name"/>
</div>
</div>
<div class="row">
<div class="col">
<label class="o_form_label" for="keep_lines_0">Keep current lines</label>
</div>
<div class="col-9">
<div name="keep_lines">
<input type="checkbox" t-ref="keepLines" id="keep_lines_0"/>
</div>
</div>
</div>
</div>
</t>
<t t-if="props.mode === 'import'" class="d-flex">
<div class="grid o_inner_group">
<div class="d-flex">
<div class="o_cell flex-grow-0 flex-sm-grow-0 text-900 pe-3">
<label class="o_form_label" for="next_serial_0">
<t t-if="props.move.data.has_tracking==='lot'">Lot numbers</t>
<t t-else="">Serial numbers</t>
</label>
</div>
</div>
<div class="o_cell flex-grow-1 flex-sm-grow-0">
<div name="next_serial" class="o_field_widget o_field_char">
<textarea
placeholder="Write one lot/serial name per line, followed by the quantity."
class="o_input" t-ref="lots" id="next_serial_0" type="text"/>
</div>
</div>
<div class="d-flex">
<div class="o_cell flex-grow-0 flex-sm-grow-0 text-900 pe-3">
<label class="o_form_label" for="keep_lines_0">Keep current lines</label>
</div>
</div>
<div class="o_cell flex-grow-1 flex-sm-grow-0">
<div name="keep_lines">
<input type="checkbox" t-ref="keepLines" id="keep_lines_0"/>
</div>
</div>
</div>
</t>
</div>
</Dialog>
</t>
</templates>

View File

@@ -0,0 +1,30 @@
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import {
Many2ManyTagsField,
many2ManyTagsField,
} from "@web/views/fields/many2many_tags/many2many_tags_field";
import { registry } from "@web/core/registry";
export class Many2XBarcodeTagsAutocomplete extends Many2XAutocomplete {
onQuickCreateError(error, request) {
if (error.data?.debug && error.data.debug.includes("psycopg2.errors.UniqueViolation")) {
throw error;
}
super.onQuickCreateError(error, request);
}
}
export class Many2ManyBarcodeTagsField extends Many2ManyTagsField {
static components = {
...Many2ManyTagsField.components,
Many2XAutocomplete: Many2XBarcodeTagsAutocomplete,
};
}
export const many2ManyBarcodeTagsField = {
...many2ManyTagsField,
component: Many2ManyBarcodeTagsField,
additionalClasses: ['o_field_many2many_tags'],
}
registry.category("fields").add("many2many_barcode_tags", many2ManyBarcodeTagsField);

View File

@@ -0,0 +1,51 @@
import { registry } from "@web/core/registry";
import { usePopover } from "@web/core/popover/popover_hook";
import { Component } from "@odoo/owl";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
/**
* Extend this to add functionality to Popover (custom methods etc.)
* need to extend PopoverWidgetField as well and set its Popover Component to new extension
*/
export class PopoverComponent extends Component {
static template = "stock.popoverContent";
static props = ["record", "*"];
}
/**
* Widget Popover for JSON field (char), renders a popover above an icon button on click
* {
* 'msg': '<CONTENT OF THE POPOVER>' required if not 'popoverTemplate' is given,
* 'icon': '<FONT AWESOME CLASS>' default='fa-info-circle',
* 'color': '<COLOR CLASS OF ICON>' default='text-primary',
* 'position': <POSITION OF THE POPOVER> default='top',
* 'popoverTemplate': '<TEMPLATE OF THE POPOVER>' default='stock.popoverContent'
* pass a template for popover to use, other data passed in JSON field will be passed
* to popover template inside props (ex. props.someValue), must be owl template
* }
*/
export class PopoverWidgetField extends Component {
static template = "stock.popoverButton";
static components = { Popover: PopoverComponent };
static props = {...standardFieldProps};
setup(){
let fieldValue = this.props.record.data[this.props.name];
this.jsonValue = JSON.parse(fieldValue || "{}");
const position = this.jsonValue.position || "top";
this.popover = usePopover(this.constructor.components.Popover, { position });
this.color = this.jsonValue.color || 'text-primary';
this.icon = this.jsonValue.icon || 'fa-info-circle';
}
showPopup(ev){
this.popover.open(ev.currentTarget, { ...this.jsonValue, record: this.props.record });
}
}
export const popoverWidgetField = {
component: PopoverWidgetField,
supportedTypes: ['char'],
};
registry.category("fields").add("popover_widget", popoverWidgetField);

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="stock.popoverButton">
<a tabindex="0" t-on-click.stop="showPopup" t-attf-class="p-1 fa #{ icon || 'fa-info-circle'} #{ color || 'text-primary'}"/>
</t>
<div t-name="stock.popoverContent" class="m-3">
<h6 t-out="props.title"/>
<t t-if="props.popoverTemplate" t-call="{{props.popoverTemplate}}" />
<t t-else="" t-out="props.msg"/>
</div>
</templates>

View File

@@ -0,0 +1,43 @@
import {
Many2ManyTagsField,
many2ManyTagsField,
} from "@web/views/fields/many2many_tags/many2many_tags_field";
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation";
export class Many2ManyPackageTagsField extends Many2ManyTagsField {
setup() {
this.hasNoneTag = this.props.record.data?.has_lines_without_result_package || false;
}
get tags() {
const tags = super.tags;
if (this.hasNoneTag) {
tags.push({
...this.getTagProps(this.props.record.data[this.props.name].records.at(-1)),
id: "datapoint_None",
text: _t("No Package"),
});
}
return tags;
}
getTagProps(record) {
return {
...super.getTagProps(record),
text: record.data.name,
};
}
}
export const many2ManyPackageTagsField = {
...many2ManyTagsField,
component: Many2ManyPackageTagsField,
additionalClasses: ['o_field_many2many_tags'],
relatedFields: () => [
{ name: "name", type: "char" },
],
}
registry.category("fields").add("package_m2m", many2ManyPackageTagsField);

View File

@@ -0,0 +1,98 @@
import { Component } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
import {
buildM2OFieldDescription,
extractM2OFieldProps,
Many2OneField,
} from "@web/views/fields/many2one/many2one_field";
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
class PackageFormDialog extends FormViewDialog {}
class Many2XStockPackageAutocomplete extends Many2XAutocomplete {
get createDialog() {
const packageFormDialog = PackageFormDialog;
packageFormDialog.defaultProps = {
...packageFormDialog.defaultProps,
onRecordSave: async (record) => {
// We need to reload to get the name computed from the backend.
const saved = await record.save({ reload: true });
if (saved && this.props.update) {
// Without this, the package is named 'Unnamed' in the UI until the record is saved.
this.props.update([{ ...record.data, id: record.resId }]);
}
return saved;
},
};
return packageFormDialog;
}
}
class StockPackageMany2OneReplacer extends Many2One {
static components = {
...Many2One.components,
Many2XAutocomplete: Many2XStockPackageAutocomplete,
};
}
export class StockPackageMany2One extends Component {
static template = "stock.StockPackageMany2One";
static components = { Many2One: StockPackageMany2OneReplacer };
static props = {
...Many2OneField.props,
displaySource: { type: Boolean },
displayDestination: { type: Boolean },
};
setup() {
this.orm = useService("orm");
this.isDone = ["done", "cancel"].includes(this.props.record?.data?.state);
}
get m2oProps() {
const props = computeM2OProps(this.props);
return {
...props,
context: {
...props.context,
...this.displayNameContext,
},
value: this.displayValue,
};
}
get isEditing() {
return this.props.record.isInEdition;
}
get displayValue() {
const displayVal = this.props.record.data[this.props.name];
if (this.isDone && displayVal?.display_name) {
displayVal["display_name"] = displayVal["display_name"].split(" > ").pop();
}
return displayVal;
}
get displayNameContext() {
return {
show_src_package: this.props.displaySource,
show_dest_package: this.props.displayDestination,
is_done: this.isDone,
};
}
}
registry.category("fields").add("package_m2o", {
...buildM2OFieldDescription(StockPackageMany2One),
extractProps(staticInfo, dynamicInfo) {
const context = dynamicInfo.context;
return {
...extractM2OFieldProps(staticInfo, dynamicInfo),
displaySource: !!context?.show_src_package,
displayDestination: !!context?.show_dest_package,
};
},
});

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="stock.StockPackageMany2One">
<t t-if="!isEditing" t-out="displayValue.display_name"/>
<Many2One t-else="" t-props="m2oProps"/>
</t>
</templates>

View File

@@ -0,0 +1,53 @@
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
import { buildM2OFieldDescription, Many2OneField } from "@web/views/fields/many2one/many2one_field";
export class StockPickFrom extends Component {
static template = "stock.StockPickFrom";
static components = { Many2One };
static props = { ...Many2OneField.props };
get m2oProps() {
const props = computeM2OProps(this.props);
return {
...props,
value: props.value || { id: 0, display_name: this._quant_display_name() },
};
}
_quant_display_name() {
let name_parts = [];
// if location group is activated
const data = this.props.record.data;
name_parts.push(data.location_id?.display_name)
if (data.lot_id) {
name_parts.push(data.lot_id?.display_name || data.lot_name)
}
if (data.package_id) {
let packageName = data.package_id?.display_name;
if (packageName && ["done", "cancel"].includes(data.state)) {
packageName = packageName.split(" > ").pop();
}
name_parts.push(packageName);
}
if (data.owner) {
name_parts.push(data.owner?.display_name)
}
const result = name_parts.join(" - ");
if (result) return result;
return "";
}
}
registry.category("fields").add("pick_from", {
...buildM2OFieldDescription(StockPickFrom),
fieldDependencies: [
// dependencies to build the quant display name
{ name: "location_id", type: "relation" },
{ name: "location_dest_id", type: "relation" },
{ name: "package_id", type: "relation" },
{ name: "owner_id", type: "relation" },
{ name: "state", type: "char" },
],
});

View File

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

View File

@@ -0,0 +1,46 @@
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
import {
PopoverComponent,
PopoverWidgetField,
popoverWidgetField,
} from "@stock/widgets/popover_widget";
export class StockRescheculingPopoverComponent extends PopoverComponent {
setup(){
this.action = useService("action");
}
openElement(ev){
this.action.doAction({
res_model: ev.currentTarget.getAttribute('element-model'),
res_id: parseInt(ev.currentTarget.getAttribute('element-id')),
views: [[false, "form"]],
type: "ir.actions.act_window",
view_mode: "form",
});
}
}
export class StockRescheculingPopover extends PopoverWidgetField {
static components = {
Popover: StockRescheculingPopoverComponent
};
setup(){
super.setup();
this.color = this.jsonValue.color || 'text-danger';
this.icon = this.jsonValue.icon || 'fa-exclamation-triangle';
}
showPopup(ev){
if (!this.jsonValue.late_elements){
return;
}
super.showPopup(ev);
}
}
registry.category("fields").add("stock_rescheduling_popover", {
...popoverWidgetField,
component: StockRescheculingPopover,
});

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<div t-name="stock.PopoverStockRescheduling">
<h6>Planning Issue</h6>
<p>Preceding operations
<t t-foreach="props.late_elements" t-as="late_element" t-key="late_element.id">
<a t-out="late_element.name" t-on-click="openElement" href="#" t-att-element-id="late_element.id" t-att-element-model="late_element.model"/>,
</t>
planned on <t t-out="props.delay_alert_date"/>.</p>
</div>
</templates>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="InventoryLines.Buttons">
<button type="button" class='btn btn-primary o_button_validate_inventory'>
Validate Inventory
</button>
</t>
</templates>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="reception_report_buttons">
<button
name="assign_all_link"
class="btn btn-secondary o_report_reception_assign o_assign_all">
Assign All
</button>
<button
name="print_all_labels"
class="btn btn-secondary o_print_label o_print_label_all">
Print Labels
</button>
</t>
</templates>

View File

@@ -0,0 +1,68 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Quant extends models.Model {
quantity = fields.Float();
inventory_quantity = fields.Float({
string: "Counted quantity",
onChange: (quant) => {
quant.inventory_diff_quantity = quant.inventory_quantity - quant.quantity;
},
});
inventory_quantity_set = fields.Boolean({
string: "Inventory quantity set",
});
inventory_diff_quantity = fields.Float({ string: "Difference" });
_records = [{ id: 1, quantity: 50 }];
}
defineModels([Quant]);
defineMailModels();
test("Test changing the inventory quantity with the widget", async function () {
await mountView({
type: "list",
resModel: "quant",
arch: `<list editable="bottom">
<field name="quantity"/>
<field name="inventory_quantity" widget="counted_quantity_widget"/>
<field name="inventory_quantity_set"/>
<field name="inventory_diff_quantity"/>
</list>
`,
});
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("23");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-27);
expect("td[name=inventory_quantity_set] div input").toBeChecked();
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("40.5");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-9.5);
});
test("Test setting the inventory quantity to its default value of 0", async function () {
await mountView({
type: "list",
resModel: "quant",
arch: `<list editable="bottom">
<field name="quantity"/>
<field name="inventory_quantity" widget="counted_quantity_widget"/>
<field name="inventory_quantity_set"/>
<field name="inventory_diff_quantity"/>
</list>
`,
});
await contains("td.o_counted_quantity_widget_cell").click();
await contains("td.o_counted_quantity_widget_cell input").edit("0");
await contains("td[name=inventory_diff_quantity]").click();
expect("td[name=inventory_diff_quantity] div input").toHaveValue(-50);
});

View File

@@ -0,0 +1,187 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { animationFrame, queryOne } from "@odoo/hoot-dom";
import {
contains,
defineModels,
fields,
MockServer,
models,
mountView,
onRpc,
} from "@web/../tests/web_test_helpers";
const { DateTime } = luxon;
const arch = `
<list editable="top" js_class="inventory_report_list">
<field name="name"/>
<field name="age"/>
<field name="job"/>
<field name="create_date" invisible="1"/>
<field name="write_date" invisible="1"/>
</list>
`;
const setup_date = "2022-01-03 08:03:44";
onRpc("person", "web_save", ({ args }) => {
// simulate 'stock.quant' create function which can return existing record
const values = args[1];
const existingRecord = MockServer.env.person.find((p) => p.name === values.name);
if (existingRecord) {
values.create_date = existingRecord.create_date;
values.write_date = DateTime.now().toSQL();
return [Object.assign(existingRecord, values)];
}
});
class Person extends models.Model {
name = fields.Char();
age = fields.Integer();
job = fields.Char({ string: "Profession" });
create_date = fields.Datetime({ string: "Created on" });
write_date = fields.Datetime({ string: "Last Updated on" });
_records = [
{
id: 1,
name: "Daniel Fortesque",
age: 32,
job: "Soldier",
create_date: setup_date,
write_date: setup_date,
},
{
id: 2,
name: "Samuel Oak",
age: 64,
job: "Professor",
create_date: setup_date,
write_date: setup_date,
},
{
id: 3,
name: "Leto II Atreides",
age: 128,
job: "Emperor",
create_date: setup_date,
write_date: setup_date,
},
];
}
defineModels([Person]);
defineMailModels();
test("Create new record correctly", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
});
// Check we have initially 3 records
expect(".o_data_row").toHaveCount(3);
// Create a new line...
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
await contains("[name=name] input").edit("Bilou", { confirm: false });
await contains("[name=age] input").edit("24", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check new record is in the list
expect(".o_data_row").toHaveCount(4);
});
test("Don't duplicate record", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
});
// Check we have initially 3 records
expect(".o_data_row").toHaveCount(3);
// Create a new line for an existing record...
await contains(".o_control_panel_main_buttons .o_list_button_add").click();
await contains("[name=name] input").edit("Leto II Atreides", { confirm: false });
await contains("[name=age] input").edit("72", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
expect(".o_data_row").toHaveCount(3, { message: "should still have 3 records" });
expect(".o_data_row:eq(2) .o_list_number").toHaveText("72", {
message: "The age field must be updated",
});
await animationFrame();
expect(".o_notification").toHaveCount(1);
expect(".o_notification .o_notification_body").toHaveText(
"This record already exists. You tried to create a record that already exists. The existing record was modified instead."
);
});
test("Work in grouped list", async function () {
await mountView({
type: "list",
resModel: "person",
arch,
context: {
inventory_mode: true,
},
groupBy: ["job"], // Groups are Emperor, Professor, Soldier
});
// Open 'Professor' group
await contains(".o_group_header:eq(1)").click();
// Check we have only 1 record...
expect(".o_data_row").toHaveCount(1);
// Create a new record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Del Tutorial", { confirm: false });
await contains("[name=age] input").edit("32", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we have 2 records...
expect(".o_data_row").toHaveCount(2);
// Create an existing record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Samuel Oak", { confirm: false });
await contains("[name=age] input").edit("55", { confirm: false });
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we still have 2 records...
expect(".o_data_row").toHaveCount(2);
// Create an existing but not displayed record...
await contains(".o_group_field_row_add a").click();
await contains("[name=name] input").edit("Daniel Fortesque", { confirm: false });
await contains("[name=age] input").edit("55", { confirm: false });
await contains("[name=job] input").edit("Soldier", { confirm: false }); // let it in its original group
await contains(".o_control_panel_main_buttons .o_list_button_save").click();
// Check we have 3 records...
expect(".o_data_row").toHaveCount(3);
// Opens 'Soldier' group
await contains(".o_group_header:eq(2)").click();
// Check 'original' record has been updated...
// : Daniel Fortesque is in record 0 for group Soldier and in record 3 for group Professor
expect('.o_data_row:eq(0) [name="age"]').toHaveText("55");
// Edit the freshly created record...
await contains(".o_data_row:eq(3) .o_field_cell").click();
await contains("[name=age] input").edit("66");
// Check both records have been updated...
expect(queryOne('.o_data_row:eq(0) [name="age"]').textContent).toBe(
queryOne('.o_data_row:eq(3) [name="age"]').textContent
);
});

View File

@@ -0,0 +1,35 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { contains, defineModels, fields, models, mountView } from "@web/../tests/web_test_helpers";
class Partner extends models.Model {
json_data = fields.Char();
_records = [
{
id: 1,
json_data:
'{"color": "text-danger", "msg": "var that = self // why not?", "title": "JS Master"}',
},
];
}
defineModels([Partner]);
defineMailModels();
test("Test creation/usage form popover widget", async () => {
await mountView({
type: "form",
resModel: "partner",
arch: `
<form>
<field name="json_data" widget="popover_widget"/>
</form>`,
resId: 1,
});
expect(".popover").toHaveCount(0);
expect(".fa-info-circle.text-danger").toHaveCount(1);
await contains(".fa-info-circle.text-danger").click();
expect(".popover").toHaveCount(1);
expect(".popover").toHaveText("JS Master\nvar that = self // why not?");
});

View File

@@ -0,0 +1,24 @@
import { defineMailModels } from "@mail/../tests/mail_test_helpers";
import { expect, test } from "@odoo/hoot";
import { defineActions, getService, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
import { WebClient } from "@web/webclient/webclient";
defineActions([
{
id: 42,
name: "Stock report",
tag: "stock_report_generic",
type: "ir.actions.client",
context: {},
params: {},
},
]);
defineMailModels();
test("Rendering with no lines", async function () {
onRpc("get_main_lines", () => []);
await mountWithCleanup(WebClient);
await getService("action").doAction(42);
expect(".o_stock_reports_page").toHaveText("No operation made on this lot.");
});

View File

@@ -0,0 +1,120 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_basic_stock_flow_with_minimal_access_rights", {
steps: () => [
{
trigger: ".o_menuitem[href='/odoo/inventory']",
run: "click",
},
{
trigger: "button[data-menu-xmlid='stock.menu_stock_warehouse_mgmt']",
run: "click",
},
{
trigger: ".o-dropdown-item[data-menu-xmlid='stock.in_picking']",
run: "click",
},
{
content: "check that at least one picking is present in the view",
trigger: ".o_stock_list_view_view .o_data_row",
},
{
trigger: ".o_list_button_add",
run: "click",
},
{
trigger: ".o_input[id=partner_id_0]",
run: "edit Test Partner",
},
{
trigger: ".dropdown-item:contains('Test Partner')",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add > a",
run: "click",
},
{
trigger: ".o_data_row .o_input",
run: "edit Test Product",
},
{
trigger: ".dropdown-item:contains('Test Product')",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty]",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty] .o_input",
run: "edit 1",
},
{
trigger: "button[name=action_confirm]",
run: "click",
},
{
trigger: "button[name=button_validate]",
run: "click",
},
{
trigger: ".o_arrow_button_current:contains(Done)",
},
{
trigger: "button[data-menu-xmlid='stock.menu_stock_warehouse_mgmt']",
run: "click",
},
{
trigger: ".o-dropdown-item[data-menu-xmlid='stock.out_picking']",
run: "click",
},
{
content: "check that at least one picking is present in the view",
trigger: ".o_stock_list_view_view .o_data_row",
},
{
trigger: "button:contains(New)",
run: "click",
},
{
trigger: ".o_input[id=partner_id_0]",
run: "edit Test Partner",
},
{
trigger: ".dropdown-item:contains('Test Partner')",
run: "click",
},
{
trigger: ".o_field_x2many_list_row_add > a",
run: "click",
},
{
trigger: ".o_data_row .o_input",
run: "edit Test Product",
},
{
trigger: ".dropdown-item:contains('Test Product')",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty]",
run: "click",
},
{
trigger: ".o_data_cell[name=product_uom_qty] .o_input",
run: "edit 1",
},
{
trigger: "button[name=action_confirm]",
run: "click",
},
{
trigger: "button[name=button_validate]",
run: "click",
},
{
trigger: ".o_arrow_button_current:contains(Done)",
},
],
});

View File

@@ -0,0 +1,526 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add('test_generate_serial_1', { steps: () => [
{
trigger: '.o_field_x2many_list_row_add > a',
run: "click",
},
{
trigger: ".o_field_widget[name=product_id] input",
run: "edit Serial",
},
{
trigger: ".ui-menu-item > a:contains('Product Serial')",
run: "click",
},
{
trigger: ".btn-primary[name=action_confirm]",
run: "click",
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: "h4:contains('Detailed Operations')",
run: "click",
},
{
trigger: '.o_widget_generate_serials > button',
run: "click",
},
{
trigger: ".modal h4:contains('Generate Serial numbers')",
run: "click",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit serial_n_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run: "edit 5 && click body",
},
{
trigger: ".modal .btn-primary:contains('Generate')",
run: "click",
},
{
trigger: "span[data-tooltip=Quantity]:contains('5')",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 5){
console.error("wrong number of move lines generated. " + nbLines + " instead of 5");
}
},
},
{
trigger: ".modal button:contains(save)",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: 'input[name="picked"]',
content: 'Check the picked field to display the column on the list view.',
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
},
},
{
trigger: ".o_data_cell[name=picked]",
run: "click",
},
{
trigger: ".o_field_widget[name=picked] input",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
}
},
{
trigger: ".btn-primary[name=button_validate]",
run: "click",
},
{
trigger: ".o_control_panel_actions button:contains('Traceability')",
},
]});
registry.category("web_tour.tours").add('test_generate_serial_2', { steps: () => [
{
trigger: '.o_field_x2many_list_row_add > a',
run: "click",
},
{
trigger: ".o_field_widget[name=product_id] input",
run: "edit Lot",
},
{
trigger: ".ui-menu-item > a:contains('Product Lot 1')",
run: "click",
},
{
trigger: ".o_field_widget[name=product_uom_qty] input",
run: "edit 100",
},
{
trigger: ".btn-primary[name=action_confirm]",
run: "click",
},
{
trigger: "button:contains('Details')",
run: "click",
},
{
trigger: ".modal h4:contains('Detailed Operations')",
run: "click",
},
// We generate lots for a first batch of 50 products
{
trigger: ".modal .o_widget_generate_serials > button",
run: "click",
},
{
trigger: ".modal h4:contains('Generate Lot numbers')",
run: "click",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit lot_n_1_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run() {
//input type number not supported by tour helpers.
this.anchor.value = "7.5";
}
},
{
trigger: ".modal div[name=total_received] input",
run: "edit 50",
},
{
trigger: ".modal .modal-footer button.btn-primary:contains(Generate)",
run: "click",
},
{
trigger: ".modal span[data-tooltip=Quantity]:contains(50)",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 7){
console.error("wrong number of move lines generated. " + nbLines + " instead of 7");
}
},
},
// We generate lots for the last 50 products
{
trigger: ".modal .o_widget_generate_serials > button",
run: "click",
},
{
trigger: ".modal h4:contains('Generate Lot numbers')",
},
{
trigger: ".modal div[name=next_serial] input",
run: "edit lot_n_2_1",
},
{
trigger: ".modal div[name=next_serial_count] input",
run: "edit 13",
},
{
trigger: ".modal div[name=total_received] input",
run: "edit 50",
},
{
trigger: ".modal div[name=keep_lines] input",
run: "check",
},
{
trigger: ".modal .modal-footer button.btn-primary:contains(Generate)",
run: "click",
},
{
trigger: ".modal span[data-tooltip=Quantity]:contains(100)",
run: () => {
const nbLines = document.querySelectorAll(".o_field_cell[name=lot_name]").length;
if (nbLines !== 11){
console.error("wrong number of move lines generated. " + nbLines + " instead of 11");
}
},
},
{
trigger: ".modal .o_form_button_save",
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
trigger: ".o_optional_columns_dropdown_toggle",
run: "click",
},
{
trigger: "input[name='picked']",
content: "Check the picked field to display the column on the list view.",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
},
},
{
trigger: ".o_data_cell[name=picked]",
run: "click",
},
{
trigger: ".o_field_widget[name=picked] input",
run: function (actions) {
if (!this.anchor.checked) {
actions.click();
}
}
},
{
trigger: ".btn-primary[name=button_validate]",
run: "click",
},
{
trigger: ".o_control_panel_actions button:contains('Traceability')",
},
]});
registry.category('web_tour.tours').add('test_inventory_adjustment_apply_all', { steps: () => [
{
trigger: '.o_list_button_add',
run: "click",
},
{
trigger: 'div[name=product_id] input',
run: "edit Product 1",
},
{
trigger: '.ui-menu-item > a:contains("Product 1")',
run: "click",
},
{
trigger: 'div[name=inventory_quantity] input',
run: "edit 123",
},
// Unfocus to show the "New" button again
{
trigger: '.o_searchview_input_container',
run: "click",
},
{
trigger: '.o_list_button_add',
run: "click",
},
{
trigger: 'div[name=product_id] input',
run: "edit Product 2",
},
{
trigger: '.ui-menu-item > a:contains("Product 2")',
run: "click",
},
{
trigger: 'div[name=inventory_quantity] input',
run: "edit 456",
},
{
trigger: 'button[name=action_apply_all]',
run: "click",
},
{
trigger: '.modal-content button[name=action_apply]',
run: "click",
},
{
trigger: "body:not(:has(.modal))",
},
{
content: "Check that all quants were applied.",
trigger: "body:not(:has(button[name=action_apply_inventory]))",
},
],
});
registry.category("web_tour.tours").add("test_add_new_line_in_detailled_op", {
steps: () => [
{
trigger: ".o_list_view.o_field_x2many .o_data_row button[name='action_show_details']",
run: "click",
},
{
trigger: ".modal-content",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "Pick LOT001 to create a move line with a quantity of 0.00",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
content: "check that the move contains three lines",
trigger:
".modal-content:has(.modal-header .modal-title:contains(Detailed Operations)) .o_data_row:nth-child(3)",
},
{
content: "Check that the first line is associated with LOT001 for a quantity of 0.00",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(0.00))",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT001 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT001 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT001")
);
if (lineLOT001.length) {
console.error("LOT001 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Cancel the move line creation",
trigger: ".modal-header:has(.modal-title:contains(Add line: Product Lot)) .btn-close",
run: "click",
},
{
content: "Remove the newly created line",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(0.00)) .o_list_record_remove",
run: "click",
},
{
content: "check that the move contains two lines",
trigger:
".modal-content:has(.modal-header .modal-title:contains(Detailed Operations)):not(:has(.o_data_row:nth-child(3)))",
},
{
content: "Check that the first line is associated with LOT001",
trigger:
".modal-content .o_data_row:nth-child(1):has(.o_field_pick_from:contains(WH/Stock - LOT001))",
},
{
content: "Check that the second line is associated with LOT002",
trigger:
".modal-content .o_data_row:nth-child(2):has(.o_field_pick_from:contains(WH/Stock - LOT002))",
},
{
content: "Modify the quant associated to the second line to fully use LOT003",
trigger: ".modal-content .o_data_row:nth-child(2) .o_field_pick_from",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(2) .o_field_pick_from input",
run: "edit LOT003",
},
{
trigger: ".dropdown-item:contains(LOT003)",
run: "click",
},
{
content: "Modify the quantity of the first line from 10 to 8",
trigger: ".modal-content .o_data_row:nth-child(1) .o_data_cell[name=quantity]",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(1) .o_field_widget[name=quantity] input",
run: "edit 8",
},
{
content: "Click on the header to update the total amount",
trigger: ".modal-header .modal-title:contains(Detailed Operations)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(18.00)",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT003 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT003 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT003")
);
if (lineLOT003.length) {
console.error("LOT003 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Pick LOT001 to create a move line with a quantity of 2.00",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(20.00)",
},
{
content: "Check that 2 units of LOT001 were added",
trigger:
".o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)) .o_field_widget[name=quantity] input:value(2.00)",
},
{
content: "Check that the third line is associated with LOT003",
trigger:
".modal-content .o_data_row:nth-child(3) .o_field_pick_from:contains(WH/Stock - LOT003)",
},
{
content: "Modify the quant associated to the third line to use LOT002",
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from input",
run: "edit LOT002",
},
{
trigger: ".dropdown-item:contains(LOT002)",
run: "click",
},
{
trigger: ".modal-header .modal-title:contains(Detailed Operations)",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_pick_from:contains(LOT002)",
},
{
content: "Modify the quantity of the first line from 10 to 15 to change the demand",
trigger: ".modal-content .o_data_row:nth-child(3) .o_data_cell[name=quantity]",
run: "click",
},
{
trigger: ".modal-content .o_data_row:nth-child(3) .o_field_widget[name=quantity] input",
run: "edit 15",
},
{
content: "Remove the LOT001 line with a quantity of 8.00",
trigger:
".o_data_row:has(.o_data_cell[name=quantity]:contains(8.00)) .o_list_record_remove",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(17.00)",
},
{
trigger: ".modal-content .o_field_x2many_list_row_add > a",
run: "click",
},
{
content: "LOT002 should not appear as it is not available",
trigger: ".modal-header .modal-title:contains(Add line: Product Lot)",
run: () => {
const lines = document.querySelectorAll(".o_data_row .o_data_cell[name=lot_id]");
if (lines.length !== 2) {
console.error(
"Wrong number of available quants: " + lines.length + " instead of 2."
);
}
const lineLOT002 = Array.from(lines).filter((line) =>
line.textContent.includes("LOT002")
);
if (lineLOT002.length) {
console.error("LOT002 shoudld not be displayed as unavailable.");
}
},
},
{
content: "Pick LOT001 to create move line to fullfill the demand of 3",
trigger: ".o_data_row .o_data_cell[name=lot_id]:contains(LOT001)",
run: "click",
},
{
trigger: ".modal-content .o_list_number:contains(20.00)",
},
{
content: "Check that 3 units of LOT001 were added",
trigger:
".modal-content .o_data_row:has(.o_field_pick_from input:value(WH/Stock - LOT001)):has(.o_field_float[name=quantity] input:value(3.00))",
},
{
trigger: ".modal-content .o_form_button_save",
run: "click",
},
{
trigger: ".o_list_view.o_field_x2many .o_data_row button[name='action_show_details']",
run: "click",
},
],
});

View File

@@ -0,0 +1,137 @@
import { registry } from "@web/core/registry";
registry.category("web_tour.tours").add("test_stock_route_diagram_report", {
steps: () => [
{
trigger: ".o_breadcrumb",
},
{
trigger: '.o_kanban_record',
run: "click",
},
{
trigger: '.nav-item > a:contains("Inventory")',
run: "click",
},
{
trigger: '.btn[id="stock.view_diagram_button"]',
run: "click",
},
{
trigger: ':iframe .o_report_stock_rule',
},
],
});
registry.category("web_tour.tours").add("test_context_from_warehouse_filter", {
steps: () => [
// Add "foo" to the warehouse context key
{
trigger: ".o_searchview_input",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit foo",
},
{
trigger: ".o-dropdown-item:contains(Warehouse):contains(foo)",
run: "click",
},
// Add warehouse A's id to the warehouse context key
{
trigger: ".o_searchview_input",
run: "click",
},
{
trigger: ".o_searchview_input",
run: "edit warehouse",
},
{
trigger: ".o-dropdown-item:contains(Search Warehouse for:) a.o_expand > i",
run: "click",
},
{
trigger: ".o-dropdown-item.o_indent:contains(Warehouse A) a",
run: "click",
},
// Add warehouse B's id to the warehouse context key
{
trigger: ".o_searchview_input",
run: "edit warehouse",
},
{
trigger: ".o-dropdown-item:contains(Search Warehouse for:) a.o_expand > i",
run: "click",
},
{
trigger: ".o-dropdown-item.o_indent:contains(Warehouse B) a",
run: "click",
},
{
content: "Go to product page",
trigger: ".o_kanban_record:has(span:contains(Lovely Product))",
run: "click",
},
{
trigger: ".o_form_view",
run: () => {
if (!document.querySelector("button[name=action_product_tmpl_forecast_report]")) {
const panelButtons = document.querySelectorAll(
".o_control_panel_actions button"
);
const moreButton = Array.from(panelButtons).find(
(button) => button.textContent.trim() == "More"
);
moreButton.click();
}
},
},
{
trigger: "button[name=action_product_tmpl_forecast_report]",
run: "click",
},
{
trigger: ".o_graph_view",
},
],
});
registry.category("web_tour.tours").add("test_forecast_replenishment", {
steps: () => [
{
trigger: ".o_kanban_record:contains(Lovely product)",
run: "click",
},
{
trigger: "button[name=action_product_tmpl_forecast_report]",
run: "click",
},
{
trigger: "button.o_forecasted_replenish_btn",
run: "click",
},
{
trigger: ".modal-dialog .btn-close",
run: "click",
},
{
trigger: ".o_web_client:not(:has(.modal-dialog))",
},
{
trigger: "button.o_forecasted_replenish_btn",
run: "click",
},
{
trigger: "button[name=launch_replenishment]",
run: "click",
},
{
trigger: ".o_web_client:not(:has(.modal-dialog))",
},
{
trigger:
".o_notification:contains(The following replenishment order have been generated)",
},
],
});

View File

@@ -0,0 +1,9 @@
export function fail(errorMessage) {
throw new Error(errorMessage);
}
export function assert(current, expected, info) {
if (current !== expected) {
fail(`${info}: "${current}" instead of "${expected}".`);
}
}

Binary file not shown.