mirror of
https://github.com/HendrikRauh/inventree-app.git
synced 2026-02-05 21:43:17 +00:00
Compare commits
789 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae457e8235 | ||
|
|
a1f0576671 | ||
|
|
c5bf4be3d1 | ||
|
|
772c88170e | ||
|
|
97e4c98e46 | ||
|
|
74f468dc1b | ||
|
|
9002fb78d3 | ||
|
|
80f62091b1 | ||
|
|
a4631cda7a | ||
|
|
225c40f9a6 | ||
|
|
bf19ace3e9 | ||
|
|
8c15bdafdf | ||
|
|
571ff1880f | ||
|
|
864c3eea76 | ||
|
|
346b1a150f | ||
|
|
bb10117f01 | ||
|
|
0f31638bdc | ||
|
|
7283c07b76 | ||
|
|
6e02d1da97 | ||
|
|
3ee2192c92 | ||
|
|
01ac0fc59e | ||
|
|
13d95dd1b1 | ||
|
|
e41842a31d | ||
|
|
d039f3cfcf | ||
|
|
a75245b183 | ||
|
|
8a4750d298 | ||
|
|
6707f89019 | ||
|
|
ed7d73b9c0 | ||
|
|
0eedf25d11 | ||
|
|
daf3bf8291 | ||
|
|
ae1d8dd188 | ||
|
|
2252dd2fd6 | ||
|
|
490d008447 | ||
|
|
0fc80b1be3 | ||
|
|
9083f19531 | ||
|
|
2f8f42822a | ||
|
|
2b68c30568 | ||
|
|
4a094f4a77 | ||
|
|
a078a9d126 | ||
|
|
624655ec6b | ||
|
|
6b67cc9e50 | ||
|
|
790abb4c7d | ||
|
|
1407d8bc37 | ||
|
|
bdc5573311 | ||
|
|
d237a0e076 | ||
|
|
449f4a4ce5 | ||
|
|
3739c88e93 | ||
|
|
8efae776a6 | ||
|
|
a0b8795b27 | ||
|
|
7d32bd6d88 | ||
|
|
28701359da | ||
|
|
b7a9062e0b | ||
|
|
bda9a9befb | ||
|
|
292e96b799 | ||
|
|
7ad10fea60 | ||
|
|
fe30b4ee16 | ||
|
|
a2e27e7a8a | ||
|
|
2adf8e3430 | ||
|
|
c30f1a19d1 | ||
|
|
1374bb5db1 | ||
|
|
1b59837b3b | ||
|
|
5bd5d34b24 | ||
|
|
675f9ee1bb | ||
|
|
2bb89a96ce | ||
|
|
55d14e7457 | ||
|
|
eb30bbb2fa | ||
|
|
29ccd4ebfa | ||
|
|
2619adc87b | ||
|
|
2568a299fc | ||
|
|
4444884afa | ||
|
|
e9db6532e4 | ||
|
|
a18c5d8354 | ||
|
|
fff16fec76 | ||
|
|
c4e33a4c1a | ||
|
|
cf012b2531 | ||
|
|
d651506315 | ||
|
|
d4ba866e10 | ||
|
|
13abcae84c | ||
|
|
13cb2f9164 | ||
|
|
03aba4b4bf | ||
|
|
1ab171f619 | ||
|
|
c3f390eddc | ||
|
|
9ea7a2348b | ||
|
|
5017391933 | ||
|
|
52879c0fc2 | ||
|
|
762d1ae156 | ||
|
|
dd268f4f61 | ||
|
|
1d5377ea20 | ||
|
|
72a78291b2 | ||
|
|
25d7ac9189 | ||
|
|
5ec86c4ade | ||
|
|
e11382b3b4 | ||
|
|
cae79a3922 | ||
|
|
fcb4370ce8 | ||
|
|
dc314d1c26 | ||
|
|
efe8db138b | ||
|
|
5672193ced | ||
|
|
3c425de8f7 | ||
|
|
5e9ce7c0e7 | ||
|
|
1a3f48f48c | ||
|
|
0c5944a8a0 | ||
|
|
0904d4e258 | ||
|
|
d41d9b2a99 | ||
|
|
854ef95fbf | ||
|
|
2ea29368ed | ||
|
|
0380b38311 | ||
|
|
60ec40e14c | ||
|
|
1363f7ee3a | ||
|
|
94d43bc377 | ||
|
|
8733deb46a | ||
|
|
543d041ca5 | ||
|
|
d97e4bc010 | ||
|
|
562cbffb18 | ||
|
|
f5e3b4ac80 | ||
|
|
1a5992042a | ||
|
|
aac13ed5d6 | ||
|
|
d84f76d482 | ||
|
|
bc44b99d43 | ||
|
|
dc8191c3d8 | ||
|
|
ea3410a3da | ||
|
|
09625c81e2 | ||
|
|
51fb889854 | ||
|
|
665de2bd5a | ||
|
|
524c5469f1 | ||
|
|
6b179d108c | ||
|
|
e93e0241f6 | ||
|
|
6690f10118 | ||
|
|
d25c47ccc3 | ||
|
|
e1912d6878 | ||
|
|
9006fc382f | ||
|
|
676de0dfe9 | ||
|
|
541eca387d | ||
|
|
2dd20d9565 | ||
|
|
5c1ceb7a0c | ||
|
|
35f3792d71 | ||
|
|
2d9bc145bf | ||
|
|
e44d1ea5b4 | ||
|
|
d5a9f4310e | ||
|
|
19fdac46a7 | ||
|
|
3025a3c280 | ||
|
|
d8bba1daba | ||
|
|
fbe5ee2455 | ||
|
|
0d10c0f539 | ||
|
|
50b7286be3 | ||
|
|
09dd7684f0 | ||
|
|
0bb7af7350 | ||
|
|
1e4509854b | ||
|
|
3bf9692c2a | ||
|
|
3fe350a35e | ||
|
|
5728da5361 | ||
|
|
5e7df6a3f0 | ||
|
|
1ea11b5d7e | ||
|
|
32ac2e0b6b | ||
|
|
5281a1b6d2 | ||
|
|
e3c24e76bf | ||
|
|
777a074331 | ||
|
|
968dab0a96 | ||
|
|
345bb8be12 | ||
|
|
3c5e884b38 | ||
|
|
7fa92fec17 | ||
|
|
626b66c055 | ||
|
|
85265d781a | ||
|
|
27022ac33c | ||
|
|
4e73b4e43b | ||
|
|
75fe2fe9f4 | ||
|
|
6ab241b0b5 | ||
|
|
940efa6aa9 | ||
|
|
6aa2e3cdef | ||
|
|
3937bb37e6 | ||
|
|
c2ef434988 | ||
|
|
5c85c98a0f | ||
|
|
0b043dcaeb | ||
|
|
e0857b0462 | ||
|
|
a0c0e9dca4 | ||
|
|
0f4e1c8404 | ||
|
|
18930e6201 | ||
|
|
3887458f0d | ||
|
|
9745bcfdb6 | ||
|
|
cc0085e50f | ||
|
|
52a1eedc7e | ||
|
|
d016a663d8 | ||
|
|
2141c647c5 | ||
|
|
e2a688315d | ||
|
|
08e01a729b | ||
|
|
0ef72dc3dd | ||
|
|
8f802209a7 | ||
|
|
a893d51e3c | ||
|
|
ff82ed105d | ||
|
|
e95c35eebf | ||
|
|
e70aa9c4fc | ||
|
|
d5d1573eb3 | ||
|
|
3e1018c6ad | ||
|
|
a08f64a2a2 | ||
|
|
1e7232a688 | ||
|
|
42fc796019 | ||
|
|
493f83ef9a | ||
|
|
d9f08b3128 | ||
|
|
9f392aaff7 | ||
|
|
ac3841a102 | ||
|
|
de45469954 | ||
|
|
dd9f326dcb | ||
|
|
0f49298401 | ||
|
|
58585cb213 | ||
|
|
b83ee998f0 | ||
|
|
6e0e099de8 | ||
|
|
6f9995ed3a | ||
|
|
776f0e0ea5 | ||
|
|
237d812e51 | ||
|
|
9420bf450a | ||
|
|
73c4ef270e | ||
|
|
5589afaebf | ||
|
|
8348c55f63 | ||
|
|
c9e0f90be9 | ||
|
|
79618ba298 | ||
|
|
8b5548f926 | ||
|
|
ff40777d94 | ||
|
|
9d8668745c | ||
|
|
048c25267f | ||
|
|
0c2957ae70 | ||
|
|
ac56797498 | ||
|
|
66587e71ef | ||
|
|
6af819de0c | ||
|
|
2658734a89 | ||
|
|
d008011fcc | ||
|
|
31dc193fa1 | ||
|
|
b25e763188 | ||
|
|
fc26750641 | ||
|
|
d072c77218 | ||
|
|
f7d179e041 | ||
|
|
3f7c2feeb7 | ||
|
|
5dab11ee0c | ||
|
|
707d251853 | ||
|
|
6f5fc1d8a9 | ||
|
|
b849bfc718 | ||
|
|
d4cff1a5b9 | ||
|
|
4151aeb8e1 | ||
|
|
2964950b26 | ||
|
|
d4b2204baf | ||
|
|
9c12a83176 | ||
|
|
2e798b1bd1 | ||
|
|
4698e7e82c | ||
|
|
07a3f75981 | ||
|
|
06bdd2d7f3 | ||
|
|
a06ad4e804 | ||
|
|
20e454d287 | ||
|
|
1a1521efe3 | ||
|
|
4a2e91a371 | ||
|
|
c0533db138 | ||
|
|
d990508237 | ||
|
|
29948e5809 | ||
|
|
538a3d6ff6 | ||
|
|
ad48e5e172 | ||
|
|
6c0b3cccc3 | ||
|
|
31cda0823a | ||
|
|
51ae10af7f | ||
|
|
23f27af4e6 | ||
|
|
c52885fc6b | ||
|
|
82aace9cc4 | ||
|
|
9222439186 | ||
|
|
7b60c857fd | ||
|
|
4558fff36c | ||
|
|
30cfcc5ffe | ||
|
|
f007a8da74 | ||
|
|
c9cad2f89f | ||
|
|
42de3fd7d4 | ||
|
|
693b4a4fce | ||
|
|
0485d5d089 | ||
|
|
6ba4fa747e | ||
|
|
8ba10f2578 | ||
|
|
856cf9eee4 | ||
|
|
ea9623490d | ||
|
|
5a9a0b0855 | ||
|
|
464d415115 | ||
|
|
e837394495 | ||
|
|
c3eb1a5fca | ||
|
|
b6d5d017ec | ||
|
|
aed07514dd | ||
|
|
e600c5f6b5 | ||
|
|
715cd06946 | ||
|
|
7575ba0136 | ||
|
|
4302f542dc | ||
|
|
349b3d4366 | ||
|
|
9638b782cc | ||
|
|
541060aa03 | ||
|
|
9a6e1e6381 | ||
|
|
3c0bca276d | ||
|
|
91cb24c74c | ||
|
|
86425ebb2b | ||
|
|
177f040324 | ||
|
|
f986e163e3 | ||
|
|
4499f3e00e | ||
|
|
a889417fe0 | ||
|
|
0e658febe2 | ||
|
|
3f60b52a68 | ||
|
|
4ae28d60a1 | ||
|
|
b02dc5bac7 | ||
|
|
a889c4adbe | ||
|
|
1d41d229ca | ||
|
|
d152475de4 | ||
|
|
4ef2e43bf3 | ||
|
|
571b491846 | ||
|
|
edde9a9585 | ||
|
|
3ea5f8934c | ||
|
|
70d0d4de93 | ||
|
|
1ec1a867d9 | ||
|
|
eb1be30df4 | ||
|
|
711034f402 | ||
|
|
bf3df770c7 | ||
|
|
0a85441131 | ||
|
|
1148f01d4a | ||
|
|
8cb5dd20f0 | ||
|
|
20127c6090 | ||
|
|
bb87d0dd6d | ||
|
|
e22ba95214 | ||
|
|
bdd5470e68 | ||
|
|
c1c0d46957 | ||
|
|
cf83ae86b9 | ||
|
|
40fad26c06 | ||
|
|
c641cea369 | ||
|
|
b6ab9d5da5 | ||
|
|
8f1cd1cae1 | ||
|
|
76b6191a67 | ||
|
|
382c8461f9 | ||
|
|
0d44bd3799 | ||
|
|
c65833cf6d | ||
|
|
67fd6a564a | ||
|
|
c76309341b | ||
|
|
f4be87e826 | ||
|
|
a119bcc465 | ||
|
|
3cc57101c4 | ||
|
|
6520873384 | ||
|
|
dd12769a51 | ||
|
|
9203ee8a3f | ||
|
|
1960016288 | ||
|
|
81907ad72f | ||
|
|
af09cde29e | ||
|
|
32c301a9b1 | ||
|
|
38dfb03669 | ||
|
|
e6ad1bcb98 | ||
|
|
8200140976 | ||
|
|
d81f0d532d | ||
|
|
75c4e038f4 | ||
|
|
72f243e1a5 | ||
|
|
d2a01a0286 | ||
|
|
d6460d58aa | ||
|
|
e549968d58 | ||
|
|
b044c53d91 | ||
|
|
2e2e9640d4 | ||
|
|
174f1b2f2d | ||
|
|
7a11fdead8 | ||
|
|
3085d98ce1 | ||
|
|
443e6e856c | ||
|
|
d78affc1cb | ||
|
|
76457793b3 | ||
|
|
7ef6da4b2a | ||
|
|
637b058a8a | ||
|
|
2babf27db5 | ||
|
|
6fe23fa846 | ||
|
|
e39ab9ad78 | ||
|
|
9277e18028 | ||
|
|
0365557475 | ||
|
|
138cae2da0 | ||
|
|
23abcb48f2 | ||
|
|
320b16f86e | ||
|
|
279c15509c | ||
|
|
d0d96166c4 | ||
|
|
08ebc34730 | ||
|
|
925966c627 | ||
|
|
e9eb84eace | ||
|
|
8076887e39 | ||
|
|
770d9cddb2 | ||
|
|
001450d3bb | ||
|
|
b863bbd590 | ||
|
|
ca678c1c6f | ||
|
|
367759e86c | ||
|
|
9cc666d13e | ||
|
|
71bf3ad049 | ||
|
|
ba409660f4 | ||
|
|
8cebc25bea | ||
|
|
21ace1ae02 | ||
|
|
b051aeccda | ||
|
|
45fe79daf0 | ||
|
|
7ca9a7ccc4 | ||
|
|
973f1fb002 | ||
|
|
b733d00c37 | ||
|
|
905cedf9af | ||
|
|
230627e2bb | ||
|
|
0296c4c22a | ||
|
|
4ba19fcab2 | ||
|
|
99c768a2e1 | ||
|
|
383571707e | ||
|
|
49226a5fce | ||
|
|
7abb8cf0c0 | ||
|
|
b7e806efee | ||
|
|
2c5ceeabdb | ||
|
|
bb781aaed5 | ||
|
|
e23a8b4d5e | ||
|
|
95573a2784 | ||
|
|
232f721712 | ||
|
|
1d6708fbca | ||
|
|
ac57c53202 | ||
|
|
0c4179480d | ||
|
|
caa4fdd2a1 | ||
|
|
8510034a81 | ||
|
|
b9ffabd561 | ||
|
|
28ed1ed545 | ||
|
|
87994a4912 | ||
|
|
ba1df2e817 | ||
|
|
347692bf70 | ||
|
|
612db9f194 | ||
|
|
d926686a89 | ||
|
|
01a45568a0 | ||
|
|
b5c4bda80f | ||
|
|
b54565d1c3 | ||
|
|
a3d712d11d | ||
|
|
e7f5141aa9 | ||
|
|
1d913084a6 | ||
|
|
943104f20c | ||
|
|
164295c3e2 | ||
|
|
946abb60a0 | ||
|
|
0156329fb6 | ||
|
|
2fdff02299 | ||
|
|
cb5c292326 | ||
|
|
26b86a2194 | ||
|
|
efb7ff4170 | ||
|
|
020cc4497c | ||
|
|
8631fedbfb | ||
|
|
bf8a65fadf | ||
|
|
a8f87e2f5a | ||
|
|
74176cdda8 | ||
|
|
f7d3315c99 | ||
|
|
fb0a383fff | ||
|
|
79026792e2 | ||
|
|
091f33eb10 | ||
|
|
d7f2c3939b | ||
|
|
9543490c21 | ||
|
|
73fd35d9a3 | ||
|
|
b2d4522fb2 | ||
|
|
82f25dfc90 | ||
|
|
878d9b46d2 | ||
|
|
bb1c1cf3d9 | ||
|
|
347e80d8e2 | ||
|
|
221920cbbe | ||
|
|
10ae5e47ba | ||
|
|
ee0a6815f4 | ||
|
|
84f7e90569 | ||
|
|
c8dedf2a0e | ||
|
|
4b8ab304aa | ||
|
|
fb80029c0e | ||
|
|
5c06e3c9de | ||
|
|
113b3d69a9 | ||
|
|
80b83b842d | ||
|
|
9485d858eb | ||
|
|
c7527e8b4e | ||
|
|
262a629923 | ||
|
|
7447a18e64 | ||
|
|
0493bb2a12 | ||
|
|
6d4973deb8 | ||
|
|
298ee24a9c | ||
|
|
15bf109296 | ||
|
|
ce37d0e757 | ||
|
|
dece29d1d5 | ||
|
|
9791fb7d23 | ||
|
|
e9d9cf5322 | ||
|
|
20de6e03e6 | ||
|
|
614253b901 | ||
|
|
ba72781df8 | ||
|
|
83da0638a8 | ||
|
|
1ab3940e6a | ||
|
|
d69ffc8de0 | ||
|
|
0090443495 | ||
|
|
f339ac249b | ||
|
|
8639ccf79e | ||
|
|
499bd3edf8 | ||
|
|
d2b74e7684 | ||
|
|
27040024c0 | ||
|
|
da79524dd1 | ||
|
|
d015a3087d | ||
|
|
730521fd00 | ||
|
|
efb6fc353e | ||
|
|
971c6bfcdb | ||
|
|
544b270ac5 | ||
|
|
c2574e9fa5 | ||
|
|
207e5ec6c5 | ||
|
|
15b4cbc67a | ||
|
|
a951e75a7e | ||
|
|
d122a352a6 | ||
|
|
30ea893023 | ||
|
|
b07290fee4 | ||
|
|
9475829d86 | ||
|
|
881bc0b6d9 | ||
|
|
4833424686 | ||
|
|
c87021fa67 | ||
|
|
3c36215820 | ||
|
|
f289ab637c | ||
|
|
f196ad7fb7 | ||
|
|
ca96a707f5 | ||
|
|
6401e5c81b | ||
|
|
8f0d5add44 | ||
|
|
e2e47961fd | ||
|
|
55d1b7a060 | ||
|
|
6d796a2e32 | ||
|
|
87c8a21c3c | ||
|
|
ac52feb97b | ||
|
|
7fc109e0c2 | ||
|
|
c25175ac54 | ||
|
|
38b34e0fd9 | ||
|
|
e7c5186823 | ||
|
|
61bacefd36 | ||
|
|
9559b8602e | ||
|
|
19ff6eb526 | ||
|
|
e78bb78bfd | ||
|
|
2cbcf275ab | ||
|
|
b5d26580b4 | ||
|
|
c5162c1947 | ||
|
|
b7a37e50c5 | ||
|
|
f652bebd83 | ||
|
|
e13817abed | ||
|
|
dacbf880da | ||
|
|
75e0a69eab | ||
|
|
0165a4bad5 | ||
|
|
19ad3153e4 | ||
|
|
42d69365b8 | ||
|
|
162d05aea5 | ||
|
|
01dd046dd1 | ||
|
|
277193ecb0 | ||
|
|
13ebaf43e1 | ||
|
|
e03a8561b9 | ||
|
|
7f3dfe7dd7 | ||
|
|
33d11241e5 | ||
|
|
aa274b2e45 | ||
|
|
c6678e201f | ||
|
|
acbb2d2947 | ||
|
|
900074f458 | ||
|
|
cd0336696c | ||
|
|
38004228aa | ||
|
|
6a42bc0ec0 | ||
|
|
ed2523c3c5 | ||
|
|
847fda7652 | ||
|
|
2e7abf8a1e | ||
|
|
bb73fb7400 | ||
|
|
c3e6d3f902 | ||
|
|
a450154bac | ||
|
|
6192932322 | ||
|
|
6d247f426c | ||
|
|
7301243ed6 | ||
|
|
979f950129 | ||
|
|
6c1099356f | ||
|
|
c878f37ec2 | ||
|
|
9c4f6710ff | ||
|
|
78a5a9090d | ||
|
|
591c6a5592 | ||
|
|
62df40f4b3 | ||
|
|
e35c4df846 | ||
|
|
f7e045aaeb | ||
|
|
8eba549bb0 | ||
|
|
0d06d07e0f | ||
|
|
447497f344 | ||
|
|
6463a06b35 | ||
|
|
ec94e3b1d3 | ||
|
|
a84f082bd6 | ||
|
|
deb8666172 | ||
|
|
184de1c01e | ||
|
|
46dd454c4a | ||
|
|
fe5587b3eb | ||
|
|
e7be7b4bf2 | ||
|
|
3db0f004c2 | ||
|
|
bc85140264 | ||
|
|
5a0e5d252d | ||
|
|
c534f4196d | ||
|
|
d1bd8890c0 | ||
|
|
590036f082 | ||
|
|
6f13f429ce | ||
|
|
4a695fa4ef | ||
|
|
f0d91b12de | ||
|
|
bafa378c86 | ||
|
|
3d0afa3798 | ||
|
|
51a90f5fca | ||
|
|
2a685a743f | ||
|
|
62e6009aeb | ||
|
|
302532f5a4 | ||
|
|
0237e9c819 | ||
|
|
ada64f3971 | ||
|
|
fcfda4ebff | ||
|
|
10783cd1c4 | ||
|
|
ffae280d31 | ||
|
|
b40a0f8dad | ||
|
|
8ee10a3424 | ||
|
|
7d1735b1b7 | ||
|
|
1769693687 | ||
|
|
21e7a976ee | ||
|
|
c8fa6bd992 | ||
|
|
aac215678b | ||
|
|
dbc024491c | ||
|
|
99462ffeb9 | ||
|
|
aa4317a2fe | ||
|
|
9a5cf59efb | ||
|
|
83f157536b | ||
|
|
702965c7ce | ||
|
|
b26a6377ff | ||
|
|
44bc20c55c | ||
|
|
e0cb8512b1 | ||
|
|
b6e77a5934 | ||
|
|
6e93b9c7fa | ||
|
|
7d24e1818f | ||
|
|
bbe56aba55 | ||
|
|
bf722d6b76 | ||
|
|
333e5bb41d | ||
|
|
1b901b39df | ||
|
|
850c2b8c12 | ||
|
|
6f885d3a5c | ||
|
|
69e00a5fd8 | ||
|
|
d796cd208d | ||
|
|
9f7e0a8dbf | ||
|
|
55f713e3aa | ||
|
|
cfc9f09b80 | ||
|
|
d55f594342 | ||
|
|
2cd73a98e8 | ||
|
|
b98f044204 | ||
|
|
53b69d9623 | ||
|
|
4e14bd077c | ||
|
|
625d29fcf1 | ||
|
|
2e86a02343 | ||
|
|
8e1804b39d | ||
|
|
cdeac137bf | ||
|
|
fc911ea5b5 | ||
|
|
f13b04d029 | ||
|
|
c453aaaf8a | ||
|
|
e424a3cf7b | ||
|
|
62b0fcbec5 | ||
|
|
63dd081a1c | ||
|
|
253a75129a | ||
|
|
ee3b7502dc | ||
|
|
31325f4893 | ||
|
|
12828e47f9 | ||
|
|
6b0fd2a708 | ||
|
|
b18dd92079 | ||
|
|
df48450440 | ||
|
|
a42c1dc886 | ||
|
|
6ef95499b7 | ||
|
|
237a7da54a | ||
|
|
80d898e212 | ||
|
|
b8d4130270 | ||
|
|
00baff7a97 | ||
|
|
caa10b5f8f | ||
|
|
96ae1be3ec | ||
|
|
fe3c298f86 | ||
|
|
d898efdf6d | ||
|
|
cd9af13e27 | ||
|
|
63a351bfad | ||
|
|
10b435f4fa | ||
|
|
8300cde3ec | ||
|
|
cb866fb45c | ||
|
|
a9f794af1f | ||
|
|
a4814816ad | ||
|
|
11157b7c77 | ||
|
|
941ee5e172 | ||
|
|
acf89426ce | ||
|
|
d7d8cefddd | ||
|
|
900a60aaa9 | ||
|
|
1b4c53b4b3 | ||
|
|
73a8e8da40 | ||
|
|
72e9162520 | ||
|
|
4d81cd0415 | ||
|
|
c90a849a5a | ||
|
|
e47d88a4bb | ||
|
|
b4e8d47d9a | ||
|
|
23a27fde67 | ||
|
|
6d764e32a0 | ||
|
|
e2ed80f2d0 | ||
|
|
610f6533d0 | ||
|
|
7c36659fe9 | ||
|
|
f2af364624 | ||
|
|
4bd800d273 | ||
|
|
969875ad49 | ||
|
|
e8bb56ef3f | ||
|
|
bc53dafaba | ||
|
|
9f6269375f | ||
|
|
366c732668 | ||
|
|
5ba887d59b | ||
|
|
03c6de8255 | ||
|
|
30b4ed9600 | ||
|
|
cc3f7e7f7c | ||
|
|
97ee077419 | ||
|
|
65570eec33 | ||
|
|
059b69ce99 | ||
|
|
1579278c8f | ||
|
|
da3b668e8c | ||
|
|
349eca4533 | ||
|
|
347d2175be | ||
|
|
97b4eefc13 | ||
|
|
0140ffd96f | ||
|
|
a37551a45b | ||
|
|
52b3873cd7 | ||
|
|
04ca7cc783 | ||
|
|
0c025289ad | ||
|
|
40730af102 | ||
|
|
27167f93b3 | ||
|
|
4e7930f8df | ||
|
|
943669e67f | ||
|
|
7ff61add76 | ||
|
|
23706a19bc | ||
|
|
1750f93720 | ||
|
|
407250c336 | ||
|
|
7ef7096e26 | ||
|
|
6533cc4af6 | ||
|
|
a36a251f23 | ||
|
|
020f006410 | ||
|
|
6bbae67482 | ||
|
|
b8857f2dbe | ||
|
|
a3597c5d61 | ||
|
|
b6a5af08d8 | ||
|
|
3fa68ec6da | ||
|
|
102b4e021b | ||
|
|
1e1ed96d64 | ||
|
|
15dba61300 | ||
|
|
820681364c | ||
|
|
501971681e | ||
|
|
69bbc3bec3 | ||
|
|
95c3bc5ae5 | ||
|
|
71c16e0556 | ||
|
|
5e4abcf68c | ||
|
|
26e48f57eb | ||
|
|
9043ae3bf2 | ||
|
|
9ed7a9a224 | ||
|
|
62be205ab0 | ||
|
|
570daff7ff | ||
|
|
892ad353f4 | ||
|
|
4f4c8ad556 | ||
|
|
e61949d129 | ||
|
|
95a9c21014 | ||
|
|
c4e90252e0 | ||
|
|
d571e25bc3 | ||
|
|
1585da0b6f | ||
|
|
e661f6d607 | ||
|
|
831fe8d61d | ||
|
|
09565bd462 | ||
|
|
546af96792 | ||
|
|
587ea41930 | ||
|
|
b9ced1b03b | ||
|
|
12b2c7f445 | ||
|
|
26dd45eca3 | ||
|
|
7ca05112dd | ||
|
|
e13e2de7dd | ||
|
|
eac9da3bb7 | ||
|
|
603e2e24ca | ||
|
|
b6273287a9 | ||
|
|
f780023770 | ||
|
|
a6dafd2566 | ||
|
|
a7b58f0ad6 | ||
|
|
c08b07b174 | ||
|
|
0d8ed43e47 | ||
|
|
fad347236e | ||
|
|
8bd9dd3dcf | ||
|
|
177750a419 | ||
|
|
fadbb9769d | ||
|
|
bae1a47521 | ||
|
|
9429741e12 | ||
|
|
73f90458f5 | ||
|
|
591486cdd2 | ||
|
|
fa9957de0c | ||
|
|
0a49632f11 | ||
|
|
78af823a85 | ||
|
|
cc897cea79 | ||
|
|
f0f604c586 | ||
|
|
ccfc5bc91b | ||
|
|
7f12464419 | ||
|
|
7bb6e872b9 | ||
|
|
c12bf5b269 | ||
|
|
a6f51ede67 | ||
|
|
10597cc6ed | ||
|
|
fba412ddba | ||
|
|
e99e70f89e | ||
|
|
a08e98493a | ||
|
|
2eb71194ba | ||
|
|
9b9b2b52b6 | ||
|
|
f084097b9d | ||
|
|
5b095be5f8 | ||
|
|
e28beb6a5e | ||
|
|
4db6418898 | ||
|
|
c5e65973cb | ||
|
|
2cdf927d98 | ||
|
|
ef422b4f00 | ||
|
|
a5a78f21cd | ||
|
|
245f18be6e | ||
|
|
56b11b9956 | ||
|
|
77b24ad9e4 | ||
|
|
1f75fb4c92 | ||
|
|
d92e80ce92 |
214 changed files with 71341 additions and 9519 deletions
3
.fvmrc
Normal file
3
.fvmrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"flutter": "3.32.4"
|
||||
}
|
||||
32
.github/release.yml
vendored
Normal file
32
.github/release.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# .github/release.yml
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- wontfix
|
||||
- translation
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- Semver-Major
|
||||
- breaking
|
||||
- title: Security Patches
|
||||
labels:
|
||||
- security
|
||||
- title: New Features
|
||||
labels:
|
||||
- Semver-Minor
|
||||
- enhancement
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- Semver-Patch
|
||||
- bug
|
||||
- title: Devops / Setup Changes
|
||||
labels:
|
||||
- docker
|
||||
- setup
|
||||
- demo
|
||||
- CI
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
50
.github/workflows/android.yaml
vendored
50
.github/workflows/android.yaml
vendored
|
|
@ -3,10 +3,10 @@
|
|||
name: Android
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
|
@ -17,23 +17,43 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '12.x'
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v1
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup FVM
|
||||
id: fvm-config-action
|
||||
uses: kuhnroyal/flutter-fvm-config-action@v2
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '2.10.3'
|
||||
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
|
||||
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
|
||||
cache: false
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
|
||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- run: flutter --version
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/gradle-build-action@v2.4.2
|
||||
with:
|
||||
gradle-version: 6.1.1
|
||||
gradle-version: 8.7
|
||||
|
||||
- name: Collect Translation Files
|
||||
run: |
|
||||
cd lib/l10n
|
||||
python3 collect_translations.py
|
||||
|
||||
- name: Build for Android
|
||||
run: |
|
||||
flutter pub get
|
||||
cp lib/dummy_dsn.dart lib/dsn.dart
|
||||
flutter build apk --debug
|
||||
dart pub global activate fvm
|
||||
fvm install
|
||||
fvm flutter pub get
|
||||
fvm flutter build apk --debug
|
||||
|
|
|
|||
93
.github/workflows/ci.yaml
vendored
Normal file
93
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INVENTREE_SITE_URL: http://localhost:8000
|
||||
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||
INVENTREE_DB_NAME: ../inventree_unit_test_db.sqlite3
|
||||
INVENTREE_MEDIA_ROOT: ../test_inventree_media
|
||||
INVENTREE_STATIC_ROOT: ../test_inventree_static
|
||||
INVENTREE_BACKUP_DIR: ../test_inventree_backup
|
||||
INVENTREE_ADMIN_USER: testuser
|
||||
INVENTREE_ADMIN_PASSWORD: testpassword
|
||||
INVENTREE_ADMIN_EMAIL: test@test.com
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '11'
|
||||
|
||||
- name: Setup Flutter and FVM
|
||||
id: fvm-config-action
|
||||
uses: kuhnroyal/flutter-fvm-config-action@v2
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
|
||||
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
|
||||
cache: true
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
|
||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Collect Translation Files
|
||||
run: |
|
||||
cd lib/l10n
|
||||
python collect_translations.py
|
||||
|
||||
- name: Static Analysis Tests
|
||||
working-directory: .
|
||||
run: |
|
||||
python ./find_dart_files.py
|
||||
dart pub global activate fvm
|
||||
fvm install
|
||||
fvm flutter pub get
|
||||
fvm flutter analyze
|
||||
dart format --output=none --set-exit-if-changed .
|
||||
|
||||
- name: Start InvenTree Server
|
||||
run: |
|
||||
sudo apt-get install python3-dev python3-pip python3-venv python3-wheel g++
|
||||
pip3 install invoke
|
||||
git clone --depth 1 https://github.com/inventree/inventree ./inventree_server
|
||||
cd inventree_server
|
||||
invoke install
|
||||
invoke migrate
|
||||
invoke dev.import-fixtures
|
||||
invoke dev.server -a 127.0.0.1:8000 &
|
||||
invoke wait
|
||||
sleep 30
|
||||
|
||||
- name: Unit Tests
|
||||
run: |
|
||||
fvm flutter test --coverage
|
||||
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
41
.github/workflows/ios.yaml
vendored
41
.github/workflows/ios.yaml
vendored
|
|
@ -3,10 +3,10 @@
|
|||
name: iOS
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
|
|
@ -17,23 +17,44 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '12.x'
|
||||
distribution: 'temurin'
|
||||
java-version: '11'
|
||||
|
||||
- name: Setup FVM
|
||||
id: fvm-config-action
|
||||
uses: kuhnroyal/flutter-fvm-config-action@v2
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v1
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '2.10.3'
|
||||
flutter-version: ${{ steps.fvm-config-action.outputs.FLUTTER_VERSION }}
|
||||
channel: ${{ steps.fvm-config-action.outputs.FLUTTER_CHANNEL }}
|
||||
cache: false
|
||||
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
|
||||
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
pub-cache-key: "flutter-pub:os:-:channel:-:version:-:arch:-:hash:"
|
||||
pub-cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
|
||||
|
||||
- name: Collect Translation Files
|
||||
run: |
|
||||
cd lib/l10n
|
||||
python3 collect_translations.py
|
||||
|
||||
- name: Build for iOS
|
||||
run: |
|
||||
flutter pub get
|
||||
dart pub global activate fvm
|
||||
fvm install
|
||||
fvm flutter pub get
|
||||
fvm flutter precache --ios
|
||||
cd ios
|
||||
pod repo update
|
||||
pod install
|
||||
cd ..
|
||||
cp lib/dummy_dsn.dart lib/dsn.dart
|
||||
flutter build ios --release --no-codesign
|
||||
fvm flutter build ios --release --no-codesign --no-tree-shake-icons
|
||||
|
|
|
|||
37
.github/workflows/lint.yaml
vendored
37
.github/workflows/lint.yaml
vendored
|
|
@ -1,37 +0,0 @@
|
|||
# Run flutter linting checks
|
||||
|
||||
name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '12.x'
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
flutter-version: '2.10.3'
|
||||
- run: flutter pub get
|
||||
- run: cp lib/dummy_dsn.dart lib/dsn.dart
|
||||
- run: flutter analyze
|
||||
- run: flutter test --coverage
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -11,8 +11,9 @@
|
|||
|
||||
coverage/*
|
||||
|
||||
# Sentry API key
|
||||
lib/dsn.dart
|
||||
# This file is auto-generated as part of the CI process
|
||||
test/coverage_helper_test.dart
|
||||
InvenTreeSettings.db
|
||||
|
||||
# App signing key
|
||||
android/key.properties
|
||||
|
|
@ -81,3 +82,6 @@ ios/Podfile.lock
|
|||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
|||
[submodule "lib/l10n"]
|
||||
path = lib/l10n
|
||||
url = git@github.com:inventree/inventree-app-i18n.git
|
||||
8
.pre-commit-config.yaml
Normal file
8
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: dart-format
|
||||
name: Dart Format
|
||||
entry: dart format
|
||||
language: system
|
||||
types: [dart]
|
||||
132
BUILDING.md
Normal file
132
BUILDING.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
## InvenTree App Development
|
||||
|
||||
For developers looking to contribute to the project, we use Flutter for app development. The project has been tested in Android Studio (on both Windows and Mac) and also VSCode.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To build the app from source, you will need the following tools installed on your system:
|
||||
|
||||
- Android Studio or Visual Studio Code (with Flutter and Dart plugins)
|
||||
- [Flutter Version Management (FVM)](https://fvm.app/) - We use FVM to manage Flutter versions
|
||||
|
||||
### iOS Development
|
||||
|
||||
For iOS development, you will need a Mac system with XCode installed.
|
||||
|
||||
### Java Version
|
||||
|
||||
Some versions of Android Studio ship with a built-in version of the Java JDK. However, the InvenTree app requires [JDK 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) to be installed.
|
||||
|
||||
If you see any errors related to JDK version mismatch, download and install the correct version of the JDK (from the link above) and update your Android Studio settings to point to the correct JDK location:
|
||||
|
||||
```bash
|
||||
fvm flutter config --jdk-dir /path/to/jdk
|
||||
```
|
||||
|
||||
## Invoke Tasks
|
||||
|
||||
We use the [invoke](https://www.pyinvoke.org) to run some core tasks - you will need python and invoke installed on your local system.
|
||||
|
||||
## Flutter Version Management (FVM)
|
||||
|
||||
This project uses [Flutter Version Management (FVM)](https://fvm.app/) to ensure consistent Flutter versions across development environments and CI/CD pipelines.
|
||||
|
||||
For installation instructions, please refer to the [official FVM documentation](https://fvm.app/documentation/getting-started/installation).
|
||||
|
||||
Once installed, FVM will automatically use the Flutter version specified in the `.fvmrc` file at the root of the project.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
To set up Visual Studio Code, you will need to make sure the `.vscode` directory exists. Then run `fvm use` to ensure the correct Flutter version is used.
|
||||
|
||||
```
|
||||
mkdir -p .vscode
|
||||
fvm use
|
||||
```
|
||||
|
||||
#### What happens:
|
||||
- Downloads SDK if not cached
|
||||
- Creates `.fvm` directory with SDK symlink
|
||||
- Updates `.fvmrc` configuration
|
||||
- Configures IDE settings
|
||||
- Runs `flutter pub get`
|
||||
|
||||
|
||||
### Android Studio
|
||||
|
||||
To set up Android Studio, run `fvm use` to ensure the correct Flutter version is used.
|
||||
|
||||
```
|
||||
fvm use
|
||||
```
|
||||
|
||||
#### What happens:
|
||||
- Downloads SDK if not cached
|
||||
- Creates `.fvm` directory with SDK symlink
|
||||
- Updates `.fvmrc` configuration
|
||||
- Runs `flutter pub get`
|
||||
|
||||
Set Flutter SDK path in Android Studio:
|
||||
|
||||
1. Open Android Studio
|
||||
2. Go to `File` -> `Settings` -> `Languages & Frameworks` -> `Flutter`
|
||||
3. Set `Flutter SDK path` to `.fvm/flutter_sdk`:
|
||||
|
||||

|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
Initial project setup (after you have installed all required dev tools) is as follows:
|
||||
|
||||
Generate initial translation files:
|
||||
|
||||
```
|
||||
invoke translate
|
||||
```
|
||||
|
||||
Install required flutter packages:
|
||||
```
|
||||
fvm flutter pub get
|
||||
```
|
||||
|
||||
You should now be ready to debug on a connected or emulated device!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Flutter Doctor
|
||||
|
||||
If you're experiencing issues with the development environment, run Flutter Doctor to diagnose problems:
|
||||
|
||||
```bash
|
||||
fvm flutter doctor -v
|
||||
```
|
||||
|
||||
This will check your Flutter installation and identify any issues with your setup. Common issues include:
|
||||
|
||||
- Missing Android SDK components
|
||||
- iOS development tools not properly configured
|
||||
- Missing dependencies
|
||||
|
||||
Fix any identified issues before proceeding with development.
|
||||
|
||||
|
||||
## Building Release Versions
|
||||
|
||||
Building release versions for target platforms (either android or iOS) is simplified using invoke:
|
||||
|
||||
### Android
|
||||
|
||||
Build Android release:
|
||||
|
||||
```
|
||||
invoke android
|
||||
```
|
||||
|
||||
### iOS
|
||||
|
||||
Build iOS release:
|
||||
|
||||
```
|
||||
invoke ios
|
||||
```
|
||||
50
CONTRIBUTING.md
Normal file
50
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Contributing to InvenTree App
|
||||
|
||||
Thank you for considering contributing to the InvenTree App! This document outlines some guidelines to ensure smooth collaboration.
|
||||
|
||||
## Code Style and Formatting
|
||||
|
||||
### Dart Formatting
|
||||
|
||||
We enforce consistent code formatting using Dart's built-in formatter. Before submitting a pull request:
|
||||
|
||||
1. Run the formatter on your code:
|
||||
```bash
|
||||
fvm dart format .
|
||||
```
|
||||
|
||||
2. Our CI pipeline will verify that all code follows the standard Flutter/Dart formatting rules. Pull requests with improper formatting will fail CI checks.
|
||||
|
||||
### General Guidelines
|
||||
|
||||
- Write clear, readable, and maintainable code
|
||||
- Include comments where necessary
|
||||
- Follow Flutter/Dart best practices
|
||||
- Write tests for new features when applicable
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Fork the repository and create a feature branch
|
||||
2. Make your changes
|
||||
3. Ensure your code passes all tests and linting
|
||||
4. Format your code using `invoke format`
|
||||
5. Submit a pull request with a clear description of the changes
|
||||
6. Address any review comments
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. Ensure you have Flutter installed (we use Flutter Version Management)
|
||||
2. Check the required Flutter version in the `.fvmrc` file
|
||||
3. Install dependencies with `fvm flutter pub get`
|
||||
4. Run tests with `fvm flutter test`
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
When reporting issues, please include:
|
||||
- Clear steps to reproduce the issue
|
||||
- Expected behavior
|
||||
- Actual behavior
|
||||
- Screenshots if applicable
|
||||
- Device/environment information
|
||||
|
||||
Thank you for contributing to the InvenTree App!
|
||||
25
README.md
25
README.md
|
|
@ -3,11 +3,36 @@
|
|||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
[](https://coveralls.io/github/inventree/inventree-app?branch=master)
|
||||
|
||||
The InvenTree mobile / tablet application is a companion app for the [InvenTree stock management system](https://github.com/inventree/InvenTree).
|
||||
|
||||
Written in the [Flutter](https://flutter.dev/) environment, the app provides native support for Android and iOS devices.
|
||||
|
||||
<p align="center">
|
||||
<img width="30%" src="https://github.com/user-attachments/assets/aee96f90-2953-47f6-916a-06f19d3b8aa5">
|
||||
</p>
|
||||
|
||||
## Installation
|
||||
|
||||
You can install the app via the following channels:
|
||||
|
||||
### Google Play Store (Android)
|
||||
|
||||
Download and install from the [Google Play Store](https://play.google.com/store/apps/details?id=inventree.inventree_app&hl=en_AU)
|
||||
|
||||
### Apple Store (iOS)
|
||||
|
||||
Download and install from the [Apple App Store](https://apps.apple.com/au/app/inventree/id1581731101)
|
||||
|
||||
### Direct Download (Android)
|
||||
|
||||
We provide direct downloads for Android users - view our [download page via polar.sh](https://polar.sh/inventree/products/299bf0d5-af88-4e0f-becf-c007ad37ecf2)
|
||||
|
||||
## User Documentation
|
||||
|
||||
User documentation for the InvenTree mobile app can be found [within the InvenTree documentation](https://inventree.readthedocs.io/en/latest/app/app/).
|
||||
|
||||
## Developer Documentation
|
||||
|
||||
Refer to the [build instructions](BUILDING.md) for information on how to build the app from source.
|
||||
|
|
|
|||
47
RELEASE.md
47
RELEASE.md
|
|
@ -1,47 +0,0 @@
|
|||
# Release Process
|
||||
|
||||
## Android Play Store
|
||||
|
||||
[Reference](https://flutter.dev/docs/deployment/android#signing-the-app)
|
||||
|
||||
### Key File
|
||||
|
||||
Add a file `key.properties` under the android/ directory
|
||||
|
||||
### Increment Build Number
|
||||
|
||||
Make sure that the build number is incremented every time (or it will be rejected by Play Store).
|
||||
|
||||
### Copy Translations
|
||||
|
||||
Ensure that the translation files have been updated, and copied into the correct directory!!
|
||||
|
||||
```
|
||||
cd lib/l10n
|
||||
python update_translations.py
|
||||
```
|
||||
|
||||
### Build Appbundle
|
||||
|
||||
`flutter build appbundle`
|
||||
|
||||
### Upload Appbundle
|
||||
|
||||
Upload the appbundle file to the Android developer website.
|
||||
|
||||
## Apple Store
|
||||
|
||||
Ref: https://flutter.dev/docs/deployment/ios
|
||||
|
||||
### Build ipa
|
||||
|
||||
```
|
||||
flutter clean
|
||||
flutter build ipa
|
||||
```
|
||||
|
||||
### Validate and Distribute
|
||||
|
||||
- Open `./build/ios/archive/Runner.xcarchive` in Xcode
|
||||
- Run "Validate App"
|
||||
- Run "Distribute App"
|
||||
|
|
@ -6,8 +6,6 @@ analyzer:
|
|||
- lib/generated/**
|
||||
language:
|
||||
strict-raw-types: true
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
|
@ -21,6 +19,8 @@ linter:
|
|||
|
||||
prefer_double_quotes: true
|
||||
|
||||
unreachable_from_main: false
|
||||
|
||||
prefer_final_locals: false
|
||||
|
||||
prefer_const_constructors: false
|
||||
|
|
@ -75,3 +75,11 @@ linter:
|
|||
avoid_dynamic_calls: false
|
||||
|
||||
avoid_classes_with_only_static_members: false
|
||||
|
||||
no_leading_underscores_for_local_identifiers: false
|
||||
|
||||
use_super_parameters: false
|
||||
|
||||
# TODO: Enable unnecessary_async and unawaited_futures rules
|
||||
unnecessary_async: false
|
||||
|
||||
|
|
|
|||
14
android/.gitignore
vendored
Normal file
14
android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
|
|
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
|||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
|
|
@ -21,10 +22,6 @@ if (flutterVersionName == null) {
|
|||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
|
|
@ -32,7 +29,18 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
namespace "inventree.inventree_app"
|
||||
compileSdkVersion 35
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// If using Kotlin
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
|
@ -48,8 +56,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "inventree.inventree_app"
|
||||
minSdkVersion 25
|
||||
targetSdkVersion 31
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
|
@ -83,7 +91,6 @@ dependencies {
|
|||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
androidTestImplementation 'com.android.support:multidex:2.0.1'
|
||||
implementation "androidx.core:core:1.5.0-rc01"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "androidx.core:core:1.9.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="inventree.inventree_app">
|
||||
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
|
|
@ -29,10 +31,6 @@
|
|||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- until Flutter renders its first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background" />
|
||||
|
||||
<!-- Theme to apply as soon as Flutter begins rendering frames -->
|
||||
<meta-data
|
||||
|
|
@ -53,7 +51,12 @@
|
|||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.MICROPHONE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!--
|
||||
Prevent lower level dependencies from including specific permissions.
|
||||
Ref: https://developer.android.com/studio/build/manage-manifests
|
||||
-->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.INSTALL_PACKAGES" tools:node="remove"/>
|
||||
|
||||
</manifest>
|
||||
9
android/app/src/main/res/values-v31/styles.xml
Normal file
9
android/app/src/main/res/values-v31/styles.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowSplashScreenBackground">@color/splash_screen_background</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/launch_background</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="inventree.inventree_app">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
|
|
|
|||
|
|
@ -1,22 +1,8 @@
|
|||
buildscript {
|
||||
|
||||
ext.kotlin_version = '1.5.10'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +15,6 @@ subprojects {
|
|||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.parallel=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx4096M
|
||||
android.enableD8=true
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
networkTimeout=30000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
|
||||
|
|
@ -1,15 +1,25 @@
|
|||
include ':app'
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
def plugins = new Properties()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins.each { name, path ->
|
||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||
include ":$name"
|
||||
project(":$name").projectDir = pluginDirectory
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.6.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.25" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
@ -1,6 +1,23 @@
|
|||
## InvenTree App Credits
|
||||
---
|
||||
## Contributors
|
||||
|
||||
### Sound Files
|
||||
Thanks to the following contributors, for their work building this app!
|
||||
|
||||
- Some sound files have been sourced from [https://www.zapsplat.com](https://www.zapsplat.com)
|
||||
- [SchrodingersGat](https://github.com/SchrodingersGat) (*Lead Developer*)
|
||||
- [cbenhagen](https://github.com/cbenhagen)
|
||||
- [Guusggg](https://github.com/Guusggg)
|
||||
- [GoryMoon](https://github.com/GoryMoon)
|
||||
- [simonkuehling](https://github.com/simonkuehling)
|
||||
- [Bobbe](https://github.com/30350n)
|
||||
- [awnz](https://github.com/awnz)
|
||||
- [joaomnuno](https://github.com/joaomnuno)
|
||||
- [Alex9779](https://github.com/Alex9779)
|
||||
--------
|
||||
|
||||
## Assets
|
||||
|
||||
The InvenTree App makes use of the following third party assets
|
||||
|
||||
- Icons are provided by [tabler.io](https://tabler.io/icons)
|
||||
- Sound files have been sourced from [zapsplat](https://www.zapsplat.com)
|
||||
|
||||
--------
|
||||
|
|
|
|||
BIN
assets/image/logo_transparent.png
Normal file
BIN
assets/image/logo_transparent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
|
|
@ -1,6 +1,468 @@
|
|||
## InvenTree App Release Notes
|
||||
### x.xx.x - Month Year
|
||||
---
|
||||
|
||||
- Support display of custom status codes
|
||||
- Fix default values for list sorting
|
||||
|
||||
|
||||
### 0.21.2 - January 2026
|
||||
---
|
||||
|
||||
- Fixes bug which launched camera twice when uploading an attachment
|
||||
- Fixed bug related to list sorting and filtering
|
||||
|
||||
### 0.21.1 - November 2025
|
||||
---
|
||||
|
||||
- Fixed app freeze bug after form submission
|
||||
|
||||
### 0.21.0 - November 2025
|
||||
---
|
||||
|
||||
- Support label printing again, fixing issues with new printing API
|
||||
- Adds zoom controller for barcode scanner camera view
|
||||
- Display default stock location in Part detail page
|
||||
- Display stock information in SupplierPart detail page
|
||||
|
||||
### 0.20.2 - November 2025
|
||||
---
|
||||
|
||||
- Fixes URL for reporting issues on GitHub
|
||||
- Fix for uploading files against server with self-signed certificates
|
||||
|
||||
### 0.20.1 - October 2025
|
||||
---
|
||||
|
||||
- Bug fix for camera barcode scanner
|
||||
|
||||
### 0.20.0 - October 2025
|
||||
---
|
||||
|
||||
- View pending shipments from the home screen
|
||||
- Display detail view for shipments
|
||||
- Adds ability to ship pending outgoing shipments
|
||||
- Adds ability to mark outgoing shipments as "checked" or "unchecked"
|
||||
- Updated translations
|
||||
|
||||
### 0.19.3 - September 2025
|
||||
---
|
||||
|
||||
- Fixes incorrect priority of barcode scanner results
|
||||
|
||||
### 0.19.2 - August 2025
|
||||
---
|
||||
|
||||
- Allow purchase orders to be completed
|
||||
- Improved UX across the entire app
|
||||
- Fix bug which prevented display of part images for purchase order line items
|
||||
|
||||
### 0.19.1 - July 2025
|
||||
---
|
||||
- Fixes bug related to barcode scanning with certain devices
|
||||
|
||||
### 0.19.0 - June 2025
|
||||
---
|
||||
- Replace barcode scanning library for better performance
|
||||
- Display part pricing information
|
||||
- Updated theme support
|
||||
- Fix broken documentation link
|
||||
- Reduce frequency of notification checks
|
||||
- Updated translations
|
||||
- Add image cropping functionality
|
||||
|
||||
### 0.18.1 - April 2025
|
||||
---
|
||||
- Fix bug associated with handling invalid URLs
|
||||
|
||||
### 0.18.0 - April 2025
|
||||
---
|
||||
- Adds ability to create new companies from the app
|
||||
- Allow creation of line items against pending sales orders
|
||||
- Support "extra line items" for purchase orders
|
||||
- Support "extra line items" for sales orders
|
||||
- Display start date for purchase orders
|
||||
- Display start date for sales orders
|
||||
- Fix scrolling behaviour for some widgets
|
||||
- Updated search functionality
|
||||
- Updated translations
|
||||
|
||||
### 0.17.4 - January 2025
|
||||
---
|
||||
- Display responsible owner for orders
|
||||
- Display completion date for orders
|
||||
- Updated translations
|
||||
|
||||
### 0.17.3 - January 2025
|
||||
---
|
||||
|
||||
- Fixes bug which prevent dialog boxes from being dismissed correctly
|
||||
- Enable editing of attachment comments
|
||||
- Updated translations
|
||||
|
||||
### 0.17.2 - December 2024
|
||||
---
|
||||
|
||||
- Fixed error message when printing a label to a remote machine
|
||||
- Prevent notification sounds from pause media playback
|
||||
- Display stock expiry information
|
||||
- Updated translations
|
||||
|
||||
### 0.17.1 - December 2024
|
||||
---
|
||||
|
||||
- Add support for ManufacturerPart model
|
||||
- Support barcode scanning for ManufacturerPart
|
||||
- Fix bugs in global search view
|
||||
- Fixes barcode scanning bug which prevents scanning of DataMatrix codes
|
||||
- Display "destination" information in PurchaseOrder detail view
|
||||
- Pre-fill "location" field when receiving items against PurchaseOrder
|
||||
- Fix display of part name in PurchaseOrderLineItem list
|
||||
- Adds "assigned to me" filter for Purchase Order list
|
||||
- Adds "assigned to me" filter for Sales Order list
|
||||
- Updated translations
|
||||
|
||||
### 0.17.0 - December 2024
|
||||
---
|
||||
|
||||
- Improved barcode scanning with new scanning library
|
||||
- Prevent screen turning off when scanning barcodes
|
||||
- Improved support for Stock Item test results
|
||||
- Enhanced home-screen display using grid-view
|
||||
- Improvements for image uploading
|
||||
- Provide "upload image" shortcut on Purchase Order detail view
|
||||
- Provide "upload image" shortcut on Sales Order detail view
|
||||
- Clearly indicate if a StockItem is unavailable
|
||||
- Improved list filtering management
|
||||
- Updated translations
|
||||
|
||||
### 0.16.5 - September 2024
|
||||
---
|
||||
|
||||
- Allow blank values to be entered into numerical fields
|
||||
- Updated translations
|
||||
|
||||
### 0.16.4 - September 2024
|
||||
---
|
||||
|
||||
- Fixes bug related to printing stock item labels
|
||||
|
||||
### 0.16.3 - August 2024
|
||||
---
|
||||
|
||||
- Fixes bug relating to viewing attachment files
|
||||
- Fixes bug relating to uploading attachment files
|
||||
|
||||
|
||||
### 0.16.2 - August 2024
|
||||
---
|
||||
|
||||
- Support "ON_HOLD" status for Purchase Orders
|
||||
- Support "ON_HOLD" status for Sales Orders
|
||||
- Change base icon package from FontAwesome to TablerIcons
|
||||
- Bug fixes for barcode scanning
|
||||
- Translation updates
|
||||
|
||||
### 0.16.1 - July 2024
|
||||
---
|
||||
|
||||
- Update base packages for Android
|
||||
|
||||
### 0.16.0 - June 2024
|
||||
---
|
||||
|
||||
- Add support for new file attachments API
|
||||
- Drop support for legacy servers with API version < 100
|
||||
|
||||
|
||||
### 0.15.0 - June 2024
|
||||
---
|
||||
|
||||
- Support modern label printing API
|
||||
- Improved display of stock item serial numbers
|
||||
- Updated translations
|
||||
|
||||
### 0.14.3 - April 2024
|
||||
---
|
||||
|
||||
- Support "active" field for Company model
|
||||
- Support "active" field for SupplierPart model
|
||||
- Adjustments to barcode scanning workflow
|
||||
- Updated translations
|
||||
|
||||
### 0.14.2 - February 2024
|
||||
---
|
||||
|
||||
- Updated error reporting
|
||||
- Support for updated server API endpoints
|
||||
- Updated translations
|
||||
|
||||
### 0.14.1 - January 2024
|
||||
---
|
||||
|
||||
- Squashing bugs
|
||||
|
||||
### 0.14.0 - December 2023
|
||||
---
|
||||
|
||||
- Adds support for Sales Orders
|
||||
- Adds option to pause and resume barcode scanning with camera
|
||||
- Adds option for "single shot" barcode scanning with camera
|
||||
- Fixes bug when removing entire quantity of a stock item
|
||||
- Add line items to purchase orders directly from the app
|
||||
- Add line items to purchase order using barcode scanner
|
||||
- Add line items to sales orders directly from the app
|
||||
- Add line items to sales order using barcode scanner
|
||||
- Allocate stock items against existing sales orders
|
||||
|
||||
### 0.13.0 - October 2023
|
||||
---
|
||||
|
||||
- Adds "wedge scanner" mode, allowing use with external barcode readers
|
||||
- Add ability to scan in received items using supplier barcodes
|
||||
- Store API token, rather than username:password
|
||||
- Ensure that user will lose access if token is revoked by server
|
||||
- Improve scroll-to-refresh behaviour across multiple widgets
|
||||
|
||||
|
||||
### 0.12.8 - September 2023
|
||||
---
|
||||
|
||||
- Added extra options for transferring stock items
|
||||
- Fixes bug where API data was not fetched with correct locale
|
||||
|
||||
### 0.12.7 - August 2023
|
||||
---
|
||||
|
||||
- Bug fix for Supplier Part editing page
|
||||
- Bug fix for label printing (blank template names)
|
||||
- Updated translations
|
||||
|
||||
### 0.12.6 - July 2023
|
||||
---
|
||||
|
||||
- Enable label printing for stock locations
|
||||
- Enable label printing for parts
|
||||
- Updated translation support
|
||||
- Bug fixes
|
||||
|
||||
### 0.12.5 - July 2023
|
||||
---
|
||||
|
||||
- Adds extra filtering options for stock items
|
||||
- Updated translations
|
||||
|
||||
### 0.12.4 - July 2023
|
||||
---
|
||||
|
||||
- Pre-fill stock location when transferring stock amount
|
||||
- UX improvements for searching data
|
||||
- Updated translations
|
||||
|
||||
### - 0.12.3 - June 2023
|
||||
---
|
||||
|
||||
- Edit part parameters from within the app
|
||||
- Increase visibility of stock quantity in widgets
|
||||
- Improved filters for stock list
|
||||
- Bug fix for editing stock item purchase price
|
||||
|
||||
### 0.12.2 - June 2023
|
||||
---
|
||||
|
||||
- Adds options for configuring screen orientation
|
||||
- Improvements to barcode scanning
|
||||
- Translation updates
|
||||
- Bug fix for scrolling long lists
|
||||
|
||||
### 0.12.1 - May 2023
|
||||
---
|
||||
|
||||
- Fixes bug in purchase order form
|
||||
|
||||
### 0.12.0 - April 2023
|
||||
---
|
||||
|
||||
- Add support for Project Codes
|
||||
- Improve purchase order support
|
||||
- Fix action button colors
|
||||
- Improvements for stock item test result display
|
||||
- Added Norwegian translations
|
||||
- Fix serial number field when creating stock item
|
||||
|
||||
### 0.11.5 - April 2023
|
||||
---
|
||||
|
||||
- Fix background image transparency for dark mode
|
||||
- Fix link to Bill of Materials from Part screen
|
||||
- Improvements to supplier part detail screen
|
||||
- Add "notes" field to more models
|
||||
|
||||
|
||||
### 0.11.4 - April 2023
|
||||
---
|
||||
|
||||
- Bug fix for stock history widget
|
||||
- Improved display of stock history widget
|
||||
- Theme improvements for dark mode
|
||||
|
||||
### 0.11.3 - April 2023
|
||||
---
|
||||
|
||||
- Fixes text color in dark mode
|
||||
|
||||
### 0.11.2 - April 2023
|
||||
---
|
||||
|
||||
- Adds "dark mode" display option
|
||||
- Add action to issue a purchase order
|
||||
- Add action to cancel a purchase order
|
||||
- Reimplement periodic checks for notifications
|
||||
|
||||
|
||||
### 0.11.1 - April 2023
|
||||
---
|
||||
|
||||
- Fixes keyboard bug in search widget
|
||||
- Adds ability to create new purchase orders directly from the app
|
||||
- Adds support for the "contact" field to purchase orders
|
||||
- Improved rendering of status codes for stock items
|
||||
- Added rendering of status codes for purchase orders
|
||||
|
||||
### 0.11.0 - April 2023
|
||||
---
|
||||
|
||||
- Major UI updates - [see the documentation](https://docs.inventree.org/en/latest/app/app/)
|
||||
- Adds globally accessible action button for "search"
|
||||
- Adds globally accessible action button for "barcode scan"
|
||||
- Implement context actions using floating actions buttons
|
||||
- Support barcode scanning for purchase orders
|
||||
|
||||
### 0.10.2 - March 2023
|
||||
---
|
||||
|
||||
- Adds support for proper currency rendering
|
||||
- Fix icon for supplier part detail widget
|
||||
- Support global search API endpoint
|
||||
- Updated translations
|
||||
|
||||
### 0.10.1 - February 2023
|
||||
---
|
||||
|
||||
- Add support for attachments on Companies
|
||||
- Fix duplicate scanning of barcodes
|
||||
- Updated translations
|
||||
|
||||
### 0.10.0 - February 2023
|
||||
---
|
||||
|
||||
- Add support for Supplier Parts
|
||||
- Updated translations
|
||||
|
||||
### 0.9.3 - February 2023
|
||||
---
|
||||
|
||||
- Updates to match latest server API
|
||||
- Bug fix for empty HttpResponse from server
|
||||
|
||||
### 0.9.2 - December 2022
|
||||
---
|
||||
|
||||
- Support custom icons for part category
|
||||
- Support custom icons for stock location
|
||||
- Adjustments to notification messages
|
||||
- Assorted bug fixes
|
||||
- Updated translations
|
||||
|
||||
### 0.9.1 - December 2022
|
||||
---
|
||||
|
||||
- Bug fixes for custom barcode actions
|
||||
- Updated translations
|
||||
|
||||
### 0.9.0 - December 2022
|
||||
---
|
||||
|
||||
- Added support for custom barcodes for Parts
|
||||
- Added support for custom barcode for Stock Locations
|
||||
- Support Part parameters
|
||||
- Add support for structural part categories
|
||||
- Add support for structural stock locations
|
||||
- Allow deletion of attachments via app
|
||||
- Adds option for controlling BOM display
|
||||
- Updated translations
|
||||
|
||||
|
||||
### 0.8.3 - September 2022
|
||||
---
|
||||
|
||||
- Display list of assemblies which components are used in
|
||||
- Fixes search input bug
|
||||
|
||||
### 0.8.2 - August 2022
|
||||
---
|
||||
|
||||
- Allow serial numbers to be specified when creating new stock items
|
||||
- Allow serial numbers to be edited for existing stock items
|
||||
- Allow app locale to be changed manually
|
||||
- Improved handling of certain errors
|
||||
|
||||
### 0.8.1 - August 2022
|
||||
---
|
||||
|
||||
- Added extra filtering options for PartCategory list
|
||||
- Added extra filtering options for StockLocation list
|
||||
- Fixed bug related to null widget context
|
||||
- Improved error handling and reporting
|
||||
|
||||
### 0.8.0 - July 2022
|
||||
---
|
||||
|
||||
- Display part variants in the part detail view
|
||||
- Display Bill of Materials in the part detail view
|
||||
- Indicate available quantity in stock detail view
|
||||
- Adds configurable filtering to various list views
|
||||
- Allow stock location to be "scanned" into another location using barcode
|
||||
- Improves server connection status indicator on home screen
|
||||
- Display loading indicator during long-running operations
|
||||
- Improved error handling and reporting
|
||||
|
||||
### 0.7.3 - June 2022
|
||||
---
|
||||
|
||||
- Adds ability to display link URLs in attachments view
|
||||
- Updated translations
|
||||
|
||||
### 0.7.2 - June 2022
|
||||
---
|
||||
|
||||
- Add "quarantined" status flag for stock items
|
||||
- Extends attachment support to stock items
|
||||
- Extends attachment support to purchase orders
|
||||
|
||||
### 0.7.1 - May 2022
|
||||
---
|
||||
|
||||
- Fixes issue which prevented text input in search window
|
||||
- Remove support for legacy stock adjustment API
|
||||
- App now requires server API version 20 (or newer)
|
||||
- Updated translation files
|
||||
|
||||
### 0.7.0 - May 2022
|
||||
---
|
||||
|
||||
- Refactor home screen display
|
||||
- Display notification messages from InvenTree server
|
||||
- Fixes duplicated display of units when showing stock quantity
|
||||
- Adds ability to locate / identify stock items or locations (requires server plugin)
|
||||
- Improve rendering of home screen when server is not connected
|
||||
- Adds ability to load global and user settings from the server
|
||||
- Translation updates
|
||||
|
||||
### 0.6.2 - April 2022
|
||||
---
|
||||
|
||||
- Fixes issues related to locale support (for specific locales)
|
||||
|
||||
### 0.6.1 - April 2022
|
||||
---
|
||||
|
||||
|
|
|
|||
3
crowdin.yml
Normal file
3
crowdin.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
files:
|
||||
- source: /lib/l10n/app_en.arb
|
||||
translation: /lib/l10n/%locale_with_underscore%/app_%locale_with_underscore%.arb
|
||||
BIN
docs/android_studio_fvm.png
Normal file
BIN
docs/android_studio_fvm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
50
find_dart_files.py
Normal file
50
find_dart_files.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""
|
||||
This script recursively finds any '.dart' files in the ./lib directory,
|
||||
and generates a 'test' file which includes all these files.
|
||||
|
||||
This is to ensure that *all* .dart files are included in test coverage.
|
||||
By default, source files which are not touched by the unit tests are not included!
|
||||
|
||||
Ref: https://github.com/flutter/flutter/issues/27997
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
if __name__ == "__main__":
|
||||
dart_files = Path("lib").rglob("*.dart")
|
||||
|
||||
print("Discovering dart files...");
|
||||
|
||||
with open("test/coverage_helper_test.dart", "w") as f:
|
||||
f.write("// ignore_for_file: unused_import\n\n")
|
||||
f.write("// dart format off\n\n")
|
||||
|
||||
skips = [
|
||||
"generated",
|
||||
"l10n",
|
||||
"dsn.dart",
|
||||
]
|
||||
|
||||
for path in dart_files:
|
||||
path = str(path)
|
||||
|
||||
if any([s in path for s in skips]):
|
||||
continue
|
||||
|
||||
# Remove leading 'lib\' text
|
||||
path = path[4:]
|
||||
path = path.replace("\\", "/")
|
||||
f.write(f'import "package:inventree/{path}";\n')
|
||||
|
||||
f.write("\n\n")
|
||||
|
||||
f.write(
|
||||
"// DO NOT EDIT THIS FILE - it has been auto-generated by 'find_dart_files.py'\n"
|
||||
)
|
||||
f.write(
|
||||
"// It has been created to ensure that *all* source file are included in coverage data\n"
|
||||
)
|
||||
|
||||
f.write('import "package:test/test.dart";\n\n')
|
||||
f.write("// Do not actually test anything!\n")
|
||||
f.write("void main() {}\n")
|
||||
34
ios/.gitignore
vendored
Normal file
34
ios/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
|
|
@ -21,6 +21,6 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>9.0</string>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
32
ios/Flutter/ephemeral/flutter_lldb_helper.py
Normal file
32
ios/Flutter/ephemeral/flutter_lldb_helper.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
import lldb
|
||||
|
||||
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
|
||||
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
|
||||
base = frame.register["x0"].GetValueAsAddress()
|
||||
page_len = frame.register["x1"].GetValueAsUnsigned()
|
||||
|
||||
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
|
||||
# first page to see if handled it correctly. This makes diagnosing
|
||||
# misconfiguration (e.g. missing breakpoint) easier.
|
||||
data = bytearray(page_len)
|
||||
data[0:8] = b'IHELPED!'
|
||||
|
||||
error = lldb.SBError()
|
||||
frame.GetThread().GetProcess().WriteMemory(base, data, error)
|
||||
if not error.Success():
|
||||
print(f'Failed to write into {base}[+{page_len}]', error)
|
||||
return
|
||||
|
||||
def __lldb_init_module(debugger: lldb.SBDebugger, _):
|
||||
target = debugger.GetDummyTarget()
|
||||
# Caveat: must use BreakpointCreateByRegEx here and not
|
||||
# BreakpointCreateByName. For some reasons callback function does not
|
||||
# get carried over from dummy target for the later.
|
||||
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
|
||||
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
|
||||
bp.SetAutoContinue(True)
|
||||
print("-- LLDB integration loaded --")
|
||||
5
ios/Flutter/ephemeral/flutter_lldbinit
Normal file
5
ios/Flutter/ephemeral/flutter_lldbinit
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Uncomment this line to define a global platform for your projects
|
||||
platform :ios, '9.0'
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
|
@ -39,7 +39,7 @@ post_install do |installer|
|
|||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,13 +3,13 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
|
||||
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
|
|
@ -57,6 +57,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
D95D9CD46BE28F7F69DBC0F6 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -93,6 +94,7 @@
|
|||
3B8B22940C363C2F0DDB698A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 5;
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
|
|
@ -157,6 +159,9 @@
|
|||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
|
|
@ -167,26 +172,28 @@
|
|||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 0910;
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "The Chromium Authors";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
DevelopmentTeam = A5RYN267BH;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
|
|
@ -203,7 +210,6 @@
|
|||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
|
|
@ -236,10 +242,12 @@
|
|||
};
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
|
|
@ -250,6 +258,7 @@
|
|||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
|
|
@ -271,47 +280,45 @@
|
|||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/audioplayers/audioplayers.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/camera/camera.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/audioplayers_darwin/audioplayers_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/camera_avfoundation/camera_avfoundation.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/open_file/open_file.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/mobile_scanner/mobile_scanner.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/open_filex/open_filex.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/url_launcher/url_launcher.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/sqflite_darwin/sqflite_darwin.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audioplayers_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/camera_avfoundation.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_file.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mobile_scanner.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_filex.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite_darwin.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
|
@ -357,6 +364,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -366,14 +374,17 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
|
@ -384,6 +395,7 @@
|
|||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
|
@ -392,7 +404,8 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -404,6 +417,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -415,8 +429,12 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
|
|
@ -432,6 +450,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -441,14 +460,17 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
|
@ -459,6 +481,7 @@
|
|||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
|
|
@ -473,7 +496,8 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
|
@ -485,6 +509,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -494,14 +519,17 @@
|
|||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
|
|
@ -512,6 +540,7 @@
|
|||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
|
@ -520,7 +549,8 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
|
@ -532,6 +562,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -543,8 +574,12 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
|
|
@ -560,6 +595,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -571,8 +607,12 @@
|
|||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = InvenTree;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
|
|
@ -608,6 +648,20 @@
|
|||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
@ -1,10 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0910"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
|
@ -26,6 +44,7 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
|
|
@ -43,11 +62,13 @@
|
|||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
@ -12,23 +12,35 @@
|
|||
<string>6.0</string>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>de</string>
|
||||
<string>el</string>
|
||||
<string>cs-CZ</string>
|
||||
<string>da-DK</string>
|
||||
<string>de-DE</string>
|
||||
<string>el-GR</string>
|
||||
<string>en</string>
|
||||
<string>es</string>
|
||||
<string>fr</string>
|
||||
<string>he</string>
|
||||
<string>it</string>
|
||||
<string>ja</string>
|
||||
<string>ko</string>
|
||||
<string>nl</string>
|
||||
<string>no</string>
|
||||
<string>pl</string>
|
||||
<string>ru</string>
|
||||
<string>sv</string>
|
||||
<string>tr</string>
|
||||
<string>vi</string>
|
||||
<string>es-ES</string>
|
||||
<string>es-MX</string>
|
||||
<string>fa-IR</string>
|
||||
<string>fi-FI</string>
|
||||
<string>fr-FR</string>
|
||||
<string>he-IL</string>
|
||||
<string>hu-HU</string>
|
||||
<string>id-ID</string>
|
||||
<string>it-IT</string>
|
||||
<string>ja-JP</string>
|
||||
<string>ko-KR</string>
|
||||
<string>nl-NL</string>
|
||||
<string>no-NO</string>
|
||||
<string>pl-PL</string>
|
||||
<string>pt-BR</string>
|
||||
<string>pt-PT</string>
|
||||
<string>ru-RU</string>
|
||||
<string>sl_SI</string>
|
||||
<string>sv-SE</string>
|
||||
<string>th-TH</string>
|
||||
<string>tr-TR</string>
|
||||
<string>vi-VN</string>
|
||||
<string>zh-CN</string>
|
||||
<string>zh-TW</string>
|
||||
</array>
|
||||
<key>CFBundleName</key>
|
||||
<string>InvenTree</string>
|
||||
|
|
@ -67,5 +79,16 @@
|
|||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
<string>mailto</string>
|
||||
<string>tel</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
arb-dir: lib/l10n
|
||||
arb-dir: lib/l10n/collected
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: I18N
|
||||
1339
lib/api.dart
1339
lib/api.dart
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,36 @@
|
|||
import "package:adaptive_theme/adaptive_theme.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
bool isDarkMode() {
|
||||
if (!hasContext()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
import "dart:ui";
|
||||
BuildContext? context = OneContext().context;
|
||||
|
||||
const Color COLOR_GRAY = Color.fromRGBO(50, 50, 50, 1);
|
||||
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Color COLOR_CLICK = Color.fromRGBO(150, 120, 100, 0.9);
|
||||
return AdaptiveTheme.of(context).brightness == Brightness.dark;
|
||||
}
|
||||
|
||||
const Color COLOR_BLUE = Color.fromRGBO(0, 0, 250, 1);
|
||||
// Return an "action" color based on the current theme
|
||||
Color get COLOR_ACTION {
|
||||
if (isDarkMode()) {
|
||||
return Colors.lightBlueAccent;
|
||||
} else {
|
||||
return Colors.blue;
|
||||
}
|
||||
}
|
||||
|
||||
const Color COLOR_STAR = Color.fromRGBO(250, 250, 100, 1);
|
||||
// Set to null to use the system default
|
||||
Color? COLOR_APP_BAR;
|
||||
|
||||
const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);
|
||||
const Color COLOR_DANGER = Color.fromRGBO(250, 50, 50, 1);
|
||||
const Color COLOR_SUCCESS = Color.fromRGBO(50, 250, 50, 1);
|
||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 50, 250, 1);
|
||||
|
||||
const Color COLOR_SELECTED = Color.fromRGBO(0, 0, 0, 0.05);
|
||||
const Color COLOR_DANGER = Color.fromRGBO(200, 50, 75, 1);
|
||||
const Color COLOR_SUCCESS = Color.fromRGBO(100, 200, 75, 1);
|
||||
const Color COLOR_PROGRESS = Color.fromRGBO(50, 100, 200, 1);
|
||||
const Color COLOR_GRAY_LIGHT = Color.fromRGBO(150, 150, 150, 1);
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Class for managing app-level configuration options
|
||||
*/
|
||||
|
||||
import "package:sembast/sembast.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
// Settings key values
|
||||
const String INV_HOME_SHOW_SUBSCRIBED = "homeShowSubscribed";
|
||||
const String INV_HOME_SHOW_PO = "homeShowPo";
|
||||
const String INV_HOME_SHOW_MANUFACTURERS = "homeShowManufacturers";
|
||||
const String INV_HOME_SHOW_CUSTOMERS = "homeShowCustomers";
|
||||
const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers";
|
||||
|
||||
const String INV_SOUNDS_BARCODE = "barcodeSounds";
|
||||
const String INV_SOUNDS_SERVER = "serverSounds";
|
||||
|
||||
const String INV_PART_SUBCATEGORY = "partSubcategory";
|
||||
|
||||
const String INV_STOCK_SUBLOCATION = "stockSublocation";
|
||||
const String INV_STOCK_SHOW_HISTORY = "stockShowHistory";
|
||||
|
||||
const String INV_REPORT_ERRORS = "reportErrors";
|
||||
|
||||
const String INV_STRICT_HTTPS = "strictHttps";
|
||||
|
||||
class InvenTreeSettingsManager {
|
||||
|
||||
factory InvenTreeSettingsManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
InvenTreeSettingsManager._internal();
|
||||
|
||||
final store = StoreRef("settings");
|
||||
|
||||
Future<Database> get _db async => InvenTreePreferencesDB.instance.database;
|
||||
|
||||
Future<dynamic> getValue(String key, dynamic backup) async {
|
||||
|
||||
final value = await store.record(key).get(await _db);
|
||||
|
||||
if (value == null) {
|
||||
return backup;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Load a boolean setting
|
||||
Future<bool> getBool(String key, bool backup) async {
|
||||
final dynamic value = await getValue(key, backup);
|
||||
|
||||
if (value is bool) {
|
||||
return value;
|
||||
} else {
|
||||
return backup;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setValue(String key, dynamic value) async {
|
||||
|
||||
await store.record(key).put(await _db, value);
|
||||
}
|
||||
|
||||
// Ensure we only ever create a single instance of this class
|
||||
static final InvenTreeSettingsManager _manager = InvenTreeSettingsManager._internal();
|
||||
}
|
||||
579
lib/barcode.dart
579
lib/barcode.dart
|
|
@ -1,579 +0,0 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:font_awesome_flutter/font_awesome_flutter.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:qr_code_scanner/qr_code_scanner.dart";
|
||||
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
import "package:inventree/widget/location_display.dart";
|
||||
import "package:inventree/widget/part_detail.dart";
|
||||
import "package:inventree/widget/stock_detail.dart";
|
||||
|
||||
|
||||
class BarcodeHandler {
|
||||
/*
|
||||
* Class which "handles" a barcode, by communicating with the InvenTree server,
|
||||
* and handling match / unknown / error cases.
|
||||
*
|
||||
* Override functionality of this class to perform custom actions,
|
||||
* based on the response returned from the InvenTree server
|
||||
*/
|
||||
|
||||
BarcodeHandler();
|
||||
|
||||
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
||||
|
||||
QRViewController? _controller;
|
||||
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
// Called when the server "matches" a barcode
|
||||
// Override this function
|
||||
}
|
||||
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
// Called when the server does not know about a barcode
|
||||
// Override this function
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeNoMatch,
|
||||
success: false,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onBarcodeUnhandled(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
// Called when the server returns an unhandled response
|
||||
showServerError(L10().responseUnknown, data.toString());
|
||||
|
||||
_controller?.resumeCamera();
|
||||
}
|
||||
|
||||
Future<void> processBarcode(BuildContext context, QRViewController? _controller, String barcode, {String url = "barcode/"}) async {
|
||||
this._controller = _controller;
|
||||
|
||||
print("Scanned barcode data: ${barcode}");
|
||||
|
||||
if (barcode.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await InvenTreeAPI().post(
|
||||
url,
|
||||
body: {
|
||||
"barcode": barcode,
|
||||
},
|
||||
expectedStatusCode: 200
|
||||
);
|
||||
|
||||
_controller?.resumeCamera();
|
||||
|
||||
Map<String, dynamic> data = response.asMap();
|
||||
|
||||
// Handle strange response from the server
|
||||
if (!response.isValid() || !response.isMap()) {
|
||||
onBarcodeUnknown(context, {});
|
||||
|
||||
// We want to know about this one!
|
||||
await sentryReportMessage(
|
||||
"BarcodeHandler.processBarcode returned strange value",
|
||||
context: {
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"barcode": barcode,
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
}
|
||||
);
|
||||
} else if (data.containsKey("error")) {
|
||||
onBarcodeUnknown(context, data);
|
||||
} else if (data.containsKey("success")) {
|
||||
onBarcodeMatched(context, data);
|
||||
} else {
|
||||
onBarcodeUnhandled(context, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BarcodeScanHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Class for general barcode scanning.
|
||||
* Scan *any* barcode without context, and then redirect app to correct view
|
||||
*/
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeNoMatch,
|
||||
icon: FontAwesomeIcons.exclamationCircle,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
int pk = -1;
|
||||
|
||||
// A stocklocation has been passed?
|
||||
if (data.containsKey("stocklocation")) {
|
||||
|
||||
pk = (data["stocklocation"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
successTone();
|
||||
|
||||
InvenTreeStockLocation().get(pk).then((var loc) {
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockLocation,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
|
||||
} else if (data.containsKey("stockitem")) {
|
||||
|
||||
pk = (data["stockitem"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
successTone();
|
||||
|
||||
InvenTreeStockItem().get(pk).then((var item) {
|
||||
|
||||
// Dispose of the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (item is InvenTreeStockItem) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockItem,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
} else if (data.containsKey("part")) {
|
||||
|
||||
pk = (data["part"]?["pk"] ?? -1) as int;
|
||||
|
||||
if (pk > 0) {
|
||||
|
||||
successTone();
|
||||
|
||||
InvenTreePart().get(pk).then((var part) {
|
||||
|
||||
// Dismiss the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (part is InvenTreePart) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidPart,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeUnknown,
|
||||
success: false,
|
||||
onAction: () {
|
||||
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) => SimpleDialog(
|
||||
title: Text(L10().unknownResponse),
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(L10().responseData),
|
||||
subtitle: Text(data.toString()),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StockItemScanIntoLocationHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for scanning a provided StockItem into a scanned StockLocation
|
||||
*/
|
||||
|
||||
StockItemScanIntoLocationHandler(this.item);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
// If the barcode points to a "stocklocation", great!
|
||||
if (data.containsKey("stocklocation")) {
|
||||
// Extract location information
|
||||
int location = (data["stocklocation"]["pk"] ?? -1) as int;
|
||||
|
||||
if (location == -1) {
|
||||
showSnackIcon(
|
||||
L10().invalidStockLocation,
|
||||
success: false,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transfer stock to specified location
|
||||
final result = await item.transferStock(context, location);
|
||||
|
||||
if (result) {
|
||||
|
||||
successTone();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationSuccess,
|
||||
success: true,
|
||||
);
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockLocation,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StockLocationScanInItemsHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for scanning stock item(s) into the specified StockLocation
|
||||
*/
|
||||
|
||||
StockLocationScanInItemsHandler(this.location);
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
// Returned barcode must match a stock item
|
||||
if (data.containsKey("stockitem")) {
|
||||
|
||||
int item_id = data["stockitem"]["pk"] as int;
|
||||
|
||||
final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem?;
|
||||
|
||||
if (item == null) {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().invalidStockItem,
|
||||
success: false,
|
||||
);
|
||||
} else if (item.locationId == location.pk) {
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().itemInLocation,
|
||||
success: true
|
||||
);
|
||||
} else {
|
||||
final result = await item.transferStock(context, location.pk);
|
||||
|
||||
if (result) {
|
||||
|
||||
successTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationSuccess,
|
||||
success: true
|
||||
);
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
failureTone();
|
||||
|
||||
// Does not match a valid stock item!
|
||||
showSnackIcon(
|
||||
L10().invalidStockItem,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
/*
|
||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||
*/
|
||||
|
||||
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||
|
||||
// Callback function when a "unique" barcode hash is found
|
||||
final Function(String) callback;
|
||||
|
||||
final String overlayText;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) {
|
||||
if (overlayText.isEmpty) {
|
||||
return L10().barcodeScanAssign;
|
||||
} else {
|
||||
return overlayText;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(BuildContext context, Map<String, dynamic> data) async {
|
||||
|
||||
failureTone();
|
||||
|
||||
// If the barcode is known, we can"t assign it to the stock item!
|
||||
showSnackIcon(
|
||||
L10().barcodeInUse,
|
||||
icon: Icons.qr_code,
|
||||
success: false
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(BuildContext context, Map<String, dynamic> data) async {
|
||||
// If the barcode is unknown, we *can* assign it to the stock item!
|
||||
|
||||
if (!data.containsKey("hash")) {
|
||||
showServerError(
|
||||
L10().missingData,
|
||||
L10().barcodeMissingHash,
|
||||
);
|
||||
} else {
|
||||
String hash = (data["hash"] ?? "") as String;
|
||||
|
||||
if (hash.isEmpty) {
|
||||
failureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeError,
|
||||
success: false,
|
||||
);
|
||||
} else {
|
||||
|
||||
successTone();
|
||||
|
||||
// Close the barcode scanner
|
||||
Navigator.of(context).pop();
|
||||
|
||||
callback(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class InvenTreeQRView extends StatefulWidget {
|
||||
|
||||
const InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
|
||||
|
||||
final BarcodeHandler _handler;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _QRViewState(_handler);
|
||||
}
|
||||
|
||||
|
||||
class _QRViewState extends State<InvenTreeQRView> {
|
||||
|
||||
_QRViewState(this._handler) : super();
|
||||
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: "QR");
|
||||
|
||||
QRViewController? _controller;
|
||||
|
||||
final BarcodeHandler _handler;
|
||||
|
||||
bool flash_status = false;
|
||||
|
||||
Future<void> updateFlashStatus() async {
|
||||
final bool? status = await _controller?.getFlashStatus();
|
||||
|
||||
flash_status = status != null && status;
|
||||
|
||||
// Reload
|
||||
setState(() {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// In order to get hot reload to work we need to pause the camera if the platform
|
||||
// is android, or resume the camera if the platform is iOS.
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_controller!.pauseCamera();
|
||||
}
|
||||
|
||||
_controller!.resumeCamera();
|
||||
}
|
||||
|
||||
void _onViewCreated(BuildContext context, QRViewController controller) {
|
||||
_controller = controller;
|
||||
controller.scannedDataStream.listen((barcode) {
|
||||
_controller?.pauseCamera();
|
||||
|
||||
if (barcode.code != null) {
|
||||
_handler.processBarcode(context, _controller, barcode.code ?? "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(L10().scanBarcode),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.flip_camera_android),
|
||||
onPressed: () {
|
||||
_controller?.flipCamera();
|
||||
}
|
||||
),
|
||||
IconButton(
|
||||
icon: flash_status ? Icon(Icons.flash_off) : Icon(Icons.flash_on),
|
||||
onPressed: () {
|
||||
_controller?.toggleFlash();
|
||||
updateFlashStatus();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: (QRViewController controller) {
|
||||
_onViewCreated(context, controller);
|
||||
},
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: Colors.red,
|
||||
borderRadius: 10,
|
||||
borderLength: 30,
|
||||
borderWidth: 10,
|
||||
cutOutSize: 300,
|
||||
),
|
||||
)
|
||||
)
|
||||
]
|
||||
),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Spacer(),
|
||||
Padding(
|
||||
child: Text(_handler.getOverlayText(context),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
padding: EdgeInsets.all(20),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> scanQrCode(BuildContext context) async {
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => InvenTreeQRView(BarcodeScanHandler())));
|
||||
|
||||
return;
|
||||
}
|
||||
439
lib/barcode/barcode.dart
Normal file
439
lib/barcode/barcode.dart
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/inventree/sales_order.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/widget/company/manufacturer_part_detail.dart";
|
||||
import "package:inventree/widget/order/sales_order_detail.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/camera_controller.dart";
|
||||
import "package:inventree/barcode/wedge_controller.dart";
|
||||
import "package:inventree/barcode/controller.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/barcode/tones.dart";
|
||||
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/stock/location_display.dart";
|
||||
import "package:inventree/widget/part/part_detail.dart";
|
||||
import "package:inventree/widget/order/purchase_order_detail.dart";
|
||||
import "package:inventree/widget/refreshable_state.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:inventree/widget/stock/stock_detail.dart";
|
||||
import "package:inventree/widget/company/company_detail.dart";
|
||||
import "package:inventree/widget/company/supplier_part_detail.dart";
|
||||
|
||||
// Signal a barcode scan success to the user
|
||||
Future<void> barcodeSuccess(String msg) async {
|
||||
barcodeSuccessTone();
|
||||
showSnackIcon(msg, success: true);
|
||||
}
|
||||
|
||||
// Signal a barcode scan failure to the user
|
||||
Future<void> barcodeFailure(String msg, dynamic extra) async {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
msg,
|
||||
success: false,
|
||||
onAction: () {
|
||||
if (hasContext()) {
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) => SimpleDialog(
|
||||
title: Text(L10().barcodeError),
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(L10().responseData),
|
||||
subtitle: Text(extra.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Launch a barcode scanner with a particular context and handler.
|
||||
*
|
||||
* - Can be called with a custom BarcodeHandler instance, or use the default handler
|
||||
* - Returns a Future which resolves when the scanner is dismissed
|
||||
* - The provided BarcodeHandler instance is used to handle the scanned barcode
|
||||
*/
|
||||
Future<Object?> scanBarcode(
|
||||
BuildContext context, {
|
||||
BarcodeHandler? handler,
|
||||
}) async {
|
||||
// Default to generic scan handler
|
||||
handler ??= BarcodeScanHandler();
|
||||
|
||||
InvenTreeBarcodeController controller = CameraBarcodeController(handler);
|
||||
|
||||
// Select barcode controller based on user preference
|
||||
final int barcodeControllerType =
|
||||
await InvenTreeSettingsManager().getValue(
|
||||
INV_BARCODE_SCAN_TYPE,
|
||||
BARCODE_CONTROLLER_CAMERA,
|
||||
)
|
||||
as int;
|
||||
|
||||
switch (barcodeControllerType) {
|
||||
case BARCODE_CONTROLLER_WEDGE:
|
||||
controller = WedgeBarcodeController(handler);
|
||||
case BARCODE_CONTROLLER_CAMERA:
|
||||
default:
|
||||
// Already set as default option
|
||||
break;
|
||||
}
|
||||
|
||||
return Navigator.of(context).push(
|
||||
PageRouteBuilder(pageBuilder: (context, _, _) => controller, opaque: false),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class for general barcode scanning.
|
||||
* Scan *any* barcode without context, and then redirect app to correct view.
|
||||
*
|
||||
* Handles scanning of:
|
||||
*
|
||||
* - StockLocation
|
||||
* - StockItem
|
||||
* - Part
|
||||
* - SupplierPart
|
||||
* - PurchaseOrder
|
||||
*/
|
||||
class BarcodeScanHandler extends BarcodeHandler {
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeNoMatch,
|
||||
icon: TablerIcons.exclamation_circle,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "Part" instance is scanned
|
||||
*/
|
||||
Future<void> handlePart(int pk) async {
|
||||
var part = await InvenTreePart().get(pk);
|
||||
|
||||
if (part is InvenTreePart) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(builder: (context) => PartDetailWidget(part)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "StockItem" instance is scanned
|
||||
*/
|
||||
Future<void> handleStockItem(int pk) async {
|
||||
var item = await InvenTreeStockItem().get(pk);
|
||||
|
||||
if (item is InvenTreeStockItem) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(builder: (context) => StockDetailWidget(item)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "StockLocation" instance is scanned
|
||||
*/
|
||||
Future<void> handleStockLocation(int pk) async {
|
||||
var loc = await InvenTreeStockLocation().get(pk);
|
||||
|
||||
if (loc is InvenTreeStockLocation) {
|
||||
OneContext().pop();
|
||||
OneContext().navigator.push(
|
||||
MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "SupplierPart" instance is scanned
|
||||
*/
|
||||
Future<void> handleSupplierPart(int pk) async {
|
||||
var supplierPart = await InvenTreeSupplierPart().get(pk);
|
||||
|
||||
if (supplierPart is InvenTreeSupplierPart) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SupplierPartDetailWidget(supplierPart),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "ManufacturerPart" instance is scanned
|
||||
*/
|
||||
Future<void> handleManufacturerPart(int pk) async {
|
||||
var manufacturerPart = await InvenTreeManufacturerPart().get(pk);
|
||||
|
||||
if (manufacturerPart is InvenTreeManufacturerPart) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ManufacturerPartDetailWidget(manufacturerPart),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleCompany(int pk) async {
|
||||
var company = await InvenTreeCompany().get(pk);
|
||||
|
||||
if (company is InvenTreeCompany) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(builder: (context) => CompanyDetailWidget(company)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response when a "PurchaseOrder" instance is scanned
|
||||
*/
|
||||
Future<void> handlePurchaseOrder(int pk) async {
|
||||
var order = await InvenTreePurchaseOrder().get(pk);
|
||||
|
||||
if (order is InvenTreePurchaseOrder) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PurchaseOrderDetailWidget(order),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Response when a SalesOrder instance is scanned
|
||||
Future<void> handleSalesOrder(int pk) async {
|
||||
var order = await InvenTreeSalesOrder().get(pk);
|
||||
|
||||
if (order is InvenTreeSalesOrder) {
|
||||
OneContext().pop();
|
||||
OneContext().push(
|
||||
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(order)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
int pk = -1;
|
||||
|
||||
String model = "";
|
||||
|
||||
// The following model types can be matched with barcodes
|
||||
List<String> validModels = [
|
||||
InvenTreeStockItem.MODEL_TYPE,
|
||||
InvenTreeSupplierPart.MODEL_TYPE,
|
||||
InvenTreeManufacturerPart.MODEL_TYPE,
|
||||
InvenTreePart.MODEL_TYPE,
|
||||
InvenTreeStockLocation.MODEL_TYPE,
|
||||
InvenTreeCompany.MODEL_TYPE,
|
||||
];
|
||||
|
||||
if (InvenTreeAPI().supportsOrderBarcodes) {
|
||||
validModels.add(InvenTreePurchaseOrder.MODEL_TYPE);
|
||||
validModels.add(InvenTreeSalesOrder.MODEL_TYPE);
|
||||
}
|
||||
|
||||
for (var key in validModels) {
|
||||
if (data.containsKey(key)) {
|
||||
try {
|
||||
pk = (data[key]?["pk"] ?? -1) as int;
|
||||
|
||||
// Break on the first valid match found
|
||||
if (pk > 0) {
|
||||
model = key;
|
||||
break;
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
sentryReportError("onBarcodeMatched", error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A valid result has been found
|
||||
if (pk > 0 && model.isNotEmpty) {
|
||||
barcodeSuccessTone();
|
||||
|
||||
switch (model) {
|
||||
case InvenTreeStockItem.MODEL_TYPE:
|
||||
await handleStockItem(pk);
|
||||
return;
|
||||
case InvenTreePurchaseOrder.MODEL_TYPE:
|
||||
await handlePurchaseOrder(pk);
|
||||
return;
|
||||
case InvenTreeSalesOrder.MODEL_TYPE:
|
||||
await handleSalesOrder(pk);
|
||||
return;
|
||||
case InvenTreeStockLocation.MODEL_TYPE:
|
||||
await handleStockLocation(pk);
|
||||
return;
|
||||
case InvenTreeSupplierPart.MODEL_TYPE:
|
||||
await handleSupplierPart(pk);
|
||||
return;
|
||||
case InvenTreeManufacturerPart.MODEL_TYPE:
|
||||
await handleManufacturerPart(pk);
|
||||
return;
|
||||
case InvenTreePart.MODEL_TYPE:
|
||||
await handlePart(pk);
|
||||
return;
|
||||
case InvenTreeCompany.MODEL_TYPE:
|
||||
await handleCompany(pk);
|
||||
return;
|
||||
default:
|
||||
// Fall through to failure state
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we have not found a valid barcode result!
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeUnknown,
|
||||
success: false,
|
||||
onAction: () {
|
||||
if (hasContext()) {
|
||||
OneContext().showDialog(
|
||||
builder: (BuildContext context) => SimpleDialog(
|
||||
title: Text(L10().unknownResponse),
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(L10().responseData),
|
||||
subtitle: Text(data.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barcode handler for finding a "unique" barcode (one that does not match an item in the database)
|
||||
*/
|
||||
class UniqueBarcodeHandler extends BarcodeHandler {
|
||||
UniqueBarcodeHandler(this.callback, {this.overlayText = ""});
|
||||
|
||||
// Callback function when a "unique" barcode hash is found
|
||||
final Function(String) callback;
|
||||
|
||||
final String overlayText;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) {
|
||||
if (overlayText.isEmpty) {
|
||||
return L10().barcodeScanAssign;
|
||||
} else {
|
||||
return overlayText;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
if (!data.containsKey("hash") && !data.containsKey("barcode_hash")) {
|
||||
showServerError("barcode/", L10().missingData, L10().barcodeMissingHash);
|
||||
} else {
|
||||
String barcode;
|
||||
|
||||
barcode = (data["barcode_data"] ?? "") as String;
|
||||
|
||||
if (barcode.isEmpty) {
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(L10().barcodeError, success: false);
|
||||
} else {
|
||||
barcodeSuccessTone();
|
||||
|
||||
// Close the barcode scanner
|
||||
if (OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
}
|
||||
|
||||
callback(barcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
await onBarcodeMatched(data);
|
||||
}
|
||||
}
|
||||
|
||||
SpeedDialChild customBarcodeAction(
|
||||
BuildContext context,
|
||||
RefreshableState state,
|
||||
String barcode,
|
||||
String model,
|
||||
int pk,
|
||||
) {
|
||||
if (barcode.isEmpty) {
|
||||
return SpeedDialChild(
|
||||
label: L10().barcodeAssign,
|
||||
child: Icon(Icons.barcode_reader),
|
||||
onTap: () {
|
||||
var handler = UniqueBarcodeHandler((String barcode) {
|
||||
InvenTreeAPI()
|
||||
.linkBarcode({model: pk.toString(), "barcode": barcode})
|
||||
.then((bool result) {
|
||||
showSnackIcon(
|
||||
result ? L10().barcodeAssigned : L10().barcodeNotAssigned,
|
||||
success: result,
|
||||
);
|
||||
|
||||
state.refresh(context);
|
||||
});
|
||||
});
|
||||
scanBarcode(context, handler: handler);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return SpeedDialChild(
|
||||
child: Icon(Icons.barcode_reader),
|
||||
label: L10().barcodeUnassign,
|
||||
onTap: () {
|
||||
InvenTreeAPI().unlinkBarcode({model: pk.toString()}).then((
|
||||
bool result,
|
||||
) {
|
||||
showSnackIcon(
|
||||
result ? L10().requestSuccessful : L10().requestFailed,
|
||||
success: result,
|
||||
);
|
||||
|
||||
state.refresh(context);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
398
lib/barcode/camera_controller.dart
Normal file
398
lib/barcode/camera_controller.dart
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:camera/camera.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_speed_dial/flutter_speed_dial.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:mobile_scanner/mobile_scanner.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:wakelock_plus/wakelock_plus.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/barcode/controller.dart";
|
||||
|
||||
/*
|
||||
* Barcode controller which uses the device's camera to scan barcodes.
|
||||
* Under the hood it uses the qr_code_scanner package.
|
||||
*/
|
||||
class CameraBarcodeController extends InvenTreeBarcodeController {
|
||||
const CameraBarcodeController(BarcodeHandler handler, {Key? key})
|
||||
: super(handler, key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CameraBarcodeControllerState();
|
||||
}
|
||||
|
||||
class _CameraBarcodeControllerState extends InvenTreeBarcodeControllerState {
|
||||
_CameraBarcodeControllerState() : super();
|
||||
|
||||
bool flash_status = false;
|
||||
|
||||
int scan_delay = 500;
|
||||
bool single_scanning = false;
|
||||
bool scanning_paused = false;
|
||||
bool multiple_barcodes = false;
|
||||
|
||||
String scanned_code = "";
|
||||
|
||||
double zoomFactor = 0.0;
|
||||
|
||||
final MobileScannerController controller = MobileScannerController(
|
||||
autoZoom: false, // Disable autoZoom as we implement a manual slider
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
WakelockPlus.enable();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
controller.dispose();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
|
||||
/*
|
||||
* Load the barcode scanning settings
|
||||
*/
|
||||
Future<void> _loadSettings() async {
|
||||
bool _single = await InvenTreeSettingsManager().getBool(
|
||||
INV_BARCODE_SCAN_SINGLE,
|
||||
false,
|
||||
);
|
||||
|
||||
int _delay =
|
||||
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
|
||||
as int;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scan_delay = _delay;
|
||||
single_scanning = _single;
|
||||
scanning_paused = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pauseScan() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scanning_paused = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumeScan() async {
|
||||
controller.start();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scanning_paused = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Callback function when a barcode is scanned
|
||||
*/
|
||||
Future<void> onScanSuccess(BarcodeCapture result) async {
|
||||
if (!mounted || scanning_paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Display outline of barcodes on the screen?
|
||||
|
||||
if (result.barcodes.isEmpty) {
|
||||
setState(() {
|
||||
multiple_barcodes = false;
|
||||
});
|
||||
} else if (result.barcodes.length > 1) {
|
||||
setState(() {
|
||||
multiple_barcodes = true;
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
setState(() {
|
||||
multiple_barcodes = false;
|
||||
});
|
||||
}
|
||||
|
||||
String barcode = result.barcodes.first.rawValue ?? "";
|
||||
|
||||
if (barcode.isEmpty) {
|
||||
// TODO: Error message "empty barcode"
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
scanned_code = barcode;
|
||||
});
|
||||
|
||||
pauseScan();
|
||||
|
||||
await handleBarcodeData(barcode).then((_) {
|
||||
if (!single_scanning && mounted) {
|
||||
resumeScan();
|
||||
}
|
||||
});
|
||||
|
||||
resumeScan();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scanned_code = "";
|
||||
multiple_barcodes = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void onControllerCreated(CameraController? controller, Exception? error) {
|
||||
if (error != null) {
|
||||
sentryReportError(
|
||||
"CameraBarcodeController.onControllerCreated",
|
||||
error,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
if (controller == null) {
|
||||
showSnackIcon(
|
||||
L10().cameraCreationError,
|
||||
icon: TablerIcons.camera_x,
|
||||
success: false,
|
||||
);
|
||||
|
||||
if (OneContext.hasContext) {
|
||||
Navigator.pop(OneContext().context!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget BarcodeOverlay(BuildContext context) {
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
final double width = screenSize.width;
|
||||
final double height = screenSize.height;
|
||||
|
||||
final double D = min(width, height) * 0.8;
|
||||
|
||||
// Color for the barcode scan?
|
||||
Color overlayColor = COLOR_ACTION;
|
||||
|
||||
if (multiple_barcodes) {
|
||||
overlayColor = COLOR_DANGER;
|
||||
} else if (scanned_code.isNotEmpty) {
|
||||
overlayColor = COLOR_SUCCESS;
|
||||
} else if (scanning_paused) {
|
||||
overlayColor = COLOR_WARNING;
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: D,
|
||||
height: D,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: overlayColor, width: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Build the barcode reader widget
|
||||
*/
|
||||
Widget BarcodeReader(BuildContext context) {
|
||||
final Size screenSize = MediaQuery.of(context).size;
|
||||
final double width = screenSize.width;
|
||||
final double height = screenSize.height;
|
||||
|
||||
final double D = min(width, height) * 0.8;
|
||||
|
||||
return MobileScanner(
|
||||
controller: controller,
|
||||
overlayBuilder: (context, constraints) {
|
||||
return BarcodeOverlay(context);
|
||||
},
|
||||
scanWindow: Rect.fromCenter(
|
||||
center: Offset(width / 2, height / 2),
|
||||
width: D,
|
||||
height: D,
|
||||
),
|
||||
onDetect: (result) {
|
||||
onScanSuccess(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget topCenterOverlay() {
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, right: 10, top: 75, bottom: 10),
|
||||
child: Text(
|
||||
widget.handler.getOverlayText(context),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget bottomCenterOverlay() {
|
||||
String info_text = scanning_paused
|
||||
? L10().barcodeScanPaused
|
||||
: L10().barcodeScanPause;
|
||||
|
||||
String text = scanned_code.isNotEmpty ? scanned_code : info_text;
|
||||
|
||||
if (text.length > 50) {
|
||||
text = text.substring(0, 50) + "...";
|
||||
}
|
||||
|
||||
return SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 75),
|
||||
child: Text(
|
||||
text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget? buildActions(BuildContext context) {
|
||||
List<SpeedDialChild> actions = [
|
||||
SpeedDialChild(
|
||||
child: Icon(flash_status ? TablerIcons.bulb_off : TablerIcons.bulb),
|
||||
label: L10().toggleTorch,
|
||||
onTap: () async {
|
||||
controller.toggleTorch();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
flash_status = !flash_status;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: Icon(TablerIcons.camera),
|
||||
label: L10().switchCamera,
|
||||
onTap: () async {
|
||||
controller.switchCamera();
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SpeedDial(icon: Icons.more_horiz, children: actions);
|
||||
}
|
||||
|
||||
Widget zoomSlider() {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 16,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 225,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(TablerIcons.zoom_out, color: Colors.white, size: 20),
|
||||
Expanded(
|
||||
child: Slider(
|
||||
value: zoomFactor,
|
||||
min: 0.0,
|
||||
max: 1.0,
|
||||
activeColor: Colors.white,
|
||||
inactiveColor: Colors.white.withValues(alpha: 0.3),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
zoomFactor = value;
|
||||
controller.setZoomScale(value);
|
||||
});
|
||||
},
|
||||
onChangeStart: (value) async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scanning_paused = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
onChangeEnd: (value) async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scanning_paused = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Icon(TablerIcons.zoom_in, color: Colors.white, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
title: Text(L10().scanBarcode),
|
||||
),
|
||||
floatingActionButton: buildActions(context),
|
||||
body: GestureDetector(
|
||||
onTap: () async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Toggle the 'scan paused' state
|
||||
scanning_paused = !scanning_paused;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(children: [Expanded(child: BarcodeReader(context))]),
|
||||
topCenterOverlay(),
|
||||
bottomCenterOverlay(),
|
||||
zoomSlider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/barcode/controller.dart
Normal file
103
lib/barcode/controller.dart
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
/*
|
||||
* Generic class which provides a barcode scanner interface.
|
||||
*
|
||||
* When the controller is instantiated, it is passed a "handler" class,
|
||||
* which is used to process the scanned barcode.
|
||||
*/
|
||||
class InvenTreeBarcodeController extends StatefulWidget {
|
||||
const InvenTreeBarcodeController(this.handler, {Key? key}) : super(key: key);
|
||||
|
||||
final BarcodeHandler handler;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => InvenTreeBarcodeControllerState();
|
||||
}
|
||||
|
||||
/*
|
||||
* Base state widget for the barcode controller.
|
||||
* This defines the basic interface for the barcode controller.
|
||||
*/
|
||||
class InvenTreeBarcodeControllerState
|
||||
extends State<InvenTreeBarcodeController> {
|
||||
InvenTreeBarcodeControllerState() : super();
|
||||
|
||||
final GlobalKey barcodeControllerKey = GlobalKey(
|
||||
debugLabel: "barcodeController",
|
||||
);
|
||||
|
||||
// Internal state flag to test if we are currently processing a barcode
|
||||
bool processingBarcode = false;
|
||||
|
||||
/*
|
||||
* Method to handle scanned data.
|
||||
* Any implementing class should call this method when a barcode is scanned.
|
||||
* Barcode data should be passed as a string
|
||||
*/
|
||||
Future<void> handleBarcodeData(String? data) async {
|
||||
// Check that the data is valid, and this view is still mounted
|
||||
if (!mounted || data == null || data.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Currently processing a barcode - ignore this one
|
||||
if (processingBarcode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
processingBarcode = true;
|
||||
});
|
||||
|
||||
showLoadingOverlay();
|
||||
await pauseScan();
|
||||
|
||||
await widget.handler.processBarcode(data);
|
||||
|
||||
// processBarcode may have popped the context
|
||||
if (!mounted) {
|
||||
hideLoadingOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
int delay =
|
||||
await InvenTreeSettingsManager().getValue(INV_BARCODE_SCAN_DELAY, 500)
|
||||
as int;
|
||||
|
||||
Future.delayed(Duration(milliseconds: delay), () {
|
||||
hideLoadingOverlay();
|
||||
if (mounted) {
|
||||
resumeScan().then((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
processingBarcode = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hook function to "pause" the barcode scanner
|
||||
Future<void> pauseScan() async {
|
||||
// Implement this function in subclass
|
||||
}
|
||||
|
||||
// Hook function to "resume" the barcode scanner
|
||||
Future<void> resumeScan() async {
|
||||
// Implement this function in subclass
|
||||
}
|
||||
|
||||
/*
|
||||
* Implementing classes are in control of building out the widget
|
||||
*/
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
130
lib/barcode/handler.dart
Normal file
130
lib/barcode/handler.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/tones.dart";
|
||||
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
/* Generic class which "handles" a barcode, by communicating with the InvenTree server,
|
||||
* and handling match / unknown / error cases.
|
||||
*
|
||||
* Override functionality of this class to perform custom actions,
|
||||
* based on the response returned from the InvenTree server
|
||||
*/
|
||||
class BarcodeHandler {
|
||||
BarcodeHandler();
|
||||
|
||||
// Return the text to display on the barcode overlay
|
||||
// Note: Will be overridden by child classes
|
||||
String getOverlayText(BuildContext context) => "Barcode Overlay";
|
||||
|
||||
// Called when the server "matches" a barcode
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// Override this function
|
||||
}
|
||||
|
||||
// Called when the server does not know about a barcode
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
// Override this function
|
||||
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
(data["error"] ?? L10().barcodeNoMatch) as String,
|
||||
success: false,
|
||||
icon: Icons.qr_code,
|
||||
);
|
||||
}
|
||||
|
||||
// Called when the server returns an unhandled response
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||
barcodeFailureTone();
|
||||
showServerError("barcode/", L10().responseUnknown, data.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Base function to capture and process barcode data.
|
||||
*
|
||||
* Returns true only if the barcode scanner should remain open
|
||||
*/
|
||||
Future<void> processBarcode(
|
||||
String barcode, {
|
||||
String url = "barcode/",
|
||||
Map<String, dynamic> extra_data = const {},
|
||||
}) async {
|
||||
debug("Scanned barcode data: '${barcode}'");
|
||||
|
||||
barcode = barcode.trim();
|
||||
|
||||
// Empty barcode is invalid
|
||||
if (barcode.isEmpty) {
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(
|
||||
L10().barcodeError,
|
||||
icon: TablerIcons.exclamation_circle,
|
||||
success: false,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
APIResponse? response;
|
||||
|
||||
try {
|
||||
response = await InvenTreeAPI().post(
|
||||
url,
|
||||
body: {"barcode": barcode, ...extra_data},
|
||||
expectedStatusCode: null, // Do not show an error on "unexpected code"
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
sentryReportError("Barcode.processBarcode", error, stackTrace);
|
||||
response = null;
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().barcodeError, success: false);
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Barcode scan response" + response.data.toString());
|
||||
|
||||
Map<String, dynamic> data = response.asMap();
|
||||
|
||||
// Handle strange response from the server
|
||||
if (!response.isValid() || !response.isMap()) {
|
||||
await onBarcodeUnknown({});
|
||||
|
||||
showSnackIcon(L10().serverError, success: false);
|
||||
|
||||
// We want to know about this one!
|
||||
await sentryReportMessage(
|
||||
"BarcodeHandler.processBarcode returned unexpected value",
|
||||
context: {
|
||||
"data": response.data?.toString() ?? "null",
|
||||
"barcode": barcode,
|
||||
"url": url,
|
||||
"statusCode": response.statusCode.toString(),
|
||||
"valid": response.isValid().toString(),
|
||||
"error": response.error,
|
||||
"errorDetail": response.errorDetail,
|
||||
"className": "${this}",
|
||||
},
|
||||
);
|
||||
} else if (data.containsKey("success")) {
|
||||
await onBarcodeMatched(data);
|
||||
} else if ((response.statusCode >= 400) || data.containsKey("error")) {
|
||||
await onBarcodeUnknown(data);
|
||||
} else {
|
||||
await onBarcodeUnhandled(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
194
lib/barcode/purchase_order.dart
Normal file
194
lib/barcode/purchase_order.dart
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/barcode.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/barcode/tones.dart";
|
||||
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
/*
|
||||
* Barcode handler class for scanning a supplier barcode to receive a part
|
||||
*
|
||||
* - The class can be initialized by optionally passing a valid, placed PurchaseOrder object
|
||||
* - Expects to scan supplier barcode, possibly containing order_number and quantity
|
||||
* - If location or quantity information wasn't provided, show a form to fill it in
|
||||
*/
|
||||
class POReceiveBarcodeHandler extends BarcodeHandler {
|
||||
POReceiveBarcodeHandler({this.purchaseOrder, this.location, this.lineItem});
|
||||
|
||||
InvenTreePurchaseOrder? purchaseOrder;
|
||||
InvenTreeStockLocation? location;
|
||||
InvenTreePOLineItem? lineItem;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeReceivePart;
|
||||
|
||||
@override
|
||||
Future<void> processBarcode(
|
||||
String barcode, {
|
||||
String url = "barcode/po-receive/",
|
||||
Map<String, dynamic> extra_data = const {},
|
||||
}) async {
|
||||
final bool confirm = await InvenTreeSettingsManager().getBool(
|
||||
INV_PO_CONFIRM_SCAN,
|
||||
true,
|
||||
);
|
||||
|
||||
final po_extra_data = {
|
||||
"purchase_order": purchaseOrder?.pk,
|
||||
"location": location?.pk,
|
||||
"line_item": lineItem?.pk,
|
||||
"auto_allocate": !confirm,
|
||||
...extra_data,
|
||||
};
|
||||
|
||||
return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
if (data.containsKey("lineitem") || data.containsKey("success")) {
|
||||
barcodeSuccess(L10().receivedItem);
|
||||
return;
|
||||
} else {
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||
if (!data.containsKey("action_required") || !data.containsKey("lineitem")) {
|
||||
return super.onBarcodeUnhandled(data);
|
||||
}
|
||||
|
||||
final lineItemData = data["lineitem"] as Map<String, dynamic>;
|
||||
if (!lineItemData.containsKey("pk") ||
|
||||
!lineItemData.containsKey("purchase_order")) {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().missingData, success: false);
|
||||
}
|
||||
|
||||
// At minimum, we need the line item ID value
|
||||
final int? lineItemId = lineItemData["pk"] as int?;
|
||||
|
||||
if (lineItemId == null) {
|
||||
barcodeFailureTone();
|
||||
return;
|
||||
}
|
||||
|
||||
InvenTreePOLineItem? lineItem =
|
||||
await InvenTreePOLineItem().get(lineItemId) as InvenTreePOLineItem?;
|
||||
|
||||
if (lineItem == null) {
|
||||
barcodeFailureTone();
|
||||
return;
|
||||
}
|
||||
|
||||
// Next, extract the "optional" fields
|
||||
|
||||
// Extract information from the returned server response
|
||||
double? quantity = double.tryParse(
|
||||
(lineItemData["quantity"] ?? "0").toString(),
|
||||
);
|
||||
int? destination = lineItemData["location"] as int?;
|
||||
String? barcode = data["barcode_data"] as String?;
|
||||
|
||||
// Discard the barcode scanner at this stage
|
||||
if (OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
}
|
||||
|
||||
await lineItem.receive(
|
||||
OneContext().context!,
|
||||
destination: destination,
|
||||
quantity: quantity,
|
||||
barcode: barcode,
|
||||
onSuccess: () {
|
||||
showSnackIcon(L10().receivedItem, success: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
data["error"] as String? ?? L10().barcodeError,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barcode handler to add a line item to a purchase order
|
||||
*/
|
||||
class POAllocateBarcodeHandler extends BarcodeHandler {
|
||||
POAllocateBarcodeHandler({this.purchaseOrder});
|
||||
|
||||
InvenTreePurchaseOrder? purchaseOrder;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().scanSupplierPart;
|
||||
|
||||
@override
|
||||
Future<void> processBarcode(
|
||||
String barcode, {
|
||||
String url = "barcode/po-allocate/",
|
||||
Map<String, dynamic> extra_data = const {},
|
||||
}) {
|
||||
final po_extra_data = {"purchase_order": purchaseOrder?.pk, ...extra_data};
|
||||
|
||||
return super.processBarcode(barcode, url: url, extra_data: po_extra_data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// Server must respond with a suppliertpart instance
|
||||
if (!data.containsKey("supplierpart")) {
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
|
||||
dynamic supplier_part = data["supplierpart"];
|
||||
|
||||
int supplier_part_pk = -1;
|
||||
|
||||
if (supplier_part is Map<String, dynamic>) {
|
||||
supplier_part_pk = (supplier_part["pk"] ?? -1) as int;
|
||||
} else {
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
|
||||
// Dispose of the barcode scanner
|
||||
if (OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
}
|
||||
|
||||
final context = OneContext().context!;
|
||||
|
||||
var fields = InvenTreePOLineItem().formFields();
|
||||
|
||||
fields["order"]?["value"] = purchaseOrder!.pk;
|
||||
fields["part"]?["hidden"] = false;
|
||||
fields["part"]?["value"] = supplier_part_pk;
|
||||
|
||||
InvenTreePOLineItem().createForm(
|
||||
context,
|
||||
L10().lineItemAdd,
|
||||
fields: fields,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||
print("onBarcodeUnhandled:");
|
||||
print(data.toString());
|
||||
|
||||
super.onBarcodeUnhandled(data);
|
||||
}
|
||||
}
|
||||
163
lib/barcode/sales_order.dart
Normal file
163
lib/barcode/sales_order.dart
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/api_form.dart";
|
||||
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/sales_order.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/barcode.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/barcode/tones.dart";
|
||||
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
/*
|
||||
* Barcode handler class for scanning a new part into a SalesOrder
|
||||
*/
|
||||
|
||||
class SOAddItemBarcodeHandler extends BarcodeHandler {
|
||||
SOAddItemBarcodeHandler({this.salesOrder});
|
||||
|
||||
InvenTreeSalesOrder? salesOrder;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanPart;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// Extract the part ID from the returned data
|
||||
int part_id = -1;
|
||||
|
||||
if (data.containsKey("part")) {
|
||||
part_id = (data["part"] ?? {} as Map<String, dynamic>)["pk"] as int;
|
||||
}
|
||||
|
||||
if (part_id <= 0) {
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
|
||||
// Request the part from the server
|
||||
var part = await InvenTreePart().get(part_id);
|
||||
|
||||
if (part is InvenTreePart) {
|
||||
if (part.isSalable) {
|
||||
// Dispose of the barcode scanner
|
||||
if (OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
}
|
||||
|
||||
final context = OneContext().context!;
|
||||
|
||||
var fields = InvenTreeSOLineItem().formFields();
|
||||
|
||||
fields["order"]?["value"] = salesOrder!.pk;
|
||||
fields["order"]?["hidden"] = true;
|
||||
|
||||
fields["part"]?["value"] = part.pk;
|
||||
fields["part"]?["hidden"] = false;
|
||||
|
||||
InvenTreeSOLineItem().createForm(
|
||||
context,
|
||||
L10().lineItemAdd,
|
||||
fields: fields,
|
||||
);
|
||||
} else {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().partNotSalable, success: false);
|
||||
}
|
||||
} else {
|
||||
// Failed to fetch part
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SOAllocateStockHandler extends BarcodeHandler {
|
||||
SOAllocateStockHandler({this.salesOrder, this.lineItem, this.shipment});
|
||||
|
||||
InvenTreeSalesOrder? salesOrder;
|
||||
InvenTreeSOLineItem? lineItem;
|
||||
InvenTreeSalesOrderShipment? shipment;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().allocateStock;
|
||||
|
||||
@override
|
||||
Future<void> processBarcode(
|
||||
String barcode, {
|
||||
String url = "barcode/so-allocate/",
|
||||
Map<String, dynamic> extra_data = const {},
|
||||
}) {
|
||||
final so_extra_data = {
|
||||
"sales_order": salesOrder?.pk,
|
||||
"shipment": shipment?.pk,
|
||||
"line": lineItem?.pk,
|
||||
...extra_data,
|
||||
};
|
||||
|
||||
return super.processBarcode(barcode, url: url, extra_data: so_extra_data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
if (!data.containsKey("line_item")) {
|
||||
return onBarcodeUnknown(data);
|
||||
}
|
||||
|
||||
barcodeSuccess(L10().allocated);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
|
||||
if (!data.containsKey("action_required") ||
|
||||
!data.containsKey("line_item")) {
|
||||
return super.onBarcodeUnhandled(data);
|
||||
}
|
||||
|
||||
// Prompt user for extra information to create the allocation
|
||||
var fields = InvenTreeSOLineItem().allocateFormFields();
|
||||
|
||||
// Update fields with data gathered from the API response
|
||||
fields["line_item"]?["value"] = data["line_item"];
|
||||
|
||||
Map<String, dynamic> stock_filters = {"in_stock": true, "available": true};
|
||||
|
||||
if (data.containsKey("part")) {
|
||||
stock_filters["part"] = data["part"];
|
||||
}
|
||||
|
||||
fields["stock_item"]?["filters"] = stock_filters;
|
||||
fields["stock_item"]?["value"] = data["stock_item"];
|
||||
|
||||
fields["quantity"]?["value"] = data["quantity"];
|
||||
|
||||
fields["shipment"]?["value"] = data["shipment"];
|
||||
fields["shipment"]?["filters"] = {"order": salesOrder!.pk.toString()};
|
||||
|
||||
final context = OneContext().context!;
|
||||
|
||||
launchApiForm(
|
||||
context,
|
||||
L10().allocateStock,
|
||||
salesOrder!.allocate_url,
|
||||
fields,
|
||||
method: "POST",
|
||||
icon: TablerIcons.transition_right,
|
||||
onSuccess: (data) async {
|
||||
showSnackIcon(L10().allocated, success: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
data["error"] as String? ?? L10().barcodeError,
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
270
lib/barcode/stock.dart
Normal file
270
lib/barcode/stock.dart
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/api_form.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/barcode/barcode.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
import "package:inventree/barcode/tones.dart";
|
||||
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
|
||||
import "package:inventree/widget/dialogs.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
/*
|
||||
* Generic class for scanning a StockLocation.
|
||||
*
|
||||
* - Validates that the scanned barcode matches a valid StockLocation
|
||||
* - Runs a "callback" function if a valid StockLocation is found
|
||||
*/
|
||||
class BarcodeScanStockLocationHandler extends BarcodeHandler {
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanLocation;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// We expect that the barcode points to a 'stocklocation'
|
||||
if (data.containsKey("stocklocation")) {
|
||||
int _loc = (data["stocklocation"]?["pk"] ?? -1) as int;
|
||||
|
||||
// A valid stock location!
|
||||
if (_loc > 0) {
|
||||
debug("Scanned stock location ${_loc}");
|
||||
|
||||
final bool result = await onLocationScanned(_loc);
|
||||
|
||||
if (result && hasContext()) {
|
||||
OneContext().pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get to this point, something went wrong during the scan process
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(L10().invalidStockLocation, success: false);
|
||||
}
|
||||
|
||||
// Callback function which runs when a valid StockLocation is scanned
|
||||
// If this function returns 'true' the barcode scanning dialog will be closed
|
||||
Future<bool> onLocationScanned(int locationId) async {
|
||||
// Re-implement this for particular subclass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic class for scanning a StockItem
|
||||
*
|
||||
* - Validates that the scanned barcode matches a valid StockItem
|
||||
* - Runs a "callback" function if a valid StockItem is found
|
||||
*/
|
||||
class BarcodeScanStockItemHandler extends BarcodeHandler {
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||
|
||||
@override
|
||||
Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
|
||||
// We expect that the barcode points to a 'stockitem'
|
||||
if (data.containsKey("stockitem")) {
|
||||
int _item = (data["stockitem"]?["pk"] ?? -1) as int;
|
||||
|
||||
// A valid stock location!
|
||||
if (_item > 0) {
|
||||
barcodeSuccessTone();
|
||||
|
||||
bool result = await onItemScanned(_item);
|
||||
|
||||
if (result && OneContext.hasContext) {
|
||||
OneContext().pop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we get to this point, something went wrong during the scan process
|
||||
barcodeFailureTone();
|
||||
|
||||
showSnackIcon(L10().invalidStockItem, success: false);
|
||||
}
|
||||
|
||||
// Callback function which runs when a valid StockItem is scanned
|
||||
Future<bool> onItemScanned(int itemId) async {
|
||||
// Re-implement this for particular subclass
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barcode handler for scanning a provided StockItem into a scanned StockLocation.
|
||||
*
|
||||
* - The class is initialized by passing a valid StockItem object
|
||||
* - Expects to scan barcode for a StockLocation
|
||||
* - The StockItem is transferred into the scanned location
|
||||
*/
|
||||
class StockItemScanIntoLocationHandler extends BarcodeScanStockLocationHandler {
|
||||
StockItemScanIntoLocationHandler(this.item);
|
||||
|
||||
final InvenTreeStockItem item;
|
||||
|
||||
@override
|
||||
Future<bool> onLocationScanned(int locationId) async {
|
||||
final bool confirm = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
false,
|
||||
);
|
||||
|
||||
bool result = false;
|
||||
|
||||
if (confirm) {
|
||||
Map<String, dynamic> fields = item.transferFields();
|
||||
|
||||
// Override location with scanned value
|
||||
fields["location"]?["value"] = locationId;
|
||||
|
||||
launchApiForm(
|
||||
OneContext().context!,
|
||||
L10().transferStock,
|
||||
InvenTreeStockItem.transferStockUrl(),
|
||||
fields,
|
||||
method: "POST",
|
||||
icon: TablerIcons.transfer,
|
||||
onSuccess: (data) async {
|
||||
showSnackIcon(L10().stockItemUpdated, success: true);
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
result = await item.transferStock(locationId);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
barcodeSuccess(L10().barcodeScanIntoLocationSuccess);
|
||||
} else {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().barcodeScanIntoLocationFailure, success: false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barcode handler for scanning stock item(s) into the specified StockLocation.
|
||||
*
|
||||
* - The class is initialized by passing a valid StockLocation object
|
||||
* - Expects to scan a barcode for a StockItem
|
||||
* - The scanned StockItem is transferred into the provided StockLocation
|
||||
*/
|
||||
class StockLocationScanInItemsHandler extends BarcodeScanStockItemHandler {
|
||||
StockLocationScanInItemsHandler(this.location);
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
|
||||
@override
|
||||
String getOverlayText(BuildContext context) => L10().barcodeScanItem;
|
||||
|
||||
@override
|
||||
Future<bool> onItemScanned(int itemId) async {
|
||||
final InvenTreeStockItem? item =
|
||||
await InvenTreeStockItem().get(itemId) as InvenTreeStockItem?;
|
||||
final bool confirm = await InvenTreeSettingsManager().getBool(
|
||||
INV_STOCK_CONFIRM_SCAN,
|
||||
false,
|
||||
);
|
||||
|
||||
bool result = false;
|
||||
|
||||
if (item != null) {
|
||||
// Item is already *in* the specified location
|
||||
if (item.locationId == location.pk) {
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().itemInLocation, success: true);
|
||||
return false;
|
||||
} else {
|
||||
if (confirm) {
|
||||
Map<String, dynamic> fields = item.transferFields();
|
||||
|
||||
// Override location with provided location value
|
||||
fields["location"]?["value"] = location.pk;
|
||||
|
||||
launchApiForm(
|
||||
OneContext().context!,
|
||||
L10().transferStock,
|
||||
InvenTreeStockItem.transferStockUrl(),
|
||||
fields,
|
||||
method: "POST",
|
||||
icon: TablerIcons.transfer,
|
||||
onSuccess: (data) async {
|
||||
showSnackIcon(L10().stockItemUpdated, success: true);
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
result = await item.transferStock(location.pk);
|
||||
|
||||
showSnackIcon(
|
||||
result
|
||||
? L10().barcodeScanIntoLocationSuccess
|
||||
: L10().barcodeScanIntoLocationFailure,
|
||||
success: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We always return false here, to ensure the barcode scan dialog remains open
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Barcode handler class for scanning a StockLocation into another StockLocation
|
||||
*
|
||||
* - The class is initialized by passing a valid StockLocation object
|
||||
* - Expects to scan barcode for another *parent* StockLocation
|
||||
* - The scanned StockLocation is set as the "parent" of the provided StockLocation
|
||||
*/
|
||||
class ScanParentLocationHandler extends BarcodeScanStockLocationHandler {
|
||||
ScanParentLocationHandler(this.location);
|
||||
|
||||
final InvenTreeStockLocation location;
|
||||
|
||||
@override
|
||||
Future<bool> onLocationScanned(int locationId) async {
|
||||
final response = await location.update(
|
||||
values: {"parent": locationId.toString()},
|
||||
expectedStatusCode: null,
|
||||
);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
case 201:
|
||||
barcodeSuccess(L10().barcodeScanIntoLocationSuccess);
|
||||
return true;
|
||||
case 400: // Invalid parent location chosen
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(L10().invalidStockLocation, success: false);
|
||||
return false;
|
||||
default:
|
||||
barcodeFailureTone();
|
||||
showSnackIcon(
|
||||
L10().barcodeScanIntoLocationFailure,
|
||||
success: false,
|
||||
actionText: L10().details,
|
||||
onAction: () {
|
||||
showErrorDialog(L10().barcodeError, response: response);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/barcode/tones.dart
Normal file
25
lib/barcode/tones.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
/*
|
||||
* Play an audible 'success' alert to the user.
|
||||
*/
|
||||
Future<void> barcodeSuccessTone() async {
|
||||
final bool en =
|
||||
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
|
||||
as bool;
|
||||
|
||||
if (en) {
|
||||
playAudioFile("sounds/barcode_scan.mp3");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> barcodeFailureTone() async {
|
||||
final bool en =
|
||||
await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true)
|
||||
as bool;
|
||||
|
||||
if (en) {
|
||||
playAudioFile("sounds/barcode_error.mp3");
|
||||
}
|
||||
}
|
||||
146
lib/barcode/wedge_controller.dart
Normal file
146
lib/barcode/wedge_controller.dart
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/barcode/controller.dart";
|
||||
import "package:inventree/barcode/handler.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
|
||||
/*
|
||||
* Barcode controller which acts as a keyboard wedge,
|
||||
* intercepting barcode data which is entered as rapid keyboard presses
|
||||
*/
|
||||
class WedgeBarcodeController extends InvenTreeBarcodeController {
|
||||
const WedgeBarcodeController(BarcodeHandler handler, {Key? key})
|
||||
: super(handler, key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _WedgeBarcodeControllerState();
|
||||
}
|
||||
|
||||
class _WedgeBarcodeControllerState extends InvenTreeBarcodeControllerState {
|
||||
_WedgeBarcodeControllerState() : super();
|
||||
|
||||
bool canScan = true;
|
||||
|
||||
bool get scanning => mounted && canScan;
|
||||
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
List<String> _scannedCharacters = [];
|
||||
|
||||
DateTime? _lastScanTime;
|
||||
|
||||
@override
|
||||
Future<void> pauseScan() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
canScan = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resumeScan() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
canScan = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for a single key press / scan
|
||||
void handleKeyEvent(KeyEvent event) {
|
||||
if (!scanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look only for key-down events
|
||||
if (event is! KeyDownEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore events without a character code
|
||||
if (event.character == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
// Throw away old characters
|
||||
if (_lastScanTime == null ||
|
||||
_lastScanTime!.isBefore(now.subtract(Duration(milliseconds: 250)))) {
|
||||
_scannedCharacters.clear();
|
||||
}
|
||||
|
||||
_lastScanTime = now;
|
||||
|
||||
if (event.character == "\n") {
|
||||
if (_scannedCharacters.isNotEmpty) {
|
||||
// Debug output required for unit testing
|
||||
debug("scanned: ${_scannedCharacters.join()}");
|
||||
handleBarcodeData(_scannedCharacters.join());
|
||||
}
|
||||
|
||||
_scannedCharacters.clear();
|
||||
} else {
|
||||
_scannedCharacters.add(event.character!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: COLOR_APP_BAR,
|
||||
title: Text(L10().scanBarcode),
|
||||
),
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.9),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Spacer(flex: 5),
|
||||
Icon(TablerIcons.barcode, size: 64),
|
||||
Spacer(flex: 5),
|
||||
KeyboardListener(
|
||||
autofocus: true,
|
||||
focusNode: _focusNode,
|
||||
child: SizedBox(
|
||||
child: CircularProgressIndicator(
|
||||
color: scanning ? COLOR_ACTION : COLOR_PROGRESS,
|
||||
),
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
onKeyEvent: (event) {
|
||||
handleKeyEvent(event);
|
||||
},
|
||||
// onBarcodeScanned: (String barcode) {
|
||||
// debug("scanned: ${barcode}");
|
||||
// if (scanning) {
|
||||
// // Process the barcode data
|
||||
// handleBarcodeData(barcode);
|
||||
// }
|
||||
// },
|
||||
),
|
||||
Spacer(flex: 5),
|
||||
Padding(
|
||||
child: Text(
|
||||
widget.handler.getOverlayText(context),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.all(20),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/dsn.dart
Normal file
7
lib/dsn.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* For integration with sentry.io, fill out the SENTRY_DSN_KEY value below.
|
||||
* This should be set to a valid DSN key, from your sentry.io account
|
||||
*
|
||||
*/
|
||||
String SENTRY_DSN_KEY =
|
||||
"https://fea705aa4b8e4c598dcf9b146b3d1b86@o378676.ingest.sentry.io/5202450";
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// Dummy DSN to use for unit testing, etc
|
||||
|
||||
const String SENTRY_DSN_KEY = "https://12345678901234567890@abcdef.ingest.sentry.io/11223344";
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: prefer_single_quotes
|
||||
|
||||
//This file is automatically generated. DO NOT EDIT, all your changes would be lost.
|
||||
|
||||
class S implements WidgetsLocalizations {
|
||||
const S();
|
||||
|
||||
static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate();
|
||||
|
||||
static S of(BuildContext context) => Localizations.of<S>(context, WidgetsLocalizations);
|
||||
|
||||
@override
|
||||
TextDirection get textDirection => TextDirection.ltr;
|
||||
|
||||
}
|
||||
|
||||
class en extends S {
|
||||
const en();
|
||||
}
|
||||
|
||||
|
||||
class GeneratedLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
|
||||
const GeneratedLocalizationsDelegate();
|
||||
|
||||
List<Locale> get supportedLocales {
|
||||
return const <Locale>[
|
||||
|
||||
const Locale("en", ""),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
LocaleResolutionCallback resolution({Locale fallback}) {
|
||||
return (Locale locale, Iterable<Locale> supported) {
|
||||
final Locale languageLocale = new Locale(locale.languageCode, "");
|
||||
if (supported.contains(locale))
|
||||
return locale;
|
||||
else if (supported.contains(languageLocale))
|
||||
return languageLocale;
|
||||
else {
|
||||
final Locale fallbackLocale = fallback ?? supported.first;
|
||||
return fallbackLocale;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WidgetsLocalizations> load(Locale locale) {
|
||||
final String lang = getLang(locale);
|
||||
switch (lang) {
|
||||
|
||||
case "en":
|
||||
return new SynchronousFuture<WidgetsLocalizations>(const en());
|
||||
|
||||
default:
|
||||
return new SynchronousFuture<WidgetsLocalizations>(const S());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => supportedLocales.contains(locale);
|
||||
|
||||
@override
|
||||
bool shouldReload(GeneratedLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
String getLang(Locale l) => l.countryCode != null && l.countryCode.isEmpty
|
||||
? l.languageCode
|
||||
: l.toString();
|
||||
178
lib/helpers.dart
178
lib/helpers.dart
|
|
@ -7,31 +7,173 @@
|
|||
* supressing trailing zeroes
|
||||
*/
|
||||
|
||||
import "dart:io";
|
||||
import "package:currency_formatter/currency_formatter.dart";
|
||||
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:url_launcher/url_launcher.dart";
|
||||
import "package:audioplayers/audioplayers.dart";
|
||||
import "package:inventree/app_settings.dart";
|
||||
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
List<String> debug_messages = [];
|
||||
|
||||
void clearDebugMessage() => debug_messages.clear();
|
||||
|
||||
int debugMessageCount() {
|
||||
print("Debug Messages: ${debug_messages.length}");
|
||||
return debug_messages.length;
|
||||
}
|
||||
|
||||
// Check if the debug log contains a given message
|
||||
bool debugContains(String msg, {bool raiseAssert = true}) {
|
||||
bool result = false;
|
||||
|
||||
for (String element in debug_messages) {
|
||||
if (element.contains(msg)) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
print("Debug does not contain expected string: '${msg}'");
|
||||
}
|
||||
|
||||
if (raiseAssert) {
|
||||
assert(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isTesting() {
|
||||
return Platform.environment.containsKey("FLUTTER_TEST");
|
||||
}
|
||||
|
||||
bool hasContext() {
|
||||
try {
|
||||
return !isTesting() && OneContext.hasContext;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Display a debug message if we are in testing mode, or running in debug mode
|
||||
*/
|
||||
void debug(dynamic msg) {
|
||||
if (Platform.environment.containsKey("FLUTTER_TEST")) {
|
||||
debug_messages.add(msg.toString());
|
||||
}
|
||||
|
||||
print("DEBUG: ${msg.toString()}");
|
||||
}
|
||||
|
||||
/*
|
||||
* Simplify string representation of a floating point value
|
||||
* Basically, don't display fractional component if it is an integer
|
||||
*/
|
||||
String simpleNumberString(double number) {
|
||||
// Ref: https://stackoverflow.com/questions/55152175/how-to-remove-trailing-zeros-using-dart
|
||||
|
||||
return number.toStringAsFixed(number.truncateToDouble() == number ? 0 : 1);
|
||||
}
|
||||
|
||||
Future<void> successTone() async {
|
||||
|
||||
final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
|
||||
|
||||
if (en) {
|
||||
final player = AudioCache();
|
||||
player.play("sounds/barcode_scan.mp3");
|
||||
if (number.toInt() == number) {
|
||||
return number.toInt().toString();
|
||||
} else {
|
||||
return number.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Future <void> failureTone() async {
|
||||
/*
|
||||
* Play an audio file from the requested path.
|
||||
*
|
||||
* Note: If OneContext module fails the 'hasContext' check,
|
||||
* we will not attempt to play the sound
|
||||
*/
|
||||
Future<void> playAudioFile(String path) async {
|
||||
// Debug message for unit testing
|
||||
debug("Playing audio file: '${path}'");
|
||||
|
||||
final bool en = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool;
|
||||
if (!hasContext()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (en) {
|
||||
final player = AudioCache();
|
||||
player.play("sounds/barcode_error.mp3");
|
||||
final player = AudioPlayer();
|
||||
|
||||
// Specify context options for the audio player
|
||||
// Ref: https://github.com/inventree/inventree-app/issues/582
|
||||
player.setAudioContext(
|
||||
AudioContext(
|
||||
android: AudioContextAndroid(
|
||||
usageType: AndroidUsageType.notification,
|
||||
audioFocus: AndroidAudioFocus.none,
|
||||
),
|
||||
iOS: AudioContextIOS(),
|
||||
),
|
||||
);
|
||||
|
||||
player.play(AssetSource(path));
|
||||
}
|
||||
|
||||
// Open an external URL
|
||||
Future<void> openLink(String url) async {
|
||||
final link = Uri.parse(url);
|
||||
|
||||
try {
|
||||
await launchUrl(link);
|
||||
} catch (e) {
|
||||
showSnackIcon(L10().error, success: false);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper function for rendering a money / currency object as a String
|
||||
*/
|
||||
String renderCurrency(double? amount, String currency, {int decimals = 2}) {
|
||||
if (amount == null || amount.isInfinite || amount.isNaN) return "-";
|
||||
|
||||
currency = currency.trim();
|
||||
|
||||
if (currency.isEmpty) return "-";
|
||||
|
||||
CurrencyFormat fmt =
|
||||
CurrencyFormat.fromCode(currency.toLowerCase()) ?? CurrencyFormat.usd;
|
||||
|
||||
String value = CurrencyFormatter.format(amount, fmt);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
bool isValidNumber(double? value) {
|
||||
return value != null && !value.isNaN && !value.isInfinite;
|
||||
}
|
||||
|
||||
/*
|
||||
* Render a "range" of prices between two values.
|
||||
*/
|
||||
String formatPriceRange(
|
||||
double? minPrice,
|
||||
double? maxPrice, {
|
||||
String? currency,
|
||||
}) {
|
||||
// Account for empty or null values
|
||||
if (!isValidNumber(minPrice) && !isValidNumber(maxPrice)) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (isValidNumber(minPrice) && isValidNumber(maxPrice)) {
|
||||
// Two values are equal
|
||||
if (minPrice == maxPrice) {
|
||||
return renderCurrency(minPrice, currency ?? "USD");
|
||||
} else {
|
||||
return "${renderCurrency(minPrice, currency ?? "USD")} - ${renderCurrency(maxPrice, currency ?? "USD")}";
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidNumber(minPrice)) {
|
||||
return renderCurrency(minPrice, currency ?? "USD");
|
||||
} else if (isValidNumber(maxPrice)) {
|
||||
return renderCurrency(maxPrice, currency ?? "USD");
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
176
lib/inventree/attachment.dart
Normal file
176
lib/inventree/attachment.dart
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
import "package:inventree/widget/fields.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
import "package:path/path.dart" as path;
|
||||
|
||||
class InvenTreeAttachment extends InvenTreeModel {
|
||||
// Class representing an "attachment" file
|
||||
InvenTreeAttachment() : super();
|
||||
|
||||
InvenTreeAttachment.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeAttachment createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeAttachment.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "attachment/";
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {"link": {}, "comment": {}};
|
||||
|
||||
if (!hasLink) {
|
||||
fields.remove("link");
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
// The model type of the instance this attachment is associated with
|
||||
String get modelType => getString("model_type");
|
||||
|
||||
// The ID of the instance this attachment is associated with
|
||||
int get modelId => getInt("model_id");
|
||||
|
||||
String get attachment => getString("attachment");
|
||||
|
||||
bool get hasAttachment => attachment.isNotEmpty;
|
||||
|
||||
// Return the filename of the attachment
|
||||
String get filename {
|
||||
return attachment.split("/").last;
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
String fn = filename.toLowerCase();
|
||||
|
||||
if (fn.endsWith(".pdf")) {
|
||||
return TablerIcons.file_type_pdf;
|
||||
} else if (fn.endsWith(".csv")) {
|
||||
return TablerIcons.file_type_csv;
|
||||
} else if (fn.endsWith(".doc") || fn.endsWith(".docx")) {
|
||||
return TablerIcons.file_type_doc;
|
||||
} else if (fn.endsWith(".xls") || fn.endsWith(".xlsx")) {
|
||||
return TablerIcons.file_type_xls;
|
||||
}
|
||||
|
||||
// Image formats
|
||||
final List<String> img_formats = [".png", ".jpg", ".gif", ".bmp", ".svg"];
|
||||
|
||||
for (String fmt in img_formats) {
|
||||
if (fn.endsWith(fmt)) {
|
||||
return TablerIcons.file_type_jpg;
|
||||
}
|
||||
}
|
||||
|
||||
return TablerIcons.file;
|
||||
}
|
||||
|
||||
String get comment => getString("comment");
|
||||
|
||||
DateTime? get uploadDate {
|
||||
if (jsondata.containsKey("upload_date")) {
|
||||
return DateTime.tryParse((jsondata["upload_date"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a count of how many attachments exist against the specified model ID
|
||||
Future<int> countAttachments(String modelType, int modelId) async {
|
||||
Map<String, String> filters = {};
|
||||
|
||||
if (!api.supportsModernAttachments) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
filters["model_type"] = modelType;
|
||||
filters["model_id"] = modelId.toString();
|
||||
|
||||
return count(filters: filters);
|
||||
}
|
||||
|
||||
Future<bool> uploadAttachment(
|
||||
File attachment,
|
||||
String modelType,
|
||||
int modelId, {
|
||||
String comment = "",
|
||||
Map<String, String> fields = const {},
|
||||
}) async {
|
||||
// Ensure that the correct reference field is set
|
||||
Map<String, String> data = Map<String, String>.from(fields);
|
||||
|
||||
String url = URL;
|
||||
|
||||
if (comment.isNotEmpty) {
|
||||
data["comment"] = comment;
|
||||
}
|
||||
|
||||
data["model_type"] = modelType;
|
||||
data["model_id"] = modelId.toString();
|
||||
|
||||
final APIResponse response = await InvenTreeAPI().uploadFile(
|
||||
url,
|
||||
attachment,
|
||||
method: "POST",
|
||||
name: "attachment",
|
||||
fields: data,
|
||||
);
|
||||
|
||||
return response.successful();
|
||||
}
|
||||
|
||||
Future<bool> uploadImage(
|
||||
String modelType,
|
||||
int modelId, {
|
||||
String prefix = "InvenTree",
|
||||
}) async {
|
||||
bool result = false;
|
||||
|
||||
await FilePickerDialog.pickImageFromCamera().then((File? file) {
|
||||
if (file != null) {
|
||||
String dir = path.dirname(file.path);
|
||||
String ext = path.extension(file.path);
|
||||
String now = DateTime.now().toIso8601String().replaceAll(":", "-");
|
||||
|
||||
// Rename the file with a unique name
|
||||
String filename = "${dir}/${prefix}_image_${now}${ext}";
|
||||
|
||||
try {
|
||||
return file.rename(filename).then((File renamed) {
|
||||
return uploadAttachment(renamed, modelType, modelId).then((
|
||||
success,
|
||||
) {
|
||||
result = success;
|
||||
showSnackIcon(
|
||||
result ? L10().imageUploadSuccess : L10().imageUploadFailure,
|
||||
success: result,
|
||||
);
|
||||
});
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
sentryReportError("uploadImage", error, stackTrace);
|
||||
showSnackIcon(L10().imageUploadFailure, success: false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Download this attachment file
|
||||
*/
|
||||
Future<void> downloadAttachment() async {
|
||||
await InvenTreeAPI().downloadFile(attachment);
|
||||
}
|
||||
}
|
||||
63
lib/inventree/bom.dart
Normal file
63
lib/inventree/bom.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
|
||||
/*
|
||||
* Class representing the BomItem database model
|
||||
*/
|
||||
class InvenTreeBomItem extends InvenTreeModel {
|
||||
InvenTreeBomItem() : super();
|
||||
|
||||
InvenTreeBomItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeBomItem.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "bom/";
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {
|
||||
"sub_part_detail": "true",
|
||||
"part_detail": "true",
|
||||
"show_pricing": "false",
|
||||
};
|
||||
}
|
||||
|
||||
// Extract the 'reference' value associated with this BomItem
|
||||
String get reference => getString("reference");
|
||||
|
||||
// Extract the 'quantity' value associated with this BomItem
|
||||
double get quantity => getDouble("quantity");
|
||||
|
||||
// Extract the ID of the related part
|
||||
int get partId => getInt("part");
|
||||
|
||||
// Return a Part instance for the referenced part
|
||||
InvenTreePart? get part {
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
dynamic data = jsondata["part_detail"] ?? {};
|
||||
if (data is Map<String, dynamic>) {
|
||||
return InvenTreePart.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return a Part instance for the referenced sub-part
|
||||
InvenTreePart? get subPart {
|
||||
if (jsondata.containsKey("sub_part_detail")) {
|
||||
dynamic data = jsondata["sub_part_detail"] ?? {};
|
||||
if (data is Map<String, dynamic>) {
|
||||
return InvenTreePart.fromJson(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the ID of the related sub-part
|
||||
int get subPartId => getInt("sub_part");
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/purchase_order.dart";
|
||||
|
||||
import "package:inventree/widget/company/company_detail.dart";
|
||||
import "package:inventree/widget/company/supplier_part_detail.dart";
|
||||
|
||||
/*
|
||||
* The InvenTreeCompany class repreents the Company model in the InvenTree database.
|
||||
* The InvenTreeCompany class represents the Company model in the InvenTree database.
|
||||
*/
|
||||
|
||||
class InvenTreeCompany extends InvenTreeModel {
|
||||
|
||||
InvenTreeCompany() : super();
|
||||
|
||||
InvenTreeCompany.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
|
@ -18,9 +19,26 @@ class InvenTreeCompany extends InvenTreeModel {
|
|||
@override
|
||||
String get URL => "company/";
|
||||
|
||||
static const String MODEL_TYPE = "company";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => CompanyDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => [
|
||||
"purchase_order",
|
||||
"sales_order",
|
||||
"return_order",
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"website": {},
|
||||
|
|
@ -29,41 +47,52 @@ class InvenTreeCompany extends InvenTreeModel {
|
|||
"is_customer": {},
|
||||
"currency": {},
|
||||
};
|
||||
|
||||
if (InvenTreeAPI().supportsCompanyActiveStatus) {
|
||||
fields["active"] = {};
|
||||
}
|
||||
|
||||
String get image => (jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage) as String;
|
||||
return fields;
|
||||
}
|
||||
|
||||
String get thumbnail => (jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
String get image =>
|
||||
(jsondata["image"] ?? jsondata["thumbnail"] ?? InvenTreeAPI.staticImage)
|
||||
as String;
|
||||
|
||||
String get website => (jsondata["website"] ?? "") as String;
|
||||
String get thumbnail =>
|
||||
(jsondata["thumbnail"] ?? jsondata["image"] ?? InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
String get phone => (jsondata["phone"] ?? "") as String;
|
||||
String get website => getString("website");
|
||||
|
||||
String get email => (jsondata["email"] ?? "") as String;
|
||||
String get phone => getString("phone");
|
||||
|
||||
bool get isSupplier => (jsondata["is_supplier"] ?? false) as bool;
|
||||
String get email => getString("email");
|
||||
|
||||
bool get isManufacturer => (jsondata["is_manufacturer"] ?? false) as bool;
|
||||
bool get isSupplier => getBool("is_supplier");
|
||||
|
||||
bool get isCustomer => (jsondata["is_customer"] ?? false) as bool;
|
||||
bool get isManufacturer => getBool("is_manufacturer");
|
||||
|
||||
int get partSuppliedCount => (jsondata["parts_supplied"] ?? 0) as int;
|
||||
bool get isCustomer => getBool("is_customer");
|
||||
|
||||
int get partManufacturedCount => (jsondata["parts_manufactured"] ?? 0) as int;
|
||||
bool get active => getBool("active", backup: true);
|
||||
|
||||
int get partSuppliedCount => getInt("part_supplied");
|
||||
|
||||
int get partManufacturedCount => getInt("parts_manufactured");
|
||||
|
||||
// Request a list of purchase orders against this company
|
||||
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({bool? outstanding}) async {
|
||||
|
||||
Map<String, String> filters = {
|
||||
"supplier": "${pk}"
|
||||
};
|
||||
Future<List<InvenTreePurchaseOrder>> getPurchaseOrders({
|
||||
bool? outstanding,
|
||||
}) async {
|
||||
Map<String, String> filters = {"supplier": "${pk}"};
|
||||
|
||||
if (outstanding != null) {
|
||||
filters["outstanding"] = outstanding ? "true" : "false";
|
||||
}
|
||||
|
||||
final List<InvenTreeModel> results = await InvenTreePurchaseOrder().list(
|
||||
filters: filters
|
||||
filters: filters,
|
||||
);
|
||||
|
||||
List<InvenTreePurchaseOrder> orders = [];
|
||||
|
|
@ -78,103 +107,189 @@ class InvenTreeCompany extends InvenTreeModel {
|
|||
}
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var company = InvenTreeCompany.fromJson(json);
|
||||
|
||||
return company;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeCompany.fromJson(json);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* The InvenTreeSupplierPart class represents the SupplierPart model in the InvenTree database
|
||||
*/
|
||||
class InvenTreeSupplierPart extends InvenTreeModel {
|
||||
|
||||
InvenTreeSupplierPart() : super();
|
||||
|
||||
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreeSupplierPart.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "company/part/";
|
||||
|
||||
Map<String, String> _filters() {
|
||||
static const String MODEL_TYPE = "supplierpart";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["part", "purchase_order"];
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => SupplierPartDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"supplier": {},
|
||||
"SKU": {},
|
||||
"link": {},
|
||||
"note": {},
|
||||
"packaging": {},
|
||||
};
|
||||
|
||||
// At some point, pack_size was changed to pack_quantity
|
||||
if (InvenTreeAPI().apiVersion < 117) {
|
||||
fields["pack_size"] = {};
|
||||
} else {
|
||||
fields["pack_quantity"] = {};
|
||||
}
|
||||
|
||||
if (InvenTreeAPI().supportsCompanyActiveStatus) {
|
||||
fields["active"] = {};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {
|
||||
"manufacturer_detail": "true",
|
||||
"supplier_detail": "true",
|
||||
"manufacturer_part_detail": "true",
|
||||
"part_detail": "true",
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
return _filters();
|
||||
int get manufacturerId => getInt("pk", subKey: "manufacturer_detail");
|
||||
|
||||
String get manufacturerName =>
|
||||
getString("name", subKey: "manufacturer_detail");
|
||||
|
||||
String get MPN => getString("MPN", subKey: "manufacturer_part_detail");
|
||||
|
||||
String get manufacturerImage =>
|
||||
(jsondata["manufacturer_detail"]?["image"] ??
|
||||
jsondata["manufacturer_detail"]?["thumbnail"] ??
|
||||
InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
int get manufacturerPartId => getInt("manufacturer_part");
|
||||
|
||||
int get supplierId => getInt("supplier");
|
||||
|
||||
String get supplierName => getString("name", subKey: "supplier_detail");
|
||||
|
||||
String get supplierImage =>
|
||||
(jsondata["supplier_detail"]?["image"] ??
|
||||
jsondata["supplier_detail"]?["thumbnail"] ??
|
||||
InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
String get SKU => getString("SKU");
|
||||
|
||||
bool get active => getBool("active", backup: true);
|
||||
|
||||
int get partId => getInt("part");
|
||||
|
||||
double get inStock => getDouble("in_stock");
|
||||
|
||||
double get onOrder => getDouble("on_order");
|
||||
|
||||
String get partImage =>
|
||||
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
String get partName => getString("name", subKey: "part_detail");
|
||||
|
||||
Map<String, dynamic> get partDetail => getMap("part_detail");
|
||||
|
||||
String get partDescription => getString("description", subKey: "part_detail");
|
||||
|
||||
String get note => getString("note");
|
||||
|
||||
String get packaging => getString("packaging");
|
||||
|
||||
String get pack_quantity {
|
||||
if (InvenTreeAPI().apiVersion < 117) {
|
||||
return getString("pack_size");
|
||||
} else {
|
||||
return getString("pack_quantity");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
return _filters();
|
||||
}
|
||||
|
||||
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||
|
||||
String get manufacturerName => (jsondata["manufacturer_detail"]["name"] ?? "") as String;
|
||||
|
||||
String get manufacturerImage => (jsondata["manufacturer_detail"]["image"] ?? jsondata["manufacturer_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
int get manufacturerPartId => (jsondata["manufacturer_part"] ?? -1) as int;
|
||||
|
||||
int get supplierId => (jsondata["supplier"] ?? -1) as int;
|
||||
|
||||
String get supplierName => (jsondata["supplier_detail"]["name"] ?? "") as String;
|
||||
|
||||
String get supplierImage => (jsondata["supplier_detail"]["image"] ?? jsondata["supplier_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get SKU => (jsondata["SKU"] ?? "") as String;
|
||||
|
||||
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
String get partImage => (jsondata["part_detail"]["thumbnail"] ?? InvenTreeAPI.staticThumb) as String;
|
||||
|
||||
String get partName => (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var part = InvenTreeSupplierPart.fromJson(json);
|
||||
|
||||
return part;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSupplierPart.fromJson(json);
|
||||
}
|
||||
|
||||
|
||||
class InvenTreeManufacturerPart extends InvenTreeModel {
|
||||
|
||||
InvenTreeManufacturerPart() : super();
|
||||
|
||||
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreeManufacturerPart.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String url = "company/part/manufacturer/";
|
||||
String URL = "company/part/manufacturer/";
|
||||
|
||||
static const String MODEL_TYPE = "manufacturerpart";
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
return {
|
||||
"manufacturer_detail": "true",
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"manufacturer": {},
|
||||
"MPN": {},
|
||||
"link": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
int get manufacturerId => (jsondata["manufacturer"] ?? -1) as int;
|
||||
|
||||
String get MPN => (jsondata["MPN"] ?? "") as String;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var part = InvenTreeManufacturerPart.fromJson(json);
|
||||
|
||||
return part;
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"manufacturer_detail": "true", "part_detail": "true"};
|
||||
}
|
||||
|
||||
int get partId => getInt("part");
|
||||
|
||||
String get partName => getString("name", subKey: "part_detail");
|
||||
|
||||
String get partDescription => getString("description", subKey: "part_detail");
|
||||
|
||||
String get partIPN => getString("IPN", subKey: "part_detail");
|
||||
|
||||
String get partImage =>
|
||||
(jsondata["part_detail"]?["thumbnail"] ?? InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
int get manufacturerId => getInt("manufacturer");
|
||||
|
||||
String get manufacturerName =>
|
||||
getString("name", subKey: "manufacturer_detail");
|
||||
|
||||
String get manufacturerDescription =>
|
||||
getString("description", subKey: "manufacturer_detail");
|
||||
|
||||
String get manufacturerImage =>
|
||||
(jsondata["manufacturer_detail"]?["image"] ??
|
||||
jsondata["manufacturer_detail"]?["thumbnail"] ??
|
||||
InvenTreeAPI.staticThumb)
|
||||
as String;
|
||||
|
||||
String get MPN => getString("MPN");
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeManufacturerPart.fromJson(json);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
48
lib/inventree/notification.dart
Normal file
48
lib/inventree/notification.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import "package:inventree/inventree/model.dart";
|
||||
|
||||
/*
|
||||
* Class representing a "notification"
|
||||
*/
|
||||
|
||||
class InvenTreeNotification extends InvenTreeModel {
|
||||
InvenTreeNotification() : super();
|
||||
|
||||
InvenTreeNotification.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeNotification createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreeNotification.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
String get URL => "notifications/";
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
// By default, only return 'unread' notifications
|
||||
return {"read": "false"};
|
||||
}
|
||||
|
||||
String get message => getString("message");
|
||||
|
||||
DateTime? get creationDate {
|
||||
if (jsondata.containsKey("creation")) {
|
||||
return DateTime.tryParse((jsondata["creation"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Dismiss this notification (mark as read)
|
||||
*/
|
||||
Future<void> dismiss() async {
|
||||
if (api.apiVersion >= 82) {
|
||||
// "Modern" API endpoint operates a little differently
|
||||
await update(values: {"read": "true"});
|
||||
} else {
|
||||
await api.post("${url}read/");
|
||||
}
|
||||
}
|
||||
}
|
||||
161
lib/inventree/orders.dart
Normal file
161
lib/inventree/orders.dart
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Base model for various "orders" which share common properties
|
||||
*/
|
||||
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
|
||||
/*
|
||||
* Generic class representing an "order"
|
||||
*/
|
||||
class InvenTreeOrder extends InvenTreeModel {
|
||||
InvenTreeOrder() : super();
|
||||
|
||||
InvenTreeOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
String get issueDate => getString("issue_date");
|
||||
|
||||
String get startDate => getString("start_date");
|
||||
|
||||
String get completionDate => getDateString("complete_date");
|
||||
|
||||
String get creationDate => getDateString("creation_date");
|
||||
|
||||
String get shipmentDate => getDateString("shipment_date");
|
||||
|
||||
String get targetDate => getDateString("target_date");
|
||||
|
||||
int get lineItemCount => getInt("line_items", backup: 0);
|
||||
|
||||
int get completedLineItemCount => getInt("completed_lines", backup: 0);
|
||||
|
||||
int get shipmentCount => getInt("shipments_count", backup: 0);
|
||||
|
||||
int get completedShipmentCount =>
|
||||
getInt("completed_shipments_count", backup: 0);
|
||||
|
||||
bool get complete => completedLineItemCount >= lineItemCount;
|
||||
|
||||
bool get overdue => getBool("overdue");
|
||||
|
||||
String get reference => getString("reference");
|
||||
|
||||
int get responsibleId => getInt("responsible");
|
||||
|
||||
String get responsibleName => getString("name", subKey: "responsible_detail");
|
||||
|
||||
String get responsibleLabel =>
|
||||
getString("label", subKey: "responsible_detail");
|
||||
|
||||
// Project code information
|
||||
int get projectCodeId => getInt("project_code");
|
||||
|
||||
String get projectCode => getString("code", subKey: "project_code_detail");
|
||||
|
||||
String get projectCodeDescription =>
|
||||
getString("description", subKey: "project_code_detail");
|
||||
|
||||
bool get hasProjectCode => projectCode.isNotEmpty;
|
||||
|
||||
double? get totalPrice {
|
||||
String price = getString("total_price");
|
||||
|
||||
if (price.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return double.tryParse(price);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the currency for this order
|
||||
// Note that the nomenclature in the API changed at some point
|
||||
String get totalPriceCurrency {
|
||||
if (jsondata.containsKey("order_currency")) {
|
||||
return getString("order_currency");
|
||||
} else if (jsondata.containsKey("total_price_currency")) {
|
||||
return getString("total_price_currency");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic class representing an "order line"
|
||||
*/
|
||||
class InvenTreeOrderLine extends InvenTreeModel {
|
||||
InvenTreeOrderLine() : super();
|
||||
|
||||
InvenTreeOrderLine.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
bool get overdue => getBool("overdue");
|
||||
|
||||
double get quantity => getDouble("quantity");
|
||||
|
||||
String get reference => getString("reference");
|
||||
|
||||
int get orderId => getInt("order");
|
||||
|
||||
InvenTreePart? get part {
|
||||
dynamic part_detail = jsondata["part_detail"];
|
||||
|
||||
if (part_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
int get partId => getInt("pk", subKey: "part_detail");
|
||||
|
||||
String get partName => getString("name", subKey: "part_detail");
|
||||
|
||||
String get partImage {
|
||||
String img = getString("thumbnail", subKey: "part_detail");
|
||||
|
||||
if (img.isEmpty) {
|
||||
img = getString("image", subKey: "part_detail");
|
||||
}
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
String get targetDate => getDateString("target_date");
|
||||
}
|
||||
|
||||
/*
|
||||
* Generic class representing an "ExtraLineItem"
|
||||
*/
|
||||
class InvenTreeExtraLineItem extends InvenTreeModel {
|
||||
InvenTreeExtraLineItem() : super();
|
||||
|
||||
InvenTreeExtraLineItem.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
int get orderId => getInt("order");
|
||||
|
||||
double get quantity => getDouble("quantity");
|
||||
|
||||
String get reference => getString("reference");
|
||||
|
||||
double get price => getDouble("price");
|
||||
|
||||
String get priceCurrency => getString("price_currency");
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
return {
|
||||
"order": {
|
||||
// The order cannot be edited
|
||||
"hidden": true,
|
||||
},
|
||||
"reference": {},
|
||||
"description": {},
|
||||
"quantity": {},
|
||||
"price": {},
|
||||
"price_currency": {},
|
||||
"link": {},
|
||||
"notes": {},
|
||||
};
|
||||
}
|
||||
}
|
||||
77
lib/inventree/parameter.dart
Normal file
77
lib/inventree/parameter.dart
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import "package:inventree/inventree/model.dart";
|
||||
|
||||
class InvenTreeParameter extends InvenTreeModel {
|
||||
InvenTreeParameter() : super();
|
||||
|
||||
InvenTreeParameter.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeParameter createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeParameter.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "parameter/";
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"header": {
|
||||
"type": "string",
|
||||
"read_only": true,
|
||||
"label": name,
|
||||
"help_text": description,
|
||||
"value": "",
|
||||
},
|
||||
"data": {"type": "string"},
|
||||
"note": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => getString("name", subKey: "template_detail");
|
||||
|
||||
@override
|
||||
String get description => getString("description", subKey: "template_detail");
|
||||
|
||||
String get value => getString("data");
|
||||
|
||||
String get valueString {
|
||||
String v = value;
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
v += " ";
|
||||
v += units;
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
bool get as_bool => value.toLowerCase() == "true";
|
||||
|
||||
String get units => getString("units", subKey: "template_detail");
|
||||
|
||||
bool get is_checkbox =>
|
||||
getBool("checkbox", subKey: "template_detail", backup: false);
|
||||
|
||||
// The model type of the instance this attachment is associated with
|
||||
String get modelType => getString("model_type");
|
||||
|
||||
// The ID of the instance this attachment is associated with
|
||||
int get modelId => getInt("model_id");
|
||||
|
||||
// Return a count of how many parameters exist against the specified model ID
|
||||
Future<int> countParameters(String modelType, int modelId) async {
|
||||
Map<String, String> filters = {};
|
||||
|
||||
if (!api.supportsModernParameters) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
filters["model_type"] = modelType;
|
||||
filters["model_id"] = modelId.toString();
|
||||
|
||||
return count(filters: filters);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,61 @@
|
|||
import "dart:io";
|
||||
import "dart:math";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:inventree/inventree/sentry.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/widget/part/category_display.dart";
|
||||
import "package:inventree/widget/part/part_detail.dart";
|
||||
|
||||
|
||||
/*
|
||||
* Class representing the PartCategory database model
|
||||
*/
|
||||
class InvenTreePartCategory extends InvenTreeModel {
|
||||
|
||||
InvenTreePartCategory() : super();
|
||||
|
||||
InvenTreePartCategory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreePartCategory.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/category/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
static const String MODEL_TYPE = "partcategory";
|
||||
|
||||
return {
|
||||
@override
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
// Navigate to a detail page for this item
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
// Default implementation does not do anything...
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => CategoryDisplayWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {}
|
||||
"parent": {},
|
||||
"structural": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
String get pathstring => getString("pathstring");
|
||||
|
||||
return {
|
||||
"active": "true",
|
||||
"cascade": "false"
|
||||
};
|
||||
}
|
||||
|
||||
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
String get parentPathString {
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.isNotEmpty) {
|
||||
|
|
@ -57,47 +71,45 @@ class InvenTreePartCategory extends InvenTreeModel {
|
|||
return p;
|
||||
}
|
||||
|
||||
int get partcount => (jsondata["parts"] ?? 0) as int;
|
||||
// Return the number of parts in this category
|
||||
// Note that the API changed from 'parts' to 'part_count' (v69)
|
||||
int get partcount =>
|
||||
(jsondata["part_count"] ?? jsondata["parts"] ?? 0) as int;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var cat = InvenTreePartCategory.fromJson(json);
|
||||
|
||||
// TODO ?
|
||||
|
||||
return cat;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePartCategory.fromJson(json);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Class representing the PartTestTemplate database model
|
||||
*/
|
||||
class InvenTreePartTestTemplate extends InvenTreeModel {
|
||||
|
||||
InvenTreePartTestTemplate() : super();
|
||||
|
||||
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreePartTestTemplate.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/test-template/";
|
||||
|
||||
String get key => (jsondata["key"] ?? "") as String;
|
||||
static const String MODEL_TYPE = "parttesttemplate";
|
||||
|
||||
String get testName => (jsondata["test_name"] ?? "") as String;
|
||||
String get key => getString("key");
|
||||
|
||||
bool get required => (jsondata["required"] ?? false) as bool;
|
||||
String get testName => getString("test_name");
|
||||
|
||||
bool get requiresValue => (jsondata["requires_value"] ?? false) as bool;
|
||||
bool get required => getBool("required");
|
||||
|
||||
bool get requiresAttachment => (jsondata["requires_attachment"] ?? false) as bool;
|
||||
bool get requiresValue => getBool("requires_value");
|
||||
|
||||
bool get requiresAttachment => getBool("requires_attachment");
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
var template = InvenTreePartTestTemplate.fromJson(json);
|
||||
|
||||
return template;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePartTestTemplate.fromJson(json);
|
||||
|
||||
bool passFailStatus() {
|
||||
|
||||
var result = latestResult();
|
||||
|
||||
if (result == null) {
|
||||
|
|
@ -118,12 +130,12 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
|
|||
|
||||
return results.last;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Class representing the Part database model
|
||||
*/
|
||||
class InvenTreePart extends InvenTreeModel {
|
||||
|
||||
InvenTreePart() : super();
|
||||
|
||||
InvenTreePart.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
|
@ -131,8 +143,23 @@ class InvenTreePart extends InvenTreeModel {
|
|||
@override
|
||||
String get URL => "part/";
|
||||
|
||||
static const String MODEL_TYPE = "part";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
// Navigate to a detail page for this item
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
// Default implementation does not do anything...
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => PartDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
return {
|
||||
"name": {},
|
||||
"description": {},
|
||||
|
|
@ -160,18 +187,8 @@ class InvenTreePart extends InvenTreeModel {
|
|||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
return {
|
||||
"cascade": "false",
|
||||
"active": "true",
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
return {
|
||||
"category_detail": "true", // Include category detail information
|
||||
};
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"category_detail": "true"};
|
||||
}
|
||||
|
||||
// Cached list of stock items
|
||||
|
|
@ -180,14 +197,13 @@ class InvenTreePart extends InvenTreeModel {
|
|||
int get stockItemCount => stockItems.length;
|
||||
|
||||
// Request stock items for this part
|
||||
Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
|
||||
|
||||
await InvenTreeStockItem().list(
|
||||
filters: {
|
||||
"part": "${pk}",
|
||||
"in_stock": "true",
|
||||
},
|
||||
).then((var items) {
|
||||
Future<void> getStockItems(
|
||||
BuildContext context, {
|
||||
bool showDialog = false,
|
||||
}) async {
|
||||
await InvenTreeStockItem()
|
||||
.list(filters: {"part": "${pk}", "in_stock": "true"})
|
||||
.then((var items) {
|
||||
stockItems.clear();
|
||||
|
||||
for (var item in items) {
|
||||
|
|
@ -198,16 +214,33 @@ class InvenTreePart extends InvenTreeModel {
|
|||
});
|
||||
}
|
||||
|
||||
int get supplierCount => (jsondata["suppliers"] ?? 0) as int;
|
||||
// Request pricing data for this part
|
||||
Future<InvenTreePartPricing?> getPricing() async {
|
||||
try {
|
||||
final response = await InvenTreeAPI().get("/api/part/${pk}/pricing/");
|
||||
if (response.isValid()) {
|
||||
final pricingData = response.data;
|
||||
|
||||
if (pricingData is Map<String, dynamic>) {
|
||||
return InvenTreePartPricing.fromJson(pricingData);
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print("Exception while fetching pricing data for part $pk: $e");
|
||||
sentryReportError("getPricing", e, stackTrace);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int get supplierCount => getInt("suppliers", backup: 0);
|
||||
|
||||
// Request supplier parts for this part
|
||||
Future<List<InvenTreeSupplierPart>> getSupplierParts() async {
|
||||
List<InvenTreeSupplierPart> _supplierParts = [];
|
||||
|
||||
final parts = await InvenTreeSupplierPart().list(
|
||||
filters: {
|
||||
"part": "${pk}",
|
||||
}
|
||||
filters: {"part": "${pk}"},
|
||||
);
|
||||
|
||||
for (var result in parts) {
|
||||
|
|
@ -219,7 +252,6 @@ class InvenTreePart extends InvenTreeModel {
|
|||
return _supplierParts;
|
||||
}
|
||||
|
||||
|
||||
// Cached list of test templates
|
||||
List<InvenTreePartTestTemplate> testingTemplates = [];
|
||||
|
||||
|
|
@ -227,13 +259,9 @@ class InvenTreePart extends InvenTreeModel {
|
|||
|
||||
// Request test templates from the serve
|
||||
Future<void> getTestTemplates() async {
|
||||
|
||||
InvenTreePartTestTemplate().list(
|
||||
filters: {
|
||||
"part": "${pk}",
|
||||
},
|
||||
).then((var templates) {
|
||||
|
||||
InvenTreePartTestTemplate().list(filters: {"part": "${pk}"}).then((
|
||||
var templates,
|
||||
) {
|
||||
testingTemplates.clear();
|
||||
|
||||
for (var t in templates) {
|
||||
|
|
@ -246,48 +274,36 @@ class InvenTreePart extends InvenTreeModel {
|
|||
|
||||
int? get defaultLocation => jsondata["default_location"] as int?;
|
||||
|
||||
// Get the number of stock on order for this Part
|
||||
double get onOrder => double.tryParse(jsondata["ordering"].toString()) ?? 0;
|
||||
double get onOrder => getDouble("ordering");
|
||||
|
||||
String get onOrderString {
|
||||
String get onOrderString => simpleNumberString(onOrder);
|
||||
|
||||
return simpleNumberString(onOrder);
|
||||
double get inStock {
|
||||
if (jsondata.containsKey("total_in_stock")) {
|
||||
return getDouble("total_in_stock");
|
||||
} else {
|
||||
return getDouble("in_stock");
|
||||
}
|
||||
}
|
||||
|
||||
// Get the stock count for this Part
|
||||
double get inStock => double.tryParse(jsondata["in_stock"].toString()) ?? 0;
|
||||
|
||||
String get inStockString {
|
||||
|
||||
String q = simpleNumberString(inStock);
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
String get inStockString => simpleNumberString(inStock);
|
||||
|
||||
// Get the 'available stock' for this Part
|
||||
double get unallocatedStock {
|
||||
double unallocated = 0;
|
||||
|
||||
// Note that the 'available_stock' was not added until API v35
|
||||
if (jsondata.containsKey("unallocated_stock")) {
|
||||
return double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
|
||||
unallocated =
|
||||
double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
|
||||
} else {
|
||||
return inStock;
|
||||
}
|
||||
unallocated = inStock;
|
||||
}
|
||||
|
||||
String get unallocatedStockString {
|
||||
String q = simpleNumberString(unallocatedStock);
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
return max(0, unallocated);
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
String get unallocatedStockString => simpleNumberString(unallocatedStock);
|
||||
|
||||
String stockString({bool includeUnits = true}) {
|
||||
String q = unallocatedStockString;
|
||||
|
|
@ -303,42 +319,44 @@ class InvenTreePart extends InvenTreeModel {
|
|||
return q;
|
||||
}
|
||||
|
||||
String get units => (jsondata["units"] ?? "") as String;
|
||||
String get units => getString("units");
|
||||
|
||||
// Get the ID of the Part that this part is a variant of (or null)
|
||||
int? get variantOf => jsondata["variant_of"] as int?;
|
||||
|
||||
// Get the number of units being build for this Part
|
||||
double get building => double.tryParse(jsondata["building"].toString()) ?? 0;
|
||||
|
||||
// Get the number of BOM items in this Part (if it is an assembly)
|
||||
int get bomItemCount => (jsondata["bom_items"] ?? 0) as int;
|
||||
double get building => getDouble("building");
|
||||
|
||||
// Get the number of BOMs this Part is used in (if it is a component)
|
||||
int get usedInCount => (jsondata["used_in"] ?? 0) as int;
|
||||
int get usedInCount =>
|
||||
jsondata.containsKey("used_in") ? getInt("used_in", backup: 0) : 0;
|
||||
|
||||
bool get isAssembly => (jsondata["assembly"] ?? false) as bool;
|
||||
bool get isAssembly => getBool("assembly");
|
||||
|
||||
bool get isComponent => (jsondata["component"] ?? false) as bool;
|
||||
bool get isComponent => getBool("component");
|
||||
|
||||
bool get isPurchaseable => (jsondata["purchaseable"] ?? false) as bool;
|
||||
bool get isPurchaseable => getBool("purchaseable");
|
||||
|
||||
bool get isSalable => (jsondata["salable"] ?? false) as bool;
|
||||
bool get isSalable => getBool("salable");
|
||||
|
||||
bool get isActive => (jsondata["active"] ?? false) as bool;
|
||||
bool get isActive => getBool("active");
|
||||
|
||||
bool get isVirtual => (jsondata["virtual"] ?? false) as bool;
|
||||
bool get isVirtual => getBool("virtual");
|
||||
|
||||
bool get isTrackable => (jsondata["trackable"] ?? false) as bool;
|
||||
bool get isTemplate => getBool("is_template");
|
||||
|
||||
bool get isTrackable => getBool("trackable");
|
||||
|
||||
bool get isTestable => getBool("testable");
|
||||
|
||||
// Get the IPN (internal part number) for the Part instance
|
||||
String get IPN => (jsondata["IPN"] ?? "") as String;
|
||||
String get IPN => getString("IPN");
|
||||
|
||||
// Get the revision string for the Part instance
|
||||
String get revision => (jsondata["revision"] ?? "") as String;
|
||||
String get revision => getString("revision");
|
||||
|
||||
// Get the category ID for the Part instance (or "null" if does not exist)
|
||||
int get categoryId => (jsondata["category"] ?? -1) as int;
|
||||
int get categoryId => getInt("category");
|
||||
|
||||
// Get the category name for the Part instance
|
||||
String get categoryName {
|
||||
|
|
@ -359,16 +377,16 @@ class InvenTreePart extends InvenTreeModel {
|
|||
|
||||
return (jsondata["category_detail"]?["description"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Get the image URL for the Part instance
|
||||
String get _image => (jsondata["image"] ?? "") as String;
|
||||
String get _image => getString("image");
|
||||
|
||||
// Get the thumbnail URL for the Part instance
|
||||
String get _thumbnail => (jsondata["thumbnail"] ?? "") as String;
|
||||
String get _thumbnail => getString("thumbnail");
|
||||
|
||||
// Return the fully-qualified name for the Part instance
|
||||
String get fullname {
|
||||
|
||||
String fn = (jsondata["full_name"] ?? "") as String;
|
||||
String fn = getString("full_name");
|
||||
|
||||
if (fn.isNotEmpty) return fn;
|
||||
|
||||
|
|
@ -412,30 +430,58 @@ class InvenTreePart extends InvenTreeModel {
|
|||
}
|
||||
|
||||
// Return the "starred" status of this part
|
||||
bool get starred => (jsondata["starred"] ?? false) as bool;
|
||||
bool get starred => getBool("starred");
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
|
||||
var part = InvenTreePart.fromJson(json);
|
||||
|
||||
return part;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePart.fromJson(json);
|
||||
}
|
||||
|
||||
class InvenTreePartPricing extends InvenTreeModel {
|
||||
InvenTreePartPricing() : super();
|
||||
|
||||
class InvenTreePartAttachment extends InvenTreeAttachment {
|
||||
|
||||
InvenTreePartAttachment() : super();
|
||||
|
||||
InvenTreePartAttachment.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreePartPricing.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "part/attachment/";
|
||||
List<String> get rolesRequired => ["part"];
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreePartAttachment.fromJson(json);
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePartPricing.fromJson(json);
|
||||
|
||||
// Price data accessors
|
||||
String get currency => getString("currency", backup: "USD");
|
||||
|
||||
double? get overallMin => getDoubleOrNull("overall_min");
|
||||
double? get overallMax => getDoubleOrNull("overall_max");
|
||||
|
||||
double? get overrideMin => getDoubleOrNull("override_min");
|
||||
double? get overrideMax => getDoubleOrNull("override_max");
|
||||
|
||||
String get overrideMinCurrency =>
|
||||
getString("override_min_currency", backup: currency);
|
||||
String get overrideMaxCurrency =>
|
||||
getString("override_max_currency", backup: currency);
|
||||
|
||||
double? get bomCostMin => getDoubleOrNull("bom_cost_min");
|
||||
double? get bomCostMax => getDoubleOrNull("bom_cost_max");
|
||||
|
||||
double? get purchaseCostMin => getDoubleOrNull("purchase_cost_min");
|
||||
double? get purchaseCostMax => getDoubleOrNull("purchase_cost_max");
|
||||
|
||||
double? get internalCostMin => getDoubleOrNull("internal_cost_min");
|
||||
double? get internalCostMax => getDoubleOrNull("internal_cost_max");
|
||||
|
||||
double? get supplierPriceMin => getDoubleOrNull("supplier_price_min");
|
||||
double? get supplierPriceMax => getDoubleOrNull("supplier_price_max");
|
||||
|
||||
double? get variantCostMin => getDoubleOrNull("variant_cost_min");
|
||||
double? get variantCostMax => getDoubleOrNull("variant_cost_max");
|
||||
|
||||
double? get salePriceMin => getDoubleOrNull("sale_price_min");
|
||||
double? get salePriceMax => getDoubleOrNull("sale_price_max");
|
||||
|
||||
double? get saleHistoryMin => getDoubleOrNull("sale_history_min");
|
||||
double? get saleHistoryMax => getDoubleOrNull("sale_history_max");
|
||||
}
|
||||
27
lib/inventree/project_code.dart
Normal file
27
lib/inventree/project_code.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import "package:inventree/inventree/model.dart";
|
||||
|
||||
/*
|
||||
* Class representing the ProjectCode database model
|
||||
*/
|
||||
class InvenTreeProjectCode extends InvenTreeModel {
|
||||
InvenTreeProjectCode() : super();
|
||||
|
||||
InvenTreeProjectCode.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeProjectCode.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "project-code/";
|
||||
|
||||
static const String MODEL_TYPE = "projectcode";
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
return {"code": {}, "description": {}};
|
||||
}
|
||||
|
||||
String get code => getString("code");
|
||||
}
|
||||
|
|
@ -1,73 +1,91 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/orders.dart";
|
||||
import "package:inventree/widget/order/extra_line_detail.dart";
|
||||
import "package:inventree/widget/order/purchase_order_detail.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
|
||||
// TODO: In the future, status codes should be retrieved from the server
|
||||
const int PO_STATUS_PENDING = 10;
|
||||
const int PO_STATUS_PLACED = 20;
|
||||
const int PO_STATUS_COMPLETE = 30;
|
||||
const int PO_STATUS_CANCELLED = 40;
|
||||
const int PO_STATUS_LOST = 50;
|
||||
const int PO_STATUS_RETURNED = 60;
|
||||
|
||||
class InvenTreePurchaseOrder extends InvenTreeModel {
|
||||
import "package:inventree/api_form.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
/*
|
||||
* Class representing an individual PurchaseOrder instance
|
||||
*/
|
||||
class InvenTreePurchaseOrder extends InvenTreeOrder {
|
||||
InvenTreePurchaseOrder() : super();
|
||||
|
||||
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreePurchaseOrder.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePurchaseOrder.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/po/";
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => PurchaseOrderDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
static const String MODEL_TYPE = "purchaseorder";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["purchase_order"];
|
||||
|
||||
String get receive_url => "${url}receive/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"reference": {},
|
||||
"supplier": {
|
||||
"filters": {"is_supplier": true},
|
||||
},
|
||||
"supplier_reference": {},
|
||||
"description": {},
|
||||
"project_code": {},
|
||||
"destination": {},
|
||||
"start_date": {},
|
||||
"target_date": {},
|
||||
"link": {},
|
||||
"responsible": {},
|
||||
"contact": {
|
||||
"filters": {"company": supplierId},
|
||||
},
|
||||
};
|
||||
|
||||
if (!InvenTreeAPI().supportsProjectCodes) {
|
||||
fields.remove("project_code");
|
||||
}
|
||||
|
||||
if (!InvenTreeAPI().supportsPurchaseOrderDestination) {
|
||||
fields.remove("destination");
|
||||
}
|
||||
|
||||
if (!InvenTreeAPI().supportsStartDate) {
|
||||
fields.remove("start_date");
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
return {
|
||||
"supplier_detail": "true",
|
||||
};
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"supplier_detail": "true"};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
return {
|
||||
"supplier_detail": "true",
|
||||
};
|
||||
}
|
||||
|
||||
String get issueDate => (jsondata["issue_date"] ?? "") as String;
|
||||
|
||||
String get completeDate => (jsondata["complete_date"] ?? "") as String;
|
||||
|
||||
String get creationDate => (jsondata["creation_date"] ?? "") as String;
|
||||
|
||||
String get targetDate => (jsondata["target_date"] ?? "") as String;
|
||||
|
||||
int get lineItemCount => (jsondata["line_items"] ?? 0) as int;
|
||||
|
||||
bool get overdue => (jsondata["overdue"] ?? false) as bool;
|
||||
|
||||
String get reference => (jsondata["reference"] ?? "") as String;
|
||||
|
||||
int get responsibleId => (jsondata["responsible"] ?? -1) as int;
|
||||
|
||||
int get supplierId => (jsondata["supplier"] ?? -1) as int;
|
||||
int get supplierId => getInt("supplier");
|
||||
|
||||
InvenTreeCompany? get supplier {
|
||||
|
||||
dynamic supplier_detail = jsondata["supplier_detail"];
|
||||
|
||||
if (supplier_detail == null) {
|
||||
|
|
@ -77,24 +95,30 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
|
|||
}
|
||||
}
|
||||
|
||||
String get supplierReference => (jsondata["supplier_reference"] ?? "") as String;
|
||||
String get supplierReference => getString("supplier_reference");
|
||||
|
||||
int get status => (jsondata["status"] ?? -1) as int;
|
||||
int get destinationId => getInt("destination");
|
||||
|
||||
String get statusText => (jsondata["status_text"] ?? "") as String;
|
||||
bool get isOpen => api.PurchaseOrderStatus.isNameIn(status, [
|
||||
"PENDING",
|
||||
"PLACED",
|
||||
"ON_HOLD",
|
||||
]);
|
||||
|
||||
bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED;
|
||||
bool get isPending =>
|
||||
api.PurchaseOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
|
||||
|
||||
bool get isPlaced => status == PO_STATUS_PLACED;
|
||||
bool get isPlaced => api.PurchaseOrderStatus.isNameIn(status, ["PLACED"]);
|
||||
|
||||
bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED;
|
||||
bool get isFailed => api.PurchaseOrderStatus.isNameIn(status, [
|
||||
"CANCELLED",
|
||||
"LOST",
|
||||
"RETURNED",
|
||||
]);
|
||||
|
||||
Future<List<InvenTreePOLineItem>> getLineItems() async {
|
||||
|
||||
final results = await InvenTreePOLineItem().list(
|
||||
filters: {
|
||||
"order": "${pk}",
|
||||
}
|
||||
filters: {"order": "${pk}"},
|
||||
);
|
||||
|
||||
List<InvenTreePOLineItem> items = [];
|
||||
|
|
@ -108,77 +132,92 @@ class InvenTreePurchaseOrder extends InvenTreeModel {
|
|||
return items;
|
||||
}
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreePurchaseOrder.fromJson(json);
|
||||
/// Mark this order as "placed" / "issued"
|
||||
Future<void> issueOrder() async {
|
||||
// Order can only be placed when the order is 'pending'
|
||||
if (!isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoadingOverlay();
|
||||
await api.post("${url}issue/", expectedStatusCode: 201);
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
|
||||
/// Mark this order as "cancelled"
|
||||
Future<void> cancelOrder() async {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoadingOverlay();
|
||||
await api.post("${url}cancel/", expectedStatusCode: 201);
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
class InvenTreePOLineItem extends InvenTreeModel {
|
||||
|
||||
class InvenTreePOLineItem extends InvenTreeOrderLine {
|
||||
InvenTreePOLineItem() : super();
|
||||
|
||||
InvenTreePOLineItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreePOLineItem.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePOLineItem.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/po-line/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
List<String> get rolesRequired => ["purchase_order"];
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
return {
|
||||
// TODO: @Guusggg Not sure what will come here.
|
||||
// "quantity": {},
|
||||
// "reference": {},
|
||||
// "notes": {},
|
||||
// "order": {},
|
||||
// "part": {},
|
||||
"received": {},
|
||||
// "purchase_price": {},
|
||||
// "purchase_price_currency": {},
|
||||
// "destination": {}
|
||||
"part": {
|
||||
// We cannot edit the supplier part field here
|
||||
"hidden": true,
|
||||
},
|
||||
"order": {
|
||||
// We cannot edit the order field here
|
||||
"hidden": true,
|
||||
},
|
||||
"reference": {},
|
||||
"quantity": {},
|
||||
"purchase_price": {},
|
||||
"purchase_price_currency": {},
|
||||
"destination": {},
|
||||
"notes": {},
|
||||
"link": {},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
return {
|
||||
"part_detail": "true",
|
||||
};
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"part_detail": "true", "order_detail": "true"};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
return {
|
||||
"part_detail": "true",
|
||||
};
|
||||
}
|
||||
double get received => getDouble("received");
|
||||
|
||||
bool get isComplete => received >= quantity;
|
||||
|
||||
double get quantity => (jsondata["quantity"] ?? 0) as double;
|
||||
double get progressRatio {
|
||||
if (quantity <= 0 || received <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double get received => (jsondata["received"] ?? 0) as double;
|
||||
return received / quantity;
|
||||
}
|
||||
|
||||
String get progressString =>
|
||||
simpleNumberString(received) + " / " + simpleNumberString(quantity);
|
||||
|
||||
double get outstanding => quantity - received;
|
||||
|
||||
String get reference => (jsondata["reference"] ?? "") as String;
|
||||
|
||||
int get orderId => (jsondata["order"] ?? -1) as int;
|
||||
|
||||
int get supplierPartId => (jsondata["part"] ?? -1) as int;
|
||||
|
||||
InvenTreePart? get part {
|
||||
dynamic part_detail = jsondata["part_detail"];
|
||||
|
||||
if (part_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
int get supplierPartId => getInt("part");
|
||||
|
||||
InvenTreeSupplierPart? get supplierPart {
|
||||
|
||||
dynamic detail = jsondata["supplier_part_detail"];
|
||||
|
||||
if (detail == null) {
|
||||
|
|
@ -188,18 +227,112 @@ class InvenTreePOLineItem extends InvenTreeModel {
|
|||
}
|
||||
}
|
||||
|
||||
double get purchasePrice => double.parse((jsondata["purchase_price"] ?? "") as String);
|
||||
InvenTreePurchaseOrder? get purchaseOrder {
|
||||
dynamic detail = jsondata["order_detail"];
|
||||
|
||||
String get purchasePriceCurrency => (jsondata["purchase_price_currency"] ?? "") as String;
|
||||
if (detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreePurchaseOrder.fromJson(detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
String get purchasePriceString => (jsondata["purchase_price_string"] ?? "") as String;
|
||||
String get SKU => getString("SKU", subKey: "supplier_part_detail");
|
||||
|
||||
int get destination => (jsondata["destination"] ?? -1) as int;
|
||||
double get purchasePrice => getDouble("purchase_price");
|
||||
|
||||
Map<String, dynamic> get destinationDetail => (jsondata["destination_detail"] ?? {}) as Map<String, dynamic>;
|
||||
String get purchasePriceCurrency => getString("purchase_price_currency");
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreePOLineItem.fromJson(json);
|
||||
int get destinationId => getInt("destination");
|
||||
|
||||
Map<String, dynamic> get orderDetail => getMap("order_detail");
|
||||
|
||||
Map<String, dynamic> get destinationDetail => getMap("destination_detail");
|
||||
|
||||
// Receive this line item into stock
|
||||
Future<void> receive(
|
||||
BuildContext context, {
|
||||
int? destination,
|
||||
double? quantity,
|
||||
String? barcode,
|
||||
Function? onSuccess,
|
||||
}) async {
|
||||
// Infer the destination location from the line item if not provided
|
||||
if (destinationId > 0) {
|
||||
destination = destinationId;
|
||||
}
|
||||
|
||||
destination ??= (orderDetail["destination"]) as int?;
|
||||
|
||||
quantity ??= outstanding;
|
||||
|
||||
// Construct form fields
|
||||
Map<String, dynamic> fields = {
|
||||
"line_item": {
|
||||
"parent": "items",
|
||||
"nested": true,
|
||||
"hidden": true,
|
||||
"value": pk,
|
||||
},
|
||||
"quantity": {"parent": "items", "nested": true, "value": quantity},
|
||||
"location": {},
|
||||
"status": {"parent": "items", "nested": true},
|
||||
"batch_code": {"parent": "items", "nested": true},
|
||||
"barcode": {
|
||||
"parent": "items",
|
||||
"nested": true,
|
||||
"type": "barcode",
|
||||
"label": L10().barcodeAssign,
|
||||
"value": barcode,
|
||||
"required": false,
|
||||
},
|
||||
};
|
||||
|
||||
if (destination != null && destination > 0) {
|
||||
fields["location"]?["value"] = destination;
|
||||
}
|
||||
|
||||
InvenTreePurchaseOrder? order = purchaseOrder;
|
||||
|
||||
if (order != null) {
|
||||
await launchApiForm(
|
||||
context,
|
||||
L10().receiveItem,
|
||||
order.receive_url,
|
||||
fields,
|
||||
method: "POST",
|
||||
icon: TablerIcons.transition_right,
|
||||
onSuccess: (data) {
|
||||
if (onSuccess != null) {
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InvenTreePOExtraLineItem extends InvenTreeExtraLineItem {
|
||||
InvenTreePOExtraLineItem() : super();
|
||||
|
||||
InvenTreePOExtraLineItem.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreePOExtraLineItem.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/po-extra-line/";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["purchase_order"];
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
430
lib/inventree/sales_order.dart
Normal file
430
lib/inventree/sales_order.dart
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
import "package:flutter/material.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
|
||||
import "package:inventree/inventree/company.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/inventree/orders.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/stock.dart";
|
||||
import "package:inventree/widget/order/so_shipment_detail.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/order/extra_line_detail.dart";
|
||||
import "package:inventree/widget/order/sales_order_detail.dart";
|
||||
|
||||
/*
|
||||
* Class representing an individual SalesOrder
|
||||
*/
|
||||
class InvenTreeSalesOrder extends InvenTreeOrder {
|
||||
InvenTreeSalesOrder() : super();
|
||||
|
||||
InvenTreeSalesOrder.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSalesOrder.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/so/";
|
||||
|
||||
static const String MODEL_TYPE = "salesorder";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["sales_order"];
|
||||
|
||||
String get allocate_url => "${url}allocate/";
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => SalesOrderDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"reference": {},
|
||||
"customer": {
|
||||
"filters": {"is_customer": true, "active": true},
|
||||
},
|
||||
"customer_reference": {},
|
||||
"description": {},
|
||||
"project_code": {},
|
||||
"start_date": {},
|
||||
"target_date": {},
|
||||
"link": {},
|
||||
"responsible": {},
|
||||
"contact": {
|
||||
"filters": {"company": customerId},
|
||||
},
|
||||
};
|
||||
|
||||
if (!InvenTreeAPI().supportsProjectCodes) {
|
||||
fields.remove("project_code");
|
||||
}
|
||||
|
||||
if (!InvenTreeAPI().supportsContactModel) {
|
||||
fields.remove("contact");
|
||||
}
|
||||
|
||||
if (!InvenTreeAPI().supportsStartDate) {
|
||||
fields.remove("start_date");
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"customer_detail": "true"};
|
||||
}
|
||||
|
||||
Future<void> issueOrder() async {
|
||||
if (!isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoadingOverlay();
|
||||
await api.post("${url}issue/", expectedStatusCode: 201);
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
|
||||
/// Mark this order as "cancelled"
|
||||
Future<void> cancelOrder() async {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoadingOverlay();
|
||||
await api.post("${url}cancel/", expectedStatusCode: 201);
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
|
||||
int get customerId => getInt("customer");
|
||||
|
||||
InvenTreeCompany? get customer {
|
||||
dynamic customer_detail = jsondata["customer_detail"];
|
||||
|
||||
if (customer_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeCompany.fromJson(customer_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
String get customerReference => getString("customer_reference");
|
||||
|
||||
bool get isOpen => api.SalesOrderStatus.isNameIn(status, [
|
||||
"PENDING",
|
||||
"IN_PROGRESS",
|
||||
"ON_HOLD",
|
||||
]);
|
||||
|
||||
bool get isPending =>
|
||||
api.SalesOrderStatus.isNameIn(status, ["PENDING", "ON_HOLD"]);
|
||||
|
||||
bool get isInProgress =>
|
||||
api.SalesOrderStatus.isNameIn(status, ["IN_PROGRESS"]);
|
||||
|
||||
bool get isComplete => api.SalesOrderStatus.isNameIn(status, ["SHIPPED"]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing an individual line item in a SalesOrder
|
||||
*/
|
||||
class InvenTreeSOLineItem extends InvenTreeOrderLine {
|
||||
InvenTreeSOLineItem() : super();
|
||||
|
||||
InvenTreeSOLineItem.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSOLineItem.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/so-line/";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["sales_order"];
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
return {
|
||||
"order": {"hidden": true},
|
||||
"part": {
|
||||
"filters": {"salable": true},
|
||||
},
|
||||
"quantity": {},
|
||||
"reference": {},
|
||||
"notes": {},
|
||||
"link": {},
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Map<String, dynamic>> allocateFormFields() {
|
||||
return {
|
||||
"line_item": {"parent": "items", "nested": true, "hidden": true},
|
||||
"stock_item": {"parent": "items", "nested": true, "filters": {}},
|
||||
"quantity": {"parent": "items", "nested": true},
|
||||
"shipment": {"filters": {}},
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"part_detail": "true"};
|
||||
}
|
||||
|
||||
double get allocated => getDouble("allocated");
|
||||
|
||||
bool get isAllocated => allocated >= quantity;
|
||||
|
||||
double get allocatedRatio {
|
||||
if (quantity <= 0 || allocated <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return allocated / quantity;
|
||||
}
|
||||
|
||||
double get unallocatedQuantity {
|
||||
double unallocated = quantity - allocated;
|
||||
|
||||
if (unallocated < 0) {
|
||||
unallocated = 0;
|
||||
}
|
||||
|
||||
return unallocated;
|
||||
}
|
||||
|
||||
String get allocatedString =>
|
||||
simpleNumberString(allocated) + " / " + simpleNumberString(quantity);
|
||||
|
||||
double get shipped => getDouble("shipped");
|
||||
|
||||
double get outstanding => quantity - shipped;
|
||||
|
||||
double get availableStock => getDouble("available_stock");
|
||||
|
||||
double get progressRatio {
|
||||
if (quantity <= 0 || shipped <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return shipped / quantity;
|
||||
}
|
||||
|
||||
String get progressString =>
|
||||
simpleNumberString(shipped) + " / " + simpleNumberString(quantity);
|
||||
|
||||
bool get isComplete => shipped >= quantity;
|
||||
|
||||
double get available =>
|
||||
getDouble("available_stock") + getDouble("available_variant_stock");
|
||||
|
||||
double get salePrice => getDouble("sale_price");
|
||||
|
||||
String get salePriceCurrency => getString("sale_price_currency");
|
||||
}
|
||||
|
||||
class InvenTreeSOExtraLineItem extends InvenTreeExtraLineItem {
|
||||
InvenTreeSOExtraLineItem() : super();
|
||||
|
||||
InvenTreeSOExtraLineItem.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSOExtraLineItem.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "order/so-extra-line/";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["sales_order"];
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => ExtraLineDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing a sales order shipment
|
||||
*/
|
||||
class InvenTreeSalesOrderShipment extends InvenTreeModel {
|
||||
InvenTreeSalesOrderShipment() : super();
|
||||
|
||||
InvenTreeSalesOrderShipment.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSalesOrderShipment.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "/order/so/shipment/";
|
||||
|
||||
String get SHIP_SHIPMENT_URL => "/order/so/shipment/${pk}/ship/";
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => SOShipmentDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["sales_order"];
|
||||
|
||||
static const String MODEL_TYPE = "salesordershipment";
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"order": {},
|
||||
"reference": {},
|
||||
"tracking_number": {},
|
||||
"invoice_number": {},
|
||||
"link": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
int get orderId => getInt("order");
|
||||
|
||||
InvenTreeSalesOrder? get order {
|
||||
dynamic order_detail = jsondata["order_detail"];
|
||||
|
||||
if (order_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
String get reference => getString("reference");
|
||||
|
||||
String get tracking_number => getString("tracking_number");
|
||||
|
||||
String get invoice_number => getString("invoice_number");
|
||||
|
||||
String? get shipment_date => getString("shipment_date");
|
||||
|
||||
String? get delivery_date => getString("delivery_date");
|
||||
|
||||
int? get checked_by_id => getInt("checked_by");
|
||||
|
||||
bool get isChecked => checked_by_id != null && checked_by_id! > 0;
|
||||
|
||||
bool get isShipped => shipment_date != null && shipment_date!.isNotEmpty;
|
||||
|
||||
bool get isDelivered => delivery_date != null && delivery_date!.isNotEmpty;
|
||||
}
|
||||
|
||||
/*
|
||||
* Class representing an allocation of stock against a SalesOrderShipment
|
||||
*/
|
||||
class InvenTreeSalesOrderAllocation extends InvenTreeModel {
|
||||
InvenTreeSalesOrderAllocation() : super();
|
||||
|
||||
InvenTreeSalesOrderAllocation.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeSalesOrderAllocation.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "/order/so-allocation/";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["sales_order"];
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {
|
||||
"part_detail": "true",
|
||||
"order_detail": "true",
|
||||
"item_detail": "true",
|
||||
"location_detail": "true",
|
||||
};
|
||||
}
|
||||
|
||||
static const String MODEL_TYPE = "salesorderallocation";
|
||||
|
||||
int get orderId => getInt("order");
|
||||
|
||||
InvenTreeSalesOrder? get order {
|
||||
dynamic order_detail = jsondata["order_detail"];
|
||||
|
||||
if (order_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeSalesOrder.fromJson(order_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
int get stockItemId => getInt("item");
|
||||
|
||||
InvenTreeStockItem? get stockItem {
|
||||
dynamic item_detail = jsondata["item_detail"];
|
||||
|
||||
if (item_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeStockItem.fromJson(item_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
int get partId => getInt("part");
|
||||
|
||||
InvenTreePart? get part {
|
||||
dynamic part_detail = jsondata["part_detail"];
|
||||
|
||||
if (part_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreePart.fromJson(part_detail as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
|
||||
int get shipmentId => getInt("shipment");
|
||||
|
||||
bool get hasShipment => shipmentId > 0;
|
||||
|
||||
InvenTreeSalesOrderShipment? get shipment {
|
||||
dynamic shipment_detail = jsondata["shipment_detail"];
|
||||
|
||||
if (shipment_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeSalesOrderShipment.fromJson(
|
||||
shipment_detail as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int get locationId => getInt("location");
|
||||
|
||||
InvenTreeStockLocation? get location {
|
||||
dynamic location_detail = jsondata["location_detail"];
|
||||
|
||||
if (location_detail == null) {
|
||||
return null;
|
||||
} else {
|
||||
return InvenTreeStockLocation.fromJson(
|
||||
location_detail as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:device_info_plus/device_info_plus.dart";
|
||||
import "package:inventree/app_settings.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:package_info_plus/package_info_plus.dart";
|
||||
import "package:sentry_flutter/sentry_flutter.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/dsn.dart";
|
||||
import "package:inventree/preferences.dart";
|
||||
|
||||
Future<Map<String, dynamic>> getDeviceInfo() async {
|
||||
|
||||
// Extract device information
|
||||
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
|
|
@ -28,7 +30,6 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||
"identifierForVendor": iosDeviceInfo.identifierForVendor,
|
||||
"isPhysicalDevice": iosDeviceInfo.isPhysicalDevice,
|
||||
};
|
||||
|
||||
} else if (Platform.isAndroid) {
|
||||
final androidDeviceInfo = await deviceInfo.androidInfo;
|
||||
|
||||
|
|
@ -37,13 +38,13 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||
"model": androidDeviceInfo.model,
|
||||
"device": androidDeviceInfo.device,
|
||||
"id": androidDeviceInfo.id,
|
||||
"androidId": androidDeviceInfo.androidId,
|
||||
"androidId": androidDeviceInfo.id,
|
||||
"brand": androidDeviceInfo.brand,
|
||||
"display": androidDeviceInfo.display,
|
||||
"hardware": androidDeviceInfo.hardware,
|
||||
"manufacturer": androidDeviceInfo.manufacturer,
|
||||
"product": androidDeviceInfo.product,
|
||||
"version": androidDeviceInfo.version.release,
|
||||
"systemVersion": androidDeviceInfo.version.release,
|
||||
"supported32BitAbis": androidDeviceInfo.supported32BitAbis,
|
||||
"supported64BitAbis": androidDeviceInfo.supported64BitAbis,
|
||||
"supportedAbis": androidDeviceInfo.supportedAbis,
|
||||
|
|
@ -54,12 +55,11 @@ Future<Map<String, dynamic>> getDeviceInfo() async {
|
|||
return device_info;
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> getServerInfo() => {
|
||||
"version": InvenTreeAPI().version,
|
||||
"version": InvenTreeAPI().serverVersion,
|
||||
"apiVersion": InvenTreeAPI().apiVersion,
|
||||
};
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getAppInfo() async {
|
||||
// Add app info
|
||||
final package_info = await PackageInfo.fromPlatform();
|
||||
|
|
@ -72,7 +72,6 @@ Future<Map<String, dynamic>> getAppInfo() async {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
bool isInDebugMode() {
|
||||
bool inDebugMode = false;
|
||||
|
||||
|
|
@ -81,7 +80,13 @@ bool isInDebugMode() {
|
|||
return inDebugMode;
|
||||
}
|
||||
|
||||
Future<bool> sentryReportMessage(String message, {Map<String, String>? context}) async {
|
||||
Future<bool> sentryReportMessage(
|
||||
String message, {
|
||||
Map<String, String>? context,
|
||||
}) async {
|
||||
if (SENTRY_DSN_KEY.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final server_info = getServerInfo();
|
||||
final app_info = await getAppInfo();
|
||||
|
|
@ -98,23 +103,22 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
|||
// We don't care about the server address, only the path and query parameters!
|
||||
// Overwrite the provided URL
|
||||
context["url"] = uri.path + "?" + uri.query;
|
||||
|
||||
} catch (error) {
|
||||
// Ignore if any errors are thrown here
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
print("Sending user message to Sentry: ${message}, ${context}");
|
||||
|
||||
if (isInDebugMode()) {
|
||||
|
||||
print("----- In dev mode. Not sending message to Sentry.io -----");
|
||||
return true;
|
||||
}
|
||||
|
||||
final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
|
||||
final upload =
|
||||
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
|
||||
as bool;
|
||||
|
||||
if (!upload) {
|
||||
print("----- Error reporting disabled -----");
|
||||
|
|
@ -122,13 +126,16 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
|||
}
|
||||
|
||||
Sentry.configureScope((scope) {
|
||||
scope.setExtra("server", server_info);
|
||||
scope.setExtra("app", app_info);
|
||||
scope.setExtra("device", device_info);
|
||||
scope.setContexts("server", server_info);
|
||||
scope.setContexts("app", app_info);
|
||||
scope.setContexts("device", device_info);
|
||||
|
||||
if (context != null) {
|
||||
scope.setExtra("context", context);
|
||||
scope.setContexts("context", context);
|
||||
}
|
||||
|
||||
// Catch stacktrace data if possible
|
||||
scope.setContexts("stacktrace", StackTrace.current.toString());
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -141,8 +148,19 @@ Future<bool> sentryReportMessage(String message, {Map<String, String>? context})
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
||||
/*
|
||||
* Report an error message to sentry.io
|
||||
*/
|
||||
Future<void> sentryReportError(
|
||||
String source,
|
||||
dynamic error,
|
||||
StackTrace? stackTrace, {
|
||||
Map<String, String> context = const {},
|
||||
}) async {
|
||||
if (sentryIgnoreError(error)) {
|
||||
// No action on this error
|
||||
return;
|
||||
}
|
||||
|
||||
print("----- Sentry Intercepted error: $error -----");
|
||||
print(stackTrace);
|
||||
|
|
@ -151,32 +169,85 @@ Future<void> sentryReportError(dynamic error, dynamic stackTrace) async {
|
|||
// check if you are running in dev mode using an assertion and omit sending
|
||||
// the report.
|
||||
if (isInDebugMode()) {
|
||||
|
||||
print("----- In dev mode. Not sending report to Sentry.io -----");
|
||||
return;
|
||||
}
|
||||
|
||||
final upload = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool;
|
||||
if (SENTRY_DSN_KEY.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final upload =
|
||||
await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true)
|
||||
as bool;
|
||||
|
||||
if (!upload) {
|
||||
print("----- Error reporting disabled -----");
|
||||
return;
|
||||
}
|
||||
|
||||
// Some errors are outside our control, and we do not want to "pollute" the uploaded data
|
||||
if (source == "FlutterError.onError") {
|
||||
String errorString = error.toString();
|
||||
|
||||
// Missing media file
|
||||
if (errorString.contains("HttpException") &&
|
||||
errorString.contains("404") &&
|
||||
errorString.contains("/media/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local file system exception
|
||||
if (errorString.contains("FileSystemException")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final server_info = getServerInfo();
|
||||
final app_info = await getAppInfo();
|
||||
final device_info = await getDeviceInfo();
|
||||
|
||||
// Ensure we pass the 'source' of the error
|
||||
context["source"] = source;
|
||||
|
||||
if (hasContext()) {
|
||||
final ctx = OneContext().context;
|
||||
|
||||
if (ctx != null) {
|
||||
context["widget"] = ctx.widget.toString();
|
||||
context["widgetType"] = ctx.widget.runtimeType.toString();
|
||||
}
|
||||
}
|
||||
|
||||
Sentry.configureScope((scope) {
|
||||
scope.setExtra("server", server_info);
|
||||
scope.setExtra("app", app_info);
|
||||
scope.setExtra("device", device_info);
|
||||
scope.setContexts("server", server_info);
|
||||
scope.setContexts("app", app_info);
|
||||
scope.setContexts("device", device_info);
|
||||
scope.setContexts("context", context);
|
||||
});
|
||||
|
||||
Sentry.captureException(error, stackTrace: stackTrace).catchError((error) {
|
||||
Sentry.captureException(error, stackTrace: stackTrace)
|
||||
.catchError((error) {
|
||||
print("Error uploading information to Sentry.io:");
|
||||
print(error);
|
||||
}).then((response) {
|
||||
return SentryId.empty();
|
||||
})
|
||||
.then((response) {
|
||||
print("Uploaded information to Sentry.io : ${response.toString()}");
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Test if a certain error should be ignored by Sentry
|
||||
*/
|
||||
bool sentryIgnoreError(dynamic error) {
|
||||
// Ignore 404 errors for media files
|
||||
if (error is HttpException) {
|
||||
if (error.uri.toString().contains("/media/") &&
|
||||
error.message.contains("404")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
145
lib/inventree/status_codes.dart
Normal file
145
lib/inventree/status_codes.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Code for querying the server for various status code data,
|
||||
* so that we do not have to duplicate those codes in the app.
|
||||
*
|
||||
* Ref: https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/status_codes.py
|
||||
*/
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/app_colors.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
|
||||
/*
|
||||
* Base class definition for a "status code" definition.
|
||||
*/
|
||||
class InvenTreeStatusCode {
|
||||
InvenTreeStatusCode(this.URL);
|
||||
|
||||
final String URL;
|
||||
|
||||
// Internal status code data loaded from server
|
||||
Map<String, dynamic> data = {};
|
||||
|
||||
/*
|
||||
* Construct a list of "choices" suitable for a form
|
||||
*/
|
||||
List<dynamic> get choices {
|
||||
List<dynamic> _choices = [];
|
||||
|
||||
for (String key in data.keys) {
|
||||
dynamic _entry = data[key];
|
||||
|
||||
if (_entry is Map<String, dynamic>) {
|
||||
_choices.add({"value": _entry["key"], "display_name": _entry["label"]});
|
||||
}
|
||||
}
|
||||
|
||||
return _choices;
|
||||
}
|
||||
|
||||
// Load status code information from the server
|
||||
Future<void> load({bool forceReload = false}) async {
|
||||
// Return internally cached data
|
||||
if (data.isNotEmpty && !forceReload) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The server must support this feature!
|
||||
if (!InvenTreeAPI().supportsStatusLabelEndpoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Loading status codes from ${URL}");
|
||||
|
||||
APIResponse response = await InvenTreeAPI().get(URL);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
Map<String, dynamic> results = response.data as Map<String, dynamic>;
|
||||
|
||||
if (results.containsKey("values")) {
|
||||
data = results["values"] as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the entry associated with the provided integer status
|
||||
Map<String, dynamic> entry(int status) {
|
||||
for (String key in data.keys) {
|
||||
dynamic _entry = data[key];
|
||||
|
||||
if (_entry is Map<String, dynamic>) {
|
||||
dynamic _status = _entry["key"];
|
||||
|
||||
if (_status is int) {
|
||||
if (status == _status) {
|
||||
return _entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match - return an empty map
|
||||
return {};
|
||||
}
|
||||
|
||||
// Return the 'label' associated with a given status code
|
||||
String label(int status) {
|
||||
Map<String, dynamic> _entry = entry(status);
|
||||
|
||||
String _label = (_entry["label"] ?? "") as String;
|
||||
|
||||
if (_label.isEmpty) {
|
||||
// If no match found, return the status code
|
||||
debug("No match for status code ${status} at '${URL}'");
|
||||
return status.toString();
|
||||
} else {
|
||||
return _label;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the 'name' (untranslated) associated with a given status code
|
||||
String name(int status) {
|
||||
Map<String, dynamic> _entry = entry(status);
|
||||
|
||||
String _name = (_entry["name"] ?? "") as String;
|
||||
|
||||
if (_name.isEmpty) {
|
||||
debug("No match for status code ${status} at '${URL}'");
|
||||
}
|
||||
|
||||
return _name;
|
||||
}
|
||||
|
||||
// Test if the name associated with the given code is in the provided list
|
||||
bool isNameIn(int code, List<String> names) {
|
||||
return names.contains(name(code));
|
||||
}
|
||||
|
||||
// Return the 'color' associated with a given status code
|
||||
Color color(int status) {
|
||||
Map<String, dynamic> _entry = entry(status);
|
||||
|
||||
String color_name = (_entry["color"] ?? "") as String;
|
||||
|
||||
switch (color_name.toLowerCase()) {
|
||||
case "success":
|
||||
return COLOR_SUCCESS;
|
||||
case "primary":
|
||||
return COLOR_PROGRESS;
|
||||
case "secondary":
|
||||
return Colors.grey;
|
||||
case "dark":
|
||||
return Colors.black;
|
||||
case "danger":
|
||||
return COLOR_DANGER;
|
||||
case "warning":
|
||||
return COLOR_WARNING;
|
||||
case "info":
|
||||
return Colors.lightBlue;
|
||||
default:
|
||||
return Colors.black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,182 +1,183 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:intl/intl.dart";
|
||||
import "package:inventree/api.dart";
|
||||
import "package:inventree/helpers.dart";
|
||||
import "package:inventree/inventree/part.dart";
|
||||
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/l10.dart";
|
||||
|
||||
import "package:inventree/api.dart";
|
||||
|
||||
import "package:inventree/inventree/part.dart";
|
||||
import "package:inventree/inventree/model.dart";
|
||||
import "package:inventree/widget/stock/location_display.dart";
|
||||
import "package:inventree/widget/stock/stock_detail.dart";
|
||||
|
||||
/*
|
||||
* Class representing a test result for a single stock item
|
||||
*/
|
||||
class InvenTreeStockItemTestResult extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockItemTestResult() : super();
|
||||
|
||||
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreeStockItemTestResult.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "stock/test/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
"stock_item": {
|
||||
"hidden": true
|
||||
},
|
||||
List<String> get rolesRequired => ["stock"];
|
||||
|
||||
@override
|
||||
Map<String, String> defaultFilters() {
|
||||
return {"user_detail": "true", "template_detail": "true"};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"stock_item": {"hidden": true},
|
||||
"test": {},
|
||||
"template": {
|
||||
"filters": {"enabled": "true"},
|
||||
},
|
||||
"result": {},
|
||||
"value": {},
|
||||
"notes": {},
|
||||
"attachment": {},
|
||||
};
|
||||
|
||||
if (InvenTreeAPI().supportsModernTestResults) {
|
||||
fields.remove("test");
|
||||
} else {
|
||||
fields.remove("template");
|
||||
}
|
||||
|
||||
String get key => (jsondata["key"] ?? "") as String;
|
||||
return fields;
|
||||
}
|
||||
|
||||
String get testName => (jsondata["test"] ?? "") as String;
|
||||
String get key => getString("key");
|
||||
|
||||
bool get result => (jsondata["result"] ?? false) as bool;
|
||||
int get templateId => getInt("template");
|
||||
|
||||
String get value => (jsondata["value"] ?? "") as String;
|
||||
String get testName => getString("test");
|
||||
|
||||
String get attachment => (jsondata["attachment"] ?? "") as String;
|
||||
bool get result => getBool("result");
|
||||
|
||||
String get date => (jsondata["date"] ?? "") as String;
|
||||
String get value => getString("value");
|
||||
|
||||
String get attachment => getString("attachment");
|
||||
|
||||
String get username => getString("username", subKey: "user_detail");
|
||||
|
||||
String get date => getString("date");
|
||||
|
||||
@override
|
||||
InvenTreeStockItemTestResult createFromJson(Map<String, dynamic> json) {
|
||||
var result = InvenTreeStockItemTestResult.fromJson(json);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class InvenTreeStockItemHistory extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockItemHistory() : super();
|
||||
|
||||
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreeStockItemHistory.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreeStockItemHistory.fromJson(json);
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeStockItemHistory.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "stock/track/";
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
|
||||
Map<String, String> defaultFilters() {
|
||||
// By default, order by decreasing date
|
||||
return {
|
||||
"ordering": "-date",
|
||||
};
|
||||
return {"ordering": "-date", "user_detail": "true"};
|
||||
}
|
||||
|
||||
DateTime? get date {
|
||||
if (jsondata.containsKey("date")) {
|
||||
return DateTime.tryParse((jsondata["date"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
DateTime? get date => getDate("date");
|
||||
|
||||
String get dateString {
|
||||
var d = date;
|
||||
String get dateString => getDateString("date");
|
||||
|
||||
if (d == null) {
|
||||
return "";
|
||||
}
|
||||
String get label => getString("label");
|
||||
|
||||
return DateFormat("yyyy-MM-dd").format(d);
|
||||
}
|
||||
|
||||
String get label => (jsondata["label"] ?? "") as String;
|
||||
// Return the "deltas" associated with this historical object
|
||||
Map<String, dynamic> get deltas => getMap("deltas");
|
||||
|
||||
// Return the quantity string for this historical object
|
||||
String get quantityString {
|
||||
Map<String, dynamic> deltas = (jsondata["deltas"] ?? {}) as Map<String, dynamic>;
|
||||
var _deltas = deltas;
|
||||
|
||||
// Serial number takes priority here
|
||||
if (deltas.containsKey("serial")) {
|
||||
var serial = (deltas["serial"] ?? "").toString();
|
||||
return "# ${serial}";
|
||||
} else if (deltas.containsKey("quantity")) {
|
||||
double q = (deltas["quantity"] ?? 0) as double;
|
||||
if (_deltas.containsKey("quantity")) {
|
||||
double q = double.tryParse(_deltas["quantity"].toString()) ?? 0;
|
||||
|
||||
return simpleNumberString(q);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
int? get user => getValue("user") as int?;
|
||||
|
||||
String get userString {
|
||||
if (user != null) {
|
||||
return getString("username", subKey: "user_detail");
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Class representing a StockItem database instance
|
||||
*/
|
||||
class InvenTreeStockItem extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockItem() : super();
|
||||
|
||||
InvenTreeStockItem.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
|
||||
// Stock status codes
|
||||
static const int OK = 10;
|
||||
static const int ATTENTION = 50;
|
||||
static const int DAMAGED = 55;
|
||||
static const int DESTROYED = 60;
|
||||
static const int REJECTED = 65;
|
||||
static const int LOST = 70;
|
||||
static const int RETURNED = 85;
|
||||
|
||||
String statusLabel(BuildContext context) {
|
||||
|
||||
// TODO: Delete me - The translated status values are provided by the API!
|
||||
|
||||
switch (status) {
|
||||
case OK:
|
||||
return L10().ok;
|
||||
case ATTENTION:
|
||||
return L10().attention;
|
||||
case DAMAGED:
|
||||
return L10().damaged;
|
||||
case DESTROYED:
|
||||
return L10().destroyed;
|
||||
case REJECTED:
|
||||
return L10().rejected;
|
||||
case LOST:
|
||||
return L10().lost;
|
||||
case RETURNED:
|
||||
return L10().returned;
|
||||
default:
|
||||
return status.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Return color associated with stock status
|
||||
Color get statusColor {
|
||||
switch (status) {
|
||||
case OK:
|
||||
return Colors.black;
|
||||
case ATTENTION:
|
||||
return Color(0xFFfdc82a);
|
||||
case DAMAGED:
|
||||
case DESTROYED:
|
||||
case REJECTED:
|
||||
return Color(0xFFe35a57);
|
||||
case LOST:
|
||||
default:
|
||||
return Color(0xFFAAAAAA);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String get URL => "stock/";
|
||||
|
||||
// URLs for performing stock actions
|
||||
static const String MODEL_TYPE = "stockitem";
|
||||
|
||||
@override
|
||||
List<String> get rolesRequired => ["stock"];
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => StockDetailWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
// Return a set of fields to transfer this stock item via dialog
|
||||
Map<String, dynamic> transferFields() {
|
||||
Map<String, dynamic> fields = {
|
||||
"pk": {"parent": "items", "nested": true, "hidden": true, "value": pk},
|
||||
"quantity": {"parent": "items", "nested": true, "value": quantity},
|
||||
"location": {"value": locationId},
|
||||
"status": {"parent": "items", "nested": true, "value": status},
|
||||
"packaging": {"parent": "items", "nested": true, "value": packaging},
|
||||
"notes": {},
|
||||
};
|
||||
|
||||
if (isSerialized()) {
|
||||
// Prevent editing of 'quantity' field if the item is serialized
|
||||
fields["quantity"]?["hidden"] = true;
|
||||
}
|
||||
|
||||
// Old API does not support these fields
|
||||
if (!api.supportsStockAdjustExtraFields) {
|
||||
fields.remove("packaging");
|
||||
fields.remove("status");
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
// URLs for performing stock actions
|
||||
static String transferStockUrl() => "stock/transfer/";
|
||||
|
||||
static String countStockUrl() => "stock/count/";
|
||||
|
|
@ -189,38 +190,32 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
String get WEB_URL => "stock/item/";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"part": {},
|
||||
"location": {},
|
||||
"quantity": {},
|
||||
"serial": {},
|
||||
"serial_numbers": {"label": L10().serialNumbers, "type": "string"},
|
||||
"status": {},
|
||||
"batch": {},
|
||||
"purchase_price": {},
|
||||
"purchase_price_currency": {},
|
||||
"packaging": {},
|
||||
"link": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultGetFilters() {
|
||||
|
||||
return {
|
||||
"part_detail": "true",
|
||||
"location_detail": "true",
|
||||
"supplier_detail": "true",
|
||||
"cascade": "false"
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> defaultListFilters() {
|
||||
|
||||
Map<String, String> defaultFilters() {
|
||||
return {
|
||||
"part_detail": "true",
|
||||
"location_detail": "true",
|
||||
"supplier_detail": "true",
|
||||
"supplier_part_detail": "true",
|
||||
"cascade": "false",
|
||||
"in_stock": "true",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -229,12 +224,10 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
int get testTemplateCount => testTemplates.length;
|
||||
|
||||
// Get all the test templates associated with this StockItem
|
||||
Future<void> getTestTemplates({bool showDialog=false}) async {
|
||||
await InvenTreePartTestTemplate().list(
|
||||
filters: {
|
||||
"part": "${partId}",
|
||||
},
|
||||
).then((var templates) {
|
||||
Future<void> getTestTemplates({bool showDialog = false}) async {
|
||||
await InvenTreePartTestTemplate()
|
||||
.list(filters: {"part": "${partId}", "enabled": "true"})
|
||||
.then((var templates) {
|
||||
testTemplates.clear();
|
||||
|
||||
for (var t in templates) {
|
||||
|
|
@ -250,13 +243,9 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
int get testResultCount => testResults.length;
|
||||
|
||||
Future<void> getTestResults() async {
|
||||
|
||||
await InvenTreeStockItemTestResult().list(
|
||||
filters: {
|
||||
"stock_item": "${pk}",
|
||||
"user_detail": "true",
|
||||
},
|
||||
).then((var results) {
|
||||
await InvenTreeStockItemTestResult()
|
||||
.list(filters: {"stock_item": "${pk}", "user_detail": "true"})
|
||||
.then((var results) {
|
||||
testResults.clear();
|
||||
|
||||
for (var r in results) {
|
||||
|
|
@ -267,84 +256,73 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
});
|
||||
}
|
||||
|
||||
String get uid => (jsondata["uid"] ?? "") as String;
|
||||
bool get isInStock => getBool("in_stock", backup: true);
|
||||
|
||||
int get status => (jsondata["status"] ?? -1) as int;
|
||||
String get packaging => getString("packaging");
|
||||
|
||||
String get packaging => (jsondata["packaging"] ?? "") as String;
|
||||
String get batch => getString("batch");
|
||||
|
||||
String get batch => (jsondata["batch"] ?? "") as String;
|
||||
int get partId => getInt("part");
|
||||
|
||||
int get partId => (jsondata["part"] ?? -1) as int;
|
||||
double? get purchasePrice {
|
||||
String pp = getString("purchase_price");
|
||||
|
||||
String get purchasePrice => (jsondata["purchase_price"] ?? "") as String;
|
||||
if (pp.isEmpty) {
|
||||
return null;
|
||||
} else {
|
||||
return double.tryParse(pp);
|
||||
}
|
||||
}
|
||||
|
||||
String get purchasePriceCurrency => getString("purchase_price_currency");
|
||||
|
||||
bool get hasPurchasePrice {
|
||||
|
||||
String pp = purchasePrice;
|
||||
|
||||
return pp.isNotEmpty && pp.trim() != "-";
|
||||
double? pp = purchasePrice;
|
||||
return pp != null && pp > 0;
|
||||
}
|
||||
|
||||
int get purchaseOrderId => (jsondata["purchase_order"] ?? -1) as int;
|
||||
int get purchaseOrderId => getInt("purchase_order");
|
||||
|
||||
int get trackingItemCount => (jsondata["tracking_items"] ?? 0) as int;
|
||||
int get trackingItemCount => getInt("tracking_items", backup: 0);
|
||||
|
||||
bool get isBuilding => (jsondata["is_building"] ?? false) as bool;
|
||||
bool get isBuilding => getBool("is_building");
|
||||
|
||||
int get salesOrderId => getInt("sales_order");
|
||||
|
||||
bool get hasSalesOrder => salesOrderId > 0;
|
||||
|
||||
int get customerId => getInt("customer");
|
||||
|
||||
bool get hasCustomer => customerId > 0;
|
||||
|
||||
bool get stale => getBool("stale");
|
||||
|
||||
bool get expired => getBool("expired");
|
||||
|
||||
DateTime? get expiryDate => getDate("expiry_date");
|
||||
|
||||
String get expiryDateString => getDateString("expiry_date");
|
||||
|
||||
// Date of last update
|
||||
DateTime? get updatedDate {
|
||||
if (jsondata.containsKey("updated")) {
|
||||
return DateTime.tryParse((jsondata["updated"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
DateTime? get updatedDate => getDate("updated");
|
||||
|
||||
String get updatedDateString {
|
||||
var _updated = updatedDate;
|
||||
String get updatedDateString => getDateString("updated");
|
||||
|
||||
if (_updated == null) {
|
||||
return "";
|
||||
}
|
||||
DateTime? get stocktakeDate => getDate("stocktake_date");
|
||||
|
||||
final DateFormat _format = DateFormat("yyyy-MM-dd");
|
||||
|
||||
return _format.format(_updated);
|
||||
}
|
||||
|
||||
DateTime? get stocktakeDate {
|
||||
if (jsondata.containsKey("stocktake_date")) {
|
||||
return DateTime.tryParse((jsondata["stocktake_date"] ?? "") as String);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String get stocktakeDateString {
|
||||
var _stocktake = stocktakeDate;
|
||||
|
||||
if (_stocktake == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final DateFormat _format = DateFormat("yyyy-MM-dd");
|
||||
|
||||
return _format.format(_stocktake);
|
||||
}
|
||||
String get stocktakeDateString => getDateString("stocktake_date");
|
||||
|
||||
String get partName {
|
||||
|
||||
String nm = "";
|
||||
|
||||
// Use the detailed part information as priority
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
nm = (jsondata["part_detail"]["full_name"] ?? "") as String;
|
||||
nm = (jsondata["part_detail"]?["full_name"] ?? "") as String;
|
||||
}
|
||||
|
||||
// Backup if first value fails
|
||||
if (nm.isEmpty) {
|
||||
nm = (jsondata["part__name"] ?? "") as String;
|
||||
nm = getString("part__name");
|
||||
}
|
||||
|
||||
return nm;
|
||||
|
|
@ -355,11 +333,11 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
|
||||
// Use the detailed part description as priority
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
desc = (jsondata["part_detail"]["description"] ?? "") as String;
|
||||
desc = (jsondata["part_detail"]?["description"] ?? "") as String;
|
||||
}
|
||||
|
||||
if (desc.isEmpty) {
|
||||
desc = (jsondata["part__description"] ?? "") as String;
|
||||
desc = getString("part__description");
|
||||
}
|
||||
|
||||
return desc;
|
||||
|
|
@ -369,11 +347,11 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
String img = "";
|
||||
|
||||
if (jsondata.containsKey("part_detail")) {
|
||||
img = (jsondata["part_detail"]["thumbnail"] ?? "") as String;
|
||||
img = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
|
||||
}
|
||||
|
||||
if (img.isEmpty) {
|
||||
img = (jsondata["part__thumbnail"] ?? "") as String;
|
||||
img = getString("part__thumbnail");
|
||||
}
|
||||
|
||||
return img;
|
||||
|
|
@ -383,7 +361,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
* Return the Part thumbnail for this stock item.
|
||||
*/
|
||||
String get partThumbnail {
|
||||
|
||||
String thumb = "";
|
||||
|
||||
thumb = (jsondata["part_detail"]?["thumbnail"] ?? "") as String;
|
||||
|
|
@ -395,7 +372,7 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
|
||||
// Try a different approach
|
||||
if (thumb.isEmpty) {
|
||||
thumb = (jsondata["part__thumbnail"] ?? "") as String;
|
||||
thumb = getString("part__thumbnail");
|
||||
}
|
||||
|
||||
// Still no thumbnail? Use the "no image" image
|
||||
|
|
@ -404,49 +381,42 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
return thumb;
|
||||
}
|
||||
|
||||
int get supplierPartId => (jsondata["supplier_part"] ?? -1) as int;
|
||||
int get supplierPartId => getInt("supplier_part");
|
||||
|
||||
String get supplierImage {
|
||||
String thumb = "";
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
thumb = (jsondata["supplier_detail"]["supplier_logo"] ?? "") as String;
|
||||
if (jsondata.containsKey("supplier_part_detail")) {
|
||||
thumb =
|
||||
(jsondata["supplier_part_detail"]?["supplier_detail"]?["image"] ?? "")
|
||||
as String;
|
||||
} else if (jsondata.containsKey("supplier_detail")) {
|
||||
thumb = (jsondata["supplier_detail"]?["image"] ?? "") as String;
|
||||
}
|
||||
|
||||
return thumb;
|
||||
}
|
||||
|
||||
String get supplierName {
|
||||
String sname = "";
|
||||
String get supplierName =>
|
||||
getString("supplier_name", subKey: "supplier_detail");
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
sname = (jsondata["supplier_detail"]["supplier_name"] ?? "") as String;
|
||||
String get units => getString("units", subKey: "part_detail");
|
||||
|
||||
String get supplierSKU => getString("SKU", subKey: "supplier_part_detail");
|
||||
|
||||
String get serialNumber => getString("serial");
|
||||
|
||||
double get quantity => getDouble("quantity");
|
||||
|
||||
String quantityString({bool includeUnits = true}) {
|
||||
String q = "";
|
||||
|
||||
if (allocated > 0) {
|
||||
q += simpleNumberString(available);
|
||||
q += " / ";
|
||||
}
|
||||
|
||||
return sname;
|
||||
}
|
||||
|
||||
String get units {
|
||||
return (jsondata["part_detail"]?["units"] ?? "") as String;
|
||||
}
|
||||
|
||||
String get supplierSKU {
|
||||
String sku = "";
|
||||
|
||||
if (jsondata.containsKey("supplier_detail")) {
|
||||
sku = (jsondata["supplier_detail"]["SKU"] ?? "") as String;
|
||||
}
|
||||
|
||||
return sku;
|
||||
}
|
||||
|
||||
String get serialNumber => (jsondata["serial"] ?? "") as String;
|
||||
|
||||
double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0;
|
||||
|
||||
String quantityString({bool includeUnits = false}){
|
||||
|
||||
String q = simpleNumberString(quantity);
|
||||
q += simpleNumberString(quantity);
|
||||
|
||||
if (includeUnits && units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
|
|
@ -455,39 +425,45 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
return q;
|
||||
}
|
||||
|
||||
int get locationId => (jsondata["location"] ?? -1) as int;
|
||||
double get allocated => getDouble("allocated");
|
||||
|
||||
double get available => quantity - allocated;
|
||||
|
||||
int get locationId => getInt("location");
|
||||
|
||||
bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
|
||||
|
||||
String serialOrQuantityDisplay() {
|
||||
if (isSerialized()) {
|
||||
return "SN ${serialNumber}";
|
||||
}
|
||||
|
||||
} else if (allocated > 0) {
|
||||
return "${available} / ${quantity}";
|
||||
} else {
|
||||
return simpleNumberString(quantity);
|
||||
}
|
||||
}
|
||||
|
||||
String get locationName {
|
||||
String loc = "";
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) {
|
||||
return "Unknown Location";
|
||||
}
|
||||
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) return "Unknown Location";
|
||||
|
||||
loc = (jsondata["location_detail"]["name"] ?? "") as String;
|
||||
String loc = getString("name", subKey: "location_detail");
|
||||
|
||||
// Old-style name
|
||||
if (loc.isEmpty) {
|
||||
loc = (jsondata["location__name"] ?? "") as String;
|
||||
loc = getString("location__name");
|
||||
}
|
||||
|
||||
return loc;
|
||||
}
|
||||
|
||||
String get locationPathString {
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) {
|
||||
return L10().locationNotSet;
|
||||
}
|
||||
|
||||
if (locationId == -1 || !jsondata.containsKey("location_detail")) return L10().locationNotSet;
|
||||
|
||||
String _loc = (jsondata["location_detail"]["pathstring"] ?? "") as String;
|
||||
|
||||
String _loc = getString("pathstring", subKey: "location_detail");
|
||||
if (_loc.isNotEmpty) {
|
||||
return _loc;
|
||||
} else {
|
||||
|
|
@ -501,14 +477,19 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
if (serialNumber.isNotEmpty) {
|
||||
return "SN: $serialNumber";
|
||||
} else {
|
||||
return simpleNumberString(quantity);
|
||||
String q = simpleNumberString(quantity);
|
||||
|
||||
if (units.isNotEmpty) {
|
||||
q += " ${units}";
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
return InvenTreeStockItem.fromJson(json);
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeStockItem.fromJson(json);
|
||||
|
||||
/*
|
||||
* Perform stocktake action:
|
||||
|
|
@ -517,9 +498,12 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
* - Remove
|
||||
* - Count
|
||||
*/
|
||||
// TODO: Remove this function when we deprecate support for the old API
|
||||
Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes, int? location}) async {
|
||||
|
||||
Future<bool> adjustStock(
|
||||
String endpoint,
|
||||
double q, {
|
||||
String? notes,
|
||||
int? location,
|
||||
}) async {
|
||||
// Serialized stock cannot be adjusted (unless it is a "transfer")
|
||||
if (isSerialized() && location == null) {
|
||||
return false;
|
||||
|
|
@ -532,72 +516,46 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
|
||||
Map<String, dynamic> data = {};
|
||||
|
||||
// Note: Format of adjustment API was updated in API v14
|
||||
if (InvenTreeAPI().supportModernStockTransactions()) {
|
||||
// Modern (> 14) API
|
||||
data = {
|
||||
"items": [
|
||||
{
|
||||
"pk": "${pk}",
|
||||
"quantity": "${quantity}",
|
||||
}
|
||||
{"pk": "${pk}", "quantity": "${quantity}"},
|
||||
],
|
||||
"notes": notes ?? "",
|
||||
};
|
||||
} else {
|
||||
// Legacy (<= 14) API
|
||||
data = {
|
||||
"item": {
|
||||
"pk": "${pk}",
|
||||
"quantity": "${quantity}",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
data["notes"] = notes ?? "";
|
||||
|
||||
if (location != null) {
|
||||
data["location"] = location;
|
||||
}
|
||||
|
||||
// Expected API return code depends on server API version
|
||||
final int expected_response = InvenTreeAPI().supportModernStockTransactions() ? 201 : 200;
|
||||
var response = await api.post(endpoint, body: data);
|
||||
|
||||
var response = await api.post(
|
||||
endpoint,
|
||||
body: data,
|
||||
expectedStatusCode: expected_response,
|
||||
);
|
||||
|
||||
return response.isValid();
|
||||
return response.isValid() &&
|
||||
(response.statusCode == 200 || response.statusCode == 201);
|
||||
}
|
||||
|
||||
// TODO: Remove this function when we deprecate support for the old API
|
||||
Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/count/", q, notes: notes);
|
||||
Future<bool> countStock(double q, {String? notes}) async {
|
||||
final bool result = await adjustStock("/stock/count/", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Remove this function when we deprecate support for the old API
|
||||
Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
|
||||
Future<bool> addStock(double q, {String? notes}) async {
|
||||
final bool result = await adjustStock("/stock/add/", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Remove this function when we deprecate support for the old API
|
||||
Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
|
||||
|
||||
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
|
||||
Future<bool> removeStock(double q, {String? notes}) async {
|
||||
final bool result = await adjustStock("/stock/remove/", q, notes: notes);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Remove this function when we deprecate support for the old API
|
||||
Future<bool> transferStock(BuildContext context, int location, {double? quantity, String? notes}) async {
|
||||
|
||||
Future<bool> transferStock(
|
||||
int location, {
|
||||
double? quantity,
|
||||
String? notes,
|
||||
}) async {
|
||||
double q = this.quantity;
|
||||
|
||||
if (quantity != null) {
|
||||
|
|
@ -605,7 +563,6 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
}
|
||||
|
||||
final bool result = await adjustStock(
|
||||
context,
|
||||
"/stock/transfer/",
|
||||
q,
|
||||
notes: notes,
|
||||
|
|
@ -616,29 +573,43 @@ class InvenTreeStockItem extends InvenTreeModel {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class InvenTreeStockLocation extends InvenTreeModel {
|
||||
|
||||
InvenTreeStockLocation() : super();
|
||||
|
||||
InvenTreeStockLocation.fromJson(Map<String, dynamic> json) : super.fromJson(json);
|
||||
InvenTreeStockLocation.fromJson(Map<String, dynamic> json)
|
||||
: super.fromJson(json);
|
||||
|
||||
@override
|
||||
String get URL => "stock/location/";
|
||||
|
||||
String get pathstring => (jsondata["pathstring"] ?? "") as String;
|
||||
static const String MODEL_TYPE = "stocklocation";
|
||||
|
||||
@override
|
||||
Map<String, dynamic> formFields() {
|
||||
return {
|
||||
List<String> get rolesRequired => ["stock"];
|
||||
|
||||
String get pathstring => getString("pathstring");
|
||||
|
||||
@override
|
||||
Future<Object?> goToDetailPage(BuildContext context) async {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => LocationDisplayWidget(this)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Map<String, dynamic>> formFields() {
|
||||
Map<String, Map<String, dynamic>> fields = {
|
||||
"name": {},
|
||||
"description": {},
|
||||
"parent": {},
|
||||
"structural": {},
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
String get parentpathstring {
|
||||
// TODO - Drive the refactor tractor through this
|
||||
String get parentPathString {
|
||||
List<String> psplit = pathstring.split("/");
|
||||
|
||||
if (psplit.isNotEmpty) {
|
||||
|
|
@ -657,10 +628,6 @@ class InvenTreeStockLocation extends InvenTreeModel {
|
|||
int get itemcount => (jsondata["items"] ?? 0) as int;
|
||||
|
||||
@override
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) {
|
||||
|
||||
var loc = InvenTreeStockLocation.fromJson(json);
|
||||
|
||||
return loc;
|
||||
}
|
||||
InvenTreeModel createFromJson(Map<String, dynamic> json) =>
|
||||
InvenTreeStockLocation.fromJson(json);
|
||||
}
|
||||
14
lib/l10.dart
14
lib/l10.dart
|
|
@ -1,12 +1,18 @@
|
|||
import "package:flutter_gen/gen_l10n/app_localizations.dart";
|
||||
import "package:flutter_gen/gen_l10n/app_localizations_en.dart";
|
||||
import "package:inventree/l10n/collected/app_localizations.dart";
|
||||
import "package:inventree/l10n/collected/app_localizations_en.dart";
|
||||
|
||||
import "package:one_context/one_context.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "package:inventree/helpers.dart";
|
||||
|
||||
// Shortcut function to reduce boilerplate!
|
||||
I18N L10()
|
||||
{
|
||||
I18N L10() {
|
||||
// Testing mode - ignore context
|
||||
if (!hasContext()) {
|
||||
return I18NEn();
|
||||
}
|
||||
|
||||
BuildContext? _ctx = OneContext().context;
|
||||
|
||||
if (_ctx != null) {
|
||||
|
|
|
|||
1
lib/l10n
1
lib/l10n
|
|
@ -1 +0,0 @@
|
|||
Subproject commit a1683e462bb8c91d481cdacb169cb98d63e93acc
|
||||
3
lib/l10n/.gitignore
vendored
Normal file
3
lib/l10n/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Do not track the collected translation files
|
||||
collected/
|
||||
supported_locales.dart
|
||||
23
lib/l10n/README.md
Normal file
23
lib/l10n/README.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
## InvenTree Translation Files
|
||||
|
||||
This directory contains translation files for the InvenTree mobile app.
|
||||
|
||||
### File Structure
|
||||
|
||||
**Translation Source File** - app_en.arb
|
||||
|
||||
This file contains the source strings for translating. If you want to add a new translatable string to the app, is must be added to this file!
|
||||
|
||||
**Translated Files** - <lc>/app_<lb>arb
|
||||
|
||||
Each directory contains a single translation output file, generated by the [crowdin translation service](https://crowdin.com/project/inventree). *Do not edit these files*
|
||||
|
||||
**collected** - Collected files
|
||||
|
||||
Before building the app, the translation files are collected from the various directories into a single directory, so they can be accessed by the app.
|
||||
|
||||
### Translating
|
||||
|
||||
DO NOT EDIT THE TRANSLATION FILES DIRECTLY!
|
||||
|
||||
Translation files are crowd sourced using the [crowdin service](https://crowdin.com/project/inventree). Contributions are welcomed (and encouraged!)
|
||||
1793
lib/l10n/app_en.arb
Normal file
1793
lib/l10n/app_en.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/ar_SA/app_ar_SA.arb
Normal file
1215
lib/l10n/ar_SA/app_ar_SA.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/bg_BG/app_bg_BG.arb
Normal file
1215
lib/l10n/bg_BG/app_bg_BG.arb
Normal file
File diff suppressed because it is too large
Load diff
154
lib/l10n/collect_translations.py
Normal file
154
lib/l10n/collect_translations.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
"""
|
||||
Collect translation files into a single directory,
|
||||
where they can be accessed by the flutter i18n library.
|
||||
|
||||
Translations provided from crowdin are located in subdirectories,
|
||||
but we need the .arb files to appear in this top level directory
|
||||
to be accessed by the app.
|
||||
|
||||
So, simply copy them here!
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
from posixpath import dirname
|
||||
import shutil
|
||||
import re
|
||||
|
||||
|
||||
def process_locale_file(filename, locale_name):
|
||||
"""
|
||||
Process a locale file after copying
|
||||
|
||||
- Ensure the 'locale' matches
|
||||
"""
|
||||
|
||||
# TODO: Use JSON processing instead of manual
|
||||
# Need to work out unicode issues for this to work
|
||||
|
||||
with open(filename, "r", encoding="utf-8") as input_file:
|
||||
lines = input_file.readlines()
|
||||
|
||||
with open(filename, "w", encoding="utf-8") as output_file:
|
||||
# Using JSON processing would be simpler here,
|
||||
# but it does not preserve unicode data!
|
||||
for line in lines:
|
||||
if "@@locale" in line:
|
||||
new_line = f' "@@locale": "{locale_name}"'
|
||||
|
||||
if "," in line:
|
||||
new_line += ","
|
||||
|
||||
new_line += "\n"
|
||||
|
||||
line = new_line
|
||||
|
||||
output_file.write(line)
|
||||
|
||||
|
||||
def copy_locale_file(path):
|
||||
"""
|
||||
Locate and copy the locale file from the provided directory
|
||||
"""
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
for f in os.listdir(path):
|
||||
src = os.path.join(path, f)
|
||||
dst = os.path.join(here, "collected", f)
|
||||
|
||||
if os.path.exists(src) and os.path.isfile(src) and f.endswith(".arb"):
|
||||
shutil.copyfile(src, dst)
|
||||
print(f"Copied file '{f}'")
|
||||
|
||||
locale = os.path.split(path)[-1]
|
||||
|
||||
process_locale_file(dst, locale)
|
||||
|
||||
# Create a "fallback" locale file, without a country code specifier, if it does not exist
|
||||
r = re.search(r"app_(\w+)_(\w+).arb", f)
|
||||
locale = r.groups()[0]
|
||||
fallback = f"app_{locale}.arb"
|
||||
|
||||
fallback_file = os.path.join(here, "collected", fallback)
|
||||
|
||||
if not os.path.exists(fallback_file):
|
||||
print(f"Creating fallback file:", fallback_file)
|
||||
shutil.copyfile(dst, fallback_file)
|
||||
|
||||
process_locale_file(fallback_file, locale)
|
||||
|
||||
|
||||
def generate_locale_list(locales):
|
||||
"""
|
||||
Generate a .dart file which contains all the supported locales,
|
||||
for importing into the project
|
||||
"""
|
||||
|
||||
with open("supported_locales.dart", "w") as output:
|
||||
output.write(
|
||||
"// This file is auto-generated by the 'collect_translations.py' script - do not edit it directly!\n\n"
|
||||
)
|
||||
output.write("// dart format off\n\n")
|
||||
output.write('import "package:flutter/material.dart";\n\n')
|
||||
output.write("const List<Locale> supported_locales = [\n")
|
||||
|
||||
locales = sorted(locales)
|
||||
|
||||
for locale in locales:
|
||||
if locale.startswith("."):
|
||||
continue
|
||||
|
||||
splt = locale.split("_")
|
||||
|
||||
if len(splt) == 2:
|
||||
lc, cc = splt
|
||||
else:
|
||||
lc = locale
|
||||
cc = ""
|
||||
|
||||
output.write(
|
||||
f' Locale("{lc}", "{cc}"), // Translations available in app_{locale}.arb\n'
|
||||
)
|
||||
|
||||
output.write("];\n")
|
||||
output.write("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# Ensure the 'collected' output directory exists
|
||||
output_dir = os.path.join(here, "collected")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Remove existing .arb files from output directory
|
||||
arbs = glob.glob(os.path.join(output_dir, "*.arb"))
|
||||
|
||||
for arb in arbs:
|
||||
os.remove(arb)
|
||||
|
||||
locales = ["en"]
|
||||
|
||||
for locale in os.listdir(here):
|
||||
# Ignore the output directory
|
||||
if locale == "collected":
|
||||
continue
|
||||
|
||||
f = os.path.join(here, locale)
|
||||
|
||||
if os.path.exists(f) and os.path.isdir(locale):
|
||||
copy_locale_file(f)
|
||||
locales.append(locale)
|
||||
|
||||
# Ensure the translation source file ('app_en.arb') is copied also
|
||||
# Note that this does not require any further processing
|
||||
src = os.path.join(here, "app_en.arb")
|
||||
dst = os.path.join(here, "collected", "app_en.arb")
|
||||
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
generate_locale_list(locales)
|
||||
|
||||
print(f"Updated translations for {len(locales)} locales.")
|
||||
1215
lib/l10n/cs_CZ/app_cs_CZ.arb
Normal file
1215
lib/l10n/cs_CZ/app_cs_CZ.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/da_DK/app_da_DK.arb
Normal file
1215
lib/l10n/da_DK/app_da_DK.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/de_DE/app_de_DE.arb
Normal file
1215
lib/l10n/de_DE/app_de_DE.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/el_GR/app_el_GR.arb
Normal file
1215
lib/l10n/el_GR/app_el_GR.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/es_ES/app_es_ES.arb
Normal file
1215
lib/l10n/es_ES/app_es_ES.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/es_MX/app_es_MX.arb
Normal file
1215
lib/l10n/es_MX/app_es_MX.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/et_EE/app_et_EE.arb
Normal file
1215
lib/l10n/et_EE/app_et_EE.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/fa_IR/app_fa_IR.arb
Normal file
1215
lib/l10n/fa_IR/app_fa_IR.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/fi_FI/app_fi_FI.arb
Normal file
1215
lib/l10n/fi_FI/app_fi_FI.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/fr_FR/app_fr_FR.arb
Normal file
1215
lib/l10n/fr_FR/app_fr_FR.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/he_IL/app_he_IL.arb
Normal file
1215
lib/l10n/he_IL/app_he_IL.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/hi_IN/app_hi_IN.arb
Normal file
1215
lib/l10n/hi_IN/app_hi_IN.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/hu_HU/app_hu_HU.arb
Normal file
1215
lib/l10n/hu_HU/app_hu_HU.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/id_ID/app_id_ID.arb
Normal file
1215
lib/l10n/id_ID/app_id_ID.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/it_IT/app_it_IT.arb
Normal file
1215
lib/l10n/it_IT/app_it_IT.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/ja_JP/app_ja_JP.arb
Normal file
1215
lib/l10n/ja_JP/app_ja_JP.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/ko_KR/app_ko_KR.arb
Normal file
1215
lib/l10n/ko_KR/app_ko_KR.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/lt_LT/app_lt_LT.arb
Normal file
1215
lib/l10n/lt_LT/app_lt_LT.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/lv_LV/app_lv_LV.arb
Normal file
1215
lib/l10n/lv_LV/app_lv_LV.arb
Normal file
File diff suppressed because it is too large
Load diff
1215
lib/l10n/nl_NL/app_nl_NL.arb
Normal file
1215
lib/l10n/nl_NL/app_nl_NL.arb
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue