diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/stock/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/stock/static')
40 files changed, 2115 insertions, 0 deletions
diff --git a/addons/stock/static/description/icon.png b/addons/stock/static/description/icon.png Binary files differnew file mode 100644 index 00000000..692e7624 --- /dev/null +++ b/addons/stock/static/description/icon.png diff --git a/addons/stock/static/description/icon.svg b/addons/stock/static/description/icon.svg new file mode 100644 index 00000000..834a8e1f --- /dev/null +++ b/addons/stock/static/description/icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#B06161"/><stop offset="45.785%" stop-color="#984E4E"/><stop offset="100%" stop-color="#7C3838"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M32.44 69H4c-2 0-4-1-4-4V42.15l15.632-16.68L36 21l16 4 7.943 5.353L55 36v8.576L32.44 69z" opacity=".33"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" fill-rule="nonzero" d="M54.85 36.807a.36.36 0 0 1 .15.293v9.393a.36.36 0 0 1-.222.333l-17.28 7.147a.357.357 0 0 1-.338-.034.36.36 0 0 1-.16-.3V36.36a.36.36 0 0 1 .64-.226l3.85 4.76s.182.243.462.146c.334-.115 12.572-4.281 12.572-4.281a.36.36 0 0 1 .326.048zm5.075-4.873a.345.345 0 0 1 .058.322.369.369 0 0 1-.24.233l-17.035 5.487a.387.387 0 0 1-.415-.114l-4.769-5.578-1.413.37-.021.004a.396.396 0 0 1-.2 0l-1.414-.369-4.769 5.578a.387.387 0 0 1-.415.114l-17.035-5.487a.369.369 0 0 1-.24-.233.344.344 0 0 1 .058-.322l3.466-4.395a.379.379 0 0 1 .2-.13l20.146-5.398a.484.484 0 0 1 .226-.005L56.26 27.41c.08.021.15.067.2.13l3.466 4.395zM36.01 31.13l14.426-3.75-14.426-3.844-14.447 3.849L36.01 31.13zm-1.264 4.89c.152.052.253.188.253.34v17.28c0 .12-.063.232-.169.3a.394.394 0 0 1-.356.033l-18.24-7.147a.36.36 0 0 1-.235-.333V37.1c0-.116.059-.225.159-.293a.398.398 0 0 1 .343-.048s12.918 4.166 13.27 4.281c.297.097.488-.146.488-.146l4.065-4.76a.393.393 0 0 1 .422-.113z" opacity=".3"/><path fill="#FFF" fill-rule="nonzero" d="M54.85 34.807a.36.36 0 0 1 .15.293v9.393a.36.36 0 0 1-.222.333l-17.28 7.147a.357.357 0 0 1-.338-.034.36.36 0 0 1-.16-.3V34.36a.36.36 0 0 1 .64-.226l3.85 4.76s.182.243.462.146c.334-.115 12.572-4.281 12.572-4.281a.36.36 0 0 1 .326.048zm5.075-4.873a.345.345 0 0 1 .058.322.369.369 0 0 1-.24.233l-17.035 5.487a.387.387 0 0 1-.415-.114l-4.769-5.578-1.413.37-.021.004a.396.396 0 0 1-.2 0l-1.414-.369-4.769 5.578a.387.387 0 0 1-.415.114l-17.035-5.487a.369.369 0 0 1-.24-.233.344.344 0 0 1 .058-.322l3.466-4.395a.379.379 0 0 1 .2-.13l20.146-5.398a.484.484 0 0 1 .226-.005L56.26 25.41c.08.021.15.067.2.13l3.466 4.395zM36.01 29.13l14.426-3.75-14.426-3.844-14.447 3.849L36.01 29.13zm-1.264 4.89c.152.052.253.188.253.34v17.28c0 .12-.063.232-.169.3a.394.394 0 0 1-.356.033l-18.24-7.147a.36.36 0 0 1-.235-.333V35.1c0-.116.059-.225.159-.293a.398.398 0 0 1 .343-.048s12.918 4.166 13.27 4.281c.297.097.488-.146.488-.146l4.065-4.76a.393.393 0 0 1 .422-.113z"/></g></g></svg>
\ No newline at end of file diff --git a/addons/stock/static/img/barcode_scanner.png b/addons/stock/static/img/barcode_scanner.png Binary files differnew file mode 100644 index 00000000..e8f7a7fa --- /dev/null +++ b/addons/stock/static/img/barcode_scanner.png diff --git a/addons/stock/static/img/cable_management.png b/addons/stock/static/img/cable_management.png Binary files differnew file mode 100644 index 00000000..bddc79b8 --- /dev/null +++ b/addons/stock/static/img/cable_management.png diff --git a/addons/stock/static/img/replenishment.svg b/addons/stock/static/img/replenishment.svg new file mode 100644 index 00000000..5e56d8e5 --- /dev/null +++ b/addons/stock/static/img/replenishment.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 900.84 584.93"><defs><style>.cls-1,.cls-16,.cls-17,.cls-19,.cls-2,.cls-3,.cls-5,.cls-6{fill:none;stroke-miterlimit:10;}.cls-1,.cls-2,.cls-3{stroke:#eaeaea;}.cls-2{stroke-width:1.03px;}.cls-3{stroke-width:1.02px;}.cls-4{fill:url(#linear-gradient);}.cls-5{stroke:#875a7b;stroke-width:3.54px;}.cls-6{stroke:#827882;stroke-width:2px;}.cls-21,.cls-7{fill:#827882;}.cls-10,.cls-8,.cls-9{fill:#282828;}.cls-10,.cls-15,.cls-21,.cls-9{font-size:20px;}.cls-20,.cls-21,.cls-9{font-family:GothamMedium, Gotham Medium;}.cls-10{font-family:Gotham-Book, Gotham Book;}.cls-11{letter-spacing:-0.01em;}.cls-12{letter-spacing:0em;}.cls-13{letter-spacing:-0.01em;}.cls-14{fill:#00a09d;}.cls-15,.cls-18{fill:#b79cb0;}.cls-15{font-family:GothamBold, Gotham Bold;}.cls-16,.cls-19{stroke:#00a09d;}.cls-16,.cls-17{stroke-width:4px;}.cls-17{stroke:#b79cb0;}.cls-19{stroke-width:2.26px;}.cls-20{font-size:18px;fill:#fff;}</style><linearGradient id="linear-gradient" x1="344.19" y1="510.33" x2="344.19" y2="98.95" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="0.48" stop-color="#875a7b" stop-opacity="0.27"/></linearGradient></defs><g id="Layer_3" data-name="Layer 3"><polyline class="cls-1" points="725.53 60.15 725.53 537.44 29.58 537.44 29.58 60.15"/><line class="cls-2" x1="29.58" y1="474.17" x2="766.26" y2="474.17"/><line class="cls-2" x1="29.58" y1="410.9" x2="766.26" y2="410.9"/><line class="cls-2" x1="29.58" y1="347.63" x2="766.26" y2="347.63"/><line class="cls-2" x1="29.58" y1="284.36" x2="766.26" y2="284.36"/><line class="cls-2" x1="29.58" y1="221.1" x2="766.26" y2="221.1"/><line class="cls-2" x1="29.58" y1="157.83" x2="766.26" y2="157.83"/><line class="cls-2" x1="29.58" y1="94.56" x2="766.26" y2="94.56"/><line class="cls-3" x1="662.26" y1="60.15" x2="662.26" y2="537.44"/><line class="cls-3" x1="598.99" y1="60.15" x2="598.99" y2="537.44"/><line class="cls-3" x1="535.72" y1="60.15" x2="535.72" y2="537.44"/><line class="cls-3" x1="472.45" y1="60.15" x2="472.45" y2="537.44"/><line class="cls-3" x1="409.19" y1="60.15" x2="409.19" y2="537.44"/><line class="cls-3" x1="345.92" y1="60.15" x2="345.92" y2="537.44"/><line class="cls-3" x1="282.65" y1="60.15" x2="282.65" y2="537.44"/><line class="cls-3" x1="219.38" y1="60.15" x2="219.38" y2="537.44"/><line class="cls-3" x1="156.11" y1="60.15" x2="156.11" y2="537.44"/><line class="cls-3" x1="92.85" y1="60.15" x2="92.85" y2="537.44"/><polyline class="cls-4" points="29.88 298 69.47 347.63 94.04 347.63 119.44 397.27 156.11 397.27 156.11 91.28 175.38 91.28 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67 658.81 537.44 29.58 537.44 29.88 338.89"/><polyline class="cls-5" points="29.6 298.66 69.47 347.63 94.04 347.63 119.44 400.98 156.11 400.98 156.11 94.56 175.38 94.56 222.92 165.56 260.08 166.16 288.42 204.93 334.09 208.19 358.04 240.45 387.79 259.84 411.45 262.09 436.6 318.28 475.13 326.27 504.04 359.81 550 376.84 572.86 417.57 634.24 438.17 645.3 472.67 658.81 472.67"/><polyline class="cls-6" points="29.58 67.33 29.58 537.44 759.08 537.44"/><polygon class="cls-7" points="24.59 68.78 29.58 60.15 34.56 68.78 24.59 68.78"/><polygon class="cls-7" points="757.62 542.42 766.26 537.44 757.62 532.45 757.62 542.42"/><path class="cls-8" d="M36.24,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,36.24,94.11Z"/><ellipse class="cls-8" cx="41.24" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M49.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,49.58,94.11Z"/><path class="cls-8" d="M56.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,56.25,94.11Z"/><path class="cls-8" d="M62.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,62.91,94.11Z"/><path class="cls-8" d="M69.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,69.58,94.11Z"/><path class="cls-8" d="M76.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,76.25,94.11Z"/><path class="cls-8" d="M82.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,82.91,94.11Z"/><path class="cls-8" d="M89.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,89.58,94.11Z"/><path class="cls-8" d="M96.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,96.25,94.11Z"/><path class="cls-8" d="M102.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,102.91,94.11Z"/><path class="cls-8" d="M109.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,109.58,94.11Z"/><path class="cls-8" d="M116.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,116.25,94.11Z"/><path class="cls-8" d="M122.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,122.91,94.11Z"/><path class="cls-8" d="M129.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,129.58,94.11Z"/><ellipse class="cls-8" cx="134.58" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M142.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,142.91,94.11Z"/><path class="cls-8" d="M149.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,149.58,94.11Z"/><path class="cls-8" d="M156.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,156.25,94.11Z"/><path class="cls-8" d="M162.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,162.91,94.11Z"/><path class="cls-8" d="M169.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,169.58,94.11Z"/><ellipse class="cls-8" cx="174.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="181.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="187.91" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="194.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="201.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="207.91" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="214.58" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M222.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,222.91,94.11Z"/><path class="cls-8" d="M229.58,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,229.58,94.11Z"/><path class="cls-8" d="M236.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,236.25,94.11Z"/><path class="cls-8" d="M242.91,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,242.91,94.11Z"/><ellipse class="cls-8" cx="247.91" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="254.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="261.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="267.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="274.58" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M282.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,282.92,94.11Z"/><path class="cls-8" d="M289.58,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,289.58,94.11Z"/><path class="cls-8" d="M296.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,296.25,94.11Z"/><path class="cls-8" d="M302.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,302.92,94.11Z"/><ellipse class="cls-8" cx="307.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="314.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="321.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="327.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="334.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="341.25" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M349.58,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,349.58,94.11Z"/><path class="cls-8" d="M356.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,356.25,94.11Z"/><path class="cls-8" d="M362.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,362.92,94.11Z"/><path class="cls-8" d="M369.58,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,369.58,94.11Z"/><ellipse class="cls-8" cx="374.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="381.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="387.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="394.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="401.25" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M409.58,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,409.58,94.11Z"/><path class="cls-8" d="M416.25,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,416.25,94.11Z"/><path class="cls-8" d="M422.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,422.92,94.11Z"/><path class="cls-8" d="M429.58,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,429.58,94.11Z"/><ellipse class="cls-8" cx="434.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="441.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="447.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="454.58" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="461.25" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M469.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,469.59,94.11Z"/><path class="cls-8" d="M476.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,476.25,94.11Z"/><path class="cls-8" d="M482.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,482.92,94.11Z"/><path class="cls-8" d="M489.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,489.59,94.11Z"/><path class="cls-8" d="M496.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,496.25,94.11Z"/><ellipse class="cls-8" cx="501.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="507.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="514.59" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="521.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="527.92" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M536.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,536.25,94.11Z"/><path class="cls-8" d="M542.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,542.92,94.11Z"/><path class="cls-8" d="M549.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,549.59,94.11Z"/><path class="cls-8" d="M556.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,556.25,94.11Z"/><ellipse class="cls-8" cx="561.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="567.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="574.59" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="581.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="587.92" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M596.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,596.25,94.11Z"/><path class="cls-8" d="M602.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,602.92,94.11Z"/><path class="cls-8" d="M609.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,609.59,94.11Z"/><path class="cls-8" d="M616.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,616.25,94.11Z"/><path class="cls-8" d="M622.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,622.92,94.11Z"/><ellipse class="cls-8" cx="627.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="634.59" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="641.25" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="647.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="654.59" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M662.92,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,662.92,94.11Z"/><path class="cls-8" d="M669.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,669.59,94.11Z"/><path class="cls-8" d="M676.25,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,676.25,94.11Z"/><path class="cls-8" d="M682.92,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,682.92,94.11Z"/><ellipse class="cls-8" cx="687.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="694.59" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="701.26" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="707.92" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="714.59" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M722.92,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,722.92,94.11Z"/><path class="cls-8" d="M729.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,729.59,94.11Z"/><path class="cls-8" d="M736.26,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,736.26,94.11Z"/><path class="cls-8" d="M742.92,94.11a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,742.92,94.11Z"/><path class="cls-8" d="M749.59,94.11a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,749.59,94.11Z"/><ellipse class="cls-8" cx="754.59" cy="94.11" rx="1.67" ry="1.67"/><ellipse class="cls-8" cx="761.26" cy="94.11" rx="1.67" ry="1.67"/><path class="cls-8" d="M36.24,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,36.24,410.9Z"/><path class="cls-8" d="M42.91,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,42.91,410.9Z"/><path class="cls-8" d="M49.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,49.58,410.9Z"/><path class="cls-8" d="M56.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,56.25,410.9Z"/><path class="cls-8" d="M62.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,62.91,410.9Z"/><path class="cls-8" d="M69.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,69.58,410.9Z"/><path class="cls-8" d="M76.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,76.25,410.9Z"/><path class="cls-8" d="M82.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,82.91,410.9Z"/><path class="cls-8" d="M89.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,89.58,410.9Z"/><path class="cls-8" d="M96.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,96.25,410.9Z"/><path class="cls-8" d="M102.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,102.91,410.9Z"/><path class="cls-8" d="M109.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,109.58,410.9Z"/><path class="cls-8" d="M116.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,116.25,410.9Z"/><path class="cls-8" d="M122.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,122.91,410.9Z"/><path class="cls-8" d="M129.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,129.58,410.9Z"/><path class="cls-8" d="M136.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,136.25,410.9Z"/><path class="cls-8" d="M142.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,142.91,410.9Z"/><path class="cls-8" d="M149.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,149.58,410.9Z"/><path class="cls-8" d="M156.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,156.25,410.9Z"/><path class="cls-8" d="M162.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,162.91,410.9Z"/><path class="cls-8" d="M169.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,169.58,410.9Z"/><path class="cls-8" d="M176.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,176.25,410.9Z"/><path class="cls-8" d="M182.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,182.91,410.9Z"/><path class="cls-8" d="M189.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,189.58,410.9Z"/><path class="cls-8" d="M196.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,196.25,410.9Z"/><path class="cls-8" d="M202.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,202.91,410.9Z"/><path class="cls-8" d="M209.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,209.58,410.9Z"/><path class="cls-8" d="M216.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,216.25,410.9Z"/><path class="cls-8" d="M222.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,222.91,410.9Z"/><path class="cls-8" d="M229.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,229.58,410.9Z"/><path class="cls-8" d="M236.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,236.25,410.9Z"/><path class="cls-8" d="M242.91,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,242.91,410.9Z"/><path class="cls-8" d="M249.58,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,249.58,410.9Z"/><path class="cls-8" d="M256.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,256.25,410.9Z"/><path class="cls-8" d="M262.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,262.92,410.9Z"/><path class="cls-8" d="M269.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,269.58,410.9Z"/><path class="cls-8" d="M276.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,276.25,410.9Z"/><path class="cls-8" d="M282.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,282.92,410.9Z"/><path class="cls-8" d="M289.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,289.58,410.9Z"/><path class="cls-8" d="M296.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,296.25,410.9Z"/><path class="cls-8" d="M302.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,302.92,410.9Z"/><path class="cls-8" d="M309.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,309.58,410.9Z"/><path class="cls-8" d="M316.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,316.25,410.9Z"/><path class="cls-8" d="M322.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,322.92,410.9Z"/><path class="cls-8" d="M329.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,329.58,410.9Z"/><path class="cls-8" d="M336.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,336.25,410.9Z"/><path class="cls-8" d="M342.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,342.92,410.9Z"/><path class="cls-8" d="M349.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,349.58,410.9Z"/><path class="cls-8" d="M356.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,356.25,410.9Z"/><path class="cls-8" d="M362.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,362.92,410.9Z"/><path class="cls-8" d="M369.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,369.58,410.9Z"/><path class="cls-8" d="M376.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,376.25,410.9Z"/><path class="cls-8" d="M382.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,382.92,410.9Z"/><path class="cls-8" d="M389.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,389.58,410.9Z"/><path class="cls-8" d="M396.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,396.25,410.9Z"/><path class="cls-8" d="M402.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,402.92,410.9Z"/><path class="cls-8" d="M409.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,409.58,410.9Z"/><path class="cls-8" d="M416.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,416.25,410.9Z"/><path class="cls-8" d="M422.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,422.92,410.9Z"/><path class="cls-8" d="M429.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,429.58,410.9Z"/><path class="cls-8" d="M436.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,436.25,410.9Z"/><path class="cls-8" d="M442.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,442.92,410.9Z"/><path class="cls-8" d="M449.58,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,449.58,410.9Z"/><path class="cls-8" d="M456.25,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,456.25,410.9Z"/><path class="cls-8" d="M462.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,462.92,410.9Z"/><path class="cls-8" d="M469.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,469.59,410.9Z"/><path class="cls-8" d="M476.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,476.25,410.9Z"/><path class="cls-8" d="M482.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,482.92,410.9Z"/><path class="cls-8" d="M489.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,489.59,410.9Z"/><path class="cls-8" d="M496.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,496.25,410.9Z"/><path class="cls-8" d="M502.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,502.92,410.9Z"/><path class="cls-8" d="M509.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,509.59,410.9Z"/><path class="cls-8" d="M516.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,516.25,410.9Z"/><path class="cls-8" d="M522.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,522.92,410.9Z"/><path class="cls-8" d="M529.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,529.59,410.9Z"/><path class="cls-8" d="M536.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,536.25,410.9Z"/><path class="cls-8" d="M542.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,542.92,410.9Z"/><path class="cls-8" d="M549.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,549.59,410.9Z"/><path class="cls-8" d="M556.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,556.25,410.9Z"/><path class="cls-8" d="M562.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,562.92,410.9Z"/><path class="cls-8" d="M569.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,569.59,410.9Z"/><path class="cls-8" d="M576.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,576.25,410.9Z"/><path class="cls-8" d="M582.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,582.92,410.9Z"/><path class="cls-8" d="M589.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,589.59,410.9Z"/><path class="cls-8" d="M596.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,596.25,410.9Z"/><path class="cls-8" d="M602.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,602.92,410.9Z"/><path class="cls-8" d="M609.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,609.59,410.9Z"/><path class="cls-8" d="M616.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,616.25,410.9Z"/><path class="cls-8" d="M622.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,622.92,410.9Z"/><path class="cls-8" d="M629.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,629.59,410.9Z"/><path class="cls-8" d="M636.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,636.25,410.9Z"/><path class="cls-8" d="M642.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,642.92,410.9Z"/><path class="cls-8" d="M649.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,649.59,410.9Z"/><path class="cls-8" d="M656.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,656.25,410.9Z"/><path class="cls-8" d="M662.92,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,662.92,410.9Z"/><path class="cls-8" d="M669.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,669.59,410.9Z"/><path class="cls-8" d="M676.25,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,676.25,410.9Z"/><path class="cls-8" d="M682.92,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,682.92,410.9Z"/><path class="cls-8" d="M689.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,689.59,410.9Z"/><path class="cls-8" d="M696.26,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,696.26,410.9Z"/><path class="cls-8" d="M702.92,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,702.92,410.9Z"/><path class="cls-8" d="M709.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,709.59,410.9Z"/><path class="cls-8" d="M716.26,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,716.26,410.9Z"/><path class="cls-8" d="M722.92,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,722.92,410.9Z"/><path class="cls-8" d="M729.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,729.59,410.9Z"/><path class="cls-8" d="M736.26,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,736.26,410.9Z"/><path class="cls-8" d="M742.92,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,742.92,410.9Z"/><path class="cls-8" d="M749.59,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,749.59,410.9Z"/><path class="cls-8" d="M756.26,410.9a1.67,1.67,0,1,1-1.67-1.67A1.67,1.67,0,0,1,756.26,410.9Z"/><path class="cls-8" d="M762.92,410.9a1.67,1.67,0,1,1-1.66-1.67A1.66,1.66,0,0,1,762.92,410.9Z"/><text class="cls-9" transform="translate(774.07 99.38)">Maximum</text><text class="cls-9" transform="translate(774.07 427.3)">Minimum</text><text class="cls-10" transform="translate(774.07 403.86)">Sa<tspan class="cls-11" x="24.38" y="0">f</tspan><tspan x="31.52" y="0">ety </tspan><tspan class="cls-12" x="69.26" y="0">S</tspan><tspan class="cls-13" x="81.96" y="0">t</tspan><tspan x="89.74" y="0">ock</tspan></text><path class="cls-14" d="M653.39,274.55a7.49,7.49,0,0,1,.88,1.93,8,8,0,0,1-.25,5.21,7.6,7.6,0,0,1-1.54,2.35,7.1,7.1,0,0,1-2.29,1.54,7.24,7.24,0,0,1-2.8.55h0a7.12,7.12,0,0,1-2.81-.56,7.08,7.08,0,0,1-3.86-3.91,7.73,7.73,0,0,1-.57-3,7.61,7.61,0,0,1,.57-3,7.32,7.32,0,0,1,1.54-2.35,6.66,6.66,0,0,1,2.28-1.54,7.11,7.11,0,0,1,2.81-.55h0a6.85,6.85,0,0,1,2.12.32,7.17,7.17,0,0,1,1.89.91l1.23-1.43,2.18,2Zm-1.64,4.12a4.37,4.37,0,0,0-.39-1.84l-1.93,2.28-2.2-2,2.08-2.3a4.51,4.51,0,0,0-1.92-.38h0a4.58,4.58,0,0,0-1.7.31A4.2,4.2,0,0,0,643.3,277a4,4,0,0,0-.36,1.7,4.18,4.18,0,0,0,.35,1.71,3.75,3.75,0,0,0,.94,1.31,4.49,4.49,0,0,0,1.38.87,4.79,4.79,0,0,0,1.7.31h0a4.88,4.88,0,0,0,1.7-.31,4.17,4.17,0,0,0,1.39-.88,4.27,4.27,0,0,0,1-1.34A4.18,4.18,0,0,0,651.75,278.67Z"/><path class="cls-14" d="M654.55,265.27a3.43,3.43,0,0,1-1.07,2.69,4.21,4.21,0,0,1-2.92,1h-6.93v-3h6a2.42,2.42,0,0,0,1.63-.48,1.74,1.74,0,0,0,.55-1.36,1.82,1.82,0,0,0-.55-1.39,2.32,2.32,0,0,0-1.63-.51h-6v-3h10.72v3h-1.54a7,7,0,0,1,.66.56,3.78,3.78,0,0,1,.55.7,3.22,3.22,0,0,1,.38.84A3.3,3.3,0,0,1,654.55,265.27Z"/><path class="cls-14" d="M654.55,253.33a4.8,4.8,0,0,1-.21,1.43,3.87,3.87,0,0,1-.63,1.17,3.22,3.22,0,0,1-1,.79,3.32,3.32,0,0,1-1.41.29h0a3.38,3.38,0,0,1-1.52-.32,3,3,0,0,1-1.06-.89,3.71,3.71,0,0,1-.63-1.36,6.83,6.83,0,0,1-.21-1.75,7.42,7.42,0,0,1,.12-1.43,7.71,7.71,0,0,1,.32-1.19h-.19a1.8,1.8,0,0,0-1.45.58,2.52,2.52,0,0,0-.5,1.72,6.63,6.63,0,0,0,.15,1.55,10.56,10.56,0,0,0,.43,1.41l-2.33.76a11.34,11.34,0,0,1-.61-1.84,9.73,9.73,0,0,1-.24-2.32,6.76,6.76,0,0,1,.3-2.15,3.73,3.73,0,0,1,.9-1.47,3.53,3.53,0,0,1,1.47-.88,6.2,6.2,0,0,1,2-.28h6.22v2.94h-1.16a4.29,4.29,0,0,1,1,1.34A4.5,4.5,0,0,1,654.55,253.33Zm-2.11-.92a2.72,2.72,0,0,0-.51-1.73,1.65,1.65,0,0,0-1.37-.65H650a4.53,4.53,0,0,0-.39,1.94,2.68,2.68,0,0,0,.38,1.54,1.25,1.25,0,0,0,1.1.56h0a1.15,1.15,0,0,0,1-.46A2,2,0,0,0,652.44,252.41Z"/><path class="cls-14" d="M643.63,244.53v-3h1.53q-.35-.27-.66-.57a4.07,4.07,0,0,1-.55-.69,3.54,3.54,0,0,1-.38-.84,3.4,3.4,0,0,1-.14-1,3.44,3.44,0,0,1,1.05-2.69,4.21,4.21,0,0,1,2.92-.95h7v3h-6a2.42,2.42,0,0,0-1.63.48,1.72,1.72,0,0,0-.54,1.36,1.83,1.83,0,0,0,.54,1.39,2.32,2.32,0,0,0,1.63.51h6v3Z"/><path class="cls-14" d="M654.52,228.45a5,5,0,0,1-.14,1.27,2.46,2.46,0,0,1-.53,1,2.78,2.78,0,0,1-1,.64,4.57,4.57,0,0,1-1.55.23h-5.08v1.28h-2.6v-1.28h-2.74v-3h2.74V226h2.6v2.52h4.59c.7,0,1-.33,1-1a3,3,0,0,0-.39-1.5h2.43a4.33,4.33,0,0,1,.62,2.4Z"/><path class="cls-14" d="M639.75,223.71v-3.2h2.69v3.2Zm3.88-.08v-3h10.72v3Z"/><path class="cls-14" d="M654.52,214.11a5,5,0,0,1-.14,1.27,2.46,2.46,0,0,1-.53,1,2.78,2.78,0,0,1-1,.64,4.57,4.57,0,0,1-1.55.23h-5.08v1.28h-2.6v-1.28h-2.74v-3h2.74v-2.52h2.6v2.52h4.59c.7,0,1-.33,1-1a3,3,0,0,0-.39-1.5h2.43a4.33,4.33,0,0,1,.62,2.4Z"/><path class="cls-14" d="M657.61,206.87a4.76,4.76,0,0,1-.21,1.48,5.75,5.75,0,0,1-.54,1.22l-2.19-1a3.5,3.5,0,0,0,.3-.65,2,2,0,0,0,.12-.65,1.13,1.13,0,0,0-.15-.63,1.34,1.34,0,0,0-.55-.43l-10.76,4.2v-3.22l7.3-2.44-7.3-2.34v-3.16l11,4.12A8.66,8.66,0,0,1,656,204a3.55,3.55,0,0,1,.93.75,2.36,2.36,0,0,1,.51.93A3.9,3.9,0,0,1,657.61,206.87Z"/><path class="cls-14" d="M654.52,187.81a5,5,0,0,1-.14,1.27,2.46,2.46,0,0,1-.53,1,2.78,2.78,0,0,1-1,.64,4.57,4.57,0,0,1-1.55.23h-5.08v1.28h-2.6v-1.28h-2.74v-3h2.74v-2.52h2.6v2.52h4.59c.7,0,1-.33,1-1a3,3,0,0,0-.39-1.5h2.43a4.33,4.33,0,0,1,.62,2.4Z"/><path class="cls-14" d="M654.59,177.87a6,6,0,0,1-.44,2.3A5.62,5.62,0,0,1,653,182a5.7,5.7,0,0,1-1.76,1.21,5.47,5.47,0,0,1-2.17.44h0a5.39,5.39,0,0,1-2.17-.44,5.64,5.64,0,0,1-1.78-1.22,5.81,5.81,0,0,1-1.63-4.16,6,6,0,0,1,.43-2.3A5.44,5.44,0,0,1,645,173.7a5.44,5.44,0,0,1,3.93-1.65h0a5.3,5.3,0,0,1,2.17.44,5.4,5.4,0,0,1,1.76,1.22,5.83,5.83,0,0,1,1.21,1.84A6,6,0,0,1,654.59,177.87Zm-2.62,0a2.72,2.72,0,0,0-.24-1.17,2.6,2.6,0,0,0-.62-.87,2.73,2.73,0,0,0-.93-.55,3.31,3.31,0,0,0-1.13-.19h0a3,3,0,0,0-1.13.2,2.61,2.61,0,0,0-.94.58,2.54,2.54,0,0,0-.65.9,2.64,2.64,0,0,0-.24,1.14,2.87,2.87,0,0,0,.22,1.17,2.78,2.78,0,0,0,.63.87,2.84,2.84,0,0,0,.94.55,3.43,3.43,0,0,0,1.13.19h0a3.32,3.32,0,0,0,1.13-.2,3.14,3.14,0,0,0,.94-.58,3.19,3.19,0,0,0,.65-.89A2.69,2.69,0,0,0,652,177.83Z"/><path class="cls-14" d="M654.59,158.59a6,6,0,0,1-.44,2.3,5.42,5.42,0,0,1-1.17,1.83,5.53,5.53,0,0,1-1.76,1.21,5.47,5.47,0,0,1-2.17.44h0a5.39,5.39,0,0,1-2.17-.44,5.48,5.48,0,0,1-1.78-1.22,5.81,5.81,0,0,1-1.63-4.16,6,6,0,0,1,.43-2.3,5.44,5.44,0,0,1,1.18-1.83,5.27,5.27,0,0,1,1.76-1.21,5.39,5.39,0,0,1,2.17-.44h0a5.47,5.47,0,0,1,2.17.44,5.4,5.4,0,0,1,1.76,1.22,5.83,5.83,0,0,1,1.21,1.84A6.07,6.07,0,0,1,654.59,158.59Zm-2.62,0a2.72,2.72,0,0,0-.24-1.17,2.6,2.6,0,0,0-.62-.87,2.9,2.9,0,0,0-.93-.55,3.31,3.31,0,0,0-1.13-.19h0a3.24,3.24,0,0,0-1.13.2,2.75,2.75,0,0,0-.94.58,2.54,2.54,0,0,0-.65.9,2.71,2.71,0,0,0-.24,1.14,2.87,2.87,0,0,0,.22,1.17,2.78,2.78,0,0,0,.63.87,2.84,2.84,0,0,0,.94.55,3.19,3.19,0,0,0,1.13.19h0a3.32,3.32,0,0,0,1.13-.2,3.14,3.14,0,0,0,.94-.58,3,3,0,0,0,.65-.89A2.66,2.66,0,0,0,652,158.55Z"/><path class="cls-14" d="M643.63,150.57v-3h2.16a4.59,4.59,0,0,1-1.77-1.26,3.12,3.12,0,0,1-.59-2.16h3.18v.16a3.14,3.14,0,0,0,.91,2.39,4.07,4.07,0,0,0,2.87.87h4v3Z"/><path class="cls-14" d="M654.55,137.81a4.76,4.76,0,0,1-.36,1.82,4.51,4.51,0,0,1-1.08,1.56,5.32,5.32,0,0,1-1.73,1.09,6.47,6.47,0,0,1-2.37.41h0a6.39,6.39,0,0,1-2.37-.41,5.24,5.24,0,0,1-1.74-1.08,4.2,4.2,0,0,1-1.07-1.55,4.83,4.83,0,0,1-.36-1.84,3.93,3.93,0,0,1,.47-2,5.68,5.68,0,0,1,1.12-1.33h-5.27v-3h14.6v3h-1.54a4.88,4.88,0,0,1,1.25,1.35A3.78,3.78,0,0,1,654.55,137.81ZM652,137a2.38,2.38,0,0,0-.82-1.83,3,3,0,0,0-.93-.56,3.42,3.42,0,0,0-1.21-.21h0a3.3,3.3,0,0,0-1.21.21,3.18,3.18,0,0,0-1,.56,2.53,2.53,0,0,0-.59.82,2.53,2.53,0,0,0,0,2,2.68,2.68,0,0,0,.59.83,3.07,3.07,0,0,0,.94.57,3.32,3.32,0,0,0,1.22.21h0a3.42,3.42,0,0,0,1.21-.21,2.8,2.8,0,0,0,.93-.57,2.59,2.59,0,0,0,.61-.83A2.44,2.44,0,0,0,652,137Z"/><path class="cls-14" d="M654.59,123.63a6.26,6.26,0,0,1-.4,2.23,5.23,5.23,0,0,1-1.14,1.78,5.38,5.38,0,0,1-1.75,1.17,5.85,5.85,0,0,1-2.25.42h0a6,6,0,0,1-2.16-.39,5.52,5.52,0,0,1-1.79-1.09,5.32,5.32,0,0,1-1.2-1.67,5.19,5.19,0,0,1-.43-2.15,5.36,5.36,0,0,1,.47-2.32,4.84,4.84,0,0,1,1.29-1.63,5.53,5.53,0,0,1,1.87-1,7.53,7.53,0,0,1,2.19-.31h.38l.42,0v7.46a2.55,2.55,0,0,0,1.56-.91,2.69,2.69,0,0,0,.54-1.71,3.44,3.44,0,0,0-.26-1.36,5,5,0,0,0-.84-1.24l1.54-1.74a5.59,5.59,0,0,1,1.47,1.84A5.76,5.76,0,0,1,654.59,123.63Zm-6.44-2a2.92,2.92,0,0,0-1.65.72,2,2,0,0,0-.64,1.54,2,2,0,0,0,.62,1.55,3.17,3.17,0,0,0,1.67.77Z"/><path class="cls-14" d="M643.63,116.45v-3h2.16a4.59,4.59,0,0,1-1.77-1.26,3.1,3.1,0,0,1-.59-2.16h3.18v.16a3.14,3.14,0,0,0,.91,2.39,4.07,4.07,0,0,0,2.87.87h4v3Z"/><text class="cls-15" transform="translate(407.96 485)">Lead time</text><line class="cls-16" x1="662.26" y1="472.67" x2="663.97" y2="90.95"/><path class="cls-14" d="M666.77,472.69a4.51,4.51,0,1,1-4.49-4.53A4.51,4.51,0,0,1,666.77,472.69Z"/><path class="cls-14" d="M668.48,91A4.51,4.51,0,1,0,664,95.46,4.51,4.51,0,0,0,668.48,91Z"/><line class="cls-17" x1="662.54" y1="495" x2="282.65" y2="495"/><circle class="cls-18" cx="662.54" cy="495" r="4.51"/><circle class="cls-18" cx="282.65" cy="495" r="4.51"/><line class="cls-19" x1="282.52" y1="523.22" x2="282.52" y2="551.66"/><rect class="cls-14" x="248.78" y="551.66" width="66.6" height="29.39"/><text class="cls-20" transform="translate(254.23 572.34)">Today</text><text class="cls-21" transform="translate(703.81 560.88)">Time</text><path class="cls-7" d="M17.17,150.07a7.35,7.35,0,0,1,1,2,7.7,7.7,0,0,1,.34,2.3,7.47,7.47,0,0,1-.57,3,7.12,7.12,0,0,1-1.55,2.3,7.22,7.22,0,0,1-2.29,1.5,7.3,7.3,0,0,1-2.79.54h0a7.22,7.22,0,0,1-2.79-.54,7,7,0,0,1-2.3-1.52,7.21,7.21,0,0,1-1.57-2.32,7.31,7.31,0,0,1-.58-3,7.47,7.47,0,0,1,.57-3,7,7,0,0,1,3.84-3.8,7.22,7.22,0,0,1,2.79-.54h0a7.27,7.27,0,0,1,2.29.36,6.93,6.93,0,0,1,2,1.06l1.36-1.6,1.76,1.58Zm-.94,4.21a4.47,4.47,0,0,0-.18-1.29,4.41,4.41,0,0,0-.5-1.1l-2,2.4-1.8-1.58,2.2-2.41a4.78,4.78,0,0,0-2.62-.71h0a5.37,5.37,0,0,0-1.93.35,4.77,4.77,0,0,0-1.58,1,4.55,4.55,0,0,0-1.07,1.51,4.48,4.48,0,0,0-.4,1.91,4.56,4.56,0,0,0,1.45,3.39,4.81,4.81,0,0,0,1.57,1,5.32,5.32,0,0,0,1.92.35h0a5.41,5.41,0,0,0,1.93-.35,4.84,4.84,0,0,0,1.58-1,4.93,4.93,0,0,0,1.07-1.5A4.68,4.68,0,0,0,16.23,154.28Z"/><path class="cls-7" d="M18.47,140.66a3.51,3.51,0,0,1-1.1,2.77,4.31,4.31,0,0,1-3,1H7.69V142h6a2.87,2.87,0,0,0,1.91-.58,2.13,2.13,0,0,0,.67-1.66,2.26,2.26,0,0,0-.69-1.71,2.71,2.71,0,0,0-1.93-.65H7.69v-2.44H18.25v2.44H16.59a5.26,5.26,0,0,1,1.32,1.3A3.32,3.32,0,0,1,18.47,140.66Z"/><path class="cls-7" d="M18.25,125.43H17A4.74,4.74,0,0,1,18,126.81a4.39,4.39,0,0,1,.44,2,4.82,4.82,0,0,1-.21,1.44,3.64,3.64,0,0,1-.62,1.19,3.27,3.27,0,0,1-1,.82,3,3,0,0,1-1.41.31h0a3.45,3.45,0,0,1-1.53-.32,2.85,2.85,0,0,1-1.08-.89,3.81,3.81,0,0,1-.64-1.36,6.68,6.68,0,0,1-.21-1.71,9.45,9.45,0,0,1,.12-1.63,8.69,8.69,0,0,1,.32-1.29H12a2.07,2.07,0,0,0-1.68.67A2.85,2.85,0,0,0,9.69,128a6.47,6.47,0,0,0,.18,1.6,10.33,10.33,0,0,0,.48,1.42l-1.94.66a11,11,0,0,1-.64-1.8,8.86,8.86,0,0,1-.24-2.22,4.7,4.7,0,0,1,1.17-3.5A4.51,4.51,0,0,1,12,123h6.26Zm-4.42-.06a6.28,6.28,0,0,0-.3,1.09,7.07,7.07,0,0,0-.12,1.33,3.07,3.07,0,0,0,.44,1.78,1.37,1.37,0,0,0,1.2.64h0a1.33,1.33,0,0,0,1.16-.59,2.58,2.58,0,0,0,.4-1.45,3.92,3.92,0,0,0-.16-1.11,3,3,0,0,0-.45-.89,2.19,2.19,0,0,0-.7-.59,2,2,0,0,0-.91-.21Z"/><path class="cls-7" d="M7.69,120.15v-2.44H9.34a5.54,5.54,0,0,1-.71-.57,3.89,3.89,0,0,1-.6-.72,3.54,3.54,0,0,1-.56-2,3.48,3.48,0,0,1,1.1-2.77,4.25,4.25,0,0,1,3-1h6.72v2.44h-6a2.79,2.79,0,0,0-1.9.58,2.08,2.08,0,0,0-.67,1.66,2.26,2.26,0,0,0,.69,1.71,2.69,2.69,0,0,0,1.92.65h5.95v2.44Z"/><path class="cls-7" d="M18.43,104.19a4.68,4.68,0,0,1-.16,1.22,2.5,2.5,0,0,1-.52,1,2.32,2.32,0,0,1-.95.64,4.46,4.46,0,0,1-1.47.22H9.77v1.34H7.69v-1.34H4.79v-2.44h2.9V102H9.77v2.84H15a1.31,1.31,0,0,0,1-.34,1.42,1.42,0,0,0,.31-1,3.31,3.31,0,0,0-.36-1.5h2a4.19,4.19,0,0,1,.54,2.18Z"/><path class="cls-7" d="M3.77,99.51V96.89h2.3v2.62Zm3.92-.1V97H18.25v2.44Z"/><path class="cls-7" d="M18.43,90.27a4.68,4.68,0,0,1-.16,1.22,2.5,2.5,0,0,1-.52,1,2.32,2.32,0,0,1-.95.64,4.46,4.46,0,0,1-1.47.22H9.77v1.34H7.69V93.33H4.79V90.89h2.9V88.05H9.77v2.84H15a1.31,1.31,0,0,0,1-.34,1.42,1.42,0,0,0,.31-1,3.31,3.31,0,0,0-.36-1.5h2a4.69,4.69,0,0,1,.4,1A4.76,4.76,0,0,1,18.43,90.27Z"/><path class="cls-7" d="M21.51,83.47a5,5,0,0,1-.17,1.35A6.32,6.32,0,0,1,20.87,86l-1.8-.82a5.07,5.07,0,0,0,.29-.69,2.53,2.53,0,0,0,.11-.73,1.4,1.4,0,0,0-.26-.86,2.45,2.45,0,0,0-1-.64L7.69,86.63V84.05l7.84-3L7.69,78.31V75.79L18.55,80a8.93,8.93,0,0,1,1.38.66,3.87,3.87,0,0,1,.91.76,2.78,2.78,0,0,1,.51.92A3.92,3.92,0,0,1,21.51,83.47Z"/></g></svg>
\ No newline at end of file diff --git a/addons/stock/static/img/res_partner_address_41.jpg b/addons/stock/static/img/res_partner_address_41.jpg Binary files differnew file mode 100644 index 00000000..1de43e92 --- /dev/null +++ b/addons/stock/static/img/res_partner_address_41.jpg diff --git a/addons/stock/static/src/img/barcode.gif b/addons/stock/static/src/img/barcode.gif Binary files differnew file mode 100644 index 00000000..ef828b3b --- /dev/null +++ b/addons/stock/static/src/img/barcode.gif diff --git a/addons/stock/static/src/js/basic_model.js b/addons/stock/static/src/js/basic_model.js new file mode 100644 index 00000000..6a5653c6 --- /dev/null +++ b/addons/stock/static/src/js/basic_model.js @@ -0,0 +1,16 @@ +odoo.define('stock.BasicModel', function (require) { +"use strict"; + +var BasicModel = require('web.BasicModel'); +var localStorage = require('web.local_storage'); + +BasicModel.include({ + + _invalidateCache: function (dataPoint) { + this._super.apply(this, arguments); + if (dataPoint.model === 'stock.warehouse' && !localStorage.getItem('running_tour')) { + this.do_action('reload_context'); + } + } +}); +}); diff --git a/addons/stock/static/src/js/forecast_widget.js b/addons/stock/static/src/js/forecast_widget.js new file mode 100644 index 00000000..b1c4dc31 --- /dev/null +++ b/addons/stock/static/src/js/forecast_widget.js @@ -0,0 +1,76 @@ +odoo.define('stock.forecast_widget', function (require) { +'use strict'; + +const AbstractField = require('web.AbstractField'); +const fieldRegistry = require('web.field_registry'); +const field_utils = require('web.field_utils'); +const utils = require('web.utils'); +const core = require('web.core'); +const QWeb = core.qweb; + +const ForecastWidgetField = AbstractField.extend({ + supportedFieldTypes: ['float'], + + _render: function () { + var data = Object.assign({}, this.record.data, { + forecast_availability_str: field_utils.format.float( + this.record.data.forecast_availability, + this.record.fields.forecast_availability, + this.nodeOptions + ), + reserved_availability_str: field_utils.format.float( + this.record.data.reserved_availability, + this.record.fields.reserved_availability, + this.nodeOptions + ), + forecast_expected_date_str: field_utils.format.date( + this.record.data.forecast_expected_date, + this.record.fields.forecast_expected_date + ), + }); + if (data.forecast_expected_date && data.date_deadline) { + data.forecast_is_late = data.forecast_expected_date > data.date_deadline; + } + data.will_be_fulfilled = utils.round_decimals(data.forecast_availability, this.record.fields.forecast_availability.digits[1]) >= utils.round_decimals(data.product_qty, this.record.fields.forecast_availability.digits[1]); + + this.$el.html(QWeb.render('stock.forecastWidget', data)); + this.$('.o_forecast_report_button').on('click', this._onOpenReport.bind(this)); + }, + + isSet: function () { + return true; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Opens the Forecast Report for the `stock.move` product. + * + * @param {MouseEvent} ev + */ + _onOpenReport: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + if (!this.recordData.id) { + return; + } + this._rpc({ + model: 'stock.move', + method: 'action_product_forecast_report', + args: [this.recordData.id], + }).then(action => { + action.context = Object.assign(action.context || {}, { + active_model: 'product.product', + active_id: this.recordData.product_id.res_id, + }); + this.do_action(action); + }); + }, +}); + +fieldRegistry.add('forecast_widget', ForecastWidgetField); + +return ForecastWidgetField; +}); diff --git a/addons/stock/static/src/js/inventory_report_list_controller.js b/addons/stock/static/src/js/inventory_report_list_controller.js new file mode 100644 index 00000000..eb6a3ed6 --- /dev/null +++ b/addons/stock/static/src/js/inventory_report_list_controller.js @@ -0,0 +1,66 @@ +odoo.define('stock.InventoryReportListController', function (require) { +"use strict"; + +var core = require('web.core'); +var ListController = require('web.ListController'); + +var qweb = core.qweb; + + +var InventoryReportListController = ListController.extend({ + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- + + init: function (parent, model, renderer, params) { + this.context = renderer.state.getContext(); + return this._super.apply(this, arguments); + }, + + /** + * @override + */ + renderButtons: function ($node) { + this._super.apply(this, arguments); + if (this.context.no_at_date) { + return; + } + var $buttonToDate = $(qweb.render('InventoryReport.Buttons')); + $buttonToDate.on('click', this._onOpenWizard.bind(this)); + this.$buttons.prepend($buttonToDate); + }, + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + + /** + * Handler called when the user clicked on the 'Inventory at Date' button. + * Opens wizard to display, at choice, the products inventory or a computed + * inventory at a given date. + */ + _onOpenWizard: function () { + var state = this.model.get(this.handle, {raw: true}); + var stateContext = state.getContext(); + var context = { + active_model: this.modelName, + }; + if (stateContext.default_product_id) { + context.product_id = stateContext.default_product_id; + } else if (stateContext.product_tmpl_id) { + context.product_tmpl_id = stateContext.product_tmpl_id; + } + this.do_action({ + res_model: 'stock.quantity.history', + views: [[false, 'form']], + target: 'new', + type: 'ir.actions.act_window', + context: context, + }); + }, +}); + +return InventoryReportListController; + +}); diff --git a/addons/stock/static/src/js/inventory_report_list_view.js b/addons/stock/static/src/js/inventory_report_list_view.js new file mode 100644 index 00000000..494dd838 --- /dev/null +++ b/addons/stock/static/src/js/inventory_report_list_view.js @@ -0,0 +1,19 @@ +odoo.define('stock.InventoryReportListView', function (require) { +"use strict"; + +var ListView = require('web.ListView'); +var InventoryReportListController = require('stock.InventoryReportListController'); +var viewRegistry = require('web.view_registry'); + + +var InventoryReportListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: InventoryReportListController, + }), +}); + +viewRegistry.add('inventory_report_list', InventoryReportListView); + +return InventoryReportListView; + +}); diff --git a/addons/stock/static/src/js/inventory_singleton_list_controller.js b/addons/stock/static/src/js/inventory_singleton_list_controller.js new file mode 100644 index 00000000..9e5bd4db --- /dev/null +++ b/addons/stock/static/src/js/inventory_singleton_list_controller.js @@ -0,0 +1,68 @@ +odoo.define('stock.SingletonListController', function (require) { +"use strict"; + +var core = require('web.core'); +var InventoryReportListController = require('stock.InventoryReportListController'); + +var _t = core._t; + +/** + * The purpose of this override is to avoid to have two or more similar records + * in the list view. + * + * It's used in quant list view, a list editable where when you create a new + * line about a quant who already exists, we want to update the existing one + * instead of create a new one, and then we don't want to have two similar line + * in the list view, so we refresh it. + */ + +var SingletonListController = InventoryReportListController.extend({ + /** + * @override + * @return {Promise} rejected when update the list because we don't want + * anymore to select a cell who maybe doesn't exist anymore. + */ + _confirmSave: function (id) { + var newRecord = this.model.localData[id]; + var model = newRecord.model; + var res_id = newRecord.res_id; + + var findSimilarRecords = function (record) { + if ((record.groupedBy && record.groupedBy.length > 0) || record.data.length) { + var recordsToReturn = []; + for (var i in record.data) { + var foundRecords = findSimilarRecords(record.data[i]); + recordsToReturn = recordsToReturn.concat(foundRecords || []); + } + return recordsToReturn; + } else { + if (record.res_id === res_id && record.model === model) { + if (record.count === 0){ + return [record]; + } + else if (record.ref && record.ref.indexOf('virtual') !== -1) { + return [record]; + } + } + } + }; + + var handle = this.model.get(this.handle); + var similarRecords = findSimilarRecords(handle); + + if (similarRecords.length > 1) { + var notification = _t("You tried to create a record who already exists."+ + "<br/>This last one has been modified instead."); + this.do_notify(_t("This record already exists."), notification); + this.reload(); + return Promise.reject(); + } + else { + return this._super.apply(this, arguments); + } + }, +}); + +return SingletonListController; + +}); diff --git a/addons/stock/static/src/js/inventory_singleton_list_view.js b/addons/stock/static/src/js/inventory_singleton_list_view.js new file mode 100644 index 00000000..53faf5b8 --- /dev/null +++ b/addons/stock/static/src/js/inventory_singleton_list_view.js @@ -0,0 +1,18 @@ +odoo.define('stock.SingletonListView', function (require) { +'use strict'; + +var InventoryReportListView = require('stock.InventoryReportListView'); +var SingletonListController = require('stock.SingletonListController'); +var viewRegistry = require('web.view_registry'); + +var SingletonListView = InventoryReportListView.extend({ + config: _.extend({}, InventoryReportListView.prototype.config, { + Controller: SingletonListController, + }), +}); + +viewRegistry.add('singleton_list', SingletonListView); + +return SingletonListView; + +}); diff --git a/addons/stock/static/src/js/inventory_validate_button_controller.js b/addons/stock/static/src/js/inventory_validate_button_controller.js new file mode 100644 index 00000000..cd554bd1 --- /dev/null +++ b/addons/stock/static/src/js/inventory_validate_button_controller.js @@ -0,0 +1,89 @@ +odoo.define('stock.InventoryValidationController', function (require) { +"use strict"; + +var core = require('web.core'); +var ListController = require('web.ListController'); + +var _t = core._t; +var qweb = core.qweb; + +var InventoryValidationController = ListController.extend({ + events: _.extend({ + 'click .o_button_validate_inventory': '_onValidateInventory' + }, ListController.prototype.events), + /** + * @override + */ + init: function (parent, model, renderer, params) { + var context = renderer.state.getContext(); + this.inventory_id = context.active_id; + return this._super.apply(this, arguments); + }, + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- + + /** + * @override + */ + renderButtons: function () { + this._super.apply(this, arguments); + var $validationButton = $(qweb.render('InventoryLines.Buttons')); + this.$buttons.prepend($validationButton); + }, + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + + /** + * Handler called when user click on validation button in inventory lines + * view. Makes an rpc to try to validate the inventory, then will go back on + * the inventory view form if it was validated. + * This method could also open a wizard in case something was missing. + * + * @private + */ + _onValidateInventory: function () { + var self = this; + var prom = Promise.resolve(); + var recordID = this.renderer.getEditableRecordID(); + if (recordID) { + // If user's editing a record, we wait to save it before to try to + // validate the inventory. + prom = this.saveRecord(recordID); + } + + prom.then(function () { + self._rpc({ + model: 'stock.inventory', + method: 'action_validate', + args: [self.inventory_id] + }).then(function (res) { + var exitCallback = function (infos) { + // In case we discarded a wizard, we do nothing to stay on + // the same view... + if (infos && infos.special) { + return; + } + // ... but in any other cases, we go back on the inventory form. + self.do_notify( + false, + _t("The inventory has been validated")); + self.trigger_up('history_back'); + }; + + if (_.isObject(res)) { + self.do_action(res, { on_close: exitCallback }); + } else { + return exitCallback(); + } + }); + }); + }, +}); + +return InventoryValidationController; + +}); diff --git a/addons/stock/static/src/js/inventory_validate_button_view.js b/addons/stock/static/src/js/inventory_validate_button_view.js new file mode 100644 index 00000000..ed5a5f42 --- /dev/null +++ b/addons/stock/static/src/js/inventory_validate_button_view.js @@ -0,0 +1,16 @@ +odoo.define('stock.InventoryValidationView', function (require) { +"use strict"; + +var InventoryValidationController = require('stock.InventoryValidationController'); +var ListView = require('web.ListView'); +var viewRegistry = require('web.view_registry'); + +var InventoryValidationView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: InventoryValidationController + }) +}); + +viewRegistry.add('inventory_validate_button', InventoryValidationView); + +}); diff --git a/addons/stock/static/src/js/popover_widget.js b/addons/stock/static/src/js/popover_widget.js new file mode 100644 index 00000000..567dd494 --- /dev/null +++ b/addons/stock/static/src/js/popover_widget.js @@ -0,0 +1,84 @@ +odoo.define('stock.popover_widget', function (require) { +'use strict'; + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var QWeb = core.qweb; +var Context = require('web.Context'); +var data_manager = require('web.data_manager'); +var fieldRegistry = require('web.field_registry'); + +/** + * Widget Popover for JSON field (char), by default render a simple html message + * { + * 'msg': '<CONTENT OF THE POPOVER>', + * 'icon': '<FONT AWESOME CLASS>' (optionnal), + * 'color': '<COLOR CLASS OF ICON>' (optionnal), + * 'title': '<TITLE OF POPOVER>' (optionnal), + * 'popoverTemplate': '<TEMPLATE OF THE TEMPLATE>' (optionnal) + * } + */ +var PopoverWidgetField = AbstractField.extend({ + supportedFieldTypes: ['char'], + buttonTemplape: 'stock.popoverButton', + popoverTemplate: 'stock.popoverContent', + trigger: 'focus', + placement: 'top', + html: true, + color: 'text-primary', + icon: 'fa-info-circle', + + _render: function () { + var value = JSON.parse(this.value); + if (!value) { + this.$el.html(''); + return; + } + this.$el.css('max-width', '17px'); + this.$el.html(QWeb.render(this.buttonTemplape, _.defaults(value, {color: this.color, icon: this.icon}))); + this.$el.find('a').prop('special_click', true); + this.$popover = $(QWeb.render(value.popoverTemplate || this.popoverTemplate, value)); + this.$popover.on('click', '.action_open_forecast', this._openForecast.bind(this)); + this.$el.find('a').popover({ + content: this.$popover, + html: this.html, + placement: this.placement, + title: value.title || this.title.toString(), + trigger: this.trigger, + delay: {'show': 0, 'hide': 100}, + }); + }, + + /** + * Redirect to the product forecasted report. + * + * @private + * @param {MouseEvent} event + * @returns {Promise} action loaded + */ + async _openForecast(ev) { + ev.stopPropagation(); + const reportContext = { + active_model: 'product.product', + active_id: this.recordData.product_id.data.id, + }; + const action = await this._rpc({ + model: reportContext.active_model, + method: 'action_product_forecast_report', + args: [[reportContext.active_id]], + }); + action.context = new Context(action.context, reportContext); + return this.do_action(action); + }, + + destroy: function () { + this.$el.find('a').popover('dispose'); + this._super.apply(this, arguments); + }, + +}); + +fieldRegistry.add('popover_widget', PopoverWidgetField); + +return PopoverWidgetField; +}); diff --git a/addons/stock/static/src/js/report_stock_forecasted.js b/addons/stock/static/src/js/report_stock_forecasted.js new file mode 100644 index 00000000..27121e8e --- /dev/null +++ b/addons/stock/static/src/js/report_stock_forecasted.js @@ -0,0 +1,268 @@ +odoo.define('stock.ReplenishReport', function (require) { +"use strict"; + +const clientAction = require('report.client_action'); +const core = require('web.core'); +const dom = require('web.dom'); +const GraphView = require('web.GraphView'); + +const qweb = core.qweb; +const _t = core._t; + + +const ReplenishReport = clientAction.extend({ + /** + * @override + */ + init: function (parent, action, options) { + this._super.apply(this, arguments); + this.context = action.context; + this.productId = this.context.active_id; + this.resModel = this.context.active_model || this.context.params.active_model || 'product.template'; + const isTemplate = this.resModel === 'product.template'; + this.actionMethod = `action_product_${isTemplate ? 'tmpl_' : ''}forecast_report`; + const reportName = `report_product_${isTemplate ? 'template' : 'product'}_replenishment`; + this.report_url = `/report/html/stock.${reportName}/${this.productId}`; + if (this.context.warehouse) { + this.active_warehouse = {id: this.context.warehouse}; + } + this.report_url += `?context=${JSON.stringify(this.context)}&force_context_lang=1`; + this._title = action.name; + }, + + /** + * @override + */ + start: function () { + return Promise.all([ + this._super(...arguments), + this._renderWarehouseFilters(), + ]).then(() => { + this._renderButtons(); + }); + }, + + /** + * @override + */ + on_attach_callback: function () { + this._super(); + this._createGraphView(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Instanciates a chart graph and moves it into the report (which is in the iframe). + */ + _createGraphView: async function () { + let viewController; + const appendGraph = () => { + promController.then(() => { + this.iframe.removeEventListener('load', appendGraph); + const $reportGraphDiv = $(this.iframe).contents().find('.o_report_graph'); + dom.append(this.$el, viewController.$el, { + in_DOM: true, + callbacks: [{widget: viewController}], + }); + const renderer = viewController.renderer; + // Remove the graph control panel. + $('.o_control_panel:last').remove(); + const $graphPanel = $('.o_graph_controller'); + $graphPanel.appendTo($reportGraphDiv); + + if (!renderer.state.dataPoints.length) { + // Changes the "No Data" helper message. + const graphHelper = renderer.$('.o_view_nocontent'); + const newMessage = qweb.render('View.NoContentHelper', { + description: _t("Try to add some incoming or outgoing transfers."), + }); + graphHelper.replaceWith(newMessage); + } else { + this.chart = renderer.chart; + // Lame hack to fix the size of the graph. + setTimeout(() => { + this.chart.canvas.height = 300; + this.chart.canvas.style.height = "300px"; + this.chart.resize(); + }, 1); + } + }); + }; + // Wait the iframe fo append the graph chart and move it into the iframe. + this.iframe.addEventListener('load', appendGraph); + + const model = 'report.stock.quantity'; + const promController = this._rpc({ + model: model, + method: 'fields_view_get', + kwargs: { + view_type: 'graph', + } + }).then(viewInfo => { + const params = { + modelName: model, + domain: this._getReportDomain(), + hasActionMenus: false, + }; + const graphView = new GraphView(viewInfo, params); + return graphView.getController(this); + }).then(res => { + viewController = res; + + // Hack to put the res_model on the url. This way, the report always know on with res_model it refers. + if (location.href.indexOf('active_model') === -1) { + const url = window.location.href + `&active_model=${this.resModel}`; + window.history.pushState({}, "", url); + } + const fragment = document.createDocumentFragment(); + return viewController.appendTo(fragment); + }); + }, + + /** + * Return the action to open this report. + * + * @returns {Promise} + */ + _getForecastedReportAction: function () { + return this._rpc({ + model: this.resModel, + method: this.actionMethod, + args: [this.productId], + context: this.context, + }); + }, + + /** + * Returns a domain to filter on the product variant or product template + * depending of the active model. + * + * @returns {Array} + */ + _getReportDomain: function () { + const domain = [ + ['state', '=', 'forecast'], + ['warehouse_id', '=', this.active_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; + }, + + /** + * TODO + * + * @param {Object} additionnalContext + */ + _reloadReport: function (additionnalContext) { + return this._getForecastedReportAction().then((action) => { + action.context = Object.assign({ + active_id: this.productId, + active_model: this.resModel, + }, this.context, additionnalContext); + return this.do_action(action, {replace_last_action: true}); + }); + }, + + /** + * Renders the 'Replenish' button and replaces the default 'Print' button by this new one. + */ + _renderButtons: function () { + const $newButtons = $(qweb.render('replenish_report_buttons', {})); + this.$buttons.find('.o_report_print').replaceWith($newButtons); + this.$buttons.on('click', '.o_report_replenish_buy', this._onClickReplenish.bind(this)); + this.controlPanelProps.cp_content = { + $buttons: this.$buttons, + }; + }, + + /** + * TODO + * @returns {Promise} + */ + _renderWarehouseFilters: function () { + return this._rpc({ + model: 'report.stock.report_product_product_replenishment', + method: 'get_filter_state', + }).then((res) => { + const warehouses = res.warehouses; + const active_warehouse = (this.active_warehouse && this.active_warehouse.id) || res.active_warehouse; + if (active_warehouse) { + this.active_warehouse = _.findWhere(warehouses, {id: active_warehouse}); + } else { + this.active_warehouse = warehouses[0]; + } + const $filters = $(qweb.render('warehouseFilter', { + active_warehouse: this.active_warehouse, + warehouses: warehouses, + displayWarehouseFilter: (warehouses.length > 1), + })); + // Bind handlers. + $filters.on('click', '.warehouse_filter', this._onClickFilter.bind(this)); + this.$('.o_search_options').append($filters); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Opens the product replenish wizard. Could re-open the report if pending + * forecasted quantities need to be updated. + * + * @returns {Promise} + */ + _onClickReplenish: function () { + const context = Object.assign({}, 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.active_warehouse.id; + + const on_close = function (res) { + if (res && res.special) { + // Do nothing when the wizard is discarded. + return; + } + // Otherwise, opens again the report. + return this._reloadReport(); + }; + + const action = { + res_model: 'product.replenish', + name: _t('Product Replenish'), + type: 'ir.actions.act_window', + views: [[false, 'form']], + target: 'new', + context: context, + }; + + return this.do_action(action, { + on_close: on_close.bind(this), + }); + }, + + /** + * Re-opens the report with data for the specified warehouse. + * + * @returns {Promise} + */ + _onClickFilter: function (ev) { + const data = ev.target.dataset; + const warehouse_id = Number(data.warehouseId); + return this._reloadReport({warehouse: warehouse_id}); + } +}); + +core.action_registry.add('replenish_report', ReplenishReport); + +});
\ No newline at end of file diff --git a/addons/stock/static/src/js/stock_orderpoint_list_controller.js b/addons/stock/static/src/js/stock_orderpoint_list_controller.js new file mode 100644 index 00000000..4ad07508 --- /dev/null +++ b/addons/stock/static/src/js/stock_orderpoint_list_controller.js @@ -0,0 +1,74 @@ +odoo.define('stock.StockOrderpointListController', function (require) { +"use strict"; + +var core = require('web.core'); +var ListController = require('web.ListController'); + +var qweb = core.qweb; + + +var StockOrderpointListController = ListController.extend({ + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- + + /** + * @override + */ + renderButtons: function () { + this._super.apply(this, arguments); + this.$buttons.find('.o_button_import').addClass('d-none'); + this.$buttons.find('.o_list_export_xlsx').addClass('d-none'); + this.$buttons.find('.o_list_button_add').removeClass('btn-primary').addClass('btn-secondary'); + var $buttons = $(qweb.render('StockOrderpoint.Buttons')); + var $buttonOrder = $buttons.find('.o_button_order'); + var $buttonSnooze = $buttons.find('.o_button_snooze'); + $buttonOrder.on('click', this._onReplenish.bind(this)); + $buttonSnooze.on('click', this._onSnooze.bind(this)); + $buttons.prependTo(this.$buttons); + }, + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + + _onButtonClicked: function (ev) { + if (ev.data.attrs.class.split(' ').includes('o_replenish_buttons')) { + ev.stopPropagation(); + var self = this; + this._callButtonAction(ev.data.attrs, ev.data.record).then(function () { + self.reload(); + }); + } else { + this._super.apply(this, arguments); + } + }, + + _onReplenish: function () { + var records = this.getSelectedRecords(); + this.model.replenish(records); + }, + + _onSelectionChanged: function (ev) { + this._super(ev); + var $buttonOrder = this.$el.find('.o_button_order'); + var $buttonSnooze = this.$el.find('.o_button_snooze'); + if (this.getSelectedIds().length === 0){ + $buttonOrder.addClass('d-none'); + $buttonSnooze.addClass('d-none'); + } else { + $buttonOrder.removeClass('d-none'); + $buttonSnooze.removeClass('d-none'); + } + }, + + _onSnooze: function () { + var records = this.getSelectedRecords(); + this.model.snooze(records); + }, +}); + +return StockOrderpointListController; + +}); diff --git a/addons/stock/static/src/js/stock_orderpoint_list_model.js b/addons/stock/static/src/js/stock_orderpoint_list_model.js new file mode 100644 index 00000000..c9f0acb9 --- /dev/null +++ b/addons/stock/static/src/js/stock_orderpoint_list_model.js @@ -0,0 +1,46 @@ +odoo.define('stock.StockOrderpointListModel', function (require) { +"use strict"; + +var core = require('web.core'); +var ListModel = require('web.ListModel'); + +var qweb = core.qweb; + + +var StockOrderpointListModel = ListModel.extend({ + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- + /** + */ + replenish: function (records) { + var self = this; + var model = records[0].model; + var recordResIds = _.pluck(records, 'res_id'); + var context = records[0].getContext(); + return this._rpc({ + model: model, + method: 'action_replenish', + args: [recordResIds], + context: context, + }).then(function () { + return self.do_action('stock.action_replenishment'); + }); + }, + + snooze: function (records) { + var recordResIds = _.pluck(records, 'res_id'); + var self = this; + return this.do_action('stock.action_orderpoint_snooze', { + additional_context: { + default_orderpoint_ids: recordResIds + }, + on_close: () => self.do_action('stock.action_replenishment') + }); + }, +}); + +return StockOrderpointListModel; + +}); diff --git a/addons/stock/static/src/js/stock_orderpoint_list_view.js b/addons/stock/static/src/js/stock_orderpoint_list_view.js new file mode 100644 index 00000000..d893ba9e --- /dev/null +++ b/addons/stock/static/src/js/stock_orderpoint_list_view.js @@ -0,0 +1,21 @@ +odoo.define('stock.StockOrderpointListView', function (require) { +"use strict"; + +var ListView = require('web.ListView'); +var StockOrderpointListController = require('stock.StockOrderpointListController'); +var StockOrderpointListModel = require('stock.StockOrderpointListModel'); +var viewRegistry = require('web.view_registry'); + + +var StockOrderpointListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: StockOrderpointListController, + Model: StockOrderpointListModel, + }), +}); + +viewRegistry.add('stock_orderpoint_list', StockOrderpointListView); + +return StockOrderpointListView; + +}); diff --git a/addons/stock/static/src/js/stock_rescheduling_popover.js b/addons/stock/static/src/js/stock_rescheduling_popover.js new file mode 100644 index 00000000..c3959642 --- /dev/null +++ b/addons/stock/static/src/js/stock_rescheduling_popover.js @@ -0,0 +1,39 @@ +odoo.define('stock.PopoverStockPicking', function (require) { +"use strict"; + +var core = require('web.core'); + +var PopoverWidgetField = require('stock.popover_widget'); +var registry = require('web.field_registry'); +var _lt = core._lt; + +var PopoverStockPicking = PopoverWidgetField.extend({ + title: _lt('Planning Issue'), + trigger: 'focus', + color: 'text-danger', + icon: 'fa-exclamation-triangle', + + _render: function () { + this._super(); + if (this.$popover) { + var self = this; + this.$popover.find('a').on('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + self.do_action({ + type: 'ir.actions.act_window', + res_model: ev.currentTarget.getAttribute('element-model'), + res_id: parseInt(ev.currentTarget.getAttribute('element-id'), 10), + views: [[false, 'form']], + target: 'current' + }); + }); + } + }, + +}); + +registry.add('stock_rescheduling_popover', PopoverStockPicking); + +return PopoverStockPicking; +}); diff --git a/addons/stock/static/src/js/stock_traceability_report_backend.js b/addons/stock/static/src/js/stock_traceability_report_backend.js new file mode 100644 index 00000000..6a504cd2 --- /dev/null +++ b/addons/stock/static/src/js/stock_traceability_report_backend.js @@ -0,0 +1,106 @@ +odoo.define('stock.stock_report_generic', function (require) { +'use strict'; + +var AbstractAction = require('web.AbstractAction'); +var core = require('web.core'); +var session = require('web.session'); +var ReportWidget = require('stock.ReportWidget'); +var framework = require('web.framework'); + +var QWeb = core.qweb; + +var stock_report_generic = AbstractAction.extend({ + hasControlPanel: true, + + // Stores all the parameters of the action. + init: function(parent, action) { + this._super.apply(this, arguments); + this.actionManager = parent; + this.given_context = Object.assign({}, session.user_context); + this.controller_url = action.context.url; + if (action.context.context) { + this.given_context = action.context.context; + } + this.given_context.active_id = action.context.active_id || action.params.active_id; + this.given_context.model = action.context.active_model || false; + this.given_context.ttype = action.context.ttype || false; + this.given_context.auto_unfold = action.context.auto_unfold || false; + this.given_context.lot_name = action.context.lot_name || false; + }, + willStart: function() { + return Promise.all([this._super.apply(this, arguments), this.get_html()]); + }, + set_html: function() { + var self = this; + var def = Promise.resolve(); + if (!this.report_widget) { + this.report_widget = new ReportWidget(this, this.given_context); + def = this.report_widget.appendTo(this.$('.o_content')); + } + return def.then(function () { + self.report_widget.$el.html(self.html); + self.report_widget.$el.find('.o_report_heading').html('<h1>Traceability Report</h1>'); + if (self.given_context.auto_unfold) { + _.each(self.$el.find('.fa-caret-right'), function (line) { + self.report_widget.autounfold(line, self.given_context.lot_name); + }); + } + }); + }, + start: async function() { + this.controlPanelProps.cp_content = { $buttons: this.$buttons }; + await this._super(...arguments); + this.set_html(); + }, + // Fetches the html and is previous report.context if any, else create it + get_html: async function() { + const { html } = await this._rpc({ + args: [this.given_context], + method: 'get_html', + model: 'stock.traceability.report', + }); + this.html = html; + this.renderButtons(); + }, + // Updates the control panel and render the elements that have yet to be rendered + update_cp: function() { + if (!this.$buttons) { + this.renderButtons(); + } + this.controlPanelProps.cp_content = { $buttons: this.$buttons }; + return this.updateControlPanel(); + }, + renderButtons: function() { + var self = this; + this.$buttons = $(QWeb.render("stockReports.buttons", {})); + // pdf output + this.$buttons.bind('click', function () { + var $element = $(self.$el[0]).find('.o_stock_reports_table tbody tr'); + var dict = []; + + $element.each(function( index ) { + var $el = $($element[index]); + dict.push({ + 'id': $el.data('id'), + 'model_id': $el.data('model_id'), + 'model_name': $el.data('model'), + 'unfoldable': $el.data('unfold'), + 'level': $el.find('td:first').data('level') || 1 + }); + }); + framework.blockUI(); + var url_data = self.controller_url.replace('active_id', self.given_context.active_id); + session.get_file({ + url: url_data.replace('output_format', 'pdf'), + data: {data: JSON.stringify(dict)}, + complete: framework.unblockUI, + error: (error) => self.call('crash_manager', 'rpc_error', error), + }); + }); + return this.$buttons; + }, +}); + +core.action_registry.add("stock_report_generic", stock_report_generic); +return stock_report_generic; +}); diff --git a/addons/stock/static/src/js/stock_traceability_report_widgets.js b/addons/stock/static/src/js/stock_traceability_report_widgets.js new file mode 100644 index 00000000..97468a48 --- /dev/null +++ b/addons/stock/static/src/js/stock_traceability_report_widgets.js @@ -0,0 +1,131 @@ +odoo.define('stock.ReportWidget', function (require) { +'use strict'; + +var core = require('web.core'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; + +var _t = core._t; + +var ReportWidget = Widget.extend({ + events: { + 'click span.o_stock_reports_foldable': 'fold', + 'click span.o_stock_reports_unfoldable': 'unfold', + 'click .o_stock_reports_web_action': 'boundLink', + 'click .o_stock_reports_stream': 'updownStream', + 'click .o_stock_report_lot_action': 'actionOpenLot' + }, + init: function(parent) { + this._super.apply(this, arguments); + }, + start: function() { + QWeb.add_template("/stock/static/src/xml/stock_traceability_report_line.xml"); + return this._super.apply(this, arguments); + }, + boundLink: function(e) { + e.preventDefault(); + return this.do_action({ + type: 'ir.actions.act_window', + res_model: $(e.target).data('res-model'), + res_id: $(e.target).data('active-id'), + views: [[false, 'form']], + target: 'current' + }); + }, + actionOpenLot: function(e) { + e.preventDefault(); + var $el = $(e.target).parents('tr'); + this.do_action({ + type: 'ir.actions.client', + tag: 'stock_report_generic', + name: $el.data('lot_name') !== undefined && $el.data('lot_name').toString(), + context: { + active_id : $el.data('lot_id'), + active_model : 'stock.production.lot', + url: '/stock/output_format/stock/active_id' + }, + }); + }, + updownStream: function(e) { + var $el = $(e.target).parents('tr'); + this.do_action({ + type: "ir.actions.client", + tag: 'stock_report_generic', + name: _t("Traceability Report"), + context: { + active_id : $el.data('model_id'), + active_model : $el.data('model'), + auto_unfold: true, + lot_name: $el.data('lot_name') !== undefined && $el.data('lot_name').toString(), + url: '/stock/output_format/stock/active_id' + }, + }); + }, + removeLine: function(element) { + var self = this; + var el, $el; + var rec_id = element.data('id'); + var $stockEl = element.nextAll('tr[data-parent_id=' + rec_id + ']') + for (el in $stockEl) { + $el = $($stockEl[el]).find(".o_stock_reports_domain_line_0, .o_stock_reports_domain_line_1"); + if ($el.length === 0) { + break; + } + else { + var $nextEls = $($el[0]).parents("tr"); + self.removeLine($nextEls); + $nextEls.remove(); + } + $el.remove(); + } + return true; + }, + fold: function(e) { + this.removeLine($(e.target).parents('tr')); + var active_id = $(e.target).parents('tr').find('td.o_stock_reports_foldable').data('id'); + $(e.target).parents('tr').find('td.o_stock_reports_foldable').attr('class', 'o_stock_reports_unfoldable ' + active_id); // Change the class, rendering, and remove line from model + $(e.target).parents('tr').find('span.o_stock_reports_foldable').replaceWith(QWeb.render("unfoldable", {lineId: active_id})); + $(e.target).parents('tr').toggleClass('o_stock_reports_unfolded'); + }, + autounfold: function(target, lot_name) { + var self = this; + var $CurretElement; + $CurretElement = $(target).parents('tr').find('td.o_stock_reports_unfoldable'); + var active_id = $CurretElement.data('id'); + var active_model_name = $CurretElement.data('model'); + var active_model_id = $CurretElement.data('model_id'); + var row_level = $CurretElement.data('level'); + var $cursor = $(target).parents('tr'); + this._rpc({ + model: 'stock.traceability.report', + method: 'get_lines', + args: [parseInt(active_id, 10)], + kwargs: { + 'model_id': active_model_id, + 'model_name': active_model_name, + 'level': parseInt(row_level) + 30 || 1 + }, + }) + .then(function (lines) {// After loading the line + _.each(lines, function (line) { // Render each line + $cursor.after(QWeb.render("report_mrp_line", {l: line})); + $cursor = $cursor.next(); + if ($cursor && line.unfoldable && line.lot_name == lot_name) { + self.autounfold($cursor.find(".fa-caret-right"), lot_name); + } + }); + }); + $CurretElement.attr('class', 'o_stock_reports_foldable ' + active_id); // Change the class, and rendering of the unfolded line + $(target).parents('tr').find('span.o_stock_reports_unfoldable').replaceWith(QWeb.render("foldable", {lineId: active_id})); + $(target).parents('tr').toggleClass('o_stock_reports_unfolded'); + }, + unfold: function(e) { + this.autounfold($(e.target)); + }, + +}); + +return ReportWidget; + +}); diff --git a/addons/stock/static/src/scss/forecast_widget.scss b/addons/stock/static/src/scss/forecast_widget.scss new file mode 100644 index 00000000..d9167a69 --- /dev/null +++ b/addons/stock/static/src/scss/forecast_widget.scss @@ -0,0 +1,8 @@ +.o_forecast_widget_cell { + text-align: right; + padding-right: 24px!important; + + button { + position: absolute; + } +} diff --git a/addons/stock/static/src/scss/report_stock_forecasted.scss b/addons/stock/static/src/scss/report_stock_forecasted.scss new file mode 100644 index 00000000..edb6aabd --- /dev/null +++ b/addons/stock/static/src/scss/report_stock_forecasted.scss @@ -0,0 +1,11 @@ +.o_report_replenishment_page { + .o_report_replenishment { + .o_grid_warning { + background-color: #f4cccc; + } + } + + .o_forecasted_row { + background-color: #dee2e6; + } +} diff --git a/addons/stock/static/src/scss/report_stock_rule.scss b/addons/stock/static/src/scss/report_stock_rule.scss new file mode 100644 index 00000000..2e554762 --- /dev/null +++ b/addons/stock/static/src/scss/report_stock_rule.scss @@ -0,0 +1,122 @@ +.o_report_stock_rule{ + .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; + } + } + } +} diff --git a/addons/stock/static/src/scss/stock_empty_screen.scss b/addons/stock/static/src/scss/stock_empty_screen.scss new file mode 100644 index 00000000..44187f77 --- /dev/null +++ b/addons/stock/static/src/scss/stock_empty_screen.scss @@ -0,0 +1,16 @@ +.o_view_nocontent { + &_barcode_scanner:before { + @extend %o-nocontent-init-image; + @include size(250px, 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; + background: transparent url(/stock/static/img/replenishment.svg) no-repeat center; + } +} diff --git a/addons/stock/static/src/scss/stock_traceability_report.scss b/addons/stock/static/src/scss/stock_traceability_report.scss new file mode 100644 index 00000000..b58f4846 --- /dev/null +++ b/addons/stock/static/src/scss/stock_traceability_report.scss @@ -0,0 +1,83 @@ +@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 { + 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; + } + .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; + } + } +} diff --git a/addons/stock/static/src/xml/forecast_widget.xml b/addons/stock/static/src/xml/forecast_widget.xml new file mode 100644 index 00000000..463e14d6 --- /dev/null +++ b/addons/stock/static/src/xml/forecast_widget.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="stock.forecastWidget"> + <span t-if="['draft', 'partially_available', 'assigned', 'cancel', 'done'].includes(state)" t-esc="reserved_availability_str"/> + <span t-elif="!forecast_expected_date_str and will_be_fulfilled" class="text-success">Available</span> + <span t-elif="forecast_expected_date_str and will_be_fulfilled" t-att-class="forecast_is_late ? 'text-danger' : 'text-warning'">Exp <t t-esc="forecast_expected_date_str"/></span> + <span t-else="" class="text-danger">Not Available</span> + <button t-if="product_type == 'product'" t-att="id ? {} : {'disabled': ''}" class="o_forecast_report_button btn btn-link o_icon_button ml-2" title="Forecasted Report"> + <i t-attf-class="fa fa-fw fa-area-chart {{ state != 'draft' and (!will_be_fulfilled or forecast_is_late) ? 'text-danger' : '' }}"/> + </button> + </t> +</templates> diff --git a/addons/stock/static/src/xml/inventory_lines.xml b/addons/stock/static/src/xml/inventory_lines.xml new file mode 100644 index 00000000..37fa0f5b --- /dev/null +++ b/addons/stock/static/src/xml/inventory_lines.xml @@ -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> diff --git a/addons/stock/static/src/xml/inventory_report.xml b/addons/stock/static/src/xml/inventory_report.xml new file mode 100644 index 00000000..f33d7297 --- /dev/null +++ b/addons/stock/static/src/xml/inventory_report.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + +<button t-name="InventoryReport.Buttons" class="btn btn-primary" type="button"> + Inventory at Date +</button> + +</templates> diff --git a/addons/stock/static/src/xml/popover_widget.xml b/addons/stock/static/src/xml/popover_widget.xml new file mode 100644 index 00000000..d5c50356 --- /dev/null +++ b/addons/stock/static/src/xml/popover_widget.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="stock.popoverButton"> + <a tabindex="0" t-attf-class="p-1 fa #{ icon || 'fa-info-circle'} #{ color || 'text-primary'}"/> + </t> + + <div t-name="stock.popoverContent"> + <t t-esc="msg"/> + </div> + + <div t-name="stock.PopoverStockRescheduling"> + <p>Preceding operations + <t t-foreach="late_elements" t-as="late_element"> + <a t-esc="late_element.name" href="#" t-att-element-id="late_element.id" t-att-element-model="late_element.model"/>, + </t> + planned on <t t-esc="delay_alert_date"/>.</p> + </div> +</templates> diff --git a/addons/stock/static/src/xml/report_stock_forecasted.xml b/addons/stock/static/src/xml/report_stock_forecasted.xml new file mode 100644 index 00000000..7b6240b3 --- /dev/null +++ b/addons/stock/static/src/xml/report_stock_forecasted.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + +<button t-name="replenish_report_buttons" + class="btn btn-primary o_report_replenish_buy" + type="button" title="Replenish"> + Replenish +</button> + +<t t-name="warehouseFilter"> + <div id="warehouse_filter" class="btn-group o_dropdown o_stock_report_warehouse_filter" + t-if="displayWarehouseFilter"> + <button type="button" class="o_dropdown_toggler_btn btn btn-secondary dropdown-toggle" + data-toggle="dropdown"> + <span class="fa fa-home"/> Warehouse: <t t-esc="active_warehouse['name']"/> + </button> + <div class="dropdown-menu o_dropdown_menu o_filter_menu" role="menu"> + <t t-foreach="warehouses" t-as="wh"> + <a role="menuitem" class="dropdown-item warehouse_filter" + data-filter="warehouses" t-att-data-warehouse-id="wh['id']" + t-esc="wh['name']"/> + </t> + </div> + </div> +</t> + +</templates> diff --git a/addons/stock/static/src/xml/stock_orderpoint.xml b/addons/stock/static/src/xml/stock_orderpoint.xml new file mode 100644 index 00000000..a9155181 --- /dev/null +++ b/addons/stock/static/src/xml/stock_orderpoint.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + <div t-name="stock.leadDaysPopOver"> + <p> + The forecasted stock on the <t t-esc="lead_days_date"/> + is <t t-if="qty_to_order <= 0"><t t-esc="qty_forecast"/> <t t-esc="product_uom_name"/></t><t t-else=""> + below the minimum inventory of <t t-esc="product_min_qty"/> <t t-esc="product_uom_name"/> + : <t t-esc="qty_to_order"/> <t t-esc="product_uom_name"/> should be replenished to reach the maximum of + <t t-esc="product_max_qty"/> <t t-esc="product_uom_name"/>.</t> + </p> + <table t-if="lead_days_description" class="table table-borderless"> + <tbody> + <tr> + <td> + Today + </td> + <td class="text-right"> + <t t-esc="today"/> + </td> + </tr> + <t t-raw="lead_days_description"/> + <tr class="table-info"> + <td> + Forecasted Date + </td> + <td class="text-right text-nowrap"> + = <t t-esc="lead_days_date"/> + </td> + </tr> + </tbody> + </table> + <button class="text-left btn btn-link action_open_forecast" + type="button"> + <i class="fa fa-fw o_button_icon fa-arrow-right"></i> + View Forecast + </button> + </div> + + <t t-name="StockOrderpoint.Buttons"> + <span> + <button type="button" class="btn d-none btn-primary o_button_order"> + Order + </button> + <button type="button" class="btn d-none btn-primary o_button_snooze"> + Snooze + </button> + </span> + </t> + +</templates> diff --git a/addons/stock/static/src/xml/stock_traceability_report_backend.xml b/addons/stock/static/src/xml/stock_traceability_report_backend.xml new file mode 100644 index 00000000..e2aa016b --- /dev/null +++ b/addons/stock/static/src/xml/stock_traceability_report_backend.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates> + + <t t-name="stockReports.buttons"> + <button type="button" class='btn btn-primary o_stock-widget-pdf'>PRINT</button> + </t> + + <div role="dialog" t-name='stockReports.errorModal' class="modal" id="editable_error" tabindex="-1" data-backdrop="static" style="z-index:9999;"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <header class="modal-header"> + <h3 class="modal-title">Error</h3> + <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> + </header> + <main class="modal-body"> + <p id='insert_error' class='text-center'></p> + </main> + </div> + </div> + </div> + +</templates> diff --git a/addons/stock/static/src/xml/stock_traceability_report_line.xml b/addons/stock/static/src/xml/stock_traceability_report_line.xml new file mode 100644 index 00000000..11504054 --- /dev/null +++ b/addons/stock/static/src/xml/stock_traceability_report_line.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates> + + <t t-name="foldable"> + <span t-att-class="'o_stock_reports_foldable ' + lineId + ' o_stock_reports_caret_icon'"><i class="fa fa-fw fa-caret-down" role="img" aria-label="Fold" title="Fold"></i></span> + </t> + + <t t-name="unfoldable"> + <span t-att-class="'o_stock_reports_unfoldable ' + lineId + ' o_stock_reports_caret_icon'"><i class="fa fa-fw fa-caret-right" role="img" aria-label="Unfold" title="Unfold"></i></span> + </t> + + <t t-name="report_mrp_line"> + <t t-set="trclass" t-value="'o_stock_reports_default_style'"/> + <t t-if="l.model == 'stock.move.line'"><t t-set="trclass" t-value="'o_stock_reports_level0'"/></t> + <t t-set="space_td" t-value="'margin-left: '+ l.level + 'px;'"/> + <t t-set="domainClass" t-value="'o_stock_reports_domain_line_0'"/> + <t t-if="l.unfoldable == false"> + <t t-set="spanclass" t-value="'o_stock_reports_nofoldable'" /> + <t t-set="domainClass" t-value="'o_stock_reports_domain_line_1'"/> + </t> + + <tr t-att-data-unfold="l.unfoldable" t-att-data-parent_id="l.parent_id" t-att-data-id="l.id" t-att-data-model_id="l.model_id" t-att-data-model="l.model" t-att-class="trclass" t-att-data-lot_name="l.lot_name" t-att-data-lot_id="l.lot_id"> + <t t-if="l.unfoldable == true"><t t-set="tdclass" t-value="'o_stock_reports_unfoldable'" /></t> + <t t-set="column" t-value="0" /> + <t t-foreach="l.columns" t-as="c"> + <t t-set="column" t-value="column + 1" /> + <td style="white-space: nowrap;" t-att-data-id="l.id" t-att-data-model="l.model" t-att-data-model_id="l.model_id" t-att-class="tdclass" t-att-data-level="l.level" t-att-data-lot_name="l.lot_name"> + <t t-if="column == 1"> + <span t-att-style="space_td" t-att-class="domainClass"></span> + <t t-if="l.unfoldable"> + <span class="o_stock_reports_unfoldable o_stock_reports_caret_icon"><i class="fa fa-fw fa-caret-right" role="img" aria-label="Unfold" title="Unfold"></i></span> + </t> + </t> + <t t-if="l.reference == c"> + <span t-if="c" t-att-class="spanclass"> + <a t-att-data-active-id="l.res_id" t-att-data-res-model="l.res_model" class="o_stock_reports_web_action" href="#"><t t-esc="c"/></a> + </span> + </t><t t-elif="l.lot_name == c and l.lot_name != false"> + <span> + <a class="o_stock_report_lot_action" href="#"><t t-esc="c"/></a> + </span> + </t> + <t t-if="l.reference != c and l.lot_name != c"> + <t t-if="typeof c == 'string' || typeof c == 'number'"> + <t t-esc="c"/> + </t> + <t t-if="typeof c != 'string' & typeof c != 'number'"><span t-att-style="c[1]"> + <t t-esc="c[0]"/> + </span></t> + </t> + </td> + </t> + </tr> + </t> + +</templates> diff --git a/addons/stock/static/tests/popover_widget_tests.js b/addons/stock/static/tests/popover_widget_tests.js new file mode 100644 index 00000000..dff13d60 --- /dev/null +++ b/addons/stock/static/tests/popover_widget_tests.js @@ -0,0 +1,53 @@ +odoo.define('stock.popover_widget_tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); +var FormView = require('web.FormView'); +var createView = testUtils.createView; + +QUnit.module('widgets', {}, function () { +QUnit.module('ModelFieldSelector', { + beforeEach: function () { + this.data = { + partner: { + fields: { + json_data: {string: " ", type: "char"}, + }, + records: [ + {id:1, json_data:'{"color": "text-danger", "msg": "var that = self // why not?", "title": "JS Master"}'} + ] + } + }; + }, +}, function () { + QUnit.test("Test creation/usage popover widget form", async function (assert) { + assert.expect(6); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="json_data" widget="popover_widget"/>' + + '</form>', + res_id: 1 + }); + + var $popover = $('div.popover'); + assert.strictEqual($popover.length, 0, "Shouldn't have a popover container in DOM"); + + var $popoverButton = form.$('a.fa.fa-info-circle.text-danger'); + assert.strictEqual($popoverButton.length, 1, "Should have a popover icon/button in red"); + assert.strictEqual($popoverButton.prop('special_click'), true, "Special click properpy should be activated"); + await testUtils.dom.triggerEvents($popoverButton, ['focus']); + $popover = $('div.popover'); + assert.strictEqual($popover.length, 1, "Should have a popover container in DOM"); + assert.strictEqual($popover.html().includes("var that = self // why not?"), true, "The message should be in DOM"); + assert.strictEqual($popover.html().includes("JS Master"), true, "The title should be in DOM"); + + form.destroy(); + }); +}); +}); + +}); diff --git a/addons/stock/static/tests/singleton_list_tests.js b/addons/stock/static/tests/singleton_list_tests.js new file mode 100644 index 00000000..aad90a29 --- /dev/null +++ b/addons/stock/static/tests/singleton_list_tests.js @@ -0,0 +1,305 @@ +odoo.define('web.singleton_list_tests', function (require) { +"use strict"; + +var SingletonListView = require('stock.SingletonListView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + + +QUnit.module('Views', { + beforeEach: function () { + this.data = { + person: { + fields: { + name: {string: "Name", type: "char"}, + age: {string: "Age", type: "integer"}, + job: {string: "Profession", type: "char"}, + }, + records: [ + {id: 1, name: 'Daniel Fortesque', age: 32, job: 'Soldier'}, + {id: 2, name: 'Samuel Oak', age: 64, job: 'Professor'}, + {id: 3, name: 'Leto II Atreides', age: 128, job: 'Emperor'}, + ] + }, + }; + this.mockRPC = function (route, args) { + if (route === '/web/dataset/call_kw/person/create') { + var name = args.args[0].name; + var age = args.args[0].age; + var job = args.args[0].job; + for (var d of this.data.person.records) { + if (d.name === name) { + d.age = age; + d.job = job; + return Promise.resolve(d.id); + } + } + } + return this._super.apply(this, arguments); + }; + } +}, function () { + + QUnit.module('SingletonListView'); + + QUnit.test('Create new record correctly', async function (assert) { + assert.expect(2); + + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 3 records + assert.containsN(list, '.o_data_row', 3, "should have 3 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with new values + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Bilou'); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '24'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + + // Checks new record is in the list + assert.containsN(list, '.o_data_row', 4, "should now have 4 records"); + list.destroy(); + }); + + QUnit.test('Don\'t duplicate record', async function (assert) { + assert.expect(3); + + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 3 records + assert.containsN(list, '.o_data_row', 3, "should have 3 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with already existing value + var $input = $('.o_selected_row input[name=name]'); + var name = 'Samuel Oak'; + await testUtils.fields.editInput($input, name); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + var age = '72'; + await testUtils.fields.editInput($input, age); + await testUtils.fields.triggerKeydown($input, 'enter'); + + // Checks we have still only 3 records... + assert.containsN(list, '.o_data_row', 3, "should still have 3 records"); + // ... and verify modification was occured. + var nameField = list.$('td[title="' + name + '"]'); + var ageField = nameField.parent().find('.o_list_number'); + assert.strictEqual(ageField.text(), age, "The age field must be updated"); + list.destroy(); + }); + + QUnit.test('Don\'t raise error when trying to create duplicate line', async function (assert) { + assert.expect(3); + /* In some condition, a list editable with the `singletonlist` js_class + can try to select a record at a line who isn't the same place anymore. + In this case, the list can try to find the id of an undefined record. + This test just insures we don't raise a traceback in this case. + */ + var list = await createView({ + View: SingletonListView, + model: 'person', + data: { + person: { + fields: { + name: {string: "Name", type: "char"}, + age: {string: "Age", type: "integer"}, + }, + records: [ + {id: 1, name: 'Bobby B. Bop', age: 18}, + ] + } + }, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + // Checks we have initially 1 record + assert.containsN(list, '.o_data_row', 1, "should have 1 records"); + + // Creates a new line... + await testUtils.dom.click($('.o_list_button_add')); + // ... and fills fields with already existing value + var $input = $('.o_selected_row input[name=name]'); + var name = 'Bobby B. Bop'; + await testUtils.fields.editInput($input, name); + await testUtils.fields.triggerKeydown($input, 'tab'); + + $input = $('.o_selected_row input[name=age]'); + var age = '22'; + await testUtils.fields.editInput($input, age); + // This operation causes list'll try to select undefined record. + await testUtils.fields.triggerKeydown($input, 'enter'); + + // Checks we have still only 1 record... + assert.containsN(list, '.o_data_row', 1, "should now have 1 records"); + // ... and verify modification was occured. + var nameField = list.$('td[title="' + name + '"]'); + var ageField = nameField.parent().find('.o_list_number'); + assert.strictEqual(ageField.text(), age, "The age field must be updated"); + list.destroy(); + }); + + QUnit.test('Refresh the list only when needed', async function (assert) { + assert.expect(3); + + var refresh_count = 0; + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '</tree>', + mockRPC: this.mockRPC, + }); + list.realReload = list.reload; + list.reload = function () { + refresh_count++; + return this.realReload(); + }; + // Modify Record + await testUtils.dom.click(list.$('.o_data_row:nth-child(2) > .o_list_number')); + var $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '70'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 0, "don't refresh when edit existing line"); + + // Add existing record + await testUtils.dom.click($('.o_list_button_add')); + $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Leto II Atreides'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '800'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 1, "refresh after tried to create an existing record"); + + // Add new record + await testUtils.dom.click($('.o_list_button_add')); + $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Valentin Cognito'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '37'); + await testUtils.fields.triggerKeydown($input, 'enter'); + await testUtils.dom.click($('.o_list_button_save')); + assert.strictEqual(refresh_count, 1, "don't refresh when create entirely new record"); + + list.destroy(); + }); + + QUnit.test('Work in grouped list', async function (assert) { + assert.expect(6); + + var refresh_count = 0; + var list = await createView({ + View: SingletonListView, + model: 'person', + data: this.data, + arch: '<tree editable="top" js_class="singleton_list">'+ + '<field name="name"/>'+ + '<field name="age"/>'+ + '<field name="job"/>'+ + '</tree>', + mockRPC: this.mockRPC, + groupBy: ['job'], + }); + list.realReload = list.reload; + list.reload = function () { + refresh_count++; + return this.realReload(); + }; + // Opens 'Professor' group + await testUtils.dom.click(list.$('.o_group_header:nth-child(2)')); + + // Creates a new record... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Del Tutorial'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '32'); + await testUtils.fields.triggerKeydown($input, 'tab'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list didn't refresh + assert.strictEqual(refresh_count, 0, + "don't refresh when creating new record"); + + // Creates an existing record in same group... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Samuel Oak'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list has been refreshed + assert.strictEqual(refresh_count, 1, + "refresh when try to create an existing record"); + + // Creates an existing but not displayed record... + await testUtils.dom.click(list.$('.o_add_record_row a')); + var $input = $('.o_selected_row input[name=name]'); + await testUtils.fields.editInput($input, 'Daniel Fortesque'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '55'); + await testUtils.fields.triggerKeydown($input, 'tab'); + $input = $('.o_selected_row input[name=job]'); + await testUtils.fields.editInput($input, 'Soldier'); + await testUtils.dom.click($('.o_list_button_save')); + // .. then checks the list didn't refresh + assert.strictEqual(refresh_count, 1, + "don't refresh when creating an existing record but this record " + + "isn't present in the view"); + + // Opens 'Soldier' group + await testUtils.dom.click(list.$('.o_group_header:nth-child(1)').first()); + // Checks the record has been correctly updated + var ageCell = $('tr.o_data_row td.o_list_number').first(); + assert.strictEqual(ageCell.text(), "55", + "age of the record must be updated"); + // Edits the freshly created record... + await testUtils.dom.click(list.$('tr.o_data_row td.o_list_number').eq(1)); + $input = $('.o_selected_row input[name=age]'); + await testUtils.fields.editInput($input, '66'); + await testUtils.dom.click($('.o_list_button_save')); + // ... then checks the list and data have been refreshed + assert.strictEqual(refresh_count, 2, + "refresh when try to create an existing record present in the view"); + ageCell = $('tr.o_data_row td.o_list_number').first(); + assert.strictEqual(ageCell.text(), "66", + "age of the record must be updated"); + + list.destroy(); + }); +}); + +}); diff --git a/addons/stock/static/tests/stock_traceability_report_backend_tests.js b/addons/stock/static/tests/stock_traceability_report_backend_tests.js new file mode 100644 index 00000000..5ab69403 --- /dev/null +++ b/addons/stock/static/tests/stock_traceability_report_backend_tests.js @@ -0,0 +1,151 @@ +odoo.define('stock.stock_traceability_report_backend_tests', function (require) { + "use strict"; + + const ControlPanel = require('web.ControlPanel'); + const dom = require('web.dom'); + const StockReportGeneric = require('stock.stock_report_generic'); + const testUtils = require('web.test_utils'); + + const { createActionManager, dom: domUtils } = testUtils; + + /** + * Helper function to instantiate a stock report action. + * @param {Object} params + * @param {Object} params.action + * @param {boolean} [params.debug] + * @returns {Promise<StockReportGeneric>} + */ + async function createStockReportAction(params) { + const parent = await testUtils.createParent(params); + const report = new StockReportGeneric(parent, params.action); + const target = testUtils.prepareTarget(params.debug); + + const _destroy = report.destroy; + report.destroy = function () { + report.destroy = _destroy; + parent.destroy(); + }; + const fragment = document.createDocumentFragment(); + await report.appendTo(fragment); + dom.prepend(target, fragment, { + callbacks: [{ widget: report }], + in_DOM: true, + }); + // Wait for the ReportWidget to be appended + await testUtils.nextTick(); + + return report; + } + + QUnit.module('Stock', {}, function () { + QUnit.module('Traceability report'); + + QUnit.test("Rendering with no lines", async function (assert) { + assert.expect(1); + + const template = ` + <div class="container-fluid o_stock_reports_page o_stock_reports_no_print"> + <div class="o_stock_reports_table table-responsive"> + <span class="text-center"> + <h1>No operation made on this lot.</h1> + </span> + </div> + </div>`; + const report = await createStockReportAction({ + action: { + context: {}, + params: {}, + }, + data: { + 'stock.traceability.report': { + fields: {}, + get_html: () => ({ html: template }), + }, + }, + }); + + // HTML content is nested in a div inside of the content + assert.strictEqual(report.el.querySelector('.o_content > div').innerHTML, template, + "Displayed template should match"); + + report.destroy(); + }); + + QUnit.test("mounted is called once when returning on 'Stock report' from breadcrumb", async assert => { + // This test can be removed as soon as we don't mix legacy and owl layers anymore. + assert.expect(7); + + let mountCount = 0; + + ControlPanel.patch('test.ControlPanel', T => { + class ControlPanelPatchTest extends T { + mounted() { + mountCount = mountCount + 1; + this.__uniqueId = mountCount; + assert.step(`mounted ${this.__uniqueId}`); + super.mounted(...arguments); + } + willUnmount() { + assert.step(`willUnmount ${this.__uniqueId}`); + super.mounted(...arguments); + } + } + return ControlPanelPatchTest; + }); + + const actionManager = await createActionManager({ + actions: [ + { + id: 42, + name: "Stock report", + tag: 'stock_report_generic', + type: 'ir.actions.client', + context: {}, + params: {}, + }, + ], + archs: { + 'partner,false,form': '<form><field name="display_name"/></form>', + 'partner,false,search': '<search></search>', + }, + data: { + partner: { + fields: { + display_name: { string: "Displayed name", type: "char" }, + }, + records: [ + {id: 1, display_name: "Genda Swami"}, + ], + }, + }, + mockRPC: function (route) { + if (route === '/web/dataset/call_kw/stock.traceability.report/get_html') { + return Promise.resolve({ + html: '<a class="o_stock_reports_web_action" href="#" data-active-id="1" data-res-model="partner">Go to form view</a>', + }); + } + return this._super.apply(this, arguments); + }, + intercepts: { + do_action: ev => actionManager.doAction(ev.data.action, ev.data.options), + }, + }); + + await actionManager.doAction(42); + await domUtils.click(actionManager.$('.o_stock_reports_web_action')); + await domUtils.click(actionManager.$('.breadcrumb-item:first')); + actionManager.destroy(); + + assert.verifySteps([ + 'mounted 1', + 'willUnmount 1', + 'mounted 2', + 'willUnmount 2', + 'mounted 3', + 'willUnmount 3', + ]); + + ControlPanel.unpatch('test.ControlPanel'); + }); + }); +}); diff --git a/addons/stock/static/tests/tours/stock_report_tests.js b/addons/stock/static/tests/tours/stock_report_tests.js new file mode 100644 index 00000000..c757390f --- /dev/null +++ b/addons/stock/static/tests/tours/stock_report_tests.js @@ -0,0 +1,23 @@ +odoo.define('stock.reports.setup.tour', function (require) { + "use strict"; + + const tour = require('web_tour.tour'); + + tour.register('test_stock_route_diagram_report', { + test: true, + }, [ + { + trigger: '.o_kanban_record', + extra_trigger:'.breadcrumb', + }, + { + trigger: '.nav-item > a:contains("Inventory")', + }, + { + trigger: '.btn[id="stock.view_diagram_button"]', + }, + { + trigger: 'iframe .o_report_stock_rule', + }, + ]); +}); |
