Compare commits
885 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ec9989229 | ||
|
|
ca3b6e62cf | ||
|
|
838f412c6b | ||
|
|
bfbd604d0a | ||
|
|
a75535d336 | ||
|
|
f333532a1f | ||
|
|
36d6628eb6 | ||
|
|
f948290b21 | ||
|
|
e44446793d | ||
|
|
cfde81d09f | ||
|
|
4628bb8f08 | ||
|
|
dc17e3998a | ||
|
|
5e8e900b04 | ||
|
|
94c25c93d6 | ||
|
|
9e41eb23ac | ||
|
|
99352f8f84 | ||
|
|
bfb162c688 | ||
|
|
12d3646da1 | ||
|
|
a21e4560f1 | ||
|
|
66c037b9f8 | ||
|
|
66d4b14ba4 | ||
|
|
1c912088a2 | ||
|
|
793fe39fe7 | ||
|
|
a6f5a8107a | ||
|
|
d3cdb34151 | ||
|
|
638b478d1f | ||
|
|
4efa8a5d3b | ||
|
|
1273d93c8c | ||
|
|
db59f99f2d | ||
|
|
7df5215404 | ||
|
|
6e47a1feb6 | ||
|
|
88464ad640 | ||
|
|
32556d660e | ||
|
|
ca0caa3d2b | ||
|
|
f6bcee06cb | ||
|
|
08394574ce | ||
|
|
4a74294123 | ||
|
|
e84cad1660 | ||
|
|
86c8d86b67 | ||
|
|
c567b7a84c | ||
|
|
1132b6c51a | ||
|
|
10e3a5f5a9 | ||
|
|
95bf39c127 | ||
|
|
f661a4f4ec | ||
|
|
3d067b39b1 | ||
|
|
d01686248b | ||
|
|
094a63f751 | ||
|
|
024552e4d0 | ||
|
|
a5e26ceeac | ||
|
|
2fedc1267c | ||
|
|
7d3c0a7aa8 | ||
|
|
018ab0cd05 | ||
|
|
0b5a4efef6 | ||
|
|
a3c2f8b36b | ||
|
|
4893cd527f | ||
|
|
32e82488d3 | ||
|
|
f2050f7cab | ||
|
|
827534138b | ||
|
|
1ef1fdc6e6 | ||
|
|
7f93b37437 | ||
|
|
9c2f4ce491 | ||
|
|
d56da99c0d | ||
|
|
79686ebb2a | ||
|
|
71b3dd3e76 | ||
|
|
dcdb2add28 | ||
|
|
d14b763ef9 | ||
|
|
7522a80f96 | ||
|
|
fb27eb48c4 | ||
|
|
a1d54690c2 | ||
|
|
b3c8bd7779 | ||
|
|
f53aac0784 | ||
|
|
d2b9993e96 | ||
|
|
5efba2dad0 | ||
|
|
d7ac9978eb | ||
|
|
39cab4690d | ||
|
|
1d85ccd543 | ||
|
|
d943020d56 | ||
|
|
80d2fc4b9e | ||
|
|
2e1277839e | ||
|
|
a41db6ae28 | ||
|
|
b123135df6 | ||
|
|
3b763e95fd | ||
|
|
c1a827819c | ||
|
|
5443beef65 | ||
|
|
11d2d5588f | ||
|
|
ae3c9817be | ||
|
|
8d6d16c07b | ||
|
|
7f09ad2b38 | ||
|
|
7b2b01c26f | ||
|
|
e7963f8a85 | ||
|
|
11cc0c7ced | ||
|
|
ebc95cb326 | ||
|
|
d73a7176fd | ||
|
|
44e0fd1a68 | ||
|
|
5bd5c61b9d | ||
|
|
2018229dc5 | ||
|
|
1643a2ccb2 | ||
|
|
847fb62ab5 | ||
|
|
c890a4a6d0 | ||
|
|
224b372eae | ||
|
|
95924852a4 | ||
|
|
7d286cf4b8 | ||
|
|
dfd500097e | ||
|
|
7a7684b018 | ||
|
|
de565c6e67 | ||
|
|
35dd50e94f | ||
|
|
4b9fd13622 | ||
|
|
06c03039da | ||
|
|
695370c8d7 | ||
|
|
7b21270baf | ||
|
|
08397c5e5f | ||
|
|
3d4a8bdd22 | ||
|
|
06f323fe47 | ||
|
|
6baaf98d7e | ||
|
|
5770789ddb | ||
|
|
fd192b65bf | ||
|
|
5ee004eabd | ||
|
|
429add9000 | ||
|
|
71ca60d679 | ||
|
|
b9b23a3853 | ||
|
|
829dd0d637 | ||
|
|
387680fbcd | ||
|
|
20788bb559 | ||
|
|
a32a30de52 | ||
|
|
1a21576f28 | ||
|
|
ba787a0485 | ||
|
|
8368798280 | ||
|
|
6012855ec4 | ||
|
|
919b39515f | ||
|
|
22aa0a03c9 | ||
|
|
84528df6f8 | ||
|
|
a70f4c86eb | ||
|
|
f349dc01ea | ||
|
|
f38bf6e20a | ||
|
|
bf05c9cfae | ||
|
|
ccb191e5b1 | ||
|
|
50ae331afd | ||
|
|
125554c53f | ||
|
|
ecc7bd2d5b | ||
|
|
5ab4be7025 | ||
|
|
918106c225 | ||
|
|
7d3cd03d6c | ||
|
|
4b1c2677c5 | ||
|
|
8b6ebc092a | ||
|
|
f363c0f084 | ||
|
|
c7d6b985bd | ||
|
|
42637ddefa | ||
|
|
9b001cd298 | ||
|
|
f0e5d1984c | ||
|
|
0a8365e3bc | ||
|
|
514db57c0c | ||
|
|
c275bf8d98 | ||
|
|
e5872f4b67 | ||
|
|
888fa51cd7 | ||
|
|
3db830e7cd | ||
|
|
5d703af932 | ||
|
|
43c4e936a7 | ||
|
|
c90f9b0447 | ||
|
|
0ebc45e834 | ||
|
|
6a420fd95c | ||
|
|
edb1602c7c | ||
|
|
2b1279e647 | ||
|
|
5ad2eabab1 | ||
|
|
cb403a5b29 | ||
|
|
a1b7239b7e | ||
|
|
6dde353646 | ||
|
|
4ffcf48ab3 | ||
|
|
fc125ca20c | ||
|
|
9781d585b0 | ||
|
|
3d5144b9bc | ||
|
|
4c39607e00 | ||
|
|
e1adef5010 | ||
|
|
6177fe0c5a | ||
|
|
2112c6a9ad | ||
|
|
7d4945d302 | ||
|
|
0835fe92a0 | ||
|
|
51eb40f3bc | ||
|
|
bb3b6fdc4d | ||
|
|
e0e7788af6 | ||
|
|
50b54f0966 | ||
|
|
00f012311d | ||
|
|
90a75ac7cb | ||
|
|
7d5b859233 | ||
|
|
3e03b1c31d | ||
|
|
c846aeb60f | ||
|
|
65874447ed | ||
|
|
5d45fce240 | ||
|
|
24ca1c5641 | ||
|
|
140a2092c8 | ||
|
|
0620e656a0 | ||
|
|
e85ddf3579 | ||
|
|
f7c515b889 | ||
|
|
09a7a7d2e4 | ||
|
|
d0ccf8647d | ||
|
|
f57a31c9b5 | ||
|
|
880a701881 | ||
|
|
62d877ba54 | ||
|
|
e261fa6b29 | ||
|
|
3adf30a00c | ||
|
|
e5de69cd96 | ||
|
|
320ca451cc | ||
|
|
610c05384b | ||
|
|
ec88415f3d | ||
|
|
ac8a0be74a | ||
|
|
bad246bca6 | ||
|
|
212a7eeed1 | ||
|
|
8660f13ef5 | ||
|
|
9b6b5825f3 | ||
|
|
51992a92c1 | ||
|
|
4b8ef2ad62 | ||
|
|
75152fab0e | ||
|
|
9c9407b1ab | ||
|
|
dcc8acb49a | ||
|
|
44ab487b62 | ||
|
|
c9756d30bd | ||
|
|
2923589c4a | ||
|
|
f6b9d9e6d0 | ||
|
|
42af52ee51 | ||
|
|
28878b4b0d | ||
|
|
18297cd2fb | ||
|
|
bdcc66bac7 | ||
|
|
0d7eb6b72c | ||
|
|
5275d3943b | ||
|
|
d87ab0033a | ||
|
|
1360b1592d | ||
|
|
e3605001e4 | ||
|
|
d5e58fd798 | ||
|
|
86e3e26196 | ||
|
|
0073a77e16 | ||
|
|
7f75c610ef | ||
|
|
36a0496dd7 | ||
|
|
44ecd958a2 | ||
|
|
56f27aae23 | ||
|
|
8fc79f45a3 | ||
|
|
7646535aaf | ||
|
|
1b65fbad2c | ||
|
|
d30173132a | ||
|
|
446243f2b5 | ||
|
|
2aa505b2cb | ||
|
|
5349addc06 | ||
|
|
391bb0dbe4 | ||
|
|
4efcfbecaf | ||
|
|
35738ce026 | ||
|
|
6f742319e5 | ||
|
|
6091f2ba33 | ||
|
|
dc53a433a7 | ||
|
|
28cc241354 | ||
|
|
f136c974cf | ||
|
|
90d0b8b15d | ||
|
|
5d4f35958d | ||
|
|
bb8b85c375 | ||
|
|
d267d04bed | ||
|
|
f96051d863 | ||
|
|
8662e6a109 | ||
|
|
1fb76b9987 | ||
|
|
4c8fdab072 | ||
|
|
fd1dd792c6 | ||
|
|
87a41c7e39 | ||
|
|
47524f6ea3 | ||
|
|
a5f26c7f09 | ||
|
|
9fd4b5cce3 | ||
|
|
b674d851f2 | ||
|
|
3682eaac14 | ||
|
|
d4efdf86e5 | ||
|
|
8edfada22a | ||
|
|
7f126e58d2 | ||
|
|
b6df623554 | ||
|
|
174b7ba7f2 | ||
|
|
449e0c0af2 | ||
|
|
3cf06b4960 | ||
|
|
f2e58ebbdd | ||
|
|
370fbea396 | ||
|
|
51de0a982a | ||
|
|
38af66c0de | ||
|
|
8fad704d76 | ||
|
|
5a59a37a89 | ||
|
|
109b8c943e | ||
|
|
7533a9ac0c | ||
|
|
8646c73021 | ||
|
|
5bd31f11eb | ||
|
|
831ccec399 | ||
|
|
e24a158919 | ||
|
|
c4700d0e10 | ||
|
|
e17f15f403 | ||
|
|
e34c27e360 | ||
|
|
414d2dbc96 | ||
|
|
575cf87b98 | ||
|
|
ee33de711a | ||
|
|
cb1e7a6cc5 | ||
|
|
ec7392303d | ||
|
|
c16ce925dd | ||
|
|
f0415640d5 | ||
|
|
c3048a1bf1 | ||
|
|
9ed2025021 | ||
|
|
52bdfe5465 | ||
|
|
eeac561b9b | ||
|
|
2095d66677 | ||
|
|
79d7a9f922 | ||
|
|
41db0ff60d | ||
|
|
db477bceab | ||
|
|
d756579a06 | ||
|
|
c1ea6dbb9b | ||
|
|
c2af401854 | ||
|
|
8fea9bc645 | ||
|
|
7bfddd6d51 | ||
|
|
3b8ee48581 | ||
|
|
da834d8bcc | ||
|
|
b48db6f8fe | ||
|
|
187c9b0971 | ||
|
|
8b66babd49 | ||
|
|
b6e97f06dd | ||
|
|
0294a1c323 | ||
|
|
895f9f3ce0 | ||
|
|
d5d89c67b1 | ||
|
|
a474000361 | ||
|
|
7bf3229595 | ||
|
|
07857c3088 | ||
|
|
206743b58d | ||
|
|
a5808d4360 | ||
|
|
92aace1278 | ||
|
|
d8eefec065 | ||
|
|
d6c9ff41ff | ||
|
|
99839e78fd | ||
|
|
fa163b8866 | ||
|
|
23b2b56de4 | ||
|
|
6141ddc3eb | ||
|
|
f8b22bc7b7 | ||
|
|
d9f29b4a70 | ||
|
|
3dcf1746e6 | ||
|
|
ff8dcabb12 | ||
|
|
81a8aac623 | ||
|
|
1998dabe9b | ||
|
|
4061693522 | ||
|
|
8861ffad81 | ||
|
|
cef09acd54 | ||
|
|
faab1f2464 | ||
|
|
6c17e330c4 | ||
|
|
28bccea57b | ||
|
|
32fafc76d7 | ||
|
|
f753e11f10 | ||
|
|
f72eb4266a | ||
|
|
1db654e990 | ||
|
|
2b13512145 | ||
|
|
6fa4e33062 | ||
|
|
26c07961cb | ||
|
|
6eb4709658 | ||
|
|
00d4efb920 | ||
|
|
f0325fe30f | ||
|
|
a2ffd06abf | ||
|
|
2be9399d2c | ||
|
|
ad844c4393 | ||
|
|
ba1ba67f87 | ||
|
|
78340a71a9 | ||
|
|
6218f1c7e6 | ||
|
|
5cbb67b91c | ||
|
|
cb11df4dba | ||
|
|
1396c349c8 | ||
|
|
5b42ab7332 | ||
|
|
cd4a797e71 | ||
|
|
e8d47a4c76 | ||
|
|
9205d6d67c | ||
|
|
26ddd36666 | ||
|
|
537573d0e3 | ||
|
|
1da004d30a | ||
|
|
7df4451c08 | ||
|
|
a3ff90754d | ||
|
|
31bb7734ce | ||
|
|
fcff8d4825 | ||
|
|
f59ed27cf9 | ||
|
|
e36b1e6c70 | ||
|
|
6748f37405 | ||
|
|
96ec8c4eb6 | ||
|
|
032057c93a | ||
|
|
7665e83001 | ||
|
|
91e314ddb5 | ||
|
|
a2590f1a3b | ||
|
|
44818ca0c5 | ||
|
|
3cdcdd0535 | ||
|
|
47c385cac2 | ||
|
|
aea43924ae | ||
|
|
2cf7592198 | ||
|
|
46fa60953d | ||
|
|
68282c93f4 | ||
|
|
a1922bff81 | ||
|
|
b94104f7ab | ||
|
|
bfc489a35b | ||
|
|
535c36b75e | ||
|
|
1bf2a3e23f | ||
|
|
4c8eaf3942 | ||
|
|
799f17ef50 | ||
|
|
7ef5c0058e | ||
|
|
b92ec751ef | ||
|
|
641233b140 | ||
|
|
0e0f490f8d | ||
|
|
3df7299a61 | ||
|
|
c32b6b2272 | ||
|
|
e03afce335 | ||
|
|
7117c33379 | ||
|
|
fc06bc7574 | ||
|
|
6620d34f25 | ||
|
|
0a1ce59dfc | ||
|
|
69d1c3cea2 | ||
|
|
372d252333 | ||
|
|
98211f1ae1 | ||
|
|
c564896355 | ||
|
|
50198c0f1e | ||
|
|
970d2ac1f8 | ||
|
|
bb8b9dfcec | ||
|
|
90d753001f | ||
|
|
ad656b7ca7 | ||
|
|
a846334698 | ||
|
|
e8d4e2a7e6 | ||
|
|
9565b3004d | ||
|
|
c58ed5a07e | ||
|
|
681f285aa3 | ||
|
|
e3efd12184 | ||
|
|
52ba2201b2 | ||
|
|
4e6c8c45ee | ||
|
|
1dc5682767 | ||
|
|
0ad206d9eb | ||
|
|
720b332f71 | ||
|
|
94c1ed882f | ||
|
|
b960ce839a | ||
|
|
fd917b2e41 | ||
|
|
25af8559ba | ||
|
|
759a3724b5 | ||
|
|
0d80ae9f93 | ||
|
|
e583d1a159 | ||
|
|
f405e4b701 | ||
|
|
5448165ef3 | ||
|
|
ca9536f687 | ||
|
|
8e8696eda9 | ||
|
|
d77b99c0ca | ||
|
|
174ac64235 | ||
|
|
24638a7229 | ||
|
|
7629077deb | ||
|
|
6d42cfab75 | ||
|
|
e72e34413d | ||
|
|
512eceb2a6 | ||
|
|
2e5d5bcc84 | ||
|
|
6234581fab | ||
|
|
6280ed1ade | ||
|
|
1eb8a9f310 | ||
|
|
1bf49a1458 | ||
|
|
ce62da5a42 | ||
|
|
76572bf82f | ||
|
|
96378cb556 | ||
|
|
599c53ea53 | ||
|
|
a45437dac8 | ||
|
|
00ffab472c | ||
|
|
fa6daeb679 | ||
|
|
d6c6cb96ba | ||
|
|
58f2dce18d | ||
|
|
d6672372a5 | ||
|
|
96b5f70c21 | ||
|
|
be67832821 | ||
|
|
38815211af | ||
|
|
c0ccb8f588 | ||
|
|
3ecb1e6577 | ||
|
|
fbdf11e6e7 | ||
|
|
c7712d4235 | ||
|
|
655e5692e9 | ||
|
|
6acff2a26e | ||
|
|
aaf394ca7a | ||
|
|
dd78464a74 | ||
|
|
aa4ed9feb0 | ||
|
|
1b79ef940e | ||
|
|
0e8fb6a5ad | ||
|
|
2cb0b448b7 | ||
|
|
408ff639dd | ||
|
|
1fafaf8577 | ||
|
|
b04f22fc53 | ||
|
|
2bf3e3ab02 | ||
|
|
a64ee23afc | ||
|
|
5aa111b0aa | ||
|
|
2e8a490ca9 | ||
|
|
75a1be0284 | ||
|
|
efd4644045 | ||
|
|
96a79e557b | ||
|
|
1f70538b04 | ||
|
|
db6d7c2d27 | ||
|
|
7911801dbf | ||
|
|
56c0e289bd | ||
|
|
6cd87e830d | ||
|
|
f95346f214 | ||
|
|
7545591c59 | ||
|
|
989983bdb5 | ||
|
|
83d8226ad6 | ||
|
|
c4570a79de | ||
|
|
8daf601f00 | ||
|
|
69f242d11d | ||
|
|
910c57c92d | ||
|
|
fa3c5ae108 | ||
|
|
29c8daed0a | ||
|
|
172a08fbba | ||
|
|
073bb7c488 | ||
|
|
529742b520 | ||
|
|
f057937df0 | ||
|
|
1c4924a4a5 | ||
|
|
ac3dcac641 | ||
|
|
9aa5086067 | ||
|
|
b18f360daf | ||
|
|
20cc952982 | ||
|
|
cd39fd1dc2 | ||
|
|
d154ca08ea | ||
|
|
db57e9516b | ||
|
|
0f11ab527f | ||
|
|
83dab558d7 | ||
|
|
55762f2a96 | ||
|
|
c0921fc7ce | ||
|
|
ae8e58ac12 | ||
|
|
2347f15c2e | ||
|
|
369864574e | ||
|
|
634e5e0da6 | ||
|
|
20b21a2b71 | ||
|
|
7ed43f5a11 | ||
|
|
1d19393442 | ||
|
|
6d0b01d0fb | ||
|
|
13898d6687 | ||
|
|
0e59c15773 | ||
|
|
eb883d7e70 | ||
|
|
d9f4c34a42 | ||
|
|
3806e3ebeb | ||
|
|
c39e3aaa82 | ||
|
|
60e4022568 | ||
|
|
542c204ca0 | ||
|
|
dd12a593f4 | ||
|
|
935ef968de | ||
|
|
7756c766c3 | ||
|
|
4381a16b0e | ||
|
|
6fe5f0e0e6 | ||
|
|
ba5479090a | ||
|
|
28bf5bfdbc | ||
|
|
a222efda33 | ||
|
|
27ec65a002 | ||
|
|
915756eacf | ||
|
|
8e97d14f1f | ||
|
|
14aebfdae1 | ||
|
|
bc3c3be751 | ||
|
|
0a73032950 | ||
|
|
2009773d9d | ||
|
|
d43312d162 | ||
|
|
430f737953 | ||
|
|
baa6283d20 | ||
|
|
5744796506 | ||
|
|
a7229b5b0b | ||
|
|
50eb70f538 | ||
|
|
399e44fce7 | ||
|
|
20b6e0fd1a | ||
|
|
9d9bfd6c30 | ||
|
|
5dc11ad8e9 | ||
|
|
839cd55f20 | ||
|
|
78ed2776d1 | ||
|
|
6fd70e4741 | ||
|
|
cd1ecc5e8f | ||
|
|
1fe00ef7df | ||
|
|
25ff74835d | ||
|
|
29b496b588 | ||
|
|
d6420341f4 | ||
|
|
e9be3fb4ec | ||
|
|
4e3635d1fe | ||
|
|
c08cb43c39 | ||
|
|
a93d96de58 | ||
|
|
990b987692 | ||
|
|
46d5a6f00b | ||
|
|
33a0b73a05 | ||
|
|
3e0655a9d8 | ||
|
|
7fdc0546b4 | ||
|
|
985870d626 | ||
|
|
fff4f27816 | ||
|
|
461238f8ae | ||
|
|
783f43db5f | ||
|
|
cfd4626c96 | ||
|
|
35d64b7069 | ||
|
|
de2d9b30ec | ||
|
|
c63a061cf3 | ||
|
|
57551d3ac3 | ||
|
|
0186d23563 | ||
|
|
f9918758dd | ||
|
|
a6b52a9fae | ||
|
|
f61c768bbe | ||
|
|
3349e9ff6c | ||
|
|
b7b0574a44 | ||
|
|
9b0fbb7006 | ||
|
|
ca1c692b15 | ||
|
|
de89c3997d | ||
|
|
125260160c | ||
|
|
aabefc2815 | ||
|
|
bcfb3ac067 | ||
|
|
4a32bdb7ab | ||
|
|
175b24a794 | ||
|
|
a82483dbaa | ||
|
|
431b35ed32 | ||
|
|
31050f23aa | ||
|
|
32eace0c36 | ||
|
|
e97ee95deb | ||
|
|
46b0db8263 | ||
|
|
e167f27258 | ||
|
|
50356afd41 | ||
|
|
7abf70fdd7 | ||
|
|
3b12b0231e | ||
|
|
5f2bef7ee1 | ||
|
|
3f6c7df7a8 | ||
|
|
e287860e10 | ||
|
|
449fc329c9 | ||
|
|
7ef87320a0 | ||
|
|
6f5fc528b7 | ||
|
|
8f374e255e | ||
|
|
69ff0ac248 | ||
|
|
c0d6ef80fc | ||
|
|
3f44233074 | ||
|
|
d7028b6d74 | ||
|
|
9b542ed23f | ||
|
|
2bf7b61668 | ||
|
|
964672d6cc | ||
|
|
afde997cf9 | ||
|
|
20a30f317f | ||
|
|
4ee0004c97 | ||
|
|
9cf372f633 | ||
|
|
85a40ec418 | ||
|
|
bee0a519ef | ||
|
|
df48df8119 | ||
|
|
dad9239a1c | ||
|
|
5f8c9a0f31 | ||
|
|
2ffae368f1 | ||
|
|
140396ccdd | ||
|
|
15b3055e53 | ||
|
|
893628d1b8 | ||
|
|
8cb336f581 | ||
|
|
2d6a78ffb8 | ||
|
|
8eeb88a0ea | ||
|
|
598ea11211 | ||
|
|
96a065cdb7 | ||
|
|
8ac3d42fd8 | ||
|
|
ff07cf5516 | ||
|
|
30f94bef41 | ||
|
|
5e2145e151 | ||
|
|
b04a403081 | ||
|
|
cb0b7209ec | ||
|
|
4199e7567f | ||
|
|
dbe550a159 | ||
|
|
84fc2785d6 | ||
|
|
d0e425ad30 | ||
|
|
289b030f4e | ||
|
|
d9673244d5 | ||
|
|
b0c4a58f30 | ||
|
|
4600b0d337 | ||
|
|
ce3f7b698d | ||
|
|
ec53099872 | ||
|
|
17c8dc3441 | ||
|
|
8d04acd5f5 | ||
|
|
ebb202b19b | ||
|
|
456710c5ce | ||
|
|
c2ce569506 | ||
|
|
53f2aa107a | ||
|
|
8d2e910323 | ||
|
|
9acd57f8e0 | ||
|
|
c1db4c7b3d | ||
|
|
3ab058e84b | ||
|
|
23db7a89a9 | ||
|
|
2703ae520e | ||
|
|
af68ea23c3 | ||
|
|
ca36775f62 | ||
|
|
4413699844 | ||
|
|
efb4f194b6 | ||
|
|
87d4a51575 | ||
|
|
4fdb18318a | ||
|
|
1d549bcdbd | ||
|
|
e7d9485c16 | ||
|
|
43f1e2b8f9 | ||
|
|
e04bbb016d | ||
|
|
44d45050b9 | ||
|
|
40c203c123 | ||
|
|
bbada3e873 | ||
|
|
fa9ef02d23 | ||
|
|
33e70ec5a7 | ||
|
|
efd0caba6e | ||
|
|
565fe9a98e | ||
|
|
4013abe4de | ||
|
|
6ca34276bd | ||
|
|
866c8af393 | ||
|
|
17eee66b95 | ||
|
|
c71fbf7893 | ||
|
|
0d660e3c69 | ||
|
|
0288a1acbf | ||
|
|
0c91691ed2 | ||
|
|
29d7cb40e1 | ||
|
|
56fa6c512b | ||
|
|
f9b7257bdb | ||
|
|
14e2cabffa | ||
|
|
b1165af3c3 | ||
|
|
b04d6051a4 | ||
|
|
e17b92b126 | ||
|
|
c33cfe9503 | ||
|
|
753fe9c80f | ||
|
|
36cf614aed | ||
|
|
8c1248d74b | ||
|
|
eba5512a38 | ||
|
|
f4f7514b45 | ||
|
|
9719a14587 | ||
|
|
44482800e4 | ||
|
|
210a4bccde | ||
|
|
f938e722b9 | ||
|
|
86f3f56a60 | ||
|
|
5ce6dd325d | ||
|
|
9ded804707 | ||
|
|
733951883e | ||
|
|
0fcb4e3170 | ||
|
|
3f3d058bfa | ||
|
|
db16b23287 | ||
|
|
886d95e467 | ||
|
|
bfc421c50b | ||
|
|
cbbd58c743 | ||
|
|
bb60eed897 | ||
|
|
28b3432afe | ||
|
|
e86e15df99 | ||
|
|
e116ef9a8b | ||
|
|
09d175f7cf | ||
|
|
c61fc7c1df | ||
|
|
0fc558068f | ||
|
|
533a3aa368 | ||
|
|
b1640fcc23 | ||
|
|
676cca89a1 | ||
|
|
52da678636 | ||
|
|
72337dab49 | ||
|
|
d5bf108fef | ||
|
|
df89008116 | ||
|
|
e38d740bbc | ||
|
|
e7c7bdcd00 | ||
|
|
57851b0eaf | ||
|
|
c8c7f78ce9 | ||
|
|
8607d702c4 | ||
|
|
985bd39234 | ||
|
|
84149d34a6 | ||
|
|
984e16d5af | ||
|
|
352a58b373 | ||
|
|
3786454e4c | ||
|
|
1a30a5bc16 | ||
|
|
2d2ad91545 | ||
|
|
0667857754 | ||
|
|
94792596e9 | ||
|
|
c816afbe62 | ||
|
|
3d9ad24e27 | ||
|
|
8fed3b3522 | ||
|
|
8dde89e781 | ||
|
|
c1c0a262b2 | ||
|
|
b6227f7d28 | ||
|
|
65de52b705 | ||
|
|
9889e314a9 | ||
|
|
970f08260c | ||
|
|
39c58e4015 | ||
|
|
a0b3359d62 | ||
|
|
2295008944 | ||
|
|
cf23fb6fe8 | ||
|
|
b1af07c8cb | ||
|
|
77d80f5c0f | ||
|
|
6af05b2b83 | ||
|
|
374c3676a3 | ||
|
|
0cb2b49333 | ||
|
|
bd8b52d7d2 | ||
|
|
7c80767414 | ||
|
|
30fd3c8841 | ||
|
|
e9c7937ef4 | ||
|
|
c3b0593aba | ||
|
|
ccf17bf4c5 | ||
|
|
77cfadad42 | ||
|
|
52eedef820 | ||
|
|
edf4aab063 | ||
|
|
a1579eecfd | ||
|
|
e9bf4b4cef | ||
|
|
2ea4824030 | ||
|
|
9fc7976569 | ||
|
|
74e5b2cd3f | ||
|
|
2bebf2d41a | ||
|
|
11ee8e8369 | ||
|
|
5329e3e56c | ||
|
|
51314a0261 | ||
|
|
e04828214a | ||
|
|
0c41cc7c77 | ||
|
|
e3f85414fa | ||
|
|
747cccfa42 | ||
|
|
7531984c78 | ||
|
|
cc90c8abbe | ||
|
|
9eb1367d80 | ||
|
|
3efd7f7777 | ||
|
|
ca5d3a57de | ||
|
|
9e4bc274cf | ||
|
|
c045a3b6f6 | ||
|
|
0be787ea5b | ||
|
|
03fb6e5c61 | ||
|
|
14ab1bef14 | ||
|
|
0507e8a3bc | ||
|
|
02214ea713 | ||
|
|
7cc1063114 | ||
|
|
73e03636a2 | ||
|
|
d9c2d061cc | ||
|
|
cf66a386ea | ||
|
|
bf2774eb21 | ||
|
|
f6d5bd4ed8 | ||
|
|
fe6da32e64 | ||
|
|
9ae5c241f5 | ||
|
|
ff92210b25 | ||
|
|
6eec6a0599 | ||
|
|
b89fe4f9d4 | ||
|
|
47a56f7f5d | ||
|
|
c694c9467b | ||
|
|
6b73e7a408 | ||
|
|
5235ad1b0c | ||
|
|
7168607a88 | ||
|
|
079e331bf6 | ||
|
|
558e24c985 | ||
|
|
c45e2d682e | ||
|
|
9d7f9a9aa3 | ||
|
|
7539bd47fe | ||
|
|
6a995042c9 | ||
|
|
4b6ca548b6 | ||
|
|
40ac1f39e0 | ||
|
|
19ba9bf93c | ||
|
|
0c73fa3b58 | ||
|
|
fd5d20ad13 | ||
|
|
433098ce6e | ||
|
|
0a86d947bc | ||
|
|
cbf0e0bd4a | ||
|
|
cbd291849c | ||
|
|
6e26bd0b71 | ||
|
|
337223b8eb | ||
|
|
ecf47aa69d | ||
|
|
60e8a17f07 | ||
|
|
9947a0cf90 | ||
|
|
7e7fe7d63f | ||
|
|
bec98d355a | ||
|
|
be6ecd9587 | ||
|
|
13ca076f42 | ||
|
|
004b36b1df | ||
|
|
5016d44b83 | ||
|
|
245c04367b | ||
|
|
522432f4aa | ||
|
|
bd4dde2cb7 | ||
|
|
15cb1e0005 | ||
|
|
c39f705ef7 | ||
|
|
1830467487 | ||
|
|
a1a4bddcc6 | ||
|
|
0599fbaf26 | ||
|
|
a985e11aa8 | ||
|
|
c7f79a5a08 | ||
|
|
79d90b1c4a | ||
|
|
f0e7826fdc | ||
|
|
81010994e7 | ||
|
|
a8a21f7c9d | ||
|
|
ba0a13443f | ||
|
|
376428b80b | ||
|
|
800cb9606a | ||
|
|
2467690111 | ||
|
|
a2870b60d9 | ||
|
|
f1797dbe2f | ||
|
|
ae68463f46 | ||
|
|
309097293f | ||
|
|
fcc244b52f | ||
|
|
1deab0c040 | ||
|
|
939d5838fa | ||
|
|
41268d92bf | ||
|
|
1a979fc113 | ||
|
|
ff8ddfbb5c | ||
|
|
1dbdcb6d9c | ||
|
|
2db42eff50 | ||
|
|
98c52c06ac | ||
|
|
9ed2338162 | ||
|
|
87a7081185 | ||
|
|
c2df1fcd95 | ||
|
|
db21ccdb30 | ||
|
|
6687661928 | ||
|
|
58efc952db | ||
|
|
1d0dd04ca4 | ||
|
|
f72762ceb7 | ||
|
|
f790d6a6a5 | ||
|
|
84b67e2cc1 | ||
|
|
1786c029b4 | ||
|
|
369acb494b | ||
|
|
eaf191dc8d | ||
|
|
4711f0b823 | ||
|
|
7b3bc33b88 | ||
|
|
4f31cee6a3 | ||
|
|
ffa9dd18cf | ||
|
|
4f432d4db2 | ||
|
|
c6fd2281d6 |
@@ -1,9 +0,0 @@
|
||||
[run]
|
||||
source = ./InvenTree
|
||||
omit =
|
||||
InvenTree/manage.py
|
||||
InvenTree/setup.py
|
||||
InvenTree/InvenTree/middleware.py
|
||||
InvenTree/InvenTree/utils.py
|
||||
InvenTree/InvenTree/wsgi.py
|
||||
InvenTree/users/apps.py
|
||||
25
.eslintrc.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
env:
|
||||
commonjs: false
|
||||
browser: true
|
||||
es2021: true
|
||||
jquery: true
|
||||
extends:
|
||||
- google
|
||||
parserOptions:
|
||||
ecmaVersion: 12
|
||||
rules:
|
||||
no-var: off
|
||||
guard-for-in: off
|
||||
no-trailing-spaces: off
|
||||
camelcase: off
|
||||
padded-blocks: off
|
||||
prefer-const: off
|
||||
max-len: off
|
||||
require-jsdoc: off
|
||||
valid-jsdoc: off
|
||||
no-multiple-empty-lines: off
|
||||
comma-dangle: off
|
||||
prefer-spread: off
|
||||
indent:
|
||||
- error
|
||||
- 4
|
||||
2
.gitattributes
vendored
@@ -7,5 +7,5 @@
|
||||
*.yml text
|
||||
*.yaml text
|
||||
*.conf text
|
||||
*.sh text
|
||||
*.sh text eol=lf
|
||||
*.js text
|
||||
30
.github/ISSUE_TEMPLATE/app_issue.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: App issue
|
||||
about: Report a bug or issue with the InvenTree app
|
||||
title: "[APP] Enter bug description"
|
||||
labels: bug, app
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug or issue
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to ...
|
||||
2. Select ...
|
||||
3. ...
|
||||
|
||||
**Expected Behavior**
|
||||
A clear and concise description of what you expected to happen
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem
|
||||
|
||||
**Version Information**
|
||||
|
||||
- App platform: *Select iOS or Android*
|
||||
- App version: *Enter app version*
|
||||
- Server version: *Enter server version*
|
||||
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve InvenTree
|
||||
title: "[BUG] Enter bug description"
|
||||
labels: bug, question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Deployment Method**
|
||||
Docker
|
||||
Bare Metal
|
||||
|
||||
**Version Information**
|
||||
You can get this by going to the "About InvenTree" section in the upper right corner and cicking on to the "copy version information"
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FR]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request the result of a bug?**
|
||||
Please link it here.
|
||||
|
||||
**Problem**
|
||||
A clear and concise description of what the problem is. e.g. I'm always frustrated when [...]
|
||||
|
||||
**Suggested solution**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Examples of other systems**
|
||||
Show how other software handles your FR if you have examples.
|
||||
|
||||
**Do you want to develop this?**
|
||||
If so please describe briefly how you would like to implement it (so we can give advice) and if you have experience in the needed technology (you do not need to be a pro - this is just as a information for us).
|
||||
@@ -15,6 +15,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --dev
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
42
.github/workflows/docker_stable.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Build and push latest docker image on push to master branch
|
||||
|
||||
name: Docker Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'stable'
|
||||
|
||||
jobs:
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --release
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: ./docker
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
target: production
|
||||
build-args: |
|
||||
branch=stable
|
||||
repository: inventree/inventree
|
||||
tags: inventree/inventree:stable
|
||||
- name: Image Digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
@@ -13,6 +13,9 @@ jobs:
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@v2
|
||||
- name: Check Release tag
|
||||
run: |
|
||||
python3 ci/check_version_number.py --release --tag ${{ github.event.release.tag_name }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
@@ -29,5 +32,7 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
target: production
|
||||
build-args: |
|
||||
tag=${{ github.event.release.tag_name }}
|
||||
repository: inventree/inventree
|
||||
tags: inventree/inventree:${{ github.event.release.tag_name }}
|
||||
54
.github/workflows/html.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Check javascript template files
|
||||
|
||||
name: HTML Templates
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
html:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
steps:
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Check HTML Files
|
||||
run: |
|
||||
npm install markuplint
|
||||
npx markuplint InvenTree/build/templates/build/*.html
|
||||
npx markuplint InvenTree/common/templates/common/*.html
|
||||
npx markuplint InvenTree/company/templates/company/*.html
|
||||
npx markuplint InvenTree/order/templates/order/*.html
|
||||
npx markuplint InvenTree/part/templates/part/*.html
|
||||
npx markuplint InvenTree/stock/templates/stock/*.html
|
||||
npx markuplint InvenTree/templates/*.html
|
||||
npx markuplint InvenTree/templates/InvenTree/*.html
|
||||
npx markuplint InvenTree/templates/InvenTree/settings/*.html
|
||||
|
||||
50
.github/workflows/javascript.yaml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# Check javascript template files
|
||||
|
||||
name: Javascript Templates
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
javascript:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_DB_ENGINE: sqlite3
|
||||
INVENTREE_DB_NAME: inventree
|
||||
INVENTREE_MEDIA_ROOT: ./media
|
||||
INVENTREE_STATIC_ROOT: ./static
|
||||
steps:
|
||||
- name: Install node.js
|
||||
uses: actions/setup-node@v2
|
||||
- run: npm install
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install gettext
|
||||
pip3 install invoke
|
||||
invoke install
|
||||
invoke static
|
||||
- name: Check Templated Files
|
||||
run: |
|
||||
cd ci
|
||||
python check_js_templates.py
|
||||
- name: Lint Javascript Files
|
||||
run: |
|
||||
npm install eslint eslint-config-google
|
||||
invoke render-js-files
|
||||
npx eslint js_tmp/*.js
|
||||
20
.github/workflows/version.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Check that the version number format matches the current branch
|
||||
|
||||
name: Version Numbering
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10*
|
||||
|
||||
jobs:
|
||||
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check version number
|
||||
run: |
|
||||
python3 ci/check_version_number.py --branch ${{ github.base_ref }}
|
||||
@@ -1,29 +1,102 @@
|
||||
Contributions to InvenTree are welcomed - please follow the guidelines below.
|
||||
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
|
||||
|
||||
## Feature Branches
|
||||
## Branches and Versioning
|
||||
|
||||
No pushing to master! New featues must be submitted in a separate branch (one branch per feature).
|
||||
InvenTree roughly follow the [GitLab flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) branching style, to allow simple management of multiple tagged releases, short-lived branches, and development on the main branch.
|
||||
|
||||
## Include Migration Files
|
||||
### Version Numbering
|
||||
|
||||
InvenTree version numbering follows the [semantic versioning](https://semver.org/) specification.
|
||||
|
||||
### Master Branch
|
||||
|
||||
The HEAD of the "main" or "master" branch of InvenTree represents the current "latest" state of code development.
|
||||
|
||||
- All feature branches are merged into master
|
||||
- All bug fixes are merged into master
|
||||
|
||||
**No pushing to master:** New featues must be submitted as a pull request from a separate branch (one branch per feature).
|
||||
|
||||
#### Feature Branches
|
||||
|
||||
Feature branches should be branched *from* the *master* branch.
|
||||
|
||||
- One major feature per branch / pull request
|
||||
- Feature pull requests are merged back *into* the master branch
|
||||
- Features *may* also be merged into a release candidate branch
|
||||
|
||||
### Stable Branch
|
||||
|
||||
The HEAD of the "stable" branch represents the latest stable release code.
|
||||
|
||||
- Versioned releases are merged into the "stable" branch
|
||||
- Bug fix branches are made *from* the "stable" branch
|
||||
|
||||
#### Release Candidate Branches
|
||||
|
||||
- Release candidate branches are made from master, and merged into stable.
|
||||
- RC branches are targetted at a major/minor version e.g. "0.5"
|
||||
- When a release candidate branch is merged into *stable*, the release is tagged
|
||||
|
||||
#### Bugfix Branches
|
||||
|
||||
- If a bug is discovered in a tagged release version of InvenTree, a "bugfix" or "hotfix" branch should be made *from* that tagged release
|
||||
- When approved, the branch is merged back *into* stable, with an incremented PATCH number (e.g. 0.4.1 -> 0.4.2)
|
||||
- The bugfix *must* also be cherry picked into the *master* branch.
|
||||
|
||||
## Migration Files
|
||||
|
||||
Any required migration files **must** be included in the commit, or the pull-request will be rejected. If you change the underlying database schema, make sure you run `invoke migrate` and commit the migration files before submitting the PR.
|
||||
|
||||
## Update Translation Files
|
||||
*Note: A github action checks for unstaged migration files and will reject the PR if it finds any!*
|
||||
|
||||
Any PRs which update translatable strings (i.e. text strings that will appear in the web-front UI) must also update the translation (locale) files to include hooks for the translated strings.
|
||||
## Unit Testing
|
||||
|
||||
*This does not mean that all translations must be provided, but that the translation files must include locations for the translated strings to be written.*
|
||||
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage for any new features is insufficient, or the overall code coverage is decreased.
|
||||
|
||||
To perform this step, simply run `invoke translate` from the top level directory before submitting the PR.
|
||||
The InvenTree code base makes use of [GitHub actions](https://github.com/features/actions) to run a suite of automated tests against the code base every time a new pull request is received. These actions include (but are not limited to):
|
||||
|
||||
## Testing
|
||||
- Checking Python and Javascript code against standard style guides
|
||||
- Running unit test suite
|
||||
- Automated building and pushing of docker images
|
||||
- Generating translation files
|
||||
|
||||
Any new code should be covered by unit tests - a submitted PR may not be accepted if the code coverage is decreased.
|
||||
The various github actions can be found in the `./github/workflows` directory
|
||||
|
||||
## Code Style
|
||||
|
||||
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
||||
|
||||
## Documentation
|
||||
|
||||
New features or updates to existing features should be accompanied by user documentation. A PR with associated documentation should link to the matching PR at https://github.com/inventree/inventree-docs/
|
||||
|
||||
## Code Style
|
||||
## Translations
|
||||
|
||||
Sumbitted Python code is automatically checked against PEP style guidelines. Locally you can run `invoke style` to ensure the style checks will pass, before submitting the PR.
|
||||
Any user-facing strings *must* be passed through the translation engine.
|
||||
|
||||
- InvenTree code is written in English
|
||||
- User translatable strings are provided in English as the primary language
|
||||
- Secondary language translations are provided [via Crowdin](https://crowdin.com/project/inventree)
|
||||
|
||||
*Note: Translation files are updated via GitHub actions - you do not need to compile translations files before submitting a pull request!*
|
||||
|
||||
### Python Code
|
||||
|
||||
For strings exposed via Python code, use the following format:
|
||||
|
||||
```python
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
user_facing_string = _('This string will be exposed to the translation engine!')
|
||||
```
|
||||
|
||||
### Templated Strings
|
||||
|
||||
HTML and javascript files are passed through the django templating engine. Translatable strings are implemented as follows:
|
||||
|
||||
```html
|
||||
{% load i18n %}
|
||||
|
||||
<span>{% trans "This string will be translated" %} - this string will not!</span>
|
||||
```
|
||||
@@ -32,27 +32,44 @@ class InvenTreeConfig(AppConfig):
|
||||
|
||||
logger.info("Starting background tasks...")
|
||||
|
||||
# Remove successful task results from the database
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.delete_successful_tasks',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Check for InvenTree updates
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.check_for_updates',
|
||||
schedule_type=Schedule.DAILY
|
||||
)
|
||||
|
||||
# Heartbeat to let the server know the background worker is running
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.heartbeat',
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=15
|
||||
)
|
||||
|
||||
# Keep exchange rates up to date
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.update_exchange_rates',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Remove expired sessions
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.delete_expired_sessions',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
||||
# Delete "old" stock items
|
||||
InvenTree.tasks.schedule_task(
|
||||
'stock.tasks.delete_old_stock_items',
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=30,
|
||||
)
|
||||
|
||||
def update_exchange_rates(self):
|
||||
"""
|
||||
Update exchange rates each time the server is started, *if*:
|
||||
|
||||
100
InvenTree/InvenTree/ci_render_js.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Pull rendered copies of the templated
|
||||
"""
|
||||
|
||||
from django.http import response
|
||||
from django.test import TestCase, testcases
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
|
||||
class RenderJavascriptFiles(TestCase):
|
||||
"""
|
||||
A unit test to "render" javascript files.
|
||||
|
||||
The server renders templated javascript files,
|
||||
we need the fully-rendered files for linting and static tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
user = get_user_model()
|
||||
|
||||
self.user = user.objects.create_user(
|
||||
username='testuser',
|
||||
password='testpassword',
|
||||
email='user@gmail.com',
|
||||
)
|
||||
|
||||
self.client.login(username='testuser', password='testpassword')
|
||||
|
||||
def download_file(self, filename, prefix):
|
||||
|
||||
url = os.path.join(prefix, filename)
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
output_dir = os.path.join(
|
||||
here,
|
||||
'..',
|
||||
'..',
|
||||
'js_tmp',
|
||||
)
|
||||
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
output_file = os.path.join(
|
||||
output_dir,
|
||||
filename,
|
||||
)
|
||||
|
||||
with open(output_file, 'wb') as output:
|
||||
output.write(response.content)
|
||||
|
||||
def download_files(self, subdir, prefix):
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
js_template_dir = os.path.join(
|
||||
here,
|
||||
'..',
|
||||
'templates',
|
||||
'js',
|
||||
)
|
||||
|
||||
directory = os.path.join(js_template_dir, subdir)
|
||||
|
||||
directory = os.path.abspath(directory)
|
||||
|
||||
js_files = pathlib.Path(directory).rglob('*.js')
|
||||
|
||||
n = 0
|
||||
|
||||
for f in js_files:
|
||||
js = os.path.basename(f)
|
||||
|
||||
self.download_file(js, prefix)
|
||||
|
||||
n += 1
|
||||
|
||||
return n
|
||||
|
||||
def test_render_files(self):
|
||||
"""
|
||||
Look for all javascript files
|
||||
"""
|
||||
|
||||
n = 0
|
||||
|
||||
print("Rendering javascript files...")
|
||||
|
||||
n += self.download_files('translated', '/js/i18n')
|
||||
n += self.download_files('dynamic', '/js/dynamic')
|
||||
|
||||
print(f"Rendered {n} javascript files.")
|
||||
@@ -36,9 +36,14 @@ def health_status(request):
|
||||
'email_configured': InvenTree.status.is_email_configured(),
|
||||
}
|
||||
|
||||
# The following keys are required to denote system health
|
||||
health_keys = [
|
||||
'django_q_running',
|
||||
]
|
||||
|
||||
all_healthy = True
|
||||
|
||||
for k in status.keys():
|
||||
for k in health_keys:
|
||||
if status[k] is not True:
|
||||
all_healthy = False
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from common.settings import currency_code_default, currency_codes
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from djmoney.contrib.exchange.backends.base import SimpleExchangeBackend
|
||||
|
||||
@@ -26,4 +27,8 @@ class InvenTreeExchange(SimpleExchangeBackend):
|
||||
|
||||
symbols = ','.join(currency_codes())
|
||||
|
||||
super().update_rates(base=base_currency, symbols=symbols)
|
||||
try:
|
||||
super().update_rates(base=base_currency, symbols=symbols)
|
||||
# catch connection errors
|
||||
except (HTTPError, URLError):
|
||||
print('Encountered connection error while updating')
|
||||
|
||||
@@ -20,7 +20,6 @@ from djmoney.forms.fields import MoneyField
|
||||
from djmoney.models.validators import MinMoneyValidator
|
||||
|
||||
import InvenTree.helpers
|
||||
import common.settings
|
||||
|
||||
|
||||
class InvenTreeURLFormField(FormURLField):
|
||||
@@ -42,9 +41,11 @@ class InvenTreeURLField(models.URLField):
|
||||
|
||||
def money_kwargs():
|
||||
""" returns the database settings for MoneyFields """
|
||||
from common.settings import currency_code_mappings, currency_code_default
|
||||
|
||||
kwargs = {}
|
||||
kwargs['currency_choices'] = common.settings.currency_code_mappings()
|
||||
kwargs['default_currency'] = common.settings.currency_code_default
|
||||
kwargs['currency_choices'] = currency_code_mappings()
|
||||
kwargs['default_currency'] = currency_code_default()
|
||||
return kwargs
|
||||
|
||||
|
||||
@@ -55,7 +56,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# detect if creating migration
|
||||
if 'makemigrations' in sys.argv:
|
||||
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
||||
# remove currency information for a clean migration
|
||||
kwargs['default_currency'] = ''
|
||||
kwargs['currency_choices'] = []
|
||||
|
||||
51
InvenTree/InvenTree/filters.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework.filters import OrderingFilter
|
||||
|
||||
|
||||
class InvenTreeOrderingFilter(OrderingFilter):
|
||||
"""
|
||||
Custom OrderingFilter class which allows aliased filtering of related fields.
|
||||
|
||||
To use, simply specify this filter in the "filter_backends" section.
|
||||
|
||||
filter_backends = [
|
||||
InvenTreeOrderingFilter,
|
||||
]
|
||||
|
||||
Then, specify a ordering_field_aliases attribute:
|
||||
|
||||
ordering_field_alises = {
|
||||
'name': 'part__part__name',
|
||||
'SKU': 'part__SKU',
|
||||
}
|
||||
"""
|
||||
|
||||
def get_ordering(self, request, queryset, view):
|
||||
|
||||
ordering = super().get_ordering(request, queryset, view)
|
||||
|
||||
aliases = getattr(view, 'ordering_field_aliases', None)
|
||||
|
||||
# Attempt to map ordering fields based on provided aliases
|
||||
if ordering is not None and aliases is not None:
|
||||
"""
|
||||
Ordering fields should be mapped to separate fields
|
||||
"""
|
||||
|
||||
for idx, field in enumerate(ordering):
|
||||
|
||||
reverse = False
|
||||
|
||||
if field.startswith('-'):
|
||||
field = field[1:]
|
||||
reverse = True
|
||||
|
||||
if field in aliases:
|
||||
ordering[idx] = aliases[field]
|
||||
|
||||
if reverse:
|
||||
ordering[idx] = '-' + ordering[idx]
|
||||
|
||||
return ordering
|
||||
@@ -13,7 +13,6 @@ from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Field
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||
|
||||
from common.models import ColorTheme
|
||||
from part.models import PartCategory
|
||||
|
||||
|
||||
@@ -177,39 +176,6 @@ class SetPasswordForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class ColorThemeSelectForm(forms.ModelForm):
|
||||
""" Form for setting color theme """
|
||||
|
||||
name = forms.ChoiceField(choices=(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = ColorTheme
|
||||
fields = [
|
||||
'name'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ColorThemeSelectForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# Populate color themes choices
|
||||
self.fields['name'].choices = ColorTheme.get_color_themes_choices()
|
||||
|
||||
self.helper = FormHelper()
|
||||
# Form rendering
|
||||
self.helper.form_show_labels = False
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
Div(Field('name'),
|
||||
css_class='col-sm-6',
|
||||
style='width: 200px;'),
|
||||
Div(StrictButton(_('Apply Theme'), css_class='btn btn-primary', type='submit'),
|
||||
css_class='col-sm-6',
|
||||
style='width: auto;'),
|
||||
css_class='row',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class SettingCategorySelectForm(forms.ModelForm):
|
||||
""" Form for setting category settings """
|
||||
|
||||
|
||||
@@ -344,13 +344,15 @@ def GetExportFormats():
|
||||
]
|
||||
|
||||
|
||||
def DownloadFile(data, filename, content_type='application/text'):
|
||||
""" Create a dynamic file for the user to download.
|
||||
def DownloadFile(data, filename, content_type='application/text', inline=False):
|
||||
"""
|
||||
Create a dynamic file for the user to download.
|
||||
|
||||
Args:
|
||||
data: Raw file data (string or bytes)
|
||||
filename: Filename for the file download
|
||||
content_type: Content type for the download
|
||||
inline: Download "inline" or as attachment? (Default = attachment)
|
||||
|
||||
Return:
|
||||
A StreamingHttpResponse object wrapping the supplied data
|
||||
@@ -365,7 +367,10 @@ def DownloadFile(data, filename, content_type='application/text'):
|
||||
|
||||
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
||||
response['Content-Length'] = len(data)
|
||||
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
|
||||
|
||||
disposition = "inline" if inline else "attachment"
|
||||
|
||||
response['Content-Disposition'] = f'{disposition}; filename={filename}'
|
||||
|
||||
return response
|
||||
|
||||
@@ -631,13 +636,34 @@ def clean_decimal(number):
|
||||
""" Clean-up decimal value """
|
||||
|
||||
# Check if empty
|
||||
if number is None or number == '':
|
||||
if number is None or number == '' or number == 0:
|
||||
return Decimal(0)
|
||||
|
||||
# Check if decimal type
|
||||
# Convert to string and remove spaces
|
||||
number = str(number).replace(' ', '')
|
||||
|
||||
# Guess what type of decimal and thousands separators are used
|
||||
count_comma = number.count(',')
|
||||
count_point = number.count('.')
|
||||
|
||||
if count_comma == 1:
|
||||
# Comma is used as decimal separator
|
||||
if count_point > 0:
|
||||
# Points are used as thousands separators: remove them
|
||||
number = number.replace('.', '')
|
||||
# Replace decimal separator with point
|
||||
number = number.replace(',', '.')
|
||||
elif count_point == 1:
|
||||
# Point is used as decimal separator
|
||||
if count_comma > 0:
|
||||
# Commas are used as thousands separators: remove them
|
||||
number = number.replace(',', '')
|
||||
|
||||
# Convert to Decimal type
|
||||
try:
|
||||
clean_number = Decimal(number)
|
||||
except InvalidOperation:
|
||||
clean_number = number
|
||||
# Number cannot be converted to Decimal (eg. a string containing letters)
|
||||
return Decimal(0)
|
||||
|
||||
return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
|
||||
|
||||
38
InvenTree/InvenTree/management/commands/clean_settings.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Custom management command to cleanup old settings that are not defined anymore
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Cleanup old (undefined) settings in the database
|
||||
"""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
||||
print("Collecting settings")
|
||||
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||
|
||||
# general settings
|
||||
db_settings = InvenTreeSetting.objects.all()
|
||||
model_settings = InvenTreeSetting.GLOBAL_SETTINGS
|
||||
|
||||
# check if key exist and delete if not
|
||||
for setting in db_settings:
|
||||
if setting.key not in model_settings:
|
||||
setting.delete()
|
||||
print(f"deleted setting '{setting.key}'")
|
||||
|
||||
# user settings
|
||||
db_settings = InvenTreeUserSetting.objects.all()
|
||||
model_settings = InvenTreeUserSetting.GLOBAL_SETTINGS
|
||||
|
||||
# check if key exist and delete if not
|
||||
for setting in db_settings:
|
||||
if setting.key not in model_settings:
|
||||
setting.delete()
|
||||
print(f"deleted user setting '{setting.key}'")
|
||||
|
||||
print("checked all settings")
|
||||
@@ -32,6 +32,9 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
|
||||
self.request = request
|
||||
self.view = view
|
||||
|
||||
metadata = super().determine_metadata(request, view)
|
||||
|
||||
user = request.user
|
||||
@@ -95,10 +98,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
serializer_info = super().get_serializer_info(serializer)
|
||||
|
||||
try:
|
||||
ModelClass = serializer.Meta.model
|
||||
model_class = None
|
||||
|
||||
model_fields = model_meta.get_field_info(ModelClass)
|
||||
try:
|
||||
model_class = serializer.Meta.model
|
||||
|
||||
model_fields = model_meta.get_field_info(model_class)
|
||||
|
||||
# Iterate through simple fields
|
||||
for name, field in model_fields.fields.items():
|
||||
@@ -136,6 +141,54 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Try to extract 'instance' information
|
||||
instance = None
|
||||
|
||||
# Extract extra information if an instance is available
|
||||
if hasattr(serializer, 'instance'):
|
||||
instance = serializer.instance
|
||||
|
||||
if instance is None and model_class is not None:
|
||||
# Attempt to find the instance based on kwargs lookup
|
||||
kwargs = getattr(self.view, 'kwargs', None)
|
||||
|
||||
if kwargs:
|
||||
pk = None
|
||||
|
||||
for field in ['pk', 'id', 'PK', 'ID']:
|
||||
if field in kwargs:
|
||||
pk = kwargs[field]
|
||||
break
|
||||
|
||||
if pk is not None:
|
||||
try:
|
||||
instance = model_class.objects.get(pk=pk)
|
||||
except (ValueError, model_class.DoesNotExist):
|
||||
pass
|
||||
|
||||
if instance is not None:
|
||||
"""
|
||||
If there is an instance associated with this API View,
|
||||
introspect that instance to find any specific API info.
|
||||
"""
|
||||
|
||||
if hasattr(instance, 'api_instance_filters'):
|
||||
|
||||
instance_filters = instance.api_instance_filters()
|
||||
|
||||
for field_name, field_filters in instance_filters.items():
|
||||
|
||||
if field_name not in serializer_info.keys():
|
||||
# The field might be missing, but is added later on
|
||||
# This function seems to get called multiple times?
|
||||
continue
|
||||
|
||||
if 'instance_filters' not in serializer_info[field_name].keys():
|
||||
serializer_info[field_name]['instance_filters'] = {}
|
||||
|
||||
for key, value in field_filters.items():
|
||||
serializer_info[field_name]['instance_filters'][key] = value
|
||||
|
||||
return serializer_info
|
||||
|
||||
def get_field_info(self, field):
|
||||
@@ -153,6 +206,11 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if 'default' not in field_info and not field.default == empty:
|
||||
field_info['default'] = field.get_default()
|
||||
|
||||
# Force non-nullable fields to read as "required"
|
||||
# (even if there is a default value!)
|
||||
if not field.allow_null and not (hasattr(field, 'allow_blank') and field.allow_blank):
|
||||
field_info['required'] = True
|
||||
|
||||
# Introspect writable related fields
|
||||
if field_info['type'] == 'field' and not field_info['read_only']:
|
||||
|
||||
@@ -166,7 +224,12 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if model:
|
||||
# Mark this field as "related", and point to the URL where we can get the data!
|
||||
field_info['type'] = 'related field'
|
||||
field_info['api_url'] = model.get_api_url()
|
||||
field_info['model'] = model._meta.model_name
|
||||
|
||||
# Special case for 'user' model
|
||||
if field_info['model'] == 'user':
|
||||
field_info['api_url'] = '/api/user/'
|
||||
else:
|
||||
field_info['api_url'] = model.get_api_url()
|
||||
|
||||
return field_info
|
||||
|
||||
@@ -21,28 +21,15 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
assert hasattr(request, 'user')
|
||||
|
||||
response = self.get_response(request)
|
||||
# API requests are handled by the DRF library
|
||||
if request.path_info.startswith('/api/'):
|
||||
return self.get_response(request)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
"""
|
||||
Normally, a web-based session would use csrftoken based authentication.
|
||||
However when running an external application (e.g. the InvenTree app),
|
||||
we wish to use token-based auth to grab media files.
|
||||
|
||||
So, we will allow token-based authentication but ONLY for the /media/ directory.
|
||||
|
||||
What problem is this solving?
|
||||
- The InvenTree mobile app does not use csrf token auth
|
||||
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
|
||||
- Media files (e.g. Part images) are required to be served to the app
|
||||
- We do not want to make /media/ files accessible without login!
|
||||
|
||||
There is PROBABLY a better way of going about this?
|
||||
a) Allow token-based authentication against a user?
|
||||
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
|
||||
c) Is there a "standard" way of solving this problem?
|
||||
|
||||
My [google|stackoverflow]-fu has failed me. So this hack has been created.
|
||||
However when running an external application (e.g. the InvenTree app or Python library),
|
||||
we must validate the user token manually.
|
||||
"""
|
||||
|
||||
authorized = False
|
||||
@@ -56,20 +43,23 @@ class AuthRequiredMiddleware(object):
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys():
|
||||
auth = request.headers['Authorization'].strip()
|
||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||
auth = request.headers.get('Authorization', request.headers.get('authorization')).strip()
|
||||
|
||||
if auth.startswith('Token') and len(auth.split()) == 2:
|
||||
token = auth.split()[1]
|
||||
if auth.lower().startswith('token') and len(auth.split()) == 2:
|
||||
token_key = auth.split()[1]
|
||||
|
||||
# Does the provided token match a valid user?
|
||||
if Token.objects.filter(key=token).exists():
|
||||
try:
|
||||
token = Token.objects.get(key=token_key)
|
||||
|
||||
allowed = ['/api/', '/media/']
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
|
||||
# Only allow token-auth for /media/ or /static/ dirs!
|
||||
if any([request.path_info.startswith(a) for a in allowed]):
|
||||
authorized = True
|
||||
except Token.DoesNotExist:
|
||||
logger.warning(f"Access denied for unknown token {token_key}")
|
||||
pass
|
||||
|
||||
# No authorization was found for the request
|
||||
if not authorized:
|
||||
@@ -92,8 +82,7 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
||||
|
||||
# Code to be executed for each request/response after
|
||||
# the view is called.
|
||||
response = self.get_response(request)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ Generic models which provide extra functionality over base Django model types.
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -21,6 +23,9 @@ from mptt.exceptions import InvalidMove
|
||||
from .validators import validate_tree_name
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def rename_attachment(instance, filename):
|
||||
"""
|
||||
Function for renaming an attachment file.
|
||||
@@ -77,6 +82,72 @@ class InvenTreeAttachment(models.Model):
|
||||
def basename(self):
|
||||
return os.path.basename(self.attachment.name)
|
||||
|
||||
@basename.setter
|
||||
def basename(self, fn):
|
||||
"""
|
||||
Function to rename the attachment file.
|
||||
|
||||
- Filename cannot be empty
|
||||
- Filename cannot contain illegal characters
|
||||
- Filename must specify an extension
|
||||
- Filename cannot match an existing file
|
||||
"""
|
||||
|
||||
fn = fn.strip()
|
||||
|
||||
if len(fn) == 0:
|
||||
raise ValidationError(_('Filename must not be empty'))
|
||||
|
||||
attachment_dir = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.getSubdir()
|
||||
)
|
||||
|
||||
old_file = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.attachment.name
|
||||
)
|
||||
|
||||
new_file = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
self.getSubdir(),
|
||||
fn
|
||||
)
|
||||
|
||||
new_file = os.path.abspath(new_file)
|
||||
|
||||
# Check that there are no directory tricks going on...
|
||||
if not os.path.dirname(new_file) == attachment_dir:
|
||||
logger.error(f"Attempted to rename attachment outside valid directory: '{new_file}'")
|
||||
raise ValidationError(_("Invalid attachment directory"))
|
||||
|
||||
# Ignore further checks if the filename is not actually being renamed
|
||||
if new_file == old_file:
|
||||
return
|
||||
|
||||
forbidden = ["'", '"', "#", "@", "!", "&", "^", "<", ">", ":", ";", "/", "\\", "|", "?", "*", "%", "~", "`"]
|
||||
|
||||
for c in forbidden:
|
||||
if c in fn:
|
||||
raise ValidationError(_(f"Filename contains illegal character '{c}'"))
|
||||
|
||||
if len(fn.split('.')) < 2:
|
||||
raise ValidationError(_("Filename missing extension"))
|
||||
|
||||
if not os.path.exists(old_file):
|
||||
logger.error(f"Trying to rename attachment '{old_file}' which does not exist")
|
||||
return
|
||||
|
||||
if os.path.exists(new_file):
|
||||
raise ValidationError(_("Attachment with this filename already exists"))
|
||||
|
||||
try:
|
||||
os.rename(old_file, new_file)
|
||||
self.attachment.name = os.path.join(self.getSubdir(), fn)
|
||||
self.save()
|
||||
except:
|
||||
raise ValidationError(_("Error renaming file"))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@@ -93,6 +164,17 @@ class InvenTreeTree(MPTTModel):
|
||||
parent: The item immediately above this one. An item with a null parent is a top-level item
|
||||
"""
|
||||
|
||||
def api_instance_filters(self):
|
||||
"""
|
||||
Instance filters for InvenTreeTree models
|
||||
"""
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,8 @@ import os
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
@@ -46,10 +48,12 @@ class InvenTreeMoneySerializer(MoneyField):
|
||||
amount = None
|
||||
|
||||
try:
|
||||
if amount is not None:
|
||||
if amount is not None and amount is not empty:
|
||||
amount = Decimal(amount)
|
||||
except:
|
||||
raise ValidationError(_("Must be a valid number"))
|
||||
raise ValidationError({
|
||||
self.field_name: _("Must be a valid number")
|
||||
})
|
||||
|
||||
currency = data.get(get_currency_field_name(self.field_name), self.default_currency)
|
||||
|
||||
@@ -85,14 +89,21 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
|
||||
# self.instance = instance
|
||||
"""
|
||||
Custom __init__ routine to ensure that *default* values (as specified in the ORM)
|
||||
are used by the DRF serializers, *if* the values are not provided by the user.
|
||||
"""
|
||||
|
||||
# If instance is None, we are creating a new instance
|
||||
if instance is None and data is not empty:
|
||||
|
||||
# Required to side-step immutability of a QueryDict
|
||||
data = data.copy()
|
||||
|
||||
if data is None:
|
||||
data = OrderedDict()
|
||||
else:
|
||||
new_data = OrderedDict()
|
||||
new_data.update(data)
|
||||
|
||||
data = new_data
|
||||
|
||||
# Add missing fields which have default values
|
||||
ModelClass = self.Meta.model
|
||||
@@ -165,6 +176,18 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
return self.instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""
|
||||
Catch any django ValidationError, and re-throw as a DRF ValidationError
|
||||
"""
|
||||
|
||||
try:
|
||||
instance = super().update(instance, validated_data)
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
return instance
|
||||
|
||||
def run_validation(self, data=empty):
|
||||
"""
|
||||
Perform serializer validation.
|
||||
@@ -186,18 +209,45 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Update instance fields
|
||||
for attr, value in data.items():
|
||||
setattr(instance, attr, value)
|
||||
try:
|
||||
setattr(instance, attr, value)
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
# Run a 'full_clean' on the model.
|
||||
# Note that by default, DRF does *not* perform full model validation!
|
||||
try:
|
||||
instance.full_clean()
|
||||
except (ValidationError, DjangoValidationError) as exc:
|
||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
||||
|
||||
data = exc.message_dict
|
||||
|
||||
# Change '__all__' key (django style) to 'non_field_errors' (DRF style)
|
||||
if '__all__' in data:
|
||||
data['non_field_errors'] = data['__all__']
|
||||
del data['__all__']
|
||||
|
||||
raise ValidationError(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializer(InvenTreeModelSerializer):
|
||||
"""
|
||||
Special case of an InvenTreeModelSerializer, which handles an "attachment" model.
|
||||
|
||||
The only real addition here is that we support "renaming" of the attachment file.
|
||||
"""
|
||||
|
||||
# The 'filename' field must be present in the serializer
|
||||
filename = serializers.CharField(
|
||||
label=_('Filename'),
|
||||
required=False,
|
||||
source='basename',
|
||||
allow_blank=False,
|
||||
)
|
||||
|
||||
|
||||
class InvenTreeAttachmentSerializerField(serializers.FileField):
|
||||
"""
|
||||
Override the DRF native FileField serializer,
|
||||
|
||||
@@ -12,6 +12,7 @@ database setup in this file.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
@@ -168,6 +169,30 @@ else:
|
||||
logger.exception(f"Couldn't load keyfile {key_file}")
|
||||
sys.exit(-1)
|
||||
|
||||
# The filesystem location for served static files
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_STATIC_ROOT',
|
||||
CONFIG.get('static_root', None)
|
||||
)
|
||||
)
|
||||
|
||||
if STATIC_ROOT is None:
|
||||
print("ERROR: INVENTREE_STATIC_ROOT directory not defined")
|
||||
sys.exit(1)
|
||||
|
||||
# The filesystem location for served static files
|
||||
MEDIA_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_MEDIA_ROOT',
|
||||
CONFIG.get('media_root', None)
|
||||
)
|
||||
)
|
||||
|
||||
if MEDIA_ROOT is None:
|
||||
print("ERROR: INVENTREE_MEDIA_ROOT directory is not defined")
|
||||
sys.exit(1)
|
||||
|
||||
# List of allowed hosts (default = allow all)
|
||||
ALLOWED_HOSTS = CONFIG.get('allowed_hosts', ['*'])
|
||||
|
||||
@@ -188,22 +213,12 @@ if cors_opt:
|
||||
# Web URL endpoint for served static files
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# The filesystem location for served static files
|
||||
STATIC_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_STATIC_ROOT',
|
||||
CONFIG.get('static_root', '/home/inventree/data/static')
|
||||
)
|
||||
)
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(BASE_DIR, 'InvenTree', 'static'),
|
||||
]
|
||||
STATICFILES_DIRS = []
|
||||
|
||||
# Translated Template settings
|
||||
STATICFILES_I18_PREFIX = 'i18n'
|
||||
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js')
|
||||
STATICFILES_I18_TRG = STATICFILES_DIRS[0] + '_' + STATICFILES_I18_PREFIX
|
||||
STATICFILES_I18_SRC = os.path.join(BASE_DIR, 'templates', 'js', 'translated')
|
||||
STATICFILES_I18_TRG = os.path.join(BASE_DIR, 'InvenTree', 'static_i18n')
|
||||
STATICFILES_DIRS.append(STATICFILES_I18_TRG)
|
||||
STATICFILES_I18_TRG = os.path.join(STATICFILES_I18_TRG, STATICFILES_I18_PREFIX)
|
||||
|
||||
@@ -217,19 +232,11 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
||||
# Web URL endpoint for served media files
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# The filesystem location for served static files
|
||||
MEDIA_ROOT = os.path.abspath(
|
||||
get_setting(
|
||||
'INVENTREE_MEDIA_ROOT',
|
||||
CONFIG.get('media_root', '/home/inventree/data/media')
|
||||
)
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
logger.info("InvenTree running in DEBUG mode")
|
||||
|
||||
logger.info(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||
logger.info(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -319,6 +326,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
# Custom InvenTree context processors
|
||||
'InvenTree.context.health_status',
|
||||
'InvenTree.context.status_codes',
|
||||
'InvenTree.context.user_roles',
|
||||
@@ -347,10 +355,22 @@ REST_FRAMEWORK = {
|
||||
|
||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||
|
||||
background_workers = os.environ.get('INVENTREE_BACKGROUND_WORKERS', None)
|
||||
|
||||
if background_workers is not None:
|
||||
try:
|
||||
background_workers = int(background_workers)
|
||||
except ValueError:
|
||||
background_workers = None
|
||||
|
||||
if background_workers is None:
|
||||
# Sensible default?
|
||||
background_workers = 4
|
||||
|
||||
# django-q configuration
|
||||
Q_CLUSTER = {
|
||||
'name': 'InvenTree',
|
||||
'workers': 4,
|
||||
'workers': background_workers,
|
||||
'timeout': 90,
|
||||
'retry': 120,
|
||||
'queue_limit': 50,
|
||||
@@ -400,7 +420,7 @@ Configure the database backend based on the user-specified values.
|
||||
- The following code lets the user "mix and match" database configuration
|
||||
"""
|
||||
|
||||
logger.info("Configuring database backend:")
|
||||
logger.debug("Configuring database backend:")
|
||||
|
||||
# Extract database configuration from the config.yaml file
|
||||
db_config = CONFIG.get('database', {})
|
||||
@@ -454,11 +474,9 @@ if db_engine in ['sqlite3', 'postgresql', 'mysql']:
|
||||
db_name = db_config['NAME']
|
||||
db_host = db_config.get('HOST', "''")
|
||||
|
||||
print("InvenTree Database Configuration")
|
||||
print("================================")
|
||||
print(f"ENGINE: {db_engine}")
|
||||
print(f"NAME: {db_name}")
|
||||
print(f"HOST: {db_host}")
|
||||
logger.info(f"DB_ENGINE: {db_engine}")
|
||||
logger.info(f"DB_NAME: {db_name}")
|
||||
logger.info(f"DB_HOST: {db_host}")
|
||||
|
||||
DATABASES['default'] = db_config
|
||||
|
||||
@@ -502,11 +520,24 @@ LANGUAGE_CODE = CONFIG.get('language', 'en-us')
|
||||
|
||||
# If a new language translation is supported, it must be added here
|
||||
LANGUAGES = [
|
||||
('en', _('English')),
|
||||
('fr', _('French')),
|
||||
('de', _('German')),
|
||||
('el', _('Greek')),
|
||||
('en', _('English')),
|
||||
('es', _('Spanish')),
|
||||
('fr', _('French')),
|
||||
('he', _('Hebrew')),
|
||||
('it', _('Italian')),
|
||||
('ja', _('Japanese')),
|
||||
('ko', _('Korean')),
|
||||
('nl', _('Dutch')),
|
||||
('no', _('Norwegian')),
|
||||
('pl', _('Polish')),
|
||||
('ru', _('Russian')),
|
||||
('sv', _('Swedish')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('vi', _('Vietnamese')),
|
||||
('zh-cn', _('Chinese')),
|
||||
]
|
||||
|
||||
# Currencies available for use
|
||||
|
||||
1
InvenTree/InvenTree/static/css/bootstrap.min.css.map
Normal file
@@ -640,6 +640,11 @@
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-error {
|
||||
border: 2px #FCC solid;
|
||||
background-color: #f5f0f0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
@@ -730,6 +735,13 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
.modal input {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -781,8 +793,8 @@ input[type="submit"] {
|
||||
}
|
||||
|
||||
.sidenav .list-group-item.active {
|
||||
background-color: #ddd;
|
||||
border-color: #ccc;
|
||||
background-color: #c6d4ea;
|
||||
border: 2px #aab solid;
|
||||
}
|
||||
|
||||
/* The side navigation menu */
|
||||
@@ -837,6 +849,12 @@ input[type="submit"] {
|
||||
pointer-events: none; /* Prevent this div from blocking links underneath */
|
||||
}
|
||||
|
||||
.notes {
|
||||
border-radius: 5px;
|
||||
background-color: #fafafa;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: none;
|
||||
border-radius: 5px;
|
||||
@@ -853,6 +871,11 @@ input[type="submit"] {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 3px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
padding: 3px;
|
||||
padding-left: 5px;
|
||||
@@ -901,6 +924,10 @@ input[type="submit"] {
|
||||
box-shadow: 1px 1px #DDD;
|
||||
}
|
||||
|
||||
.panel-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.float-right {
|
||||
float: right;
|
||||
}
|
||||
@@ -1022,3 +1049,19 @@ a.anchor {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* Force minimum width of number input fields to show at least ~5 digits */
|
||||
input[type='number']{
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.search-menu {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.search-menu .ui-menu-item {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.product-card-panel{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
function attachClipboard(selector, containerselector, textElement) {
|
||||
// set container
|
||||
if (containerselector){
|
||||
containerselector = document.getElementById(containerselector);
|
||||
} else {
|
||||
containerselector = document.body;
|
||||
}
|
||||
|
||||
// set text-function
|
||||
if (textElement){
|
||||
text = function() {
|
||||
return document.getElementById(textElement).textContent;
|
||||
}
|
||||
} else {
|
||||
text = function(trigger) {
|
||||
var content = trigger.parentElement.parentElement.textContent;return content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// create Clipboard
|
||||
var cis = new ClipboardJS(selector, {
|
||||
text: text,
|
||||
container: containerselector
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function inventreeDocReady() {
|
||||
/* Run this function when the HTML document is loaded.
|
||||
* This will be called for every page that extends "base.html"
|
||||
*/
|
||||
|
||||
window.addEventListener("dragover",function(e){
|
||||
e = e || event;
|
||||
e.preventDefault();
|
||||
},false);
|
||||
|
||||
window.addEventListener("drop",function(e){
|
||||
e = e || event;
|
||||
e.preventDefault();
|
||||
},false);
|
||||
|
||||
/* Add drag-n-drop functionality to any element
|
||||
* marked with the class 'dropzone'
|
||||
*/
|
||||
$('.dropzone').on('dragenter', function(event) {
|
||||
|
||||
// TODO - Only indicate that a drop event will occur if a file is being dragged
|
||||
var transfer = event.originalEvent.dataTransfer;
|
||||
|
||||
if (true || isFileTransfer(transfer)) {
|
||||
$(this).addClass('dragover');
|
||||
}
|
||||
});
|
||||
|
||||
$('.dropzone').on('dragleave drop', function(event) {
|
||||
$(this).removeClass('dragover');
|
||||
});
|
||||
|
||||
// Callback to launch the 'About' window
|
||||
$('#launch-about').click(function() {
|
||||
var modal = $('#modal-about');
|
||||
|
||||
modal.modal({
|
||||
backdrop: 'static',
|
||||
keyboard: 'false',
|
||||
});
|
||||
|
||||
modal.modal('show');
|
||||
});
|
||||
|
||||
// Callback to launch the 'Database Stats' window
|
||||
$('#launch-stats').click(function() {
|
||||
launchModalForm("/stats/", {
|
||||
no_post: true,
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize clipboard-buttons
|
||||
attachClipboard('.clip-btn');
|
||||
attachClipboard('.clip-btn', 'modal-about'); // modals
|
||||
attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text'); // version-text
|
||||
}
|
||||
|
||||
function isFileTransfer(transfer) {
|
||||
/* Determine if a transfer (e.g. drag-and-drop) is a file transfer
|
||||
*/
|
||||
|
||||
return transfer.files.length > 0;
|
||||
}
|
||||
|
||||
|
||||
function isOnlineTransfer(transfer) {
|
||||
/* Determine if a drag-and-drop transfer is from another website.
|
||||
* e.g. dragged from another browser window
|
||||
*/
|
||||
|
||||
return transfer.items.length > 0;
|
||||
}
|
||||
|
||||
|
||||
function getImageUrlFromTransfer(transfer) {
|
||||
/* Extract external image URL from a drag-and-dropped image
|
||||
*/
|
||||
|
||||
var url = transfer.getData('text/html').match(/src\s*=\s*"(.+?)"/)[1];
|
||||
|
||||
console.log('Image URL: ' + url);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function makeIconBadge(icon, title) {
|
||||
// Construct an 'icon badge' which floats to the right of an object
|
||||
|
||||
var html = `<span class='fas ${icon} label-right' title='${title}'></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function makeIconButton(icon, cls, pk, title, options={}) {
|
||||
// Construct an 'icon button' using the fontawesome set
|
||||
|
||||
var classes = `btn btn-default btn-glyph ${cls}`;
|
||||
|
||||
var id = `${cls}-${pk}`;
|
||||
|
||||
var html = '';
|
||||
|
||||
var extraProps = '';
|
||||
|
||||
if (options.disabled) {
|
||||
extraProps += "disabled='true' ";
|
||||
}
|
||||
|
||||
html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`;
|
||||
html += `<span class='fas ${icon}'></span>`;
|
||||
html += `</button>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function makeProgressBar(value, maximum, opts={}) {
|
||||
/*
|
||||
* Render a progessbar!
|
||||
*
|
||||
* @param value is the current value of the progress bar
|
||||
* @param maximum is the maximum value of the progress bar
|
||||
*/
|
||||
|
||||
var options = opts || {};
|
||||
|
||||
value = parseFloat(value);
|
||||
|
||||
var percent = 100;
|
||||
|
||||
// Prevent div-by-zero or null value
|
||||
if (maximum && maximum > 0) {
|
||||
maximum = parseFloat(maximum);
|
||||
percent = parseInt(value / maximum * 100);
|
||||
}
|
||||
|
||||
if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
|
||||
var extraclass = '';
|
||||
|
||||
if (value > maximum) {
|
||||
extraclass='progress-bar-over';
|
||||
} else if (value < maximum) {
|
||||
extraclass = 'progress-bar-under';
|
||||
}
|
||||
|
||||
var style = options.style || '';
|
||||
|
||||
var text = '';
|
||||
|
||||
if (style == 'percent') {
|
||||
// Display e.g. "50%"
|
||||
|
||||
text = `${percent}%`;
|
||||
} else if (style == 'max') {
|
||||
// Display just the maximum value
|
||||
text = `${maximum}`;
|
||||
} else if (style == 'value') {
|
||||
// Display just the current value
|
||||
text = `${value}`;
|
||||
} else if (style == 'blank') {
|
||||
// No display!
|
||||
text = '';
|
||||
} else {
|
||||
/* Default style
|
||||
* Display e.g. "5 / 10"
|
||||
*/
|
||||
|
||||
text = `${value} / ${maximum}`;
|
||||
}
|
||||
|
||||
var id = options.id || 'progress-bar';
|
||||
|
||||
return `
|
||||
<div id='${id}' class='progress'>
|
||||
<div class='progress-bar ${extraclass}' role='progressbar' aria-valuenow='${percent}' aria-valuemin='0' aria-valuemax='100' style='width:${percent}%'></div>
|
||||
<div class='progress-value'>${text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function enableDragAndDrop(element, url, options) {
|
||||
/* Enable drag-and-drop file uploading for a given element.
|
||||
|
||||
Params:
|
||||
element - HTML element lookup string e.g. "#drop-div"
|
||||
url - URL to POST the file to
|
||||
options - object with following possible values:
|
||||
label - Label of the file to upload (default='file')
|
||||
data - Other form data to upload
|
||||
success - Callback function in case of success
|
||||
error - Callback function in case of error
|
||||
method - HTTP method
|
||||
*/
|
||||
|
||||
data = options.data || {};
|
||||
|
||||
$(element).on('drop', function(event) {
|
||||
|
||||
var transfer = event.originalEvent.dataTransfer;
|
||||
|
||||
var label = options.label || 'file';
|
||||
|
||||
var formData = new FormData();
|
||||
|
||||
// Add the extra data
|
||||
for (var key in data) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
|
||||
if (isFileTransfer(transfer)) {
|
||||
formData.append(label, transfer.files[0]);
|
||||
|
||||
inventreeFormDataUpload(
|
||||
url,
|
||||
formData,
|
||||
{
|
||||
success: function(data, status, xhr) {
|
||||
console.log('Uploaded file via drag-and-drop');
|
||||
if (options.success) {
|
||||
options.success(data, status, xhr);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('File upload failed');
|
||||
if (options.error) {
|
||||
options.error(xhr, status, error);
|
||||
}
|
||||
},
|
||||
method: options.method || 'POST',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('Ignoring drag-and-drop event (not a file)');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function imageHoverIcon(url) {
|
||||
/* Render a small thumbnail icon for an image.
|
||||
* On mouseover, display a full-size version of the image
|
||||
*/
|
||||
|
||||
if (!url) {
|
||||
url = '/static/img/blank_image.png';
|
||||
}
|
||||
|
||||
var html = `
|
||||
<a class='hover-icon'>
|
||||
<img class='hover-img-thumb' src='` + url + `'>
|
||||
<img class='hover-img-large' src='` + url + `'>
|
||||
</a>
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function inventreeSave(name, value) {
|
||||
/*
|
||||
* Save a key:value pair to local storage
|
||||
*/
|
||||
|
||||
var key = "inventree-" + name;
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function inventreeLoad(name, defaultValue) {
|
||||
/*
|
||||
* Retrieve a key:value pair from local storage
|
||||
*/
|
||||
|
||||
var key = "inventree-" + name;
|
||||
|
||||
var value = localStorage.getItem(key);
|
||||
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function inventreeLoadInt(name) {
|
||||
/*
|
||||
* Retrieve a value from local storage, and attempt to cast to integer
|
||||
*/
|
||||
|
||||
var data = inventreeLoad(name);
|
||||
|
||||
return parseInt(data, 10);
|
||||
}
|
||||
|
||||
function inventreeLoadFloat(name) {
|
||||
|
||||
var data = inventreeLoad(name);
|
||||
|
||||
return parseFloat(data);
|
||||
}
|
||||
|
||||
function inventreeDel(name) {
|
||||
|
||||
var key = 'inventree-' + name;
|
||||
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
@@ -35,8 +35,8 @@ function loadTree(url, tree, options={}) {
|
||||
showTags: true,
|
||||
});
|
||||
|
||||
if (sessionStorage.getItem(key)) {
|
||||
var saved_exp = sessionStorage.getItem(key).split(",");
|
||||
if (localStorage.getItem(key)) {
|
||||
var saved_exp = localStorage.getItem(key).split(",");
|
||||
|
||||
// Automatically expand the desired notes
|
||||
for (var q = 0; q < saved_exp.length; q++) {
|
||||
@@ -57,7 +57,7 @@ function loadTree(url, tree, options={}) {
|
||||
}
|
||||
|
||||
// Save the expanded nodes
|
||||
sessionStorage.setItem(key, exp);
|
||||
localStorage.setItem(key, exp);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -106,17 +106,17 @@ function initNavTree(options) {
|
||||
width: '0px'
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
localStorage.setItem(stateLabel, 'closed');
|
||||
} else {
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
sessionStorage.setItem(widthLabel, `${width}px`);
|
||||
localStorage.setItem(stateLabel, 'open');
|
||||
localStorage.setItem(widthLabel, `${width}px`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var state = sessionStorage.getItem(stateLabel);
|
||||
var width = sessionStorage.getItem(widthLabel) || '300px';
|
||||
var state = localStorage.getItem(stateLabel);
|
||||
var width = localStorage.getItem(widthLabel) || '300px';
|
||||
|
||||
if (state && state == 'open') {
|
||||
|
||||
@@ -131,21 +131,21 @@ function initNavTree(options) {
|
||||
|
||||
$(toggleId).click(function() {
|
||||
|
||||
var state = sessionStorage.getItem(stateLabel) || 'closed';
|
||||
var width = sessionStorage.getItem(widthLabel) || '300px';
|
||||
var state = localStorage.getItem(stateLabel) || 'closed';
|
||||
var width = localStorage.getItem(widthLabel) || '300px';
|
||||
|
||||
if (state == 'open') {
|
||||
$(treeId).animate({
|
||||
width: '0px'
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
localStorage.setItem(stateLabel, 'closed');
|
||||
} else {
|
||||
$(treeId).animate({
|
||||
width: width,
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
localStorage.setItem(stateLabel, 'open');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -198,17 +198,18 @@ function enableNavbar(options) {
|
||||
width: '45px'
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
localStorage.setItem(stateLabel, 'closed');
|
||||
} else {
|
||||
sessionStorage.setItem(widthLabel, `${width}px`);
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
localStorage.setItem(widthLabel, `${width}px`);
|
||||
localStorage.setItem(stateLabel, 'open');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var state = sessionStorage.getItem(stateLabel);
|
||||
var width = sessionStorage.getItem(widthLabel) || '250px';
|
||||
var state = localStorage.getItem(stateLabel);
|
||||
|
||||
var width = localStorage.getItem(widthLabel) || '250px';
|
||||
|
||||
if (state && state == 'open') {
|
||||
|
||||
@@ -224,8 +225,8 @@ function enableNavbar(options) {
|
||||
|
||||
$(toggleId).click(function() {
|
||||
|
||||
var state = sessionStorage.getItem(stateLabel) || 'closed';
|
||||
var width = sessionStorage.getItem(widthLabel) || '250px';
|
||||
var state = localStorage.getItem(stateLabel) || 'closed';
|
||||
var width = localStorage.getItem(widthLabel) || '250px';
|
||||
|
||||
if (state == 'open') {
|
||||
$(navId).animate({
|
||||
@@ -233,7 +234,7 @@ function enableNavbar(options) {
|
||||
minWidth: '45px',
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'closed');
|
||||
localStorage.setItem(stateLabel, 'closed');
|
||||
|
||||
} else {
|
||||
|
||||
@@ -241,7 +242,7 @@ function enableNavbar(options) {
|
||||
'width': width
|
||||
}, 50);
|
||||
|
||||
sessionStorage.setItem(stateLabel, 'open');
|
||||
localStorage.setItem(stateLabel, 'open');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -59,6 +59,11 @@
|
||||
<h1>YOUR COMPONENTS:</h1>
|
||||
|
||||
|
||||
<!-- Autocomplete -->
|
||||
<h2 class="demoHeaders">Autocomplete</h2>
|
||||
<div>
|
||||
<input id="autocomplete" title="type "a"">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -248,6 +253,23 @@
|
||||
|
||||
|
||||
|
||||
<!-- Menu -->
|
||||
<h2 class="demoHeaders">Menu</h2>
|
||||
<ul style="width:100px;" id="menu">
|
||||
<li><div>Item 1</div></li>
|
||||
<li><div>Item 2</div></li>
|
||||
<li><div>Item 3</div>
|
||||
<ul>
|
||||
<li><div>Item 3-1</div></li>
|
||||
<li><div>Item 3-2</div></li>
|
||||
<li><div>Item 3-3</div></li>
|
||||
<li><div>Item 3-4</div></li>
|
||||
<li><div>Item 3-5</div></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><div>Item 4</div></li>
|
||||
<li><div>Item 5</div></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<!-- Highlight / Error -->
|
||||
@@ -270,6 +292,33 @@
|
||||
<script src="jquery-ui.js"></script>
|
||||
<script>
|
||||
|
||||
var availableTags = [
|
||||
"ActionScript",
|
||||
"AppleScript",
|
||||
"Asp",
|
||||
"BASIC",
|
||||
"C",
|
||||
"C++",
|
||||
"Clojure",
|
||||
"COBOL",
|
||||
"ColdFusion",
|
||||
"Erlang",
|
||||
"Fortran",
|
||||
"Groovy",
|
||||
"Haskell",
|
||||
"Java",
|
||||
"JavaScript",
|
||||
"Lisp",
|
||||
"Perl",
|
||||
"PHP",
|
||||
"Python",
|
||||
"Ruby",
|
||||
"Scala",
|
||||
"Scheme"
|
||||
];
|
||||
$( "#autocomplete" ).autocomplete({
|
||||
source: availableTags
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -280,6 +329,7 @@
|
||||
|
||||
|
||||
|
||||
$( "#menu" ).menu();
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Includes: core.css, resizable.css, theme.css
|
||||
* Includes: core.css, resizable.css, autocomplete.css, menu.css, theme.css
|
||||
* To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
@@ -160,6 +160,66 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Component containers
|
||||
----------------------------------*/
|
||||
|
||||
1915
InvenTree/InvenTree/static/script/jquery-ui/jquery-ui.js
vendored
@@ -164,3 +164,63 @@
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
}
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
.ui-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-menu .ui-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.ui-menu .ui-menu-item {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
/* support: IE10, see #8844 */
|
||||
list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
|
||||
}
|
||||
.ui-menu .ui-menu-item-wrapper {
|
||||
position: relative;
|
||||
padding: 3px 1em 3px .4em;
|
||||
}
|
||||
.ui-menu .ui-menu-divider {
|
||||
margin: 5px 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.ui-menu .ui-state-focus,
|
||||
.ui-menu .ui-state-active {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
/* icon support */
|
||||
.ui-menu-icons {
|
||||
position: relative;
|
||||
}
|
||||
.ui-menu-icons .ui-menu-item-wrapper {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* left-aligned */
|
||||
.ui-menu .ui-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: .2em;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
/* right-aligned */
|
||||
.ui-menu .ui-menu-icon {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}
|
||||
.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}
|
||||
@@ -1,4 +1,4 @@
|
||||
/*! jQuery UI - v1.12.1 - 2021-02-23
|
||||
/*! jQuery UI - v1.12.1 - 2021-07-18
|
||||
* http://jqueryui.com
|
||||
* Copyright jQuery Foundation and other contributors; Licensed MIT */
|
||||
|
||||
|
||||
@@ -4678,7 +4678,7 @@ input[type="submit"].btn.btn-mini {
|
||||
|
||||
.navbar .btn-navbar:active,
|
||||
.navbar .btn-navbar.active {
|
||||
background-color: #cccccc \9;
|
||||
background-color: #ba8;
|
||||
}
|
||||
|
||||
.navbar .btn-navbar .icon-bar {
|
||||
|
||||
@@ -6,7 +6,8 @@ import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
@@ -35,7 +36,7 @@ def schedule_task(taskname, **kwargs):
|
||||
# If this task is already scheduled, don't schedule it again
|
||||
# Instead, update the scheduling parameters
|
||||
if Schedule.objects.filter(func=taskname).exists():
|
||||
logger.info(f"Scheduled task '{taskname}' already exists - updating!")
|
||||
logger.debug(f"Scheduled task '{taskname}' already exists - updating!")
|
||||
|
||||
Schedule.objects.filter(func=taskname).update(**kwargs)
|
||||
else:
|
||||
@@ -51,11 +52,14 @@ def schedule_task(taskname, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def offload_task(taskname, *args, **kwargs):
|
||||
def offload_task(taskname, force_sync=False, *args, **kwargs):
|
||||
"""
|
||||
Create an AsyncTask.
|
||||
This is different to a 'scheduled' task,
|
||||
in that it only runs once!
|
||||
Create an AsyncTask if workers are running.
|
||||
This is different to a 'scheduled' task,
|
||||
in that it only runs once!
|
||||
|
||||
If workers are not running or force_sync flag
|
||||
is set then the task is ran synchronously.
|
||||
"""
|
||||
|
||||
try:
|
||||
@@ -63,10 +67,48 @@ def offload_task(taskname, *args, **kwargs):
|
||||
except (AppRegistryNotReady):
|
||||
logger.warning("Could not offload task - app registry not ready")
|
||||
return
|
||||
import importlib
|
||||
from InvenTree.status import is_worker_running
|
||||
|
||||
task = AsyncTask(taskname, *args, **kwargs)
|
||||
if is_worker_running() and not force_sync:
|
||||
# Running as asynchronous task
|
||||
try:
|
||||
task = AsyncTask(taskname, *args, **kwargs)
|
||||
task.run()
|
||||
except ImportError:
|
||||
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
|
||||
else:
|
||||
# Split path
|
||||
try:
|
||||
app, mod, func = taskname.split('.')
|
||||
app_mod = app + '.' + mod
|
||||
except ValueError:
|
||||
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
||||
return
|
||||
|
||||
task.run()
|
||||
# Import module from app
|
||||
try:
|
||||
_mod = importlib.import_module(app_mod)
|
||||
except ModuleNotFoundError:
|
||||
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
||||
return
|
||||
|
||||
# Retrieve function
|
||||
try:
|
||||
_func = getattr(_mod, func)
|
||||
except AttributeError:
|
||||
# getattr does not work for local import
|
||||
_func = None
|
||||
|
||||
try:
|
||||
if not _func:
|
||||
_func = eval(func)
|
||||
except NameError:
|
||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||
return
|
||||
|
||||
# Workers are not running: run it as synchronous task
|
||||
_func()
|
||||
|
||||
|
||||
def heartbeat():
|
||||
@@ -84,7 +126,7 @@ def heartbeat():
|
||||
except AppRegistryNotReady:
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(minutes=30)
|
||||
threshold = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
# Delete heartbeat results more than half an hour old,
|
||||
# otherwise they just create extra noise
|
||||
@@ -108,7 +150,7 @@ def delete_successful_tasks():
|
||||
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(days=30)
|
||||
threshold = timezone.now() - timedelta(days=30)
|
||||
|
||||
results = Success.objects.filter(
|
||||
started__lte=threshold
|
||||
@@ -162,6 +204,25 @@ def check_for_updates():
|
||||
)
|
||||
|
||||
|
||||
def delete_expired_sessions():
|
||||
"""
|
||||
Remove any expired user sessions from the database
|
||||
"""
|
||||
|
||||
try:
|
||||
from django.contrib.sessions.models import Session
|
||||
|
||||
# Delete any sessions that expired more than a day ago
|
||||
expired = Session.objects.filter(expire_date__lt=timezone.now() - timedelta(days=1))
|
||||
|
||||
if True or expired.count() > 0:
|
||||
logger.info(f"Deleting {expired.count()} expired sessions.")
|
||||
expired.delete()
|
||||
|
||||
except AppRegistryNotReady:
|
||||
logger.info("Could not perform 'delete_expired_sessions' - App registry not ready")
|
||||
|
||||
|
||||
def update_exchange_rates():
|
||||
"""
|
||||
Update currency exchange rates
|
||||
@@ -213,7 +274,9 @@ def send_email(subject, body, recipients, from_email=None):
|
||||
|
||||
offload_task(
|
||||
'django.core.mail.send_mail',
|
||||
subject, body,
|
||||
subject,
|
||||
body,
|
||||
from_email,
|
||||
recipients,
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
142
InvenTree/InvenTree/test_urls.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Validate that all URLs specified in template files are correct.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class URLTest(TestCase):
|
||||
|
||||
# Need fixture data in the database
|
||||
fixtures = [
|
||||
'settings',
|
||||
'build',
|
||||
'company',
|
||||
'manufacturer_part',
|
||||
'price_breaks',
|
||||
'supplier_part',
|
||||
'order',
|
||||
'sales_order',
|
||||
'bom',
|
||||
'category',
|
||||
'params',
|
||||
'part_pricebreaks',
|
||||
'part',
|
||||
'test_templates',
|
||||
'location',
|
||||
'stock_tests',
|
||||
'stock',
|
||||
'users',
|
||||
]
|
||||
|
||||
def find_files(self, suffix):
|
||||
"""
|
||||
Search for all files in the template directories,
|
||||
which can have URLs rendered
|
||||
"""
|
||||
|
||||
template_dirs = [
|
||||
('build', 'templates'),
|
||||
('common', 'templates'),
|
||||
('company', 'templates'),
|
||||
('label', 'templates'),
|
||||
('order', 'templates'),
|
||||
('part', 'templates'),
|
||||
('report', 'templates'),
|
||||
('stock', 'templates'),
|
||||
('templates', ),
|
||||
]
|
||||
|
||||
template_files = []
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
tld = os.path.join(here, '..')
|
||||
|
||||
for directory in template_dirs:
|
||||
|
||||
template_dir = os.path.join(tld, *directory)
|
||||
|
||||
for path in Path(template_dir).rglob(suffix):
|
||||
|
||||
f = os.path.abspath(path)
|
||||
|
||||
if f not in template_files:
|
||||
template_files.append(f)
|
||||
|
||||
return template_files
|
||||
|
||||
def find_urls(self, input_file):
|
||||
"""
|
||||
Search for all instances of {% url %} in supplied template file
|
||||
"""
|
||||
|
||||
urls = []
|
||||
|
||||
pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}"
|
||||
|
||||
with open(input_file, 'r') as f:
|
||||
|
||||
data = f.read()
|
||||
|
||||
results = re.findall(pattern, data)
|
||||
|
||||
for result in results:
|
||||
if len(result) == 2:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
result[1].strip()
|
||||
])
|
||||
elif len(result) == 1:
|
||||
urls.append([
|
||||
result[0].strip(),
|
||||
''
|
||||
])
|
||||
|
||||
return urls
|
||||
|
||||
def reverse_url(self, url_pair):
|
||||
"""
|
||||
Perform lookup on the URL
|
||||
"""
|
||||
|
||||
url, pk = url_pair
|
||||
|
||||
# TODO: Handle reverse lookup of admin URLs!
|
||||
if url.startswith("admin:"):
|
||||
return
|
||||
|
||||
if pk:
|
||||
# We will assume that there is at least one item in the database
|
||||
reverse(url, kwargs={"pk": 1})
|
||||
else:
|
||||
reverse(url)
|
||||
|
||||
def check_file(self, f):
|
||||
"""
|
||||
Run URL checks for the provided file
|
||||
"""
|
||||
|
||||
urls = self.find_urls(f)
|
||||
|
||||
for url in urls:
|
||||
self.reverse_url(url)
|
||||
|
||||
def test_html_templates(self):
|
||||
|
||||
template_files = self.find_files("*.html")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
||||
|
||||
def test_js_templates(self):
|
||||
|
||||
template_files = self.find_files("*.js")
|
||||
|
||||
for f in template_files:
|
||||
self.check_file(f)
|
||||
@@ -39,11 +39,11 @@ from rest_framework.documentation import include_docs_urls
|
||||
from .views import auth_request
|
||||
from .views import IndexView, SearchView, DatabaseStatsView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView
|
||||
from .views import CurrencySettingsView, CurrencyRefreshView
|
||||
from .views import CurrencyRefreshView
|
||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
from common.views import SettingEdit, UserSettingEdit
|
||||
|
||||
from .api import InfoView, NotFoundView
|
||||
from .api import ActionPluginView
|
||||
@@ -79,47 +79,48 @@ apipatterns = [
|
||||
|
||||
settings_urls = [
|
||||
|
||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||
|
||||
url(r'^global/', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
||||
url(r'^report/', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||
url(r'^part/', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'),
|
||||
url(r'^stock/', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'),
|
||||
url(r'^build/', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'),
|
||||
url(r'^purchase-order/', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'),
|
||||
url(r'^sales-order/', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'),
|
||||
url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'),
|
||||
|
||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||
|
||||
url(r'^category/', SettingCategorySelectView.as_view(), name='settings-category'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/edit/user', UserSettingEdit.as_view(), name='user-setting-edit'),
|
||||
url(r'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||
|
||||
# Catch any other urls
|
||||
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings'),
|
||||
url(r'^.*$', SettingsView.as_view(template_name='InvenTree/settings/settings.html'), name='settings'),
|
||||
]
|
||||
|
||||
# Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer
|
||||
# These javascript files are served "dynamically" - i.e. rendered on demand
|
||||
dynamic_javascript_urls = [
|
||||
url(r'^api.js', DynamicJsView.as_view(template_name='js/api.js'), name='api.js'),
|
||||
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/attachment.js'), name='attachment.js'),
|
||||
url(r'^forms.js', DynamicJsView.as_view(template_name='js/forms.js'), name='forms.js'),
|
||||
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/model_renderers.js'), name='model_renderers.js'),
|
||||
url(r'^modals.js', DynamicJsView.as_view(template_name='js/modals.js'), name='modals.js'),
|
||||
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'),
|
||||
url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'),
|
||||
url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'),
|
||||
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/calendar.js'), name='calendar.js'),
|
||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'),
|
||||
url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'),
|
||||
url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'),
|
||||
url(r'^label.js', DynamicJsView.as_view(template_name='js/label.js'), name='label.js'),
|
||||
url(r'^report.js', DynamicJsView.as_view(template_name='js/report.js'), name='report.js'),
|
||||
url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'),
|
||||
url(r'^tables.js', DynamicJsView.as_view(template_name='js/tables.js'), name='tables.js'),
|
||||
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'),
|
||||
url(r'^filters.js', DynamicJsView.as_view(template_name='js/filters.js'), name='filters.js'),
|
||||
url(r'^inventree.js', DynamicJsView.as_view(template_name='js/dynamic/inventree.js'), name='inventree.js'),
|
||||
url(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
|
||||
url(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
|
||||
url(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
|
||||
]
|
||||
|
||||
# These javascript files are pased through the Django translation layer
|
||||
translated_javascript_urls = [
|
||||
url(r'^api.js', DynamicJsView.as_view(template_name='js/translated/api.js'), name='api.js'),
|
||||
url(r'^attachment.js', DynamicJsView.as_view(template_name='js/translated/attachment.js'), name='attachment.js'),
|
||||
url(r'^barcode.js', DynamicJsView.as_view(template_name='js/translated/barcode.js'), name='barcode.js'),
|
||||
url(r'^bom.js', DynamicJsView.as_view(template_name='js/translated/bom.js'), name='bom.js'),
|
||||
url(r'^build.js', DynamicJsView.as_view(template_name='js/translated/build.js'), name='build.js'),
|
||||
url(r'^company.js', DynamicJsView.as_view(template_name='js/translated/company.js'), name='company.js'),
|
||||
url(r'^filters.js', DynamicJsView.as_view(template_name='js/translated/filters.js'), name='filters.js'),
|
||||
url(r'^forms.js', DynamicJsView.as_view(template_name='js/translated/forms.js'), name='forms.js'),
|
||||
url(r'^helpers.js', DynamicJsView.as_view(template_name='js/translated/helpers.js'), name='helpers.js'),
|
||||
url(r'^label.js', DynamicJsView.as_view(template_name='js/translated/label.js'), name='label.js'),
|
||||
url(r'^model_renderers.js', DynamicJsView.as_view(template_name='js/translated/model_renderers.js'), name='model_renderers.js'),
|
||||
url(r'^modals.js', DynamicJsView.as_view(template_name='js/translated/modals.js'), name='modals.js'),
|
||||
url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'),
|
||||
url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'),
|
||||
url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'),
|
||||
url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'),
|
||||
url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'),
|
||||
url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/translated/table_filters.js'), name='table_filters.js'),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
@@ -128,7 +129,8 @@ urlpatterns = [
|
||||
url(r'^supplier-part/', include(supplier_part_urls)),
|
||||
|
||||
# "Dynamic" javascript files which are rendered using InvenTree templating.
|
||||
url(r'^dynamic/', include(dynamic_javascript_urls)),
|
||||
url(r'^js/dynamic/', include(dynamic_javascript_urls)),
|
||||
url(r'^js/i18n/', include(translated_javascript_urls)),
|
||||
|
||||
url(r'^common/', include(common_urls)),
|
||||
|
||||
|
||||
@@ -8,29 +8,49 @@ import re
|
||||
|
||||
import common.models
|
||||
|
||||
INVENTREE_SW_VERSION = "0.2.5 pre"
|
||||
INVENTREE_SW_VERSION = "0.5.3"
|
||||
|
||||
INVENTREE_API_VERSION = 7
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 12
|
||||
|
||||
"""
|
||||
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v7 -> 2021-07-03
|
||||
v12 -> 2021-09-07
|
||||
- Adds API endpoint to receive stock items against a PurchaseOrder
|
||||
|
||||
v11 -> 2021-08-26
|
||||
- Adds "units" field to PartBriefSerializer
|
||||
- This allows units to be introspected from the "part_detail" field in the StockItem serializer
|
||||
|
||||
v10 -> 2021-08-23
|
||||
- Adds "purchase_price_currency" to StockItem serializer
|
||||
- Adds "purchase_price_string" to StockItem serializer
|
||||
- Purchase price is now writable for StockItem serializer
|
||||
|
||||
v9 -> 2021-08-09
|
||||
- Adds "price_string" to part pricing serializers
|
||||
|
||||
v8 -> 2021-07-19
|
||||
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
||||
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
||||
|
||||
v7 -> 2021-07-03
|
||||
- Introduced the concept of "API forms" in https://github.com/inventree/InvenTree/pull/1716
|
||||
- API OPTIONS endpoints provide comprehensive field metedata
|
||||
- Multiple new API endpoints added for database models
|
||||
|
||||
v6 -> 2021-06-23
|
||||
v6 -> 2021-06-23
|
||||
- Part and Company images can now be directly uploaded via the REST API
|
||||
|
||||
v5 -> 2021-06-21
|
||||
v5 -> 2021-06-21
|
||||
- Adds API interface for manufacturer part parameters
|
||||
|
||||
v4 -> 2021-06-01
|
||||
v4 -> 2021-06-01
|
||||
- BOM items can now accept "variant stock" to be assigned against them
|
||||
- Many slight API tweaks were needed to get this to work properly!
|
||||
|
||||
v3 -> 2021-05-22:
|
||||
v3 -> 2021-05-22:
|
||||
- The updated StockItem "history tracking" now uses a different interface
|
||||
|
||||
"""
|
||||
@@ -51,7 +71,7 @@ def inventreeInstanceTitle():
|
||||
|
||||
def inventreeVersion():
|
||||
""" Returns the InvenTree version string """
|
||||
return INVENTREE_SW_VERSION
|
||||
return INVENTREE_SW_VERSION.lower().strip()
|
||||
|
||||
|
||||
def inventreeVersionTuple(version=None):
|
||||
@@ -65,6 +85,28 @@ def inventreeVersionTuple(version=None):
|
||||
return [int(g) for g in match.groups()]
|
||||
|
||||
|
||||
def isInvenTreeDevelopmentVersion():
|
||||
"""
|
||||
Return True if current InvenTree version is a "development" version
|
||||
"""
|
||||
return inventreeVersion().endswith('dev')
|
||||
|
||||
|
||||
def inventreeDocsVersion():
|
||||
"""
|
||||
Return the version string matching the latest documentation.
|
||||
|
||||
Development -> "latest"
|
||||
Release -> "major.minor.sub" e.g. "0.5.2"
|
||||
|
||||
"""
|
||||
|
||||
if isInvenTreeDevelopmentVersion():
|
||||
return "latest"
|
||||
else:
|
||||
return INVENTREE_SW_VERSION
|
||||
|
||||
|
||||
def isInvenTreeUpToDate():
|
||||
"""
|
||||
Test if the InvenTree instance is "up to date" with the latest version.
|
||||
|
||||
@@ -7,11 +7,15 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.loader import render_to_string
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from django.conf import settings
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
@@ -27,10 +31,8 @@ from stock.models import StockLocation, StockItem
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
from users.models import check_user_role, RuleSet
|
||||
|
||||
import InvenTree.tasks
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||
from .forms import ColorThemeSelectForm, SettingCategorySelectForm
|
||||
from .forms import SettingCategorySelectForm
|
||||
from .helpers import str2bool
|
||||
|
||||
from rest_framework import views
|
||||
@@ -779,7 +781,7 @@ class SettingsView(TemplateView):
|
||||
""" View for configuring User settings
|
||||
"""
|
||||
|
||||
template_name = "InvenTree/settings.html"
|
||||
template_name = "InvenTree/settings/settings.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -787,6 +789,27 @@ class SettingsView(TemplateView):
|
||||
|
||||
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
|
||||
|
||||
ctx["base_currency"] = currency_code_default()
|
||||
ctx["currencies"] = currency_codes
|
||||
|
||||
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
|
||||
|
||||
ctx["categories"] = PartCategory.objects.all().order_by('tree_id', 'lft', 'name')
|
||||
|
||||
# When were the rates last updated?
|
||||
try:
|
||||
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
|
||||
ctx["rates_updated"] = backend.last_update
|
||||
except:
|
||||
ctx["rates_updated"] = None
|
||||
|
||||
# load locale stats
|
||||
STAT_FILE = os.path.abspath(os.path.join(settings.BASE_DIR, 'InvenTree/locale_stats.json'))
|
||||
try:
|
||||
ctx["locale_stats"] = json.load(open(STAT_FILE, 'r'))
|
||||
except:
|
||||
ctx["locale_stats"] = {}
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -802,46 +825,20 @@ class CurrencyRefreshView(RedirectView):
|
||||
On a POST request we will attempt to refresh the exchange rates
|
||||
"""
|
||||
|
||||
# Will block for a little bit
|
||||
InvenTree.tasks.update_exchange_rates()
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
return self.get(request, *args, **kwargs)
|
||||
# Define associated task from InvenTree.tasks list of methods
|
||||
taskname = 'InvenTree.tasks.update_exchange_rates'
|
||||
|
||||
# Run it
|
||||
offload_task(taskname, force_sync=True)
|
||||
|
||||
return redirect(reverse_lazy('settings'))
|
||||
|
||||
|
||||
class CurrencySettingsView(TemplateView):
|
||||
"""
|
||||
View for configuring currency settings
|
||||
"""
|
||||
|
||||
template_name = "InvenTree/settings/currencies.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
ctx = super().get_context_data(**kwargs).copy()
|
||||
|
||||
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
|
||||
ctx["base_currency"] = currency_code_default()
|
||||
ctx["currencies"] = currency_codes
|
||||
|
||||
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
|
||||
|
||||
# When were the rates last updated?
|
||||
try:
|
||||
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
|
||||
ctx["rates_updated"] = backend.last_update
|
||||
except:
|
||||
ctx["rates_updated"] = None
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class AppearanceSelectView(FormView):
|
||||
class AppearanceSelectView(RedirectView):
|
||||
""" View for selecting a color theme """
|
||||
|
||||
form_class = ColorThemeSelectForm
|
||||
success_url = reverse_lazy('settings-appearance')
|
||||
template_name = "InvenTree/settings/appearance.html"
|
||||
|
||||
def get_user_theme(self):
|
||||
""" Get current user color theme """
|
||||
try:
|
||||
@@ -851,40 +848,10 @@ class AppearanceSelectView(FormView):
|
||||
|
||||
return user_theme
|
||||
|
||||
def get_initial(self):
|
||||
""" Select current user color theme as initial choice """
|
||||
|
||||
initial = super(AppearanceSelectView, self).get_initial()
|
||||
|
||||
user_theme = self.get_user_theme()
|
||||
if user_theme:
|
||||
initial['name'] = user_theme.name
|
||||
return initial
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
""" Check if current color theme exists, else display alert box """
|
||||
|
||||
context = {}
|
||||
|
||||
form = self.get_form()
|
||||
context['form'] = form
|
||||
|
||||
user_theme = self.get_user_theme()
|
||||
if user_theme:
|
||||
# Check color theme is a valid choice
|
||||
if not ColorTheme.is_valid_choice(user_theme):
|
||||
user_color_theme_name = user_theme.name
|
||||
if not user_color_theme_name:
|
||||
user_color_theme_name = 'default'
|
||||
|
||||
context['invalid_color_theme'] = user_color_theme_name
|
||||
|
||||
return self.render_to_response(context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
""" Save user color theme selection """
|
||||
|
||||
form = self.get_form()
|
||||
theme = request.POST.get('theme', None)
|
||||
|
||||
# Get current user theme
|
||||
user_theme = self.get_user_theme()
|
||||
@@ -894,20 +861,10 @@ class AppearanceSelectView(FormView):
|
||||
user_theme = ColorTheme()
|
||||
user_theme.user = request.user
|
||||
|
||||
if form.is_valid():
|
||||
theme_selected = form.cleaned_data['name']
|
||||
user_theme.name = theme
|
||||
user_theme.save()
|
||||
|
||||
# Set color theme to form selection
|
||||
user_theme.name = theme_selected
|
||||
user_theme.save()
|
||||
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
# Set color theme to default
|
||||
user_theme.name = ColorTheme.default_color_theme[0]
|
||||
user_theme.save()
|
||||
|
||||
return self.form_invalid(form)
|
||||
return redirect(reverse_lazy('settings'))
|
||||
|
||||
|
||||
class SettingCategorySelectView(FormView):
|
||||
|
||||
@@ -5,11 +5,13 @@ JSON API for the Build app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from rest_framework import filters
|
||||
from rest_framework import generics
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as rest_filters
|
||||
|
||||
from InvenTree.api import AttachmentMixin
|
||||
from InvenTree.helpers import str2bool, isNull
|
||||
@@ -19,6 +21,36 @@ from .models import Build, BuildItem, BuildOrderAttachment
|
||||
from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer
|
||||
|
||||
|
||||
class BuildFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filterset for BuildList API endpoint
|
||||
"""
|
||||
|
||||
status = rest_filters.NumberFilter(label='Status')
|
||||
|
||||
active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')
|
||||
|
||||
def filter_active(self, queryset, name, value):
|
||||
|
||||
if str2bool(value):
|
||||
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
else:
|
||||
queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
return queryset
|
||||
|
||||
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
||||
|
||||
def filter_overdue(self, queryset, name, value):
|
||||
|
||||
if str2bool(value):
|
||||
queryset = queryset.filter(Build.OVERDUE_FILTER)
|
||||
else:
|
||||
queryset = queryset.exclude(Build.OVERDUE_FILTER)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class BuildList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of Build objects.
|
||||
|
||||
@@ -28,6 +60,7 @@ class BuildList(generics.ListCreateAPIView):
|
||||
|
||||
queryset = Build.objects.all()
|
||||
serializer_class = BuildSerializer
|
||||
filterset_class = BuildFilter
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
@@ -35,10 +68,6 @@ class BuildList(generics.ListCreateAPIView):
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'sales_order',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
'reference',
|
||||
'part__name',
|
||||
@@ -47,6 +76,8 @@ class BuildList(generics.ListCreateAPIView):
|
||||
'target_date',
|
||||
'completion_date',
|
||||
'quantity',
|
||||
'issued_by',
|
||||
'responsible',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
@@ -73,12 +104,33 @@ class BuildList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# exclude parent tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
if exclude_tree is not None:
|
||||
|
||||
try:
|
||||
build = Build.objects.get(pk=exclude_tree)
|
||||
|
||||
queryset = queryset.exclude(
|
||||
pk__in=[bld.pk for bld in build.get_descendants(include_self=True)]
|
||||
)
|
||||
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by "parent"
|
||||
parent = params.get('parent', None)
|
||||
|
||||
if parent is not None:
|
||||
queryset = queryset.filter(parent=parent)
|
||||
|
||||
# Filter by sales_order
|
||||
sales_order = params.get('sales_order', None)
|
||||
|
||||
if sales_order is not None:
|
||||
queryset = queryset.filter(sales_order=sales_order)
|
||||
|
||||
# Filter by "ancestor" builds
|
||||
ancestor = params.get('ancestor', None)
|
||||
|
||||
@@ -95,34 +147,6 @@ class BuildList(generics.ListCreateAPIView):
|
||||
except (ValueError, Build.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by build status?
|
||||
status = params.get('status', None)
|
||||
|
||||
if status is not None:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
# Filter by "pending" status
|
||||
active = params.get('active', None)
|
||||
|
||||
if active is not None:
|
||||
active = str2bool(active)
|
||||
|
||||
if active:
|
||||
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
else:
|
||||
queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
# Filter by "overdue" status?
|
||||
overdue = params.get('overdue', None)
|
||||
|
||||
if overdue is not None:
|
||||
overdue = str2bool(overdue)
|
||||
|
||||
if overdue:
|
||||
queryset = queryset.filter(Build.OVERDUE_FILTER)
|
||||
else:
|
||||
queryset = queryset.exclude(Build.OVERDUE_FILTER)
|
||||
|
||||
# Filter by associated part?
|
||||
part = params.get('part', None)
|
||||
|
||||
@@ -235,6 +259,14 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
queryset = BuildOrderAttachment.objects.all()
|
||||
serializer_class = BuildAttachmentSerializer
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'build',
|
||||
]
|
||||
|
||||
|
||||
class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
|
||||
20
InvenTree/build/migrations/0030_alter_build_reference.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-08 14:14
|
||||
|
||||
import InvenTree.validators
|
||||
import build.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('build', '0029_auto_20210601_1525'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='build',
|
||||
name='reference',
|
||||
field=models.CharField(default=build.models.get_next_build_number, help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'),
|
||||
),
|
||||
]
|
||||
@@ -21,6 +21,7 @@ from django.core.validators import MinValueValidator
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from mptt.exceptions import InvalidMove
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
|
||||
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
|
||||
@@ -37,6 +38,35 @@ from part import models as PartModels
|
||||
from users import models as UserModels
|
||||
|
||||
|
||||
def get_next_build_number():
|
||||
"""
|
||||
Returns the next available BuildOrder reference number
|
||||
"""
|
||||
|
||||
if Build.objects.count() == 0:
|
||||
return
|
||||
|
||||
build = Build.objects.exclude(reference=None).last()
|
||||
|
||||
attempts = set([build.reference])
|
||||
|
||||
reference = build.reference
|
||||
|
||||
while 1:
|
||||
reference = increment(reference)
|
||||
|
||||
if reference in attempts:
|
||||
# Escape infinite recursion
|
||||
return reference
|
||||
|
||||
if Build.objects.filter(reference=reference).exists():
|
||||
attempts.add(reference)
|
||||
else:
|
||||
break
|
||||
|
||||
return reference
|
||||
|
||||
|
||||
class Build(MPTTModel):
|
||||
""" A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||
|
||||
@@ -60,11 +90,28 @@ class Build(MPTTModel):
|
||||
responsible: User (or group) responsible for completing the build
|
||||
"""
|
||||
|
||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-build-list')
|
||||
|
||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||
def api_instance_filters(self):
|
||||
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
}
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except InvalidMove:
|
||||
raise ValidationError({
|
||||
'parent': _('Invalid choice for parent build'),
|
||||
})
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Build Order")
|
||||
@@ -130,6 +177,7 @@ class Build(MPTTModel):
|
||||
blank=False,
|
||||
help_text=_('Build Order Reference'),
|
||||
verbose_name=_('Reference'),
|
||||
default=get_next_build_number,
|
||||
validators=[
|
||||
validate_build_order_reference
|
||||
]
|
||||
|
||||
@@ -10,11 +10,13 @@ from django.db.models import BooleanField
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializerField
|
||||
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
|
||||
from InvenTree.serializers import InvenTreeAttachmentSerializerField, UserSerializerBrief
|
||||
|
||||
from stock.serializers import StockItemSerializerBrief
|
||||
from stock.serializers import LocationSerializer
|
||||
from part.serializers import PartSerializer, PartBriefSerializer
|
||||
from users.serializers import OwnerSerializer
|
||||
|
||||
from .models import Build, BuildItem, BuildOrderAttachment
|
||||
|
||||
@@ -31,6 +33,10 @@ class BuildSerializer(InvenTreeModelSerializer):
|
||||
|
||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||
|
||||
issued_by_detail = UserSerializerBrief(source='issued_by', read_only=True)
|
||||
|
||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
@@ -57,7 +63,7 @@ class BuildSerializer(InvenTreeModelSerializer):
|
||||
return queryset
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -70,9 +76,12 @@ class BuildSerializer(InvenTreeModelSerializer):
|
||||
'pk',
|
||||
'url',
|
||||
'title',
|
||||
'batch',
|
||||
'creation_date',
|
||||
'completed',
|
||||
'completion_date',
|
||||
'destination',
|
||||
'parent',
|
||||
'part',
|
||||
'part_detail',
|
||||
'overdue',
|
||||
@@ -82,8 +91,13 @@ class BuildSerializer(InvenTreeModelSerializer):
|
||||
'status',
|
||||
'status_text',
|
||||
'target_date',
|
||||
'take_from',
|
||||
'notes',
|
||||
'link',
|
||||
'issued_by',
|
||||
'issued_by_detail',
|
||||
'responsible',
|
||||
'responsible_detail',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
@@ -145,7 +159,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class BuildAttachmentSerializer(InvenTreeModelSerializer):
|
||||
class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializer for a BuildAttachment
|
||||
"""
|
||||
@@ -159,6 +173,7 @@ class BuildAttachmentSerializer(InvenTreeModelSerializer):
|
||||
'pk',
|
||||
'build',
|
||||
'attachment',
|
||||
'filename',
|
||||
'comment',
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
{% inventree_title %} | {% trans "Allocate Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='allocate' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Allocate Stock to Build" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if build.has_untracked_bom_items %}
|
||||
{% if build.active %}
|
||||
<div class='btn-group' role='group'>
|
||||
<button class='btn btn-success' type='button' id='btn-auto-allocate' title='{% trans "Allocate stock to build" %}'>
|
||||
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
|
||||
</button>
|
||||
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
||||
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
||||
</button>
|
||||
<!--
|
||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</button>
|
||||
-->
|
||||
</div>
|
||||
{% if build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<table class='table table-striped table-condensed' id='allocation-table-untracked'></table>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This Build Order does not have any associated untracked BOM items" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
var buildInfo = {
|
||||
pk: {{ build.pk }},
|
||||
quantity: {{ build.quantity }},
|
||||
completed: {{ build.completed }},
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% if build.has_untracked_bom_items %}
|
||||
// Load allocation table for un-tracked parts
|
||||
loadBuildOutputAllocationTable(buildInfo, null);
|
||||
{% endif %}
|
||||
|
||||
function reloadTable() {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
{% if build.active %}
|
||||
$("#btn-auto-allocate").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-auto-allocate' build.id %}",
|
||||
{
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#btn-unallocate').on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-unallocate' build.id %}",
|
||||
{
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#btn-order-parts").click(function() {
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
build: {{ build.id }},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='attachments' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Attachments" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% include "attachment_table.html" with attachments=build.attachments.all %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableDragAndDrop(
|
||||
'#attachment-dropzone',
|
||||
'{% url "api-build-attachment-list" %}',
|
||||
{
|
||||
data: {
|
||||
build: {{ build.id }},
|
||||
},
|
||||
label: 'attachment',
|
||||
success: function(data, status, xhr) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Callback for creating a new attachment
|
||||
$('#new-attachment').click(function() {
|
||||
|
||||
constructForm('{% url "api-build-attachment-list" %}', {
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
build: {
|
||||
value: {{ build.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
method: 'POST',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
});
|
||||
});
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-build-attachment-list" %}',
|
||||
{
|
||||
filters: {
|
||||
build: {{ build.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/build/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
comment: {},
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
|
||||
constructForm(`/api/build/attachment/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@
|
||||
{{ block.super }}
|
||||
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Automatically Allocate Stock" %}</b><br>
|
||||
<strong>{% trans "Automatically Allocate Stock" %}</strong><br>
|
||||
{% trans "The following stock items will be allocated to the specified build output" %}
|
||||
</div>
|
||||
{% if allocations %}
|
||||
@@ -24,7 +24,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ item.stock_item.part.full_name }}<br>
|
||||
<i>{{ item.stock_item.part.description }}</i>
|
||||
<em>{{ item.stock_item.part.description }}</em>
|
||||
</td>
|
||||
<td>{% decimal item.quantity %}</td>
|
||||
<td>{{ item.stock_item.location }}</td>
|
||||
|
||||
@@ -111,8 +111,8 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<li><a href='#' id='build-cancel'><span class='fas fa-times-circle icon-red'></span> {% trans "Cancel Build" %}</a></li>
|
||||
{% endif %}
|
||||
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
|
||||
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build"% }</a>
|
||||
{% endif %}
|
||||
<li><a href='#' id='build-delete'><span class='fas fa-trash-alt'></span> {% trans "Delete Build" %}</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -126,7 +126,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Part" %}</td>
|
||||
<td><a href="{% url 'part-detail' build.part.id %}">{{ build.part.full_name }}</a></td>
|
||||
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
@@ -196,10 +196,7 @@ src="{% static 'img/blank_image.png' %}"
|
||||
});
|
||||
|
||||
$("#build-edit").click(function () {
|
||||
launchModalForm("{% url 'build-edit' build.id %}",
|
||||
{
|
||||
reload: true
|
||||
});
|
||||
editBuildOrder({{ build.pk }});
|
||||
});
|
||||
|
||||
$("#build-cancel").click(function() {
|
||||
@@ -241,4 +238,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
);
|
||||
});
|
||||
|
||||
attachNavCallbacks({
|
||||
name: 'buildorder',
|
||||
default: 'details'
|
||||
});
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,40 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab="children" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Child Build Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid float-right'>
|
||||
<div class='filter-list' id='filter-list-sub-build'>
|
||||
<!-- Empty div for filters -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#button-toolbar'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
loadBuildTable($('#sub-build-table'), {
|
||||
url: '{% url "api-build-list" %}',
|
||||
filterTarget: "#filter-list-sub-build",
|
||||
params: {
|
||||
ancestor: {{ build.pk }},
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,103 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='output' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_panels %}
|
||||
|
||||
{% if not build.is_complete %}
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Incomplete Build Outputs" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if build.active %}
|
||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if build.incomplete_outputs %}
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
{% for item in build.incomplete_outputs %}
|
||||
{% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<b>{% trans "Create a new build output" %}</b><br>
|
||||
{% trans "No incomplete build outputs remain." %}<br>
|
||||
{% trans "Create a new build output using the button above" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Completed Build Outputs" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
{% include "stock_table.html" with read_only=True %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#btn-create-output').click(function() {
|
||||
launchModalForm('{% url "build-output-create" build.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
loadStockTable($("#stock-table"), {
|
||||
params: {
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
build: {{ build.id }},
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
});
|
||||
|
||||
var buildInfo = {
|
||||
pk: {{ build.pk }},
|
||||
quantity: {{ build.quantity }},
|
||||
completed: {{ build.completed }},
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% for item in build.incomplete_outputs %}
|
||||
// Get the build output as a javascript object
|
||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||
{
|
||||
success: function(response) {
|
||||
loadBuildOutputAllocationTable(buildInfo, response);
|
||||
}
|
||||
}
|
||||
);
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
<b>{% trans "Build Order is incomplete" %}</b><br>
|
||||
<strong>{% trans "Build Order is incomplete" %}</strong><br>
|
||||
<ul>
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<li>{% trans "Incompleted build outputs remain" %}</li>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
{% if output %}
|
||||
<p>
|
||||
{% blocktrans %}The allocated stock will be installed into the following build output:<br><i>{{output}}</i>{% endblocktrans %}
|
||||
{% blocktrans %}The allocated stock will be installed into the following build output:<br><em>{{output}}</em>{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -2,142 +2,448 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load status_codes %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='details' %}
|
||||
{% include "build/navbar.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Build Details" %}
|
||||
{% endblock %}
|
||||
{% block page_content %}
|
||||
|
||||
{% block details %}
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ build.title }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Part" %}</td>
|
||||
<td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Stock Source" %}</td>
|
||||
<td>
|
||||
{% if build.take_from %}
|
||||
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>{% include "clip.html"%}
|
||||
{% else %}
|
||||
<i>{% trans "Stock can be taken from any available location." %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Destination" %}</td>
|
||||
<td>
|
||||
{% if build.destination %}
|
||||
<a href="{% url 'stock-location-detail' build.destination.id %}">
|
||||
{{ build.destination }}
|
||||
</a>{% include "clip.html"%}
|
||||
{% else %}
|
||||
<i>{% trans "Destination location not specified" %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Status" %}</td>
|
||||
<td>{% build_status_label build.status %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-spinner'></span></td>
|
||||
<td>{% trans "Progress" %}</td>
|
||||
<td>{{ build.completed }} / {{ build.quantity }}</td>
|
||||
</tr>
|
||||
{% if build.batch %}
|
||||
<tr>
|
||||
<td><span class='fas fa-layer-group'></span></td>
|
||||
<td>{% trans "Batch" %}</td>
|
||||
<td>{{ build.batch }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.parent %}
|
||||
<tr>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Parent Build" %}</td>
|
||||
<td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.sales_order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-dolly'></span></td>
|
||||
<td>{% trans "Sales Order" %}</td>
|
||||
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span></td>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ build.link }}">{{ build.link }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.issued_by %}
|
||||
<tr>
|
||||
<td><span class='fas fa-user'></span></td>
|
||||
<td>{% trans "Issued By" %}</td>
|
||||
<td>{{ build.issued_by }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.responsible %}
|
||||
<tr>
|
||||
<td><span class='fas fa-users'></span></td>
|
||||
<td>{% trans "Responsible" %}</td>
|
||||
<td>{{ build.responsible }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-details'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Build Details" %}</h4>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Created" %}</td>
|
||||
<td>{{ build.creation_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Target Date" %}</td>
|
||||
{% if build.target_date %}
|
||||
<td>
|
||||
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td><i>{% trans "No target date set" %}</i></td>
|
||||
<div class='panel-content'>
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ build.title }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Part" %}</td>
|
||||
<td><a href="{% url 'part-detail' build.part.id %}?display=build-orders">{{ build.part.full_name }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Stock Source" %}</td>
|
||||
<td>
|
||||
{% if build.take_from %}
|
||||
<a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a>{% include "clip.html"%}
|
||||
{% else %}
|
||||
<em>{% trans "Stock can be taken from any available location." %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Destination" %}</td>
|
||||
<td>
|
||||
{% if build.destination %}
|
||||
<a href="{% url 'stock-location-detail' build.destination.id %}">
|
||||
{{ build.destination }}
|
||||
</a>{% include "clip.html"%}
|
||||
{% else %}
|
||||
<em>{% trans "Destination location not specified" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Status" %}</td>
|
||||
<td>{% build_status_label build.status %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-spinner'></span></td>
|
||||
<td>{% trans "Progress" %}</td>
|
||||
<td>{{ build.completed }} / {{ build.quantity }}</td>
|
||||
</tr>
|
||||
{% if build.batch %}
|
||||
<tr>
|
||||
<td><span class='fas fa-layer-group'></span></td>
|
||||
<td>{% trans "Batch" %}</td>
|
||||
<td>{{ build.batch }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Completed" %}</td>
|
||||
{% if build.completion_date %}
|
||||
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
|
||||
{% else %}
|
||||
<td><i>{% trans "Build not complete" %}</i></td>
|
||||
{% if build.parent %}
|
||||
<tr>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Parent Build" %}</td>
|
||||
<td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
{% if build.sales_order %}
|
||||
<tr>
|
||||
<td><span class='fas fa-dolly'></span></td>
|
||||
<td>{% trans "Sales Order" %}</td>
|
||||
<td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span></td>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ build.link }}">{{ build.link }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.issued_by %}
|
||||
<tr>
|
||||
<td><span class='fas fa-user'></span></td>
|
||||
<td>{% trans "Issued By" %}</td>
|
||||
<td>{{ build.issued_by }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if build.responsible %}
|
||||
<tr>
|
||||
<td><span class='fas fa-users'></span></td>
|
||||
<td>{% trans "Responsible" %}</td>
|
||||
<td>{{ build.responsible }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Created" %}</td>
|
||||
<td>{{ build.creation_date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Target Date" %}</td>
|
||||
{% if build.target_date %}
|
||||
<td>
|
||||
{{ build.target_date }}{% if build.is_overdue %} <span class='fas fa-calendar-times icon-red'></span>{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td><em>{% trans "No target date set" %}</em></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Completed" %}</td>
|
||||
{% if build.completion_date %}
|
||||
<td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td>
|
||||
{% else %}
|
||||
<td><em>{% trans "Build not complete" %}</em></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-children'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Child Build Orders" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='child-button-toolbar'>
|
||||
<div class='button-toolbar container-fluid float-right'>
|
||||
<div class='filter-list' id='filter-list-sub-build'>
|
||||
<!-- Empty div for filters -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#child-button-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-allocate'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Allocate Stock to Build" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if build.has_untracked_bom_items %}
|
||||
{% if build.active %}
|
||||
<div class='btn-group' role='group'>
|
||||
<button class='btn btn-success' type='button' id='btn-auto-allocate' title='{% trans "Allocate stock to build" %}'>
|
||||
<span class='fas fa-magic'></span> {% trans "Auto Allocate" %}
|
||||
</button>
|
||||
<button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'>
|
||||
<span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %}
|
||||
</button>
|
||||
<!--
|
||||
<button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
|
||||
</button>
|
||||
-->
|
||||
</div>
|
||||
{% if build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Untracked stock has been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<table class='table table-striped table-condensed' id='allocation-table-untracked'></table>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This Build Order does not have any associated untracked BOM items" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-outputs'>
|
||||
{% if not build.is_complete %}
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Incomplete Build Outputs" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if build.active %}
|
||||
<button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Create New Output" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if build.incomplete_outputs %}
|
||||
<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true">
|
||||
{% for item in build.incomplete_outputs %}
|
||||
{% include "build/allocation_card.html" with item=item tracked_items=build.has_tracked_bom_items %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
<strong>{% trans "Create a new build output" %}</strong><br>
|
||||
{% trans "No incomplete build outputs remain." %}<br>
|
||||
{% trans "Create a new build output using the button above" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Completed Build Outputs" %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
{% include "stock_table.html" with read_only=True prefix="build-" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-attachments'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Attachments" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "attachment_table.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-notes'>
|
||||
<div class='panel-heading'>
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h4>{% trans "Build Notes" %}</h4>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<div class='btn-group float-right'>
|
||||
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-small btn-default'>
|
||||
<span class='fas fa-edit'>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if build.notes %}
|
||||
{{ build.notes | markdownify }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#btn-create-output').click(function() {
|
||||
launchModalForm('{% url "build-output-create" build.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
loadStockTable($("#build-stock-table"), {
|
||||
params: {
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
build: {{ build.id }},
|
||||
is_building: false,
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
});
|
||||
|
||||
var buildInfo = {
|
||||
pk: {{ build.pk }},
|
||||
quantity: {{ build.quantity }},
|
||||
completed: {{ build.completed }},
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% for item in build.incomplete_outputs %}
|
||||
// Get the build output as a javascript object
|
||||
inventreeGet('{% url 'api-stock-detail' item.pk %}', {},
|
||||
{
|
||||
success: function(response) {
|
||||
loadBuildOutputAllocationTable(buildInfo, response);
|
||||
}
|
||||
}
|
||||
);
|
||||
{% endfor %}
|
||||
|
||||
loadBuildTable($('#sub-build-table'), {
|
||||
url: '{% url "api-build-list" %}',
|
||||
filterTarget: "#filter-list-sub-build",
|
||||
params: {
|
||||
ancestor: {{ build.pk }},
|
||||
}
|
||||
});
|
||||
|
||||
enableDragAndDrop(
|
||||
'#attachment-dropzone',
|
||||
'{% url "api-build-attachment-list" %}',
|
||||
{
|
||||
data: {
|
||||
build: {{ build.id }},
|
||||
},
|
||||
label: 'attachment',
|
||||
success: function(data, status, xhr) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Callback for creating a new attachment
|
||||
$('#new-attachment').click(function() {
|
||||
|
||||
constructForm('{% url "api-build-attachment-list" %}', {
|
||||
fields: {
|
||||
attachment: {},
|
||||
comment: {},
|
||||
build: {
|
||||
value: {{ build.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
method: 'POST',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Add Attachment" %}',
|
||||
});
|
||||
});
|
||||
|
||||
loadAttachmentTable(
|
||||
'{% url "api-build-attachment-list" %}',
|
||||
{
|
||||
filters: {
|
||||
build: {{ build.pk }},
|
||||
},
|
||||
onEdit: function(pk) {
|
||||
var url = `/api/build/attachment/${pk}/`;
|
||||
|
||||
constructForm(url, {
|
||||
fields: {
|
||||
filename: {},
|
||||
comment: {},
|
||||
},
|
||||
onSuccess: reloadAttachmentTable,
|
||||
title: '{% trans "Edit Attachment" %}',
|
||||
});
|
||||
},
|
||||
onDelete: function(pk) {
|
||||
|
||||
constructForm(`/api/build/attachment/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
confirmMessage: '{% trans "Confirm Delete Operation" %}',
|
||||
title: '{% trans "Delete Attachment" %}',
|
||||
onSuccess: reloadAttachmentTable,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$('#edit-notes').click(function() {
|
||||
constructForm('{% url "api-build-detail" build.pk %}', {
|
||||
fields: {
|
||||
notes: {
|
||||
multiline: true,
|
||||
}
|
||||
},
|
||||
title: '{% trans "Edit Notes" %}',
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
var buildInfo = {
|
||||
pk: {{ build.pk }},
|
||||
quantity: {{ build.quantity }},
|
||||
completed: {{ build.completed }},
|
||||
part: {{ build.part.pk }},
|
||||
};
|
||||
|
||||
{% if build.has_untracked_bom_items %}
|
||||
// Load allocation table for un-tracked parts
|
||||
loadBuildOutputAllocationTable(buildInfo, null);
|
||||
{% endif %}
|
||||
|
||||
function reloadTable() {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
{% if build.active %}
|
||||
$("#btn-auto-allocate").on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-auto-allocate' build.id %}",
|
||||
{
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#btn-unallocate').on('click', function() {
|
||||
launchModalForm(
|
||||
"{% url 'build-unallocate' build.id %}",
|
||||
{
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#btn-order-parts").click(function() {
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
build: {{ build.id }},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
success: function(response) {
|
||||
var prefix = '{% settings_value "BUILDORDER_REFERENCE_PREFIX" %}';
|
||||
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
|
||||
|
||||
for (var idx = 0; idx < response.length; idx++) {
|
||||
|
||||
|
||||
@@ -9,46 +9,45 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "details" %}active{% endif %}' title='{% trans "Build Order Details" %}'>
|
||||
<a href='{% url "build-detail" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Build Order Details" %}'>
|
||||
<a href='#' id='select-details' class='nav-toggle'>
|
||||
<span class='fas fa-info-circle sidebar-icon'></span>
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if build.active %}
|
||||
|
||||
<li class='list-group-item {% if tab == "allocate" %}active{% endif %}' title='{% trans "Allocate Stock" %}'>
|
||||
<a href='{% url "build-allocate" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Allocate Stock" %}'>
|
||||
<a href='#' id='select-allocate' class='nav-toggle'>
|
||||
<span class='fas fa-tools sidebar-icon'></span>
|
||||
{% trans "Allocate Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class='list-group-item {% if tab == "output" %}active{% endif %}' title='{% trans "Build Outputs" %}'>
|
||||
<a href='{% url "build-output" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Build Outputs" %}'>
|
||||
<a href='#' id='select-outputs' class='nav-toggle'>
|
||||
<span class='fas fa-box sidebar-icon'></span>
|
||||
{% trans "Build Outputs" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "children" %}active{% endif %}' title='{% trans "Child Build Orders" %}'>
|
||||
<a href='{% url "build-children" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Child Build Orders" %}'>
|
||||
<a href='#' id='select-children' class='nav-toggle'>
|
||||
<span class='fas fa-sitemap sidebar-icon'></span>
|
||||
{% trans "Child Builds" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "attachments" %}active{% endif %}' title='{% trans "Attachments" %}'>
|
||||
<a href='{% url "build-attachments" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Attachments" %}'>
|
||||
<a href='#' id='select-attachments' class='nav-toggle'>
|
||||
<span class='fas fa-paperclip sidebar-icon'></span>
|
||||
{% trans "Attachments" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "notes" %}active{% endif %}' title='{% trans "Build Order Notes" %}'>
|
||||
<a href='{% url "build-notes" build.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Build Order Notes" %}'>
|
||||
<a href='#' id='select-notes' class='nav-toggle'>
|
||||
<span class='fas fa-clipboard sidebar-icon'></span>
|
||||
{% trans "Notes" %}
|
||||
</a>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends "build/build_base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "build/navbar.html" with tab='notes' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Build Notes" %}
|
||||
{% if roles.build.change and not editing %}
|
||||
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if editing %}
|
||||
<hr>
|
||||
<form method='POST'>
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form }}
|
||||
<hr>
|
||||
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
{{ form.media }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{{ build.notes | markdownify }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if editing %}
|
||||
{% else %}
|
||||
$("#edit-notes").click(function() {
|
||||
location.href = "{% url 'build-notes' build.id %}?edit=1";
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -5,11 +5,13 @@ from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.utils import IntegrityError
|
||||
|
||||
from build.models import Build, BuildItem
|
||||
from stock.models import StockItem
|
||||
from part.models import Part, BomItem
|
||||
from InvenTree import status_codes as status
|
||||
|
||||
from build.models import Build, BuildItem, get_next_build_number
|
||||
from part.models import Part, BomItem
|
||||
from stock.models import StockItem
|
||||
from stock.tasks import delete_old_stock_items
|
||||
|
||||
|
||||
class BuildTest(TestCase):
|
||||
"""
|
||||
@@ -80,8 +82,14 @@ class BuildTest(TestCase):
|
||||
quantity=2
|
||||
)
|
||||
|
||||
ref = get_next_build_number()
|
||||
|
||||
if ref is None:
|
||||
ref = "0001"
|
||||
|
||||
# Create a "Build" object to make 10x objects
|
||||
self.build = Build.objects.create(
|
||||
reference=ref,
|
||||
title="This is a build",
|
||||
part=self.assembly,
|
||||
quantity=10
|
||||
@@ -345,6 +353,11 @@ class BuildTest(TestCase):
|
||||
# the original BuildItem objects should have been deleted!
|
||||
self.assertEqual(BuildItem.objects.count(), 0)
|
||||
|
||||
self.assertEqual(StockItem.objects.count(), 8)
|
||||
|
||||
# Clean up old stock items
|
||||
delete_old_stock_items()
|
||||
|
||||
# New stock items should have been created!
|
||||
self.assertEqual(StockItem.objects.count(), 7)
|
||||
|
||||
|
||||
@@ -252,36 +252,6 @@ class TestBuildViews(TestCase):
|
||||
|
||||
self.assertIn(build.title, content)
|
||||
|
||||
def test_build_create(self):
|
||||
""" Test the build creation view (ajax form) """
|
||||
|
||||
url = reverse('build-create')
|
||||
|
||||
# Create build without specifying part
|
||||
response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Create build with valid part
|
||||
response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Create build with invalid part
|
||||
response = self.client.get(url, {'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_allocate(self):
|
||||
""" Test the part allocation view for a Build """
|
||||
|
||||
url = reverse('build-allocate', args=(1,))
|
||||
|
||||
# Get the page normally
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Get the page in editing mode
|
||||
response = self.client.get(url, {'edit': 1})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_item_create(self):
|
||||
""" Test the BuildItem creation view (ajax form) """
|
||||
|
||||
|
||||
@@ -7,37 +7,27 @@ from django.conf.urls import url, include
|
||||
from . import views
|
||||
|
||||
build_detail_urls = [
|
||||
url(r'^edit/', views.BuildUpdate.as_view(), name='build-edit'),
|
||||
url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'),
|
||||
url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'),
|
||||
url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'),
|
||||
url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'),
|
||||
url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'),
|
||||
url(r'^complete-output/?', views.BuildOutputComplete.as_view(), name='build-output-complete'),
|
||||
url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
|
||||
url(r'^complete-output/', views.BuildOutputComplete.as_view(), name='build-output-complete'),
|
||||
url(r'^auto-allocate/', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'),
|
||||
url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'),
|
||||
url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'),
|
||||
|
||||
url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'),
|
||||
|
||||
url(r'^children/', views.BuildDetail.as_view(template_name='build/build_children.html'), name='build-children'),
|
||||
url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'),
|
||||
url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'),
|
||||
|
||||
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||
]
|
||||
|
||||
build_urls = [
|
||||
url(r'item/', include([
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url('^edit/?', views.BuildItemEdit.as_view(), name='build-item-edit'),
|
||||
url('^delete/?', views.BuildItemDelete.as_view(), name='build-item-delete'),
|
||||
url('^edit/', views.BuildItemEdit.as_view(), name='build-item-edit'),
|
||||
url('^delete/', views.BuildItemDelete.as_view(), name='build-item-delete'),
|
||||
])),
|
||||
url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'),
|
||||
])),
|
||||
|
||||
url(r'new/', views.BuildCreate.as_view(), name='build-create'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', include(build_detail_urls)),
|
||||
|
||||
url(r'.*$', views.BuildIndex.as_view(), name='build-index'),
|
||||
|
||||
@@ -7,9 +7,8 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from django.views.generic import DetailView, ListView
|
||||
from django.forms import HiddenInput
|
||||
from django.urls import reverse
|
||||
|
||||
from part.models import Part
|
||||
from .models import Build, BuildItem
|
||||
@@ -593,31 +592,6 @@ class BuildOutputComplete(AjaxUpdateView):
|
||||
}
|
||||
|
||||
|
||||
class BuildNotes(InvenTreeRoleMixin, UpdateView):
|
||||
""" View for editing the 'notes' field of a Build object.
|
||||
"""
|
||||
|
||||
context_object_name = 'build'
|
||||
template_name = 'build/notes.html'
|
||||
model = Build
|
||||
|
||||
# Override the default permission role for this View
|
||||
role_required = 'build.view'
|
||||
|
||||
fields = ['notes']
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('build-notes', kwargs={'pk': self.get_object().id})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx['editing'] = str2bool(self.request.GET.get('edit', ''))
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildDetail(InvenTreeRoleMixin, DetailView):
|
||||
""" Detail view of a single Build object. """
|
||||
|
||||
@@ -635,156 +609,15 @@ class BuildDetail(InvenTreeRoleMixin, DetailView):
|
||||
ctx['BuildStatus'] = BuildStatus
|
||||
ctx['sub_build_count'] = build.sub_build_count()
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildAllocate(InvenTreeRoleMixin, DetailView):
|
||||
""" View for allocating parts to a Build """
|
||||
model = Build
|
||||
context_object_name = 'build'
|
||||
template_name = 'build/allocate.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Provide extra context information for the Build allocation page """
|
||||
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
|
||||
build = self.get_object()
|
||||
part = build.part
|
||||
bom_items = build.bom_items
|
||||
|
||||
context['part'] = part
|
||||
context['bom_items'] = bom_items
|
||||
context['has_tracked_bom_items'] = build.has_tracked_bom_items()
|
||||
context['has_untracked_bom_items'] = build.has_untracked_bom_items()
|
||||
context['BuildStatus'] = BuildStatus
|
||||
ctx['part'] = part
|
||||
ctx['bom_items'] = bom_items
|
||||
ctx['has_tracked_bom_items'] = build.has_tracked_bom_items()
|
||||
ctx['has_untracked_bom_items'] = build.has_untracked_bom_items()
|
||||
|
||||
context['bom_price'] = build.part.get_price_info(build.quantity, buy=False)
|
||||
|
||||
if str2bool(self.request.GET.get('edit', None)):
|
||||
context['editing'] = True
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BuildCreate(AjaxCreateView):
|
||||
"""
|
||||
View to create a new Build object
|
||||
"""
|
||||
|
||||
model = Build
|
||||
context_object_name = 'build'
|
||||
form_class = forms.EditBuildForm
|
||||
ajax_form_title = _('New Build Order')
|
||||
ajax_template_name = 'modal_form.html'
|
||||
|
||||
def get_form(self):
|
||||
form = super().get_form()
|
||||
|
||||
if form['part'].value():
|
||||
form.fields['part'].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
def get_initial(self):
|
||||
""" Get initial parameters for Build creation.
|
||||
|
||||
If 'part' is specified in the GET query, initialize the Build with the specified Part
|
||||
"""
|
||||
|
||||
initials = super(BuildCreate, self).get_initial().copy()
|
||||
|
||||
initials['parent'] = self.request.GET.get('parent', None)
|
||||
|
||||
# User has provided a SalesOrder ID
|
||||
initials['sales_order'] = self.request.GET.get('sales_order', None)
|
||||
|
||||
initials['quantity'] = self.request.GET.get('quantity', 1)
|
||||
|
||||
part = self.request.GET.get('part', None)
|
||||
|
||||
if part:
|
||||
|
||||
try:
|
||||
part = Part.objects.get(pk=part)
|
||||
# User has provided a Part ID
|
||||
initials['part'] = part
|
||||
initials['destination'] = part.get_default_location()
|
||||
|
||||
to_order = part.quantity_to_order
|
||||
|
||||
if to_order < 1:
|
||||
to_order = 1
|
||||
|
||||
initials['quantity'] = to_order
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
initials['reference'] = Build.getNextBuildNumber()
|
||||
|
||||
# Pre-fill the issued_by user
|
||||
initials['issued_by'] = self.request.user
|
||||
|
||||
return initials
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'success': _('Created new build'),
|
||||
}
|
||||
|
||||
def validate(self, build, form, **kwargs):
|
||||
"""
|
||||
Perform extra form validation.
|
||||
|
||||
- If part is trackable, check that either batch or serial numbers are calculated
|
||||
|
||||
By this point form.is_valid() has been executed
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BuildUpdate(AjaxUpdateView):
|
||||
""" View for editing a Build object """
|
||||
|
||||
model = Build
|
||||
form_class = forms.EditBuildForm
|
||||
context_object_name = 'build'
|
||||
ajax_form_title = _('Edit Build Order Details')
|
||||
ajax_template_name = 'modal_form.html'
|
||||
|
||||
def get_form(self):
|
||||
|
||||
form = super().get_form()
|
||||
|
||||
build = self.get_object()
|
||||
|
||||
# Fields which are included in the form, but hidden
|
||||
hidden = [
|
||||
'parent',
|
||||
'sales_order',
|
||||
]
|
||||
|
||||
if build.is_complete:
|
||||
# Fields which cannot be edited once the build has been completed
|
||||
|
||||
hidden += [
|
||||
'part',
|
||||
'quantity',
|
||||
'batch',
|
||||
'take_from',
|
||||
'destination',
|
||||
]
|
||||
|
||||
for field in hidden:
|
||||
form.fields[field].widget = HiddenInput()
|
||||
|
||||
return form
|
||||
|
||||
def get_data(self):
|
||||
return {
|
||||
'info': _('Edited build'),
|
||||
}
|
||||
return ctx
|
||||
|
||||
|
||||
class BuildDelete(AjaxDeleteView):
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
from .models import InvenTreeSetting
|
||||
from .models import InvenTreeSetting, InvenTreeUserSetting
|
||||
|
||||
|
||||
class SettingsAdmin(ImportExportModelAdmin):
|
||||
@@ -13,4 +13,10 @@ class SettingsAdmin(ImportExportModelAdmin):
|
||||
list_display = ('key', 'value')
|
||||
|
||||
|
||||
class UserSettingsAdmin(ImportExportModelAdmin):
|
||||
|
||||
list_display = ('key', 'value', 'user', )
|
||||
|
||||
|
||||
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
||||
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
|
||||
|
||||
@@ -53,17 +53,20 @@ class FileManager:
|
||||
|
||||
ext = os.path.splitext(file.name)[-1].lower().replace('.', '')
|
||||
|
||||
if ext in ['csv', 'tsv', ]:
|
||||
# These file formats need string decoding
|
||||
raw_data = file.read().decode('utf-8')
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
||||
raw_data = file.read()
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
else:
|
||||
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
||||
try:
|
||||
if ext in ['csv', 'tsv', ]:
|
||||
# These file formats need string decoding
|
||||
raw_data = file.read().decode('utf-8')
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
elif ext in ['xls', 'xlsx', 'json', 'yaml', ]:
|
||||
raw_data = file.read()
|
||||
# Reset stream position to beginning of file
|
||||
file.seek(0)
|
||||
else:
|
||||
raise ValidationError(_(f'Unsupported file format: {ext.upper()}'))
|
||||
except UnicodeEncodeError:
|
||||
raise ValidationError(_('Error reading file (invalid encoding)'))
|
||||
|
||||
try:
|
||||
cleaned_data = tablib.Dataset().load(raw_data, format=ext)
|
||||
@@ -99,13 +102,18 @@ class FileManager:
|
||||
self.update_headers()
|
||||
|
||||
def guess_header(self, header, threshold=80):
|
||||
""" Try to match a header (from the file) to a list of known headers
|
||||
"""
|
||||
Try to match a header (from the file) to a list of known headers
|
||||
|
||||
Args:
|
||||
header - Header name to look for
|
||||
threshold - Match threshold for fuzzy search
|
||||
"""
|
||||
|
||||
# Replace null values with empty string
|
||||
if header is None:
|
||||
header = ''
|
||||
|
||||
# Try for an exact match
|
||||
for h in self.HEADERS:
|
||||
if h == header:
|
||||
|
||||
33
InvenTree/common/migrations/0011_auto_20210722_2114.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-22 21:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('common', '0010_migrate_currency_setting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InvenTreeUserSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
|
||||
('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)),
|
||||
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'InvenTree User Setting',
|
||||
'verbose_name_plural': 'InvenTree User Settings',
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='inventreeusersetting',
|
||||
constraint=models.UniqueConstraint(fields=('key', 'user'), name='unique key and user'),
|
||||
),
|
||||
]
|
||||
@@ -8,15 +8,19 @@ from __future__ import unicode_literals
|
||||
from moneyed import CURRENCIES
|
||||
from django.conf import settings
|
||||
|
||||
import common.models
|
||||
|
||||
|
||||
def currency_code_default():
|
||||
"""
|
||||
Returns the default currency code (or USD if not specified)
|
||||
"""
|
||||
from django.db.utils import ProgrammingError
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
code = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
try:
|
||||
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
except ProgrammingError:
|
||||
# database is not initialized yet
|
||||
code = ''
|
||||
|
||||
if code not in CURRENCIES:
|
||||
code = 'USD'
|
||||
@@ -42,5 +46,6 @@ def stock_expiry_enabled():
|
||||
"""
|
||||
Returns True if the stock expiry feature is enabled
|
||||
"""
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
return common.models.InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
|
||||
return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY')
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
{{ block.super }}
|
||||
<!--
|
||||
<p>
|
||||
<b>{{ name }}</b><br>
|
||||
<strong>{{ name }}</strong><br>
|
||||
{{ description }}<br>
|
||||
<i>{% trans "Current value" %}: {{ value }}</i>
|
||||
<em>{% trans "Current value" %}: {{ value }}</em>
|
||||
</p>
|
||||
-->
|
||||
{% endblock %}
|
||||
@@ -45,11 +45,28 @@ class SettingEdit(AjaxUpdateView):
|
||||
|
||||
ctx['key'] = setting.key
|
||||
ctx['value'] = setting.value
|
||||
ctx['name'] = models.InvenTreeSetting.get_setting_name(setting.key)
|
||||
ctx['description'] = models.InvenTreeSetting.get_setting_description(setting.key)
|
||||
ctx['name'] = self.model.get_setting_name(setting.key)
|
||||
ctx['description'] = self.model.get_setting_description(setting.key)
|
||||
|
||||
return ctx
|
||||
|
||||
def get_data(self):
|
||||
"""
|
||||
Custom data to return to the client after POST success
|
||||
"""
|
||||
|
||||
data = {}
|
||||
|
||||
setting = self.get_object()
|
||||
|
||||
data['pk'] = setting.pk
|
||||
data['key'] = setting.key
|
||||
data['value'] = setting.value
|
||||
data['is_bool'] = setting.is_bool()
|
||||
data['is_int'] = setting.is_int()
|
||||
|
||||
return data
|
||||
|
||||
def get_form(self):
|
||||
"""
|
||||
Override default get_form behaviour
|
||||
@@ -69,12 +86,12 @@ class SettingEdit(AjaxUpdateView):
|
||||
self.object.value = str2bool(setting.value)
|
||||
form.fields['value'].value = str2bool(setting.value)
|
||||
|
||||
name = models.InvenTreeSetting.get_setting_name(setting.key)
|
||||
name = self.model.get_setting_name(setting.key)
|
||||
|
||||
if name:
|
||||
form.fields['value'].label = name
|
||||
|
||||
description = models.InvenTreeSetting.get_setting_description(setting.key)
|
||||
description = self.model.get_setting_description(setting.key)
|
||||
|
||||
if description:
|
||||
form.fields['value'].help_text = description
|
||||
@@ -111,6 +128,18 @@ class SettingEdit(AjaxUpdateView):
|
||||
form.add_error('value', _('Supplied value must be a boolean'))
|
||||
|
||||
|
||||
class UserSettingEdit(SettingEdit):
|
||||
"""
|
||||
View for editing an InvenTree key:value user settings object,
|
||||
(or creating it if the key does not already exist)
|
||||
"""
|
||||
|
||||
model = models.InvenTreeUserSetting
|
||||
ajax_form_title = _('Change User Setting')
|
||||
form_class = forms.SettingEditForm
|
||||
ajax_template_name = "common/edit_setting.html"
|
||||
|
||||
|
||||
class MultiStepFormView(SessionWizardView):
|
||||
""" Setup basic methods of multi-step form
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ Provides a JSON API for the Company app
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as rest_filters
|
||||
|
||||
from rest_framework import filters
|
||||
from rest_framework import generics
|
||||
|
||||
@@ -84,6 +86,23 @@ class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return queryset
|
||||
|
||||
|
||||
class ManufacturerPartFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom API filters for the ManufacturerPart list endpoint.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPart
|
||||
fields = [
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
'part',
|
||||
]
|
||||
|
||||
# Filter by 'active' status of linked part
|
||||
active = rest_filters.BooleanFilter(field_name='part__active')
|
||||
|
||||
|
||||
class ManufacturerPartList(generics.ListCreateAPIView):
|
||||
""" API endpoint for list view of ManufacturerPart object
|
||||
|
||||
@@ -98,6 +117,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
||||
)
|
||||
|
||||
serializer_class = ManufacturerPartSerializer
|
||||
filterset_class = ManufacturerPartFilter
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
@@ -115,49 +135,17 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
||||
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering for the queryset.
|
||||
"""
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Filter by manufacturer
|
||||
manufacturer = params.get('manufacturer', None)
|
||||
|
||||
if manufacturer is not None:
|
||||
queryset = queryset.filter(manufacturer=manufacturer)
|
||||
|
||||
# Filter by parent part?
|
||||
part = params.get('part', None)
|
||||
|
||||
if part is not None:
|
||||
queryset = queryset.filter(part=part)
|
||||
|
||||
# Filter by 'active' status of the part?
|
||||
active = params.get('active', None)
|
||||
|
||||
if active is not None:
|
||||
active = str2bool(active)
|
||||
queryset = queryset.filter(part__active=active)
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'manufacturer__name',
|
||||
'description',
|
||||
'MPN',
|
||||
'part__IPN',
|
||||
'part__name',
|
||||
'part__description',
|
||||
]
|
||||
@@ -262,11 +250,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
||||
- POST: Create a new SupplierPart object
|
||||
"""
|
||||
|
||||
queryset = SupplierPart.objects.all().prefetch_related(
|
||||
'part',
|
||||
'supplier',
|
||||
'manufacturer_part__manufacturer',
|
||||
)
|
||||
queryset = SupplierPart.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -355,6 +339,7 @@ class SupplierPartList(generics.ListCreateAPIView):
|
||||
'manufacturer_part__manufacturer__name',
|
||||
'description',
|
||||
'manufacturer_part__MPN',
|
||||
'part__IPN',
|
||||
'part__name',
|
||||
'part__description',
|
||||
]
|
||||
|
||||
@@ -6,14 +6,12 @@ Django Forms for interacting with Company app
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.fields import InvenTreeMoneyField, RoundingDecimalFormField
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import django.forms
|
||||
|
||||
from .models import Company
|
||||
from .models import ManufacturerPart
|
||||
from .models import SupplierPart
|
||||
from .models import SupplierPriceBreak
|
||||
|
||||
|
||||
@@ -35,86 +33,6 @@ class CompanyImageDownloadForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class EditManufacturerPartForm(HelperForm):
|
||||
""" Form for editing a ManufacturerPart object """
|
||||
|
||||
field_prefix = {
|
||||
'link': 'fa-link',
|
||||
'MPN': 'fa-hashtag',
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = ManufacturerPart
|
||||
fields = [
|
||||
'part',
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
'description',
|
||||
'link',
|
||||
]
|
||||
|
||||
|
||||
class EditSupplierPartForm(HelperForm):
|
||||
""" Form for editing a SupplierPart object """
|
||||
|
||||
field_prefix = {
|
||||
'link': 'fa-link',
|
||||
'SKU': 'fa-hashtag',
|
||||
'note': 'fa-pencil-alt',
|
||||
}
|
||||
|
||||
single_pricing = InvenTreeMoneyField(
|
||||
label=_('Single Price'),
|
||||
help_text=_('Single quantity price'),
|
||||
decimal_places=4,
|
||||
max_digits=19,
|
||||
required=False,
|
||||
)
|
||||
|
||||
manufacturer = django.forms.ChoiceField(
|
||||
required=False,
|
||||
help_text=_('Select manufacturer'),
|
||||
choices=[],
|
||||
)
|
||||
|
||||
MPN = django.forms.CharField(
|
||||
required=False,
|
||||
help_text=_('Manufacturer Part Number'),
|
||||
max_length=100,
|
||||
label=_('MPN'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SupplierPart
|
||||
fields = [
|
||||
'part',
|
||||
'supplier',
|
||||
'SKU',
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
'description',
|
||||
'link',
|
||||
'note',
|
||||
'single_pricing',
|
||||
# 'base_cost',
|
||||
# 'multiple',
|
||||
'packaging',
|
||||
]
|
||||
|
||||
def get_manufacturer_choices(self):
|
||||
""" Returns tuples for all manufacturers """
|
||||
empty_choice = [('', '----------')]
|
||||
|
||||
manufacturers = [(manufacturer.id, manufacturer.name) for manufacturer in Company.objects.filter(is_manufacturer=True)]
|
||||
|
||||
return empty_choice + manufacturers
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['manufacturer'].choices = self.get_manufacturer_choices()
|
||||
|
||||
|
||||
class EditPriceBreakForm(HelperForm):
|
||||
""" Form for creating / editing a supplier price break """
|
||||
|
||||
|
||||
17
InvenTree/company/migrations/0041_alter_company_options.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.5 on 2021-10-04 20:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('company', '0040_alter_company_currency'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='company',
|
||||
options={'ordering': ['name'], 'verbose_name_plural': 'Companies'},
|
||||
),
|
||||
]
|
||||
@@ -10,8 +10,8 @@ import os
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.db import models
|
||||
from django.db.utils import IntegrityError
|
||||
from django.db.models import Sum, Q, UniqueConstraint
|
||||
|
||||
from django.apps import apps
|
||||
@@ -94,6 +94,7 @@ class Company(models.Model):
|
||||
constraints = [
|
||||
UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
|
||||
]
|
||||
verbose_name_plural = "Companies"
|
||||
|
||||
name = models.CharField(max_length=100, blank=False,
|
||||
help_text=_('Company name'),
|
||||
@@ -430,6 +431,22 @@ class ManufacturerPartParameter(models.Model):
|
||||
)
|
||||
|
||||
|
||||
class SupplierPartManager(models.Manager):
|
||||
""" Define custom SupplierPart objects manager
|
||||
|
||||
The main purpose of this manager is to improve database hit as the
|
||||
SupplierPart model involves A LOT of foreign keys lookups
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
# Always prefetch related models
|
||||
return super().get_queryset().prefetch_related(
|
||||
'part',
|
||||
'supplier',
|
||||
'manufacturer_part__manufacturer',
|
||||
)
|
||||
|
||||
|
||||
class SupplierPart(models.Model):
|
||||
""" Represents a unique part as provided by a Supplier
|
||||
Each SupplierPart is identified by a SKU (Supplier Part Number)
|
||||
@@ -450,6 +467,8 @@ class SupplierPart(models.Model):
|
||||
packaging: packaging that the part is supplied in, e.g. "Reel"
|
||||
"""
|
||||
|
||||
objects = SupplierPartManager()
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-supplier-part-list')
|
||||
@@ -457,56 +476,13 @@ class SupplierPart(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Overriding save method to process the linked ManufacturerPart
|
||||
"""
|
||||
|
||||
if 'manufacturer' in kwargs:
|
||||
manufacturer_id = kwargs.pop('manufacturer')
|
||||
|
||||
try:
|
||||
manufacturer = Company.objects.get(pk=int(manufacturer_id))
|
||||
except (ValueError, Company.DoesNotExist):
|
||||
manufacturer = None
|
||||
else:
|
||||
manufacturer = None
|
||||
if 'MPN' in kwargs:
|
||||
MPN = kwargs.pop('MPN')
|
||||
else:
|
||||
MPN = None
|
||||
|
||||
if manufacturer or MPN:
|
||||
if not self.manufacturer_part:
|
||||
# Create ManufacturerPart
|
||||
manufacturer_part = ManufacturerPart.create(part=self.part,
|
||||
manufacturer=manufacturer,
|
||||
mpn=MPN,
|
||||
description=self.description)
|
||||
self.manufacturer_part = manufacturer_part
|
||||
else:
|
||||
# Update ManufacturerPart (if ID exists)
|
||||
try:
|
||||
manufacturer_part_id = self.manufacturer_part.id
|
||||
except AttributeError:
|
||||
manufacturer_part_id = None
|
||||
|
||||
if manufacturer_part_id:
|
||||
try:
|
||||
(manufacturer_part, created) = ManufacturerPart.objects.update_or_create(part=self.part,
|
||||
manufacturer=manufacturer,
|
||||
MPN=MPN)
|
||||
except IntegrityError:
|
||||
manufacturer_part = None
|
||||
raise ValidationError(f'ManufacturerPart linked to {self.part} from manufacturer {manufacturer.name}'
|
||||
f'with part number {MPN} already exists!')
|
||||
|
||||
if manufacturer_part:
|
||||
self.manufacturer_part = manufacturer_part
|
||||
|
||||
self.clean()
|
||||
self.validate_unique()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
def api_instance_filters(self):
|
||||
|
||||
return {
|
||||
'manufacturer_part': {
|
||||
'part': self.part.pk
|
||||
}
|
||||
}
|
||||
|
||||
class Meta:
|
||||
unique_together = ('part', 'supplier', 'SKU')
|
||||
@@ -514,6 +490,46 @@ class SupplierPart(models.Model):
|
||||
# This model was moved from the 'Part' app
|
||||
db_table = 'part_supplierpart'
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
# Ensure that the linked manufacturer_part points to the same part!
|
||||
if self.manufacturer_part and self.part:
|
||||
|
||||
if not self.manufacturer_part.part == self.part:
|
||||
raise ValidationError({
|
||||
'manufacturer_part': _("Linked manufacturer part must reference the same base part"),
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Overriding save method to connect an existing ManufacturerPart """
|
||||
|
||||
manufacturer_part = None
|
||||
|
||||
if all(key in kwargs for key in ('manufacturer', 'MPN')):
|
||||
manufacturer_name = kwargs.pop('manufacturer')
|
||||
MPN = kwargs.pop('MPN')
|
||||
|
||||
# Retrieve manufacturer part
|
||||
try:
|
||||
manufacturer_part = ManufacturerPart.objects.get(manufacturer__name=manufacturer_name, MPN=MPN)
|
||||
except (ValueError, Company.DoesNotExist):
|
||||
# ManufacturerPart does not exist
|
||||
pass
|
||||
|
||||
if manufacturer_part:
|
||||
if not self.manufacturer_part:
|
||||
# Connect ManufacturerPart to SupplierPart
|
||||
self.manufacturer_part = manufacturer_part
|
||||
else:
|
||||
raise ValidationError(f'SupplierPart {self.__str__} is already linked to {self.manufacturer_part}')
|
||||
|
||||
self.clean()
|
||||
self.validate_unique()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||
related_name='supplier_parts',
|
||||
verbose_name=_('Base Part'),
|
||||
|
||||
@@ -96,7 +96,9 @@ class CompanySerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for ManufacturerPart object """
|
||||
"""
|
||||
Serializer for ManufacturerPart object
|
||||
"""
|
||||
|
||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||
|
||||
@@ -106,8 +108,8 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||
prettify = kwargs.pop('pretty', False)
|
||||
|
||||
super(ManufacturerPartSerializer, self).__init__(*args, **kwargs)
|
||||
@@ -202,28 +204,31 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
|
||||
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
|
||||
|
||||
manufacturer = serializers.PrimaryKeyRelatedField(source='manufacturer_part.manufacturer', read_only=True)
|
||||
manufacturer = serializers.CharField(read_only=True)
|
||||
|
||||
MPN = serializers.StringRelatedField(source='manufacturer_part.MPN')
|
||||
MPN = serializers.CharField(read_only=True)
|
||||
|
||||
manufacturer_part = ManufacturerPartSerializer(read_only=True)
|
||||
manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = SupplierPart
|
||||
fields = [
|
||||
'description',
|
||||
'link',
|
||||
'manufacturer',
|
||||
'manufacturer_detail',
|
||||
'manufacturer_part',
|
||||
'manufacturer_part_detail',
|
||||
'MPN',
|
||||
'note',
|
||||
'pk',
|
||||
'packaging',
|
||||
'part',
|
||||
'part_detail',
|
||||
'pretty_name',
|
||||
'SKU',
|
||||
'supplier',
|
||||
'supplier_detail',
|
||||
'SKU',
|
||||
'manufacturer',
|
||||
'MPN',
|
||||
'manufacturer_detail',
|
||||
'manufacturer_part',
|
||||
'description',
|
||||
'link',
|
||||
]
|
||||
|
||||
def create(self, validated_data):
|
||||
@@ -233,12 +238,12 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
supplier_part = super().create(validated_data)
|
||||
|
||||
# Get ManufacturerPart raw data (unvalidated)
|
||||
manufacturer_id = self.initial_data.get('manufacturer', None)
|
||||
manufacturer = self.initial_data.get('manufacturer', None)
|
||||
MPN = self.initial_data.get('MPN', None)
|
||||
|
||||
if manufacturer_id and MPN:
|
||||
if manufacturer and MPN:
|
||||
kwargs = {
|
||||
'manufacturer': manufacturer_id,
|
||||
'manufacturer': manufacturer,
|
||||
'MPN': MPN,
|
||||
}
|
||||
supplier_part.save(**kwargs)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "company/navbar.html" with tab="assigned" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Assigned Stock" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='filter-list' id='filter-list-stock'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='stock-table' data-toolbar='#button-toolbar'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadStockTable($("#stock-table"), {
|
||||
params: {
|
||||
customer: {{ company.id }},
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
},
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
filterKey: "customerstock",
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -71,6 +71,17 @@
|
||||
<td><a href="{{ company.website }}">{{ company.website }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "Currency" %}</td>
|
||||
<td>
|
||||
{% if company.currency %}
|
||||
{{ company.currency }}
|
||||
{% else %}
|
||||
<em>{% trans "Uses default currency" %}</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if company.address %}
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marked-alt'></span></td>
|
||||
@@ -99,6 +110,22 @@
|
||||
<td>{{ company.contact }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{%trans "Manufacturer" %}</td>
|
||||
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tie'></span></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
@@ -171,17 +198,16 @@
|
||||
);
|
||||
});
|
||||
|
||||
{% settings_value "INVENTREE_DOWNLOAD_FROM_URL" as allow_download %}
|
||||
if (global_settings.INVENTREE_DOWNLOAD_FROM_URL) {
|
||||
|
||||
{% if allow_download %}
|
||||
$('#company-image-url').click(function() {
|
||||
launchModalForm(
|
||||
'{% url "company-image-download" company.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
)
|
||||
});
|
||||
{% endif %}
|
||||
$('#company-image-url').click(function() {
|
||||
launchModalForm(
|
||||
'{% url "company-image-download" company.id %}',
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,79 +1,405 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='details' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Company Details" %}
|
||||
{% endblock %}
|
||||
{% block page_content %}
|
||||
|
||||
{% block details %}
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<col>
|
||||
<tr>
|
||||
<td><span class='fas fa-font'></span></td>
|
||||
<td>{% trans "Company Name" %}</td>
|
||||
<td>{{ company.name }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% if company.description %}
|
||||
<tr>
|
||||
<td><span class='fas fa-info'></span></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ company.description }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-globe'></span></td>
|
||||
<td>{% trans "Website" %}</td>
|
||||
<td>
|
||||
{% if company.website %}<a href='{{ company.website }}'>{{ company.website }}</a>{% include "clip.html"%}
|
||||
{% else %}<i>{% trans "No website specified" %}</i>
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-supplier-parts'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Supplier Parts" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if roles.purchase_order.change %}
|
||||
<div id='supplier-part-button-toolbar'>
|
||||
<div class='button-toolbar container-fluid'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class="btn btn-success" id='supplier-part-create' title='{% trans "Create new supplier part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-dollar-sign'></span></td>
|
||||
<td>{% trans "Currency" %}</td>
|
||||
<td>
|
||||
{% if company.currency %}{{ company.currency }}
|
||||
{% else %}<i>{% trans "Uses default currency" %}</i>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<div class='btn-group'>
|
||||
<button class="btn btn-primary dropdown-toggle" id='supplier-table-options' type="button" data-toggle="dropdown">{% trans "Options" %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a href='#' id='multi-supplier-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.delete %}
|
||||
<li><a href='#' id='multi-supplier-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-supplier-part'>
|
||||
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed' id='supplier-part-table' data-toolbar='#supplier-part-button-toolbar'>
|
||||
</table>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<table class='table table-striped'>
|
||||
<col width='25'>
|
||||
<col>
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
<td>{% include "yesnolabel.html" with value=company.is_manufacturer %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-building'></span></td>
|
||||
<td>{% trans "Supplier" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_supplier %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-user-tie'></span></td>
|
||||
<td>{% trans "Customer" %}</td>
|
||||
<td>{% include 'yesnolabel.html' with value=company.is_customer %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-manufacturer-parts'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Manufacturer Parts" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if roles.purchase_order.change %}
|
||||
<div id='manufacturer-part-button-toolbar'>
|
||||
<div class='button-toolbar container-fluid'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button type="button" class="btn btn-success" id='manufacturer-part-create' title='{% trans "Create new manufacturer part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='btn-group' role='group'>
|
||||
<button class="btn btn-primary dropdown-toggle" id='manufacturer-table-options' type="button" data-toggle="dropdown">{% trans "Options" %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a href='#' id='multi-manufacturer-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.delete %}
|
||||
<li><a href='#' id='multi-manufacturer-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-supplier-part'>
|
||||
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class='table table-striped table-condensed' id='manufacturer-part-table' data-toolbar='#manufacturer-part-button-toolbar'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-company-stock'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Supplier Stock" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "stock_table.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-purchase-orders'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Purchase Orders" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<div id='po-button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<button class='btn btn-success' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}</button>
|
||||
<div class='filter-list' id='filter-list-purchaseorder'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#po-button-bar'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-sales-orders'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Sales Orders" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if roles.sales_order.add %}
|
||||
<div id='so-button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<button class='btn btn-success' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'>
|
||||
<div class='fas fa-plus-circle'></div> {% trans "New Sales Order" %}
|
||||
</button>
|
||||
<div class='filter-list' id='filter-list-salesorder'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#so-button-bar'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-assigned-stock'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Assigned Stock" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='assigned-stock-button-toolbar'>
|
||||
<div class='filter-list' id='filter-list-stock'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#bassigned-stock-utton-toolbar'></table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-company-notes'>
|
||||
<div class='panel-heading'>
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h4>{% trans "Company Notes" %}</h4>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<div class='btn-group float-right'>
|
||||
<button type='button' id='edit-notes' title='{% trans "Edit Notes" %}' class='btn btn-small btn-default'>
|
||||
<span class='fas fa-edit'>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if company.notes %}
|
||||
{{ company.notes | markdownify }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#edit-notes').click(function() {
|
||||
constructForm('{% url "api-company-detail" company.pk %}', {
|
||||
fields: {
|
||||
notes: {
|
||||
multiline: true,
|
||||
}
|
||||
},
|
||||
title: '{% trans "Edit Notes" %}',
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
loadStockTable($("#assigned-stock-table"), {
|
||||
params: {
|
||||
customer: {{ company.id }},
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
},
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
filterKey: "customerstock",
|
||||
});
|
||||
|
||||
{% if company.is_customer %}
|
||||
loadSalesOrderTable("#sales-order-table", {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
customer: {{ company.id }},
|
||||
}
|
||||
});
|
||||
|
||||
$("#new-sales-order").click(function() {
|
||||
|
||||
createSalesOrder({
|
||||
customer: {{ company.pk }},
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_supplier %}
|
||||
loadPurchaseOrderTable("#purchase-order-table", {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
supplier: {{ company.id }},
|
||||
}
|
||||
});
|
||||
|
||||
function newOrder() {
|
||||
createPurchaseOrder({
|
||||
supplier: {{ company.pk }},
|
||||
});
|
||||
}
|
||||
|
||||
$("#company-order").click(function() {
|
||||
newOrder();
|
||||
});
|
||||
|
||||
$("#company-order2").click(function() {
|
||||
newOrder();
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
loadStockTable($('#stock-table'), {
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
params: {
|
||||
company: {{ company.id }},
|
||||
part_detail: true,
|
||||
supplier_part_detail: true,
|
||||
location_detail: true,
|
||||
},
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
filterKey: "companystock",
|
||||
});
|
||||
|
||||
$("#stock-export").click(function() {
|
||||
exportStock({
|
||||
supplier: {{ company.id }}
|
||||
});
|
||||
});
|
||||
|
||||
{% if company.is_manufacturer %}
|
||||
|
||||
function reloadManufacturerPartTable() {
|
||||
$('#manufacturer-part-table').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
$("#manufacturer-part-create").click(function () {
|
||||
|
||||
createManufacturerPart({
|
||||
manufacturer: {{ company.pk }},
|
||||
onSuccess: function() {
|
||||
$("#part-table").bootstrapTable("refresh");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadManufacturerPartTable(
|
||||
"#manufacturer-part-table",
|
||||
"{% url 'api-manufacturer-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part_detail: true,
|
||||
manufacturer_detail: true,
|
||||
manufacturer: {{ company.id }},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-table-options']);
|
||||
|
||||
$("#multi-manufacturer-part-delete").click(function() {
|
||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
||||
|
||||
deleteManufacturerParts(selections, {
|
||||
onSuccess: function() {
|
||||
$("#manufacturer-part-table").bootstrapTable("refresh");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#multi-manufacturer-part-order").click(function() {
|
||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.part);
|
||||
});
|
||||
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_supplier %}
|
||||
|
||||
function reloadSupplierPartTable() {
|
||||
$('#supplier-part-table').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
$("#supplier-part-create").click(function () {
|
||||
|
||||
createSupplierPart({
|
||||
supplier: {{ company.pk }},
|
||||
onSuccess: reloadSupplierPartTable,
|
||||
});
|
||||
});
|
||||
|
||||
loadSupplierPartTable(
|
||||
"#supplier-part-table",
|
||||
"{% url 'api-supplier-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part_detail: true,
|
||||
supplier_detail: true,
|
||||
manufacturer_detail: true,
|
||||
supplier: {{ company.id }},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-table-options']);
|
||||
|
||||
$("#multi-supplier-part-delete").click(function() {
|
||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
||||
|
||||
var requests = [];
|
||||
|
||||
showQuestionDialog(
|
||||
'{% trans "Delete Supplier Parts?" %}',
|
||||
'{% trans "All selected supplier parts will be deleted" %}',
|
||||
{
|
||||
accept: function() {
|
||||
selections.forEach(function(part) {
|
||||
var url = `/api/company/part/${part.pk}/`;
|
||||
|
||||
requests.push(inventreeDelete(url));
|
||||
});
|
||||
|
||||
$.when.apply($, requests).done(function() {
|
||||
$('#supplier-part-table').bootstrapTable('refresh');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#multi-supplier-part-order").click(function() {
|
||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.part);
|
||||
});
|
||||
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
attachNavCallbacks({
|
||||
name: 'company',
|
||||
default: 'company-stock'
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,121 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='manufacturer_parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Manufacturer Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% if roles.purchase_order.change %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid'>
|
||||
<div class='btn-group role='group'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class="btn btn-success" id='manufacturer-part-create' title='{% trans "Create new manufacturer part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='btn-group'>
|
||||
<div class="dropdown" style="float: right;">
|
||||
<button class="btn btn-primary dropdown-toggle" id='table-options', type="button" data-toggle="dropdown">{% trans "Options" %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.delete %}
|
||||
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-supplier-part'>
|
||||
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$("#manufacturer-part-create").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'manufacturer-part-create' %}",
|
||||
{
|
||||
data: {
|
||||
manufacturer: {{ company.id }},
|
||||
},
|
||||
success: function() {
|
||||
$("#part-table").bootstrapTable("refresh");
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'part',
|
||||
label: '{% trans "New Part" %}',
|
||||
title: '{% trans "Create new Part" %}',
|
||||
url: "{% url 'part-create' %}"
|
||||
},
|
||||
{
|
||||
field: 'manufacturer',
|
||||
label: '{% trans "New Manufacturer" %}',
|
||||
title: '{% trans "Create new Manufacturer" %}',
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
loadManufacturerPartTable(
|
||||
"#part-table",
|
||||
"{% url 'api-manufacturer-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part_detail: true,
|
||||
manufacturer_detail: true,
|
||||
company: {{ company.id }},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#manufacturer-table"), ['#table-options']);
|
||||
|
||||
$("#multi-part-delete").click(function() {
|
||||
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||
|
||||
deleteManufacturerParts(selections, {
|
||||
onSuccess: function() {
|
||||
$("#part-table").bootstrapTable("refresh");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#multi-part-order").click(function() {
|
||||
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.part);
|
||||
});
|
||||
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,49 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "company/navbar.html" with tab='stock' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Supplier Stock" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% include "stock_table.html" %}
|
||||
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadStockTable($('#stock-table'), {
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
params: {
|
||||
company: {{ company.id }},
|
||||
part_detail: true,
|
||||
supplier_detail: true,
|
||||
location_detail: true,
|
||||
},
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
filterKey: "companystock",
|
||||
});
|
||||
|
||||
$("#stock-export").click(function() {
|
||||
launchModalForm("{% url 'stock-export-options' %}", {
|
||||
submit_text: '{% trans "Export" %}',
|
||||
success: function(response) {
|
||||
var url = "{% url 'stock-export' %}";
|
||||
|
||||
url += "?format=" + response.format;
|
||||
url += "&supplier={{ company.id }}";
|
||||
|
||||
location.href = url;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,127 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='supplier_parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Supplier Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block details %}
|
||||
{% if roles.purchase_order.change %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if roles.purchase_order.add %}
|
||||
<button class="btn btn-success" id='supplier-part-create' title='{% trans "Create new supplier part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='btn-group'>
|
||||
<div class="dropdown" style="float: right;">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if roles.purchase_order.add %}
|
||||
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.delete %}
|
||||
<li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-supplier-part'>
|
||||
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$("#supplier-part-create").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'supplier-part-create' %}",
|
||||
{
|
||||
data: {
|
||||
supplier: {{ company.id }},
|
||||
},
|
||||
reload: true,
|
||||
secondary: [
|
||||
{
|
||||
field: 'part',
|
||||
label: '{% trans "New Part" %}',
|
||||
title: '{% trans "Create new Part" %}',
|
||||
url: "{% url 'part-create' %}"
|
||||
},
|
||||
{
|
||||
field: 'supplier',
|
||||
label: "{% trans 'New Supplier' %}",
|
||||
title: "{% trans 'Create new Supplier' %}",
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
loadSupplierPartTable(
|
||||
"#part-table",
|
||||
"{% url 'api-supplier-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part_detail: true,
|
||||
supplier_detail: true,
|
||||
manufacturer_detail: true,
|
||||
company: {{ company.id }},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
$("#multi-part-delete").click(function() {
|
||||
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
var url = "{% url 'supplier-part-delete' %}"
|
||||
|
||||
launchModalForm(url, {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#multi-part-order").click(function() {
|
||||
var selections = $("#part-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.part);
|
||||
});
|
||||
|
||||
launchModalForm("/order/purchase-order/order-parts/", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
321
InvenTree/company/templates/company/manufacturer_part.html
Normal file
@@ -0,0 +1,321 @@
|
||||
{% extends "two_column.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Manufacturer Part" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "company/manufacturer_part_navbar.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
{% if part.part.image %}
|
||||
src='{{ part.part.image.url }}'
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_data %}
|
||||
<h3>{% trans "Manufacturer Part" %}</h3>
|
||||
<hr>
|
||||
<h4>
|
||||
{{ part.part.full_name }}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
<a href="{% url 'admin:company_supplierpart_change' part.pk %}">
|
||||
<span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p>{{ part.manufacturer.name }} - {{ part.MPN }}</p>
|
||||
|
||||
{% if roles.purchase_order.change %}
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons' role='group'>
|
||||
{% comment "for later" %}
|
||||
{% if roles.purchase_order.add %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'>
|
||||
<span class='fas fa-shopping-cart'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
|
||||
<span class='fas fa-edit icon-green'/>
|
||||
</button>
|
||||
{% if roles.purchase_order.delete %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete manufacturer part" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_details %}
|
||||
|
||||
<h4>{% trans "Manufacturer Part Details" %}</h4>
|
||||
<table class="table table-striped table-condensed">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Internal Part" %}</td>
|
||||
<td>
|
||||
{% if part.part %}
|
||||
<a href="{% url 'part-detail' part.part.id %}?display=part-suppliers">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if part.description %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span></td>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
<td><a href="{% url 'company-detail' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "MPN" %}</td>
|
||||
<td>{{ part.MPN }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-supplier-parts'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Suppliers" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='supplier-button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class="btn btn-success" id='supplier-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#supplier-button-toolbar'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-parameters'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Parameters" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='parameter-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-success' id='parameter-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='parameter-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='multi-parameter-delete' title='{% trans "Delete parameters" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='parameter-table' data-toolbar='#parameter-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'manufacturer-part',
|
||||
toggleId: '#manufacturer-part-menu-toggle'
|
||||
});
|
||||
|
||||
function reloadParameters() {
|
||||
$("#parameter-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$('#parameter-create').click(function() {
|
||||
|
||||
constructForm('{% url "api-manufacturer-part-parameter-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
name: {},
|
||||
value: {},
|
||||
units: {},
|
||||
manufacturer_part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
title: '{% trans "Add Parameter" %}',
|
||||
onSuccess: reloadParameters
|
||||
});
|
||||
});
|
||||
|
||||
function reloadSupplierPartTable() {
|
||||
$('#supplier-table').bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
$('#supplier-create').click(function () {
|
||||
createSupplierPart({
|
||||
manufacturer_part: {{ part.pk }},
|
||||
part: {{ part.part.pk }},
|
||||
onSuccess: reloadSupplierPartTable,
|
||||
});
|
||||
});
|
||||
|
||||
$("#supplier-part-delete").click(function() {
|
||||
|
||||
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
||||
|
||||
var requests = [];
|
||||
|
||||
showQuestionDialog(
|
||||
'{% trans "Delete Supplier Parts?" %}',
|
||||
'{% trans "All selected supplier parts will be deleted" %}',
|
||||
{
|
||||
accept: function() {
|
||||
selections.forEach(function(part) {
|
||||
var url = `/api/company/part/${part.pk}/`;
|
||||
|
||||
requests.push(inventreeDelete(url));
|
||||
});
|
||||
|
||||
$.when.apply($, requests).done(function() {
|
||||
reloadSupplierPartTable();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#multi-parameter-delete").click(function() {
|
||||
|
||||
var selections = $("#parameter-table").bootstrapTable("getSelections");
|
||||
|
||||
var text = `
|
||||
<div class ='alert alert-block alert-danger'>
|
||||
<p>{% trans "Selected parameters will be deleted" %}:</p>
|
||||
<ul>`;
|
||||
|
||||
selections.forEach(function(item) {
|
||||
text += `<li>${item.name} - <em>${item.value}</em></li>`;
|
||||
});
|
||||
|
||||
text += `
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
showQuestionDialog(
|
||||
'{% trans "Delete Parameters" %}',
|
||||
text,
|
||||
{
|
||||
accept_text: '{% trans "Delete" %}',
|
||||
accept: function() {
|
||||
// Delete each parameter via the API
|
||||
var requests = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
var url = `/api/company/part/manufacturer/parameter/${item.pk}/`;
|
||||
|
||||
requests.push(inventreeDelete(url));
|
||||
});
|
||||
|
||||
$.when.apply($, requests).done(function() {
|
||||
$('#parameter-table').bootstrapTable('refresh');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
loadSupplierPartTable(
|
||||
"#supplier-table",
|
||||
"{% url 'api-supplier-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part: {{ part.part.id }},
|
||||
manufacturer_part: {{ part.id }},
|
||||
part_detail: false,
|
||||
supplier_detail: true,
|
||||
manufacturer_detail: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
loadManufacturerPartParameterTable(
|
||||
"#parameter-table",
|
||||
"{% url 'api-manufacturer-part-parameter-list' %}",
|
||||
{
|
||||
params: {
|
||||
manufacturer_part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']);
|
||||
|
||||
linkButtonsToSelection($("#parameter-table"), ['#parameter-options']);
|
||||
|
||||
$('#order-part, #order-part2').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'order-parts' %}",
|
||||
{
|
||||
data: {
|
||||
part: {{ part.part.id }},
|
||||
},
|
||||
reload: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$('#edit-part').click(function () {
|
||||
|
||||
editManufacturerPart({{ part.pk }}, {
|
||||
onSuccess: function() {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#delete-part').click(function() {
|
||||
|
||||
deleteManufacturerPart({{ part.pk }}, {
|
||||
onSuccess: function() {
|
||||
window.location.href = "{% url 'company-detail' part.manufacturer.id %}";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
attachNavCallbacks({
|
||||
name: 'manufacturerpart',
|
||||
default: 'parameters'
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,139 +0,0 @@
|
||||
{% extends "two_column.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Manufacturer Part" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
{% if part.part.image %}
|
||||
src='{{ part.part.image.url }}'
|
||||
{% else %}
|
||||
src="{% static 'img/blank_image.png' %}"
|
||||
{% endif %}/>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_data %}
|
||||
<h3>{% trans "Manufacturer Part" %}</h3>
|
||||
<hr>
|
||||
<h4>
|
||||
{{ part.part.full_name }}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
<a href="{% url 'admin:company_supplierpart_change' part.pk %}">
|
||||
<span title='{% trans "Admin view" %}' class='fas fa-user-shield'></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<p>{{ part.manufacturer.name }} - {{ part.MPN }}</p>
|
||||
|
||||
{% if roles.purchase_order.change %}
|
||||
<div class='btn-row'>
|
||||
<div class='btn-group action-buttons' role='group'>
|
||||
{% comment "for later" %}
|
||||
{% if roles.purchase_order.add %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='order-part' title='{% trans "Order part" %}'>
|
||||
<span class='fas fa-shopping-cart'></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='edit-part' title='{% trans "Edit manufacturer part" %}'>
|
||||
<span class='fas fa-edit icon-green'/>
|
||||
</button>
|
||||
{% if roles.purchase_order.delete %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='delete-part' title='{% trans "Delete manufacturer part" %}'>
|
||||
<span class='fas fa-trash-alt icon-red'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block page_details %}
|
||||
|
||||
<h4>{% trans "Manufacturer Part Details" %}</h4>
|
||||
<table class="table table-striped table-condensed">
|
||||
<col width='25'>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Internal Part" %}</td>
|
||||
<td>
|
||||
{% if part.part %}
|
||||
<a href="{% url 'part-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>{% include "clip.html"%}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if part.description %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Description" %}</td>
|
||||
<td>{{ part.description }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.link %}
|
||||
<tr>
|
||||
<td><span class='fas fa-link'></span></td>
|
||||
<td>{% trans "External Link" %}</td>
|
||||
<td><a href="{{ part.link }}">{{ part.link }}</a>{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-industry'></span></td>
|
||||
<td>{% trans "Manufacturer" %}</td>
|
||||
<td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a>{% include "clip.html"%}</td></tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "MPN" %}</td>
|
||||
<td>{{ part.MPN }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'manufacturer-part',
|
||||
toggleId: '#manufacturer-part-menu-toggle'
|
||||
})
|
||||
|
||||
$('#order-part, #order-part2').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'order-parts' %}",
|
||||
{
|
||||
data: {
|
||||
part: {{ part.part.id }},
|
||||
},
|
||||
reload: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$('#edit-part').click(function () {
|
||||
|
||||
constructForm('{% url "api-manufacturer-part-detail" part.pk %}', {
|
||||
fields: {
|
||||
part: {},
|
||||
manufacturer: {},
|
||||
MPN: {},
|
||||
description: {},
|
||||
link: {},
|
||||
},
|
||||
title: '{% trans "Edit Manufacturer Part" %}',
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$('#delete-part').click(function() {
|
||||
|
||||
constructForm('{% url "api-manufacturer-part-detail" part.pk %}', {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Manufacturer Part" %}',
|
||||
redirect: "{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}",
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if part %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% include "hover_image.html" with image=part.image %}
|
||||
{{ part.full_name}}
|
||||
<br>
|
||||
<i>{{ part.description }}</i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,38 +0,0 @@
|
||||
{% extends "company/manufacturer_part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "company/manufacturer_part_navbar.html" with tab='details' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Manufacturer Part Details" %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block details %}
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr>
|
||||
<td>{% trans "Internal Part" %}</td>
|
||||
<td>
|
||||
{% if part.part %}
|
||||
<a href="{% url 'part-manufacturers' part.part.id %}">{{ part.part.full_name }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>{% trans "Manufacturer" %}</td><td><a href="{% url 'company-detail-manufacturer-parts' part.manufacturer.id %}">{{ part.manufacturer.name }}</a></td></tr>
|
||||
<tr><td>{% trans "MPN" %}</td><td>{{ part.MPN }}</tr></tr>
|
||||
{% if part.link %}
|
||||
<tr><td>{% trans "External Link" %}</td><td><a href="{{ part.link }}">{{ part.link }}</a></td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -8,8 +8,15 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Supplier Parts" %}'>
|
||||
<a href='{% url "manufacturer-part-suppliers" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Parameters" %}'>
|
||||
<a href='#' id='select-parameters' class='nav-toggle'>
|
||||
<span class='fas fa-th-list sidebar-icon'></span>
|
||||
{% trans "Parameters" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item' title='{% trans "Supplier Parts" %}'>
|
||||
<a href='#' id='select-supplier-parts' class='nav-toggle'>
|
||||
<span class='fas fa-building sidebar-icon'></span>
|
||||
{% trans "Suppliers" %}
|
||||
</a>
|
||||
@@ -17,14 +24,14 @@
|
||||
|
||||
{% comment "for later" %}
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Manufacturer Part Stock" %}'>
|
||||
<a href='{% url "manufacturer-part-stock" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-boxes sidebar-icon'></span>
|
||||
{% trans "Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Manufacturer Part Orders" %}'>
|
||||
<a href='{% url "manufacturer-part-orders" part.id %}'>
|
||||
<a href='#'>
|
||||
<span class='fas fa-shopping-cart sidebar-icon'></span>
|
||||
{% trans "Orders" %}
|
||||
</a>
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
{% extends "company/manufacturer_part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "company/manufacturer_part_navbar.html" with tab='suppliers' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Suppliers" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class="btn btn-success" id='supplier-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content_panels %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Parameters" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='parameter-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-success' id='parameter-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='parameter-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='multi-parameter-delete' title='{% trans "Delete parameters" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='parameter-table' data-toolbar='#parameter-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
function reloadParameters() {
|
||||
$("#parameter-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$('#parameter-create').click(function() {
|
||||
|
||||
constructForm('{% url "api-manufacturer-part-parameter-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
name: {},
|
||||
value: {},
|
||||
units: {},
|
||||
manufacturer_part: {
|
||||
value: {{ part.pk }},
|
||||
hidden: true,
|
||||
}
|
||||
},
|
||||
title: '{% trans "Add Parameter" %}',
|
||||
onSuccess: reloadParameters
|
||||
});
|
||||
});
|
||||
|
||||
$('#supplier-create').click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'supplier-part-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
manufacturer_part: {{ part.id }}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'supplier',
|
||||
label: '{% trans "New Supplier" %}',
|
||||
title: '{% trans "Create new supplier" %}',
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
$("#supplier-part-delete").click(function() {
|
||||
|
||||
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
launchModalForm("{% url 'supplier-part-delete' %}", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#multi-parameter-delete").click(function() {
|
||||
|
||||
var selections = $("#parameter-table").bootstrapTable("getSelections");
|
||||
|
||||
var text = `
|
||||
<div class ='alert alert-block alert-danger'>
|
||||
<p>{% trans "Selected parameters will be deleted" %}:</p>
|
||||
<ul>`;
|
||||
|
||||
selections.forEach(function(item) {
|
||||
text += `<li>${item.name} - <i>${item.value}</i></li>`;
|
||||
});
|
||||
|
||||
text += `
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
showQuestionDialog(
|
||||
'{% trans "Delete Parameters" %}',
|
||||
text,
|
||||
{
|
||||
accept_text: '{% trans "Delete" %}',
|
||||
accept: function() {
|
||||
// Delete each parameter via the API
|
||||
var requests = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
var url = `/api/company/part/manufacturer/parameter/${item.pk}/`;
|
||||
|
||||
requests.push(inventreeDelete(url));
|
||||
});
|
||||
|
||||
$.when.apply($, requests).then(function() {
|
||||
$('#parameter-table').bootstrapTable('refresh');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
loadSupplierPartTable(
|
||||
"#supplier-table",
|
||||
"{% url 'api-supplier-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part: {{ part.part.id }},
|
||||
manufacturer_part: {{ part.id }},
|
||||
part_detail: false,
|
||||
supplier_detail: true,
|
||||
manufacturer_detail: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
loadManufacturerPartParameterTable(
|
||||
"#parameter-table",
|
||||
"{% url 'api-manufacturer-part-parameter-list' %}",
|
||||
{
|
||||
params: {
|
||||
manufacturer_part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
|
||||
linkButtonsToSelection($("#parameter-table"), ['#parameter-options'])
|
||||
{% endblock %}
|
||||
@@ -9,25 +9,18 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "details" %}active{% endif %}' title='{% trans "Company Details" %}'>
|
||||
<a href='{% url "company-detail" company.id %}'>
|
||||
<span class='fas fa-info-circle sidebar-icon'></span>
|
||||
{% trans "Details" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if company.is_manufacturer %}
|
||||
<li class='list-group-item {% if tab == "manufacturer_parts" %}active{% endif %}' title='{% trans "Manufactured Parts" %}'>
|
||||
<a href='{% url "company-detail-manufacturer-parts" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Manufactured Parts" %}'>
|
||||
<a href='#' id='select-manufacturer-parts' class='nav-toggle'>
|
||||
<span class='fas fa-industry sidebar-icon'></span>
|
||||
{% trans "Manufactured Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_supplier or company.is_manufacturer %}
|
||||
<li class='list-group-item {% if tab == "supplier_parts" %}active{% endif %}' title='{% trans "Supplied Parts" %}'>
|
||||
<a href='{% url "company-detail-supplier-parts" company.id %}'>
|
||||
{% if company.is_supplier %}
|
||||
<li class='list-group-item' title='{% trans "Supplied Parts" %}'>
|
||||
<a href='#' id='select-supplier-parts' class='nav-toggle'>
|
||||
<span class='fas fa-building sidebar-icon'></span>
|
||||
{% trans "Supplied Parts" %}
|
||||
</a>
|
||||
@@ -35,8 +28,8 @@
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_manufacturer or company.is_supplier %}
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
||||
<a href='{% url "company-detail-stock" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Stock Items" %}'>
|
||||
<a href='#' id='select-company-stock' class='nav-toggle'>
|
||||
<span class='fas fa-boxes sidebar-icon'></span>
|
||||
{% trans "Stock" %}
|
||||
</a>
|
||||
@@ -44,8 +37,8 @@
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_supplier %}
|
||||
<li class='list-group-item {% if tab == "po" %}active{% endif %}' title='{% trans "Sales Orders" %}'>
|
||||
<a href='{% url "company-detail-purchase-orders" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Purchase Orders" %}'>
|
||||
<a href='#' id='select-purchase-orders' class='nav-toggle'>
|
||||
<span class='fas fa-shopping-cart sidebar-icon'></span>
|
||||
{% trans "Purchase Orders" %}
|
||||
</a>
|
||||
@@ -53,22 +46,22 @@
|
||||
{% endif %}
|
||||
|
||||
{% if company.is_customer %}
|
||||
<li class='list-group-item {% if tab == "so" %}active{% endif %}' title='{% trans "Sales Orders" %}'>
|
||||
<a href='{% url "company-detail-sales-orders" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Sales Orders" %}'>
|
||||
<a href='#' id='select-sales-orders' class='nav-toggle'>
|
||||
<span class='fas fa-truck sidebar-icon'></span>
|
||||
{% trans "Sales Orders" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "assigned" %}active{% endif %}' title='{% trans "Assigned Stock" %}'>
|
||||
<a href='{% url "company-detail-assigned-stock" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Assigned Stock" %}'>
|
||||
<a href='#' id='select-assigned-stock' class='nav-toggle'>
|
||||
<span class='fas fa-sign-out-alt sidebar-icon'></span>
|
||||
{% trans "Assigned Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class='list-group-item {% if tab == "notes" %}active{% endif %}' titl='{% trans "Notes" %}'>
|
||||
<a href='{% url "company-notes" company.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Notes" %}'>
|
||||
<a href='#' id='select-company-notes' class='nav-toggle'>
|
||||
<span class='fas fa-clipboard sidebar-icon'></span>
|
||||
{% trans "Notes" %}
|
||||
</a>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='notes' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Company Notes" %}
|
||||
{% if not editing %}
|
||||
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if editing %}
|
||||
<form method='POST'>
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form }}
|
||||
<hr>
|
||||
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
{{ form.media }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{{ company.notes | markdownify }}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if editing %}
|
||||
{% else %}
|
||||
$("#edit-notes").click(function() {
|
||||
location.href = "{% url 'company-notes' company.id %}?edit=1";
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,56 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='po' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Purchase Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% if roles.purchase_order.add %}
|
||||
<div id='button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<button class='btn btn-primary' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}</button>
|
||||
<div class='filter-list' id='filter-list-purchaseorder'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPurchaseOrderTable("#purchase-order-table", {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
supplier: {{ company.id }},
|
||||
}
|
||||
});
|
||||
|
||||
function newOrder() {
|
||||
createPurchaseOrder({
|
||||
supplier: {{ company.pk }},
|
||||
});
|
||||
}
|
||||
|
||||
$("#company-order").click(function() {
|
||||
newOrder();
|
||||
});
|
||||
|
||||
$("#company-order2").click(function() {
|
||||
newOrder();
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,51 +0,0 @@
|
||||
{% extends "company/company_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'company/navbar.html' with tab='so' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Sales Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% if roles.sales_order.add %}
|
||||
<div id='button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<button class='btn btn-primary' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'>
|
||||
<div class='fas fa-plus-circle'></div> {% trans "New Sales Order" %}
|
||||
</button>
|
||||
<div class='filter-list' id='filter-list-salesorder'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#button-bar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadSalesOrderTable("#sales-order-table", {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
customer: {{ company.id }},
|
||||
}
|
||||
});
|
||||
|
||||
$("#new-sales-order").click(function() {
|
||||
|
||||
createSalesOrder({
|
||||
customer: {{ company.pk }},
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||