mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-01 02:03:22 +00:00
Compare commits
664 Commits
v4.5.4
...
RachL-patc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5020cc740 | ||
|
|
7a2a6fab21 | ||
|
|
0f4ca50d0e | ||
|
|
3ec8cd24d3 | ||
|
|
d0dcc92ca7 | ||
|
|
22f3afc7f7 | ||
|
|
46048dcd18 | ||
|
|
a8fb6492f4 | ||
|
|
4610141ed8 | ||
|
|
8098131dba | ||
|
|
597d9ad314 | ||
|
|
1ce0b25bb0 | ||
|
|
c07ec6cdfd | ||
|
|
48e8ad3dd0 | ||
|
|
60d4cd60ff | ||
|
|
d62d3041b4 | ||
|
|
42fc0f7230 | ||
|
|
328aee6a03 | ||
|
|
db79af45fb | ||
|
|
ed7685222e | ||
|
|
4965e2bb9a | ||
|
|
6b3b29ac39 | ||
|
|
9bcdac8f30 | ||
|
|
e2d999da8d | ||
|
|
bc57447d54 | ||
|
|
f3e086ad59 | ||
|
|
298c0e8d7f | ||
|
|
ed559b5257 | ||
|
|
1fbdf25296 | ||
|
|
ec0d2d346b | ||
|
|
68c0d98736 | ||
|
|
458c8f7608 | ||
|
|
654263a823 | ||
|
|
77f9c6587c | ||
|
|
13a614a5aa | ||
|
|
add973f1ff | ||
|
|
4349e42a84 | ||
|
|
7cb28fd064 | ||
|
|
122a64e488 | ||
|
|
39fa8e0ace | ||
|
|
8c6c1e28ff | ||
|
|
d9809fc1f4 | ||
|
|
889bec7404 | ||
|
|
3f91027c51 | ||
|
|
2f9200b68b | ||
|
|
5d9bb9a8d5 | ||
|
|
7b677796c1 | ||
|
|
528c851e89 | ||
|
|
eb66244b74 | ||
|
|
9afd545897 | ||
|
|
36f7063897 | ||
|
|
a53a697e66 | ||
|
|
e9349ce79d | ||
|
|
8709c137c7 | ||
|
|
271475893d | ||
|
|
49a24ebd33 | ||
|
|
a08b0a8b32 | ||
|
|
0d97f992b9 | ||
|
|
996d2f0d46 | ||
|
|
f01a33c545 | ||
|
|
48c88d426e | ||
|
|
f646a30dca | ||
|
|
1e21939963 | ||
|
|
337113000f | ||
|
|
3756e368c8 | ||
|
|
54acc97fa1 | ||
|
|
0f6f7b332c | ||
|
|
946471923a | ||
|
|
3f353690c7 | ||
|
|
b8822ee179 | ||
|
|
701504fbb3 | ||
|
|
e9900ec1c7 | ||
|
|
8a0d9d99e5 | ||
|
|
decf1e6f03 | ||
|
|
e0638b1765 | ||
|
|
a5f677f748 | ||
|
|
63c83a19d6 | ||
|
|
762e6ec568 | ||
|
|
d2e5087668 | ||
|
|
169e1cf288 | ||
|
|
45ca2961ec | ||
|
|
1d75aa45ef | ||
|
|
a123369f8d | ||
|
|
90589ae868 | ||
|
|
167a69d2ef | ||
|
|
09524e266f | ||
|
|
1c58b061b4 | ||
|
|
24df29ddf5 | ||
|
|
9f084057a1 | ||
|
|
3f22e8cca7 | ||
|
|
c3b5456433 | ||
|
|
7b0519dab9 | ||
|
|
355541e8de | ||
|
|
e10c3dc59b | ||
|
|
2609298d88 | ||
|
|
783de09987 | ||
|
|
7b8aeb7ef8 | ||
|
|
9c7105e764 | ||
|
|
afff200680 | ||
|
|
6c431d4052 | ||
|
|
2b8487cc6d | ||
|
|
b9a72381fc | ||
|
|
ea8e925077 | ||
|
|
a13e5ced3d | ||
|
|
aa7fffa5a2 | ||
|
|
3227922c76 | ||
|
|
aa2a5757ec | ||
|
|
197363b199 | ||
|
|
a023443c75 | ||
|
|
2e29426834 | ||
|
|
8e4d306901 | ||
|
|
38196e8ff3 | ||
|
|
475c9fb4ab | ||
|
|
c48162388c | ||
|
|
f024aff45d | ||
|
|
ed668ded0a | ||
|
|
b461d499ad | ||
|
|
c1c281122f | ||
|
|
8c4cc051a4 | ||
|
|
d5b2408947 | ||
|
|
1eb70370c7 | ||
|
|
97b6289263 | ||
|
|
bda28dfaf7 | ||
|
|
d1ebe4e1d1 | ||
|
|
a64aea4b9c | ||
|
|
cc9b764f0f | ||
|
|
ac5fa21ff2 | ||
|
|
781fcf21b9 | ||
|
|
56d2642191 | ||
|
|
f54552f939 | ||
|
|
fb5740b38b | ||
|
|
db14080a7f | ||
|
|
01337c12f0 | ||
|
|
f8eeca856e | ||
|
|
67c11333f3 | ||
|
|
40afe7e0ab | ||
|
|
ef1f3207f7 | ||
|
|
b0433bd8f5 | ||
|
|
755a394704 | ||
|
|
04e14bf38b | ||
|
|
ce0c7929a7 | ||
|
|
2a671d491d | ||
|
|
7c2c614f90 | ||
|
|
3bb2232bc1 | ||
|
|
377f035ea8 | ||
|
|
dbca2e2b56 | ||
|
|
0695b434a2 | ||
|
|
9db417319d | ||
|
|
a500c75ee9 | ||
|
|
630c398b12 | ||
|
|
64f60d1c8c | ||
|
|
218d07c90d | ||
|
|
83a619b097 | ||
|
|
fa986f3fc2 | ||
|
|
977b6e6c2a | ||
|
|
f7446749ff | ||
|
|
844cab458e | ||
|
|
8ec1f61cd7 | ||
|
|
893b541dca | ||
|
|
4ae392490b | ||
|
|
cda57fdb44 | ||
|
|
25171413ef | ||
|
|
4ad6971121 | ||
|
|
8f38762393 | ||
|
|
d55950a3c5 | ||
|
|
45075a0ccd | ||
|
|
144a09916c | ||
|
|
00dfe6810f | ||
|
|
058d7eeb69 | ||
|
|
324a4ff591 | ||
|
|
7f16b6acde | ||
|
|
ce268ec175 | ||
|
|
cc85fed7cc | ||
|
|
45b0686130 | ||
|
|
4cd83d3fd4 | ||
|
|
768825d689 | ||
|
|
e8234ee4a0 | ||
|
|
94030527a4 | ||
|
|
6ff9650eaf | ||
|
|
b1b534aa1b | ||
|
|
cd74a73680 | ||
|
|
36c4d24c93 | ||
|
|
9b4cd014bf | ||
|
|
c8bf23bdc2 | ||
|
|
df82dd0759 | ||
|
|
5ec39f994a | ||
|
|
8a31153d6d | ||
|
|
4109fbde70 | ||
|
|
37ae217afc | ||
|
|
4fd115897a | ||
|
|
e22804712e | ||
|
|
d7d253e58d | ||
|
|
e2c762f06b | ||
|
|
1ad7123a9d | ||
|
|
1793aa3532 | ||
|
|
d0fe1585d7 | ||
|
|
f58a3a859f | ||
|
|
3b89cd5957 | ||
|
|
e33ed5141b | ||
|
|
4d81b145ca | ||
|
|
7211b0d64a | ||
|
|
641b7beee3 | ||
|
|
60e8db9adc | ||
|
|
b7285e48b3 | ||
|
|
52c1491b15 | ||
|
|
95ff0d8d4a | ||
|
|
7d2d14320f | ||
|
|
7d1551ed04 | ||
|
|
3e71459346 | ||
|
|
ce2c80283c | ||
|
|
2be3f7b86d | ||
|
|
2d975c5534 | ||
|
|
86c91143b7 | ||
|
|
cde757efbd | ||
|
|
260e7ba817 | ||
|
|
bda506528f | ||
|
|
e429cb7198 | ||
|
|
a838ef4a21 | ||
|
|
f0b6403c1d | ||
|
|
71ca292c92 | ||
|
|
bc87c98e92 | ||
|
|
5b8e0d734f | ||
|
|
216883101e | ||
|
|
adf0340153 | ||
|
|
664f324db6 | ||
|
|
08308ba08e | ||
|
|
c609107379 | ||
|
|
df67b53971 | ||
|
|
6f2c5b5f7f | ||
|
|
a3d8ae693d | ||
|
|
b14a1e72f3 | ||
|
|
224738e0a1 | ||
|
|
10c3c53aad | ||
|
|
e5b7f89b32 | ||
|
|
b7c34ced26 | ||
|
|
f5baa42bfc | ||
|
|
86238cc0ee | ||
|
|
61aa02b3c3 | ||
|
|
4b2099625c | ||
|
|
f8bd0a1cc7 | ||
|
|
09de223c93 | ||
|
|
74c80c9fff | ||
|
|
11f3bbc566 | ||
|
|
e5ee398f26 | ||
|
|
99c098f567 | ||
|
|
4b1d7d8a41 | ||
|
|
1e3c18f3f6 | ||
|
|
22428fc78d | ||
|
|
f980cb45f6 | ||
|
|
097c6dee2f | ||
|
|
63a1b390e2 | ||
|
|
1a30cf6495 | ||
|
|
f7708d69a7 | ||
|
|
6eb5986c68 | ||
|
|
4d9f396f40 | ||
|
|
ac3730096f | ||
|
|
662467a1a4 | ||
|
|
af07358914 | ||
|
|
8e7e5fc20f | ||
|
|
aa5feb6605 | ||
|
|
3c613f80a3 | ||
|
|
83b6f58100 | ||
|
|
17c32ae09a | ||
|
|
0474c591de | ||
|
|
196956140e | ||
|
|
b2b6847882 | ||
|
|
d01d312b4f | ||
|
|
a74cf97083 | ||
|
|
03dbd54b25 | ||
|
|
fafd86a2db | ||
|
|
91f2ca9286 | ||
|
|
3015beab99 | ||
|
|
da0660c119 | ||
|
|
852dd41f89 | ||
|
|
48993232d1 | ||
|
|
0002b2e019 | ||
|
|
84a2e6c24d | ||
|
|
be4e0a259e | ||
|
|
c362e8dd0d | ||
|
|
1550ca5da0 | ||
|
|
f474afaceb | ||
|
|
8c71760556 | ||
|
|
37ab832b86 | ||
|
|
a11873559b | ||
|
|
51b3770188 | ||
|
|
989a6d57e0 | ||
|
|
495634b60c | ||
|
|
49fd1dc4a6 | ||
|
|
e31e45b875 | ||
|
|
61fec653cf | ||
|
|
eece738865 | ||
|
|
2465780c1c | ||
|
|
21b7e6e567 | ||
|
|
eb8050d61d | ||
|
|
9f43244312 | ||
|
|
66f080232f | ||
|
|
7f62b49da5 | ||
|
|
070b93c531 | ||
|
|
fb96f8f936 | ||
|
|
4303f0e974 | ||
|
|
2eec4c73bf | ||
|
|
5ef85aef3e | ||
|
|
283db8f9d0 | ||
|
|
95e620a78b | ||
|
|
c948efd9ce | ||
|
|
95bc0cc679 | ||
|
|
efe2b724e6 | ||
|
|
14c32c0d2e | ||
|
|
8f4f873ba0 | ||
|
|
c0ae2ede2c | ||
|
|
3ec53a7d71 | ||
|
|
3849db7c48 | ||
|
|
7b286ea31d | ||
|
|
3e0eb8708e | ||
|
|
c7fa3ff819 | ||
|
|
f839452df9 | ||
|
|
a7a38890f4 | ||
|
|
caa6d284f0 | ||
|
|
827e37cada | ||
|
|
6c6927af84 | ||
|
|
439f0cac64 | ||
|
|
98966f6b89 | ||
|
|
260e4f7b00 | ||
|
|
0824430da5 | ||
|
|
099da3fc6c | ||
|
|
7078c4ef03 | ||
|
|
318790d207 | ||
|
|
2be8ef96be | ||
|
|
f6e4b107b0 | ||
|
|
a5d17b4da9 | ||
|
|
83ab9594f6 | ||
|
|
562a24524b | ||
|
|
2809194b42 | ||
|
|
7d3eff2abb | ||
|
|
c0a49df150 | ||
|
|
f8bb33a9e8 | ||
|
|
24a25d31a0 | ||
|
|
4822a9ebcd | ||
|
|
68fa903d61 | ||
|
|
c2e0c94f2e | ||
|
|
296997d558 | ||
|
|
a9ad6a2851 | ||
|
|
1078e7cd36 | ||
|
|
40c4d38e45 | ||
|
|
a25937321a | ||
|
|
a8db288425 | ||
|
|
a106eb10b6 | ||
|
|
a6d71f8dd1 | ||
|
|
5c300d6d41 | ||
|
|
bb4ff5adc2 | ||
|
|
be548c506d | ||
|
|
955f8ba5ae | ||
|
|
ad94da975a | ||
|
|
f33eb23909 | ||
|
|
9d5806b858 | ||
|
|
35f9c420fd | ||
|
|
052e3b6380 | ||
|
|
1545708d4e | ||
|
|
2a4d275f4b | ||
|
|
9ead14b8a0 | ||
|
|
38721d9f36 | ||
|
|
3f6aaa74cc | ||
|
|
c08683412c | ||
|
|
4a38d7ef57 | ||
|
|
243a4a55b4 | ||
|
|
5be53a40a9 | ||
|
|
76fdf3725a | ||
|
|
67f037280a | ||
|
|
776b9fcdab | ||
|
|
7e84d41e8c | ||
|
|
68491559f3 | ||
|
|
f8d3467d46 | ||
|
|
1580d539df | ||
|
|
e2aac8ca1d | ||
|
|
15a2513815 | ||
|
|
00768f6ba0 | ||
|
|
908caa984b | ||
|
|
6993750757 | ||
|
|
379e5acfe5 | ||
|
|
5bf6bdf7f0 | ||
|
|
8de7c304fe | ||
|
|
b6695ba9a2 | ||
|
|
e8de76dc46 | ||
|
|
55733555bf | ||
|
|
f59ee96011 | ||
|
|
2b74bbd45d | ||
|
|
d56ab9257b | ||
|
|
f24a4edc68 | ||
|
|
27dd5def57 | ||
|
|
561f4648d2 | ||
|
|
64d3091db9 | ||
|
|
0a9b858f2a | ||
|
|
4756ab47c2 | ||
|
|
0a04342712 | ||
|
|
556539d1b1 | ||
|
|
b7aaab204c | ||
|
|
632184b0a8 | ||
|
|
8500f6c198 | ||
|
|
ec4dba71c2 | ||
|
|
6117d70fae | ||
|
|
2e5c526170 | ||
|
|
32e32117e3 | ||
|
|
2a1d494301 | ||
|
|
fd45dea9f7 | ||
|
|
9073f0e5a8 | ||
|
|
c4f2c1c3ca | ||
|
|
a23bbf8537 | ||
|
|
6fac32b446 | ||
|
|
cf21c03619 | ||
|
|
0f7f1130f1 | ||
|
|
009d033e4c | ||
|
|
983addff0d | ||
|
|
d061fe8ad9 | ||
|
|
53286c22ba | ||
|
|
0cf8f079e4 | ||
|
|
f2163a42c4 | ||
|
|
05b25c78bb | ||
|
|
cc3181c820 | ||
|
|
9cd39d5c91 | ||
|
|
7d2f3bfa2f | ||
|
|
6df0b24bcf | ||
|
|
cf5e182cf7 | ||
|
|
74bbc7c3c0 | ||
|
|
4773d1c82e | ||
|
|
fde18ebf24 | ||
|
|
fd2cbb67db | ||
|
|
3f1d99d77c | ||
|
|
9cfcab4f02 | ||
|
|
703ad26773 | ||
|
|
627c9eede2 | ||
|
|
f9a76342f8 | ||
|
|
d52134dad8 | ||
|
|
1016656781 | ||
|
|
bd1611630f | ||
|
|
ce28c10c7e | ||
|
|
4342d3b912 | ||
|
|
af3aed827a | ||
|
|
f73be6447e | ||
|
|
98eabc9d0f | ||
|
|
169cbbe1a1 | ||
|
|
72a503c3c1 | ||
|
|
afc4c1e967 | ||
|
|
d76c4bddb0 | ||
|
|
ae993784d8 | ||
|
|
d1abe22c32 | ||
|
|
2817b8891e | ||
|
|
ab87610d91 | ||
|
|
54252f5444 | ||
|
|
7b6b0dbb78 | ||
|
|
b2e15f52cf | ||
|
|
5d18c48b6c | ||
|
|
0c2dcbc50d | ||
|
|
4a028b2238 | ||
|
|
43a366005c | ||
|
|
64470e977a | ||
|
|
a696c66857 | ||
|
|
cfeb0afbd4 | ||
|
|
4968e3dc8d | ||
|
|
5324747f89 | ||
|
|
ef2856d169 | ||
|
|
d9c79ee49c | ||
|
|
d1f9b0855d | ||
|
|
b82726e7ba | ||
|
|
7e7ab2e36d | ||
|
|
e35a5179bb | ||
|
|
85385a1989 | ||
|
|
a816814819 | ||
|
|
60afa4d465 | ||
|
|
dd5175558e | ||
|
|
94b98867d8 | ||
|
|
35ef1b9c7f | ||
|
|
8badfb2505 | ||
|
|
d61acd2cc1 | ||
|
|
7417cee20a | ||
|
|
98951161b1 | ||
|
|
a745249f3b | ||
|
|
63c62cae08 | ||
|
|
d489c77efe | ||
|
|
e2423ad612 | ||
|
|
8b036113d9 | ||
|
|
7f09044ae1 | ||
|
|
e9c7e1778c | ||
|
|
32cd14ef54 | ||
|
|
ad585f1eab | ||
|
|
d9368c1bfc | ||
|
|
b6bfb4e866 | ||
|
|
4d222c61c6 | ||
|
|
d599cf77a2 | ||
|
|
8f7505d53d | ||
|
|
867e17301f | ||
|
|
95135ca526 | ||
|
|
de063fecb1 | ||
|
|
ef9ca33913 | ||
|
|
2710eafc33 | ||
|
|
4718fdb0be | ||
|
|
ce6ae04147 | ||
|
|
1621f97fdb | ||
|
|
96f9894f41 | ||
|
|
66b519bd1c | ||
|
|
1b8e256e8a | ||
|
|
b73e529bfc | ||
|
|
25b1620707 | ||
|
|
5f9b14df9f | ||
|
|
922b853e3a | ||
|
|
8d747a2508 | ||
|
|
ef6e37e7ca | ||
|
|
a50be52cde | ||
|
|
d62d002bc5 | ||
|
|
ffc2fed9b5 | ||
|
|
0c7448ba43 | ||
|
|
24afd40414 | ||
|
|
524aec7868 | ||
|
|
f2eb4b05f4 | ||
|
|
ffaf1b4ea0 | ||
|
|
eb547f4861 | ||
|
|
c9daca22d5 | ||
|
|
7b22740289 | ||
|
|
66c8a5c424 | ||
|
|
cfeac651b6 | ||
|
|
05315ff8e0 | ||
|
|
c4ee6b14ff | ||
|
|
a78f46259c | ||
|
|
1e79fde236 | ||
|
|
a9fe52a4ff | ||
|
|
075f5499f8 | ||
|
|
ed61f7e7bc | ||
|
|
bd6019036e | ||
|
|
0bbc3d2758 | ||
|
|
ae6182579b | ||
|
|
1e05811917 | ||
|
|
5f86a26f42 | ||
|
|
3f1b907ef2 | ||
|
|
08ab405893 | ||
|
|
ae2e92f09d | ||
|
|
b174080e29 | ||
|
|
a2c3ac2f60 | ||
|
|
429e2b0a86 | ||
|
|
70ca03173c | ||
|
|
7961ff7976 | ||
|
|
2d6ffc0ca1 | ||
|
|
d9c296cdb3 | ||
|
|
23aa762be2 | ||
|
|
61f2954973 | ||
|
|
d354317c73 | ||
|
|
19ef047193 | ||
|
|
037eb456c0 | ||
|
|
aed78f3138 | ||
|
|
c31416c536 | ||
|
|
917079931e | ||
|
|
46e54f48c9 | ||
|
|
059dceb304 | ||
|
|
f0abe650f6 | ||
|
|
282df9859e | ||
|
|
3474c60f4c | ||
|
|
503148b13b | ||
|
|
8442c7d334 | ||
|
|
f154de66f9 | ||
|
|
4a30493716 | ||
|
|
f325857e1f | ||
|
|
58872a7017 | ||
|
|
7392079d4d | ||
|
|
506126c1d3 | ||
|
|
fa004d0897 | ||
|
|
db7add88fe | ||
|
|
b14cd08990 | ||
|
|
f21aca234c | ||
|
|
93a6ff4b50 | ||
|
|
50ebfe412c | ||
|
|
e59ab6b2d9 | ||
|
|
417d39f684 | ||
|
|
35169f66dc | ||
|
|
64568f4aa4 | ||
|
|
734aebbaaa | ||
|
|
8a2be468fc | ||
|
|
feb429fee7 | ||
|
|
b75101f24f | ||
|
|
1e71db9315 | ||
|
|
82b742608d | ||
|
|
49aa9e0768 | ||
|
|
a85cfab506 | ||
|
|
e2e3aa9281 | ||
|
|
6bd0f2c088 | ||
|
|
ab2968ffd2 | ||
|
|
83bf19084b | ||
|
|
40a59c988b | ||
|
|
43d983cac2 | ||
|
|
ad3e772944 | ||
|
|
6a438a07fe | ||
|
|
ea238829a8 | ||
|
|
91fddeaa8b | ||
|
|
0de8a90b14 | ||
|
|
9fe128d494 | ||
|
|
193e17b625 | ||
|
|
6ad03e6d5c | ||
|
|
97a72dfde7 | ||
|
|
1f55ff4b72 | ||
|
|
be13d43e0c | ||
|
|
af7b663334 | ||
|
|
da24638079 | ||
|
|
a6d3909e95 | ||
|
|
8ab1cbe600 | ||
|
|
cad0245510 | ||
|
|
93edf4e3ad | ||
|
|
caa2764317 | ||
|
|
4f1e6382c9 | ||
|
|
54d33ca103 | ||
|
|
787205dcca | ||
|
|
fcb0996a76 | ||
|
|
76d874d5f9 | ||
|
|
81711e4c43 | ||
|
|
e64f60a166 | ||
|
|
24bc56781b | ||
|
|
6757c8df74 | ||
|
|
647a384561 | ||
|
|
ec828c335d | ||
|
|
6d03a8ddf3 | ||
|
|
05878fcbb8 | ||
|
|
fd8973862e | ||
|
|
40c77948b9 | ||
|
|
a95aa1b3e9 | ||
|
|
706eb737b1 | ||
|
|
c31758d347 | ||
|
|
6139ba3015 | ||
|
|
5ca7f40a4e | ||
|
|
a2f4df191a | ||
|
|
256d5ba61c | ||
|
|
cb42e7e119 | ||
|
|
566d310880 | ||
|
|
5cdce35ee8 | ||
|
|
ffe3f12a23 | ||
|
|
bd48a982fb | ||
|
|
5d732d80a6 | ||
|
|
254e11aa36 | ||
|
|
4223b36bc3 | ||
|
|
fcea437d7e | ||
|
|
b49da46842 | ||
|
|
8716b75d3d | ||
|
|
e055b8b16c | ||
|
|
f5875e4c0b | ||
|
|
56d23c172c | ||
|
|
19bb40d1d3 | ||
|
|
4169a956c9 | ||
|
|
fed2ae9a93 | ||
|
|
f00b2f0397 | ||
|
|
c101c4e42f | ||
|
|
d4e0b2ab51 | ||
|
|
1014a50aff | ||
|
|
2d24593403 | ||
|
|
2201d2e8c2 | ||
|
|
b6c407971d | ||
|
|
cd8dc41b15 | ||
|
|
a1887bdc76 | ||
|
|
e9f89362f4 | ||
|
|
675b7febdf | ||
|
|
90fdf59415 | ||
|
|
a1aea54405 | ||
|
|
e01354e863 | ||
|
|
ebc794194f | ||
|
|
287f65ec8e | ||
|
|
ec3c157f1e | ||
|
|
32aacbd2b0 | ||
|
|
655dc92246 | ||
|
|
e808c7fb2b | ||
|
|
2baf7c0250 |
9
.env
9
.env
@@ -61,3 +61,12 @@ SMTP_PASSWORD="f00d"
|
|||||||
# NEW_RELIC_AGENT_ENABLED=true
|
# NEW_RELIC_AGENT_ENABLED=true
|
||||||
# NEW_RELIC_APP_NAME="Open Food Network"
|
# NEW_RELIC_APP_NAME="Open Food Network"
|
||||||
# NEW_RELIC_LICENSE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
# NEW_RELIC_LICENSE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# Database encryption configuration, required for VINE connected app
|
||||||
|
# Generate with bin/rails db:encryption:init
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# VINE API settings
|
||||||
|
# VINE_API_URL="https://vine-staging.openfoodnetwork.org.au/api/v1"
|
||||||
|
|||||||
@@ -24,3 +24,8 @@ SITE_URL="0.0.0.0:3000"
|
|||||||
RACK_TIMEOUT_SERVICE_TIMEOUT="0"
|
RACK_TIMEOUT_SERVICE_TIMEOUT="0"
|
||||||
RACK_TIMEOUT_WAIT_TIMEOUT="0"
|
RACK_TIMEOUT_WAIT_TIMEOUT="0"
|
||||||
RACK_TIMEOUT_WAIT_OVERTIME="0"
|
RACK_TIMEOUT_WAIT_OVERTIME="0"
|
||||||
|
|
||||||
|
# Database encryption configuration, required for VINE connected app
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="dev_primary_key"
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="dev_determinnistic_key"
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="dev_derivation_salt"
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ STRIPE_PUBLIC_TEST_API_KEY="bogus_stripe_publishable_key"
|
|||||||
SITE_URL="test.host"
|
SITE_URL="test.host"
|
||||||
|
|
||||||
OPENID_APP_ID="test-provider"
|
OPENID_APP_ID="test-provider"
|
||||||
OPENID_APP_SECRET="12345"
|
OPENID_APP_SECRET="dummy-openid-app-secret-token"
|
||||||
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
||||||
|
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="test_primary_key"
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="test_deterministic_key"
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="test_derivation_salt"
|
||||||
|
|||||||
11
.github/ISSUE_TEMPLATE/release.md
vendored
11
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -7,10 +7,11 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Preparation on Thursday
|
## 1. Drafting on Friday
|
||||||
|
|
||||||
- [ ] Merge pull requests in the [Ready To Go] column
|
- [ ] Merge pull requests in the [Ready To Go] column
|
||||||
- [ ] Include translations: `script/release/update_locales`
|
- [ ] Include translations: `script/release/update_locales`
|
||||||
|
- You need the [Transifex Client] installed on your local dev environement to run the script.
|
||||||
- [ ] Increment version number: `git push upstream HEAD:refs/tags/vX.Y.Z`
|
- [ ] Increment version number: `git push upstream HEAD:refs/tags/vX.Y.Z`
|
||||||
- Major: if server changes are required (eg. provision with ofn-install)
|
- Major: if server changes are required (eg. provision with ofn-install)
|
||||||
- Minor: larger change that is irreversible (eg. migration deleting data)
|
- Minor: larger change that is irreversible (eg. migration deleting data)
|
||||||
@@ -25,8 +26,9 @@ assignees: ''
|
|||||||
- [ ] Move this issue to Test Ready.
|
- [ ] Move this issue to Test Ready.
|
||||||
- [ ] Notify `@testers` in [#testing].
|
- [ ] Notify `@testers` in [#testing].
|
||||||
- [ ] Test build: [Deploy to Staging] with release tag.
|
- [ ] Test build: [Deploy to Staging] with release tag.
|
||||||
|
- [ ] Notify a deployer to deploy it
|
||||||
|
|
||||||
## 3. Finish on Tuesday
|
## 3. Deployment at beginning of week
|
||||||
|
|
||||||
- [ ] Publish and notify [#global-community] (this is automatically posted with a plugin)
|
- [ ] Publish and notify [#global-community] (this is automatically posted with a plugin)
|
||||||
- [ ] Deploy the new release to all managed instances.
|
- [ ] Deploy the new release to all managed instances.
|
||||||
@@ -39,7 +41,7 @@ assignees: ''
|
|||||||
</details>
|
</details>
|
||||||
- [ ] Notify [#instance-managers]:
|
- [ ] Notify [#instance-managers]:
|
||||||
> @instance_managers The new release has been deployed.
|
> @instance_managers The new release has been deployed.
|
||||||
- [ ] [Create issue] for next release and confirm with next release manager in [#core-devs].
|
- [ ] [Create issue] for next release and confirm with next release drafter in [#delivery-circle].
|
||||||
|
|
||||||
The full process is described at https://github.com/openfoodfoundation/openfoodnetwork/wiki/Releasing.
|
The full process is described at https://github.com/openfoodfoundation/openfoodnetwork/wiki/Releasing.
|
||||||
|
|
||||||
@@ -52,4 +54,5 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
|
|||||||
[Deploy to Staging]: https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/stage.yml
|
[Deploy to Staging]: https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/stage.yml
|
||||||
[#global-community]: https://app.slack.com/client/T02G54U79/C59ADD8F2
|
[#global-community]: https://app.slack.com/client/T02G54U79/C59ADD8F2
|
||||||
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
|
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
|
||||||
[#core-devs]: https://openfoodnetwork.slack.com/archives/GK2T38QPJ
|
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
|
||||||
|
[Transifex Client]: https://developers.transifex.com/docs/cli
|
||||||
|
|||||||
125
.github/workflows/build.yml
vendored
125
.github/workflows/build.yml
vendored
@@ -3,7 +3,7 @@ name: Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- 'dependabot/**'
|
- 'dependabot/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -81,11 +81,20 @@ jobs:
|
|||||||
# RSpec split test files by test examples feature - it's optional
|
# RSpec split test files by test examples feature - it's optional
|
||||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
|
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
|
||||||
run: |
|
run: |
|
||||||
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
|
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
|
||||||
bin/rake knapsack_pro:rspec
|
bin/rake knapsack_pro:rspec
|
||||||
|
|
||||||
|
- name: Save SimpleCov file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simplecov-chunk-controllers-${{ matrix.ci_node_index }}
|
||||||
|
path: coverage/*.*
|
||||||
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
models:
|
models:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
services:
|
services:
|
||||||
@@ -116,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -141,10 +150,19 @@ jobs:
|
|||||||
# RSpec split test files by test examples feature - it's optional
|
# RSpec split test files by test examples feature - it's optional
|
||||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
|
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
|
||||||
run: |
|
run: |
|
||||||
bin/rake knapsack_pro:rspec
|
bin/rake knapsack_pro:rspec
|
||||||
|
|
||||||
|
- name: Save SimpleCov file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simplecov-chunk-models-${{ matrix.ci_node_index }}
|
||||||
|
path: coverage/*.*
|
||||||
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
system_admin:
|
system_admin:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
services:
|
services:
|
||||||
@@ -175,7 +193,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -209,16 +227,25 @@ jobs:
|
|||||||
# RSpec split test files by test examples feature - it's optional
|
# RSpec split test files by test examples feature - it's optional
|
||||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
|
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
bin/rake knapsack_pro:queue:rspec
|
bin/rake knapsack_pro:queue:rspec
|
||||||
|
|
||||||
|
- name: Save SimpleCov file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simplecov-chunk-system-admin-${{ matrix.ci_node_index }}
|
||||||
|
path: coverage/*.*
|
||||||
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
- name: Archive failed tests screenshots
|
- name: Archive failed tests screenshots
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: failed-tests-screenshots
|
name: failed-admin_${{ matrix.ci_node_index }}-tests-screenshots
|
||||||
path: tmp/capybara/screenshots/*.png
|
path: tmp/capybara/screenshots/*.png
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
@@ -247,13 +274,13 @@ jobs:
|
|||||||
ci_node_total: [12]
|
ci_node_total: [12]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -287,16 +314,25 @@ jobs:
|
|||||||
# RSpec split test files by test examples feature - it's optional
|
# RSpec split test files by test examples feature - it's optional
|
||||||
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
|
||||||
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
|
||||||
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
|
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
bin/rake knapsack_pro:queue:rspec
|
bin/rake knapsack_pro:queue:rspec
|
||||||
|
|
||||||
|
- name: Save SimpleCov file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simplecov-chunk-system-consumer-${{ matrix.ci_node_index }}
|
||||||
|
path: coverage/*.*
|
||||||
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
- name: Archive failed tests screenshots
|
- name: Archive failed tests screenshots
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: failed-tests-screenshots
|
name: failed-consumer_${{ matrix.ci_node_index }}-tests-screenshots
|
||||||
path: tmp/capybara/screenshots/*.png
|
path: tmp/capybara/screenshots/*.png
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
@@ -331,7 +367,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -371,14 +407,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bin/rake knapsack_pro:rspec
|
bin/rake knapsack_pro:rspec
|
||||||
|
|
||||||
- name: Archive failed tests screenshots
|
- name: Save SimpleCov file
|
||||||
if: failure()
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: failed-tests-screenshots
|
name: simplecov-chunk-engines-${{ matrix.ci_node_index }}
|
||||||
path: tmp/capybara/screenshots/*.png
|
path: coverage/*.*
|
||||||
retention-days: 7
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
test_the_rest:
|
test_the_rest:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -410,7 +446,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup redis
|
- name: Setup redis
|
||||||
uses: supercharge/redis-github-action@1.4.0
|
uses: supercharge/redis-github-action@1.4.0
|
||||||
with:
|
with:
|
||||||
redis-version: 6
|
redis-version: 6
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
@@ -448,6 +484,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bin/rake knapsack_pro:rspec
|
bin/rake knapsack_pro:rspec
|
||||||
|
|
||||||
|
- name: Save SimpleCov file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: simplecov-chunk-the-rest-${{ matrix.ci_node_index }}
|
||||||
|
path: coverage/*.*
|
||||||
|
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
non_knapsack_jest_karma:
|
non_knapsack_jest_karma:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
services:
|
services:
|
||||||
@@ -485,3 +530,39 @@ jobs:
|
|||||||
|
|
||||||
- name: Run jest tests
|
- name: Run jest tests
|
||||||
run: yarn jest
|
run: yarn jest
|
||||||
|
|
||||||
|
collate_simplecov_results:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- controllers
|
||||||
|
- models
|
||||||
|
- engines
|
||||||
|
- system_admin
|
||||||
|
- system_consumer
|
||||||
|
- test_the_rest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- name: Download individual results from individual runners
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: simplecov-chunk-*
|
||||||
|
path: tmp/simplecov
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: collate results from each of the workers
|
||||||
|
run: bundle exec rake 'simplecov:collate_results[tmp/simplecov]'
|
||||||
|
|
||||||
|
- name: Upload collated results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: combined-simplecov-report
|
||||||
|
path: coverage/**/*.*
|
||||||
|
retention-days: 7
|
||||||
|
if-no-files-found: ignore
|
||||||
|
include-hidden-files: true
|
||||||
|
|||||||
8
.github/workflows/mapi.yml
vendored
8
.github/workflows/mapi.yml
vendored
@@ -14,12 +14,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: docker/build
|
- run: docker/build
|
||||||
- run: docker-compose up --detach
|
- run: docker compose up --detach
|
||||||
- run: until curl -f -s http://localhost:3000; do echo "waiting for api server"; sleep 1; done
|
- run: until curl -f -s http://localhost:3000; do echo "waiting for api server"; sleep 1; done
|
||||||
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="update spree_users set spree_api_key='testing' where login='ofn@example.com'"
|
- run: docker compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="update spree_users set spree_api_key='testing' where login='ofn@example.com'"
|
||||||
# equivalent to Flipper.enable(:api_v1)
|
# equivalent to Flipper.enable(:api_v1)
|
||||||
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_features (key, created_at, updated_at) values ('api_v1', localtimestamp, localtimestamp)"
|
- run: docker compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_features (key, created_at, updated_at) values ('api_v1', localtimestamp, localtimestamp)"
|
||||||
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_gates (feature_key, key, value, created_at, updated_at) values ('api_v1', 'boolean', 'true', localtimestamp, localtimestamp)"
|
- run: docker compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_gates (feature_key, key, value, created_at, updated_at) values ('api_v1', 'boolean', 'true', localtimestamp, localtimestamp)"
|
||||||
|
|
||||||
# Run Mayhem for API
|
# Run Mayhem for API
|
||||||
- name: Run Mayhem for API
|
- name: Run Mayhem for API
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*.yaml
|
*.yaml
|
||||||
*.json
|
*.json
|
||||||
*.html
|
*.html
|
||||||
|
**/*.rb
|
||||||
|
|
||||||
# JS
|
# JS
|
||||||
# Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js
|
# Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js
|
||||||
@@ -27,6 +28,5 @@ postcss.config.js
|
|||||||
/coverage/
|
/coverage/
|
||||||
/engines/
|
/engines/
|
||||||
/public/
|
/public/
|
||||||
/spec/
|
|
||||||
/tmp/
|
/tmp/
|
||||||
/vendor/
|
/vendor/
|
||||||
|
|||||||
@@ -12,22 +12,6 @@ Layout/EmptyLines:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/services/products_renderer.rb'
|
- 'app/services/products_renderer.rb'
|
||||||
|
|
||||||
# Offense count: 6
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
||||||
# SupportedStyles: aligned, indented, indented_relative_to_receiver
|
|
||||||
Layout/MultilineMethodCallIndentation:
|
|
||||||
Exclude:
|
|
||||||
- 'app/services/products_renderer.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
||||||
# SupportedStyles: aligned, indented
|
|
||||||
Layout/MultilineOperationIndentation:
|
|
||||||
Exclude:
|
|
||||||
- 'app/services/products_renderer.rb'
|
|
||||||
|
|
||||||
# Offense count: 16
|
# Offense count: 16
|
||||||
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
# Configuration parameters: AllowComments, AllowEmptyLambdas.
|
||||||
Lint/EmptyBlock:
|
Lint/EmptyBlock:
|
||||||
@@ -101,7 +85,7 @@ Lint/UselessMethodDefinition:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/spree/gateway.rb'
|
- 'app/models/spree/gateway.rb'
|
||||||
|
|
||||||
# Offense count: 23
|
# Offense count: 24
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -114,6 +98,7 @@ Metrics/AbcSize:
|
|||||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||||
- 'app/models/enterprise_group.rb'
|
- 'app/models/enterprise_group.rb'
|
||||||
- 'app/models/enterprise_relationship.rb'
|
- 'app/models/enterprise_relationship.rb'
|
||||||
|
- 'app/models/product_import/entry_processor.rb'
|
||||||
- 'app/models/spree/ability.rb'
|
- 'app/models/spree/ability.rb'
|
||||||
- 'app/models/spree/address.rb'
|
- 'app/models/spree/address.rb'
|
||||||
- 'app/models/spree/order/checkout.rb'
|
- 'app/models/spree/order/checkout.rb'
|
||||||
@@ -142,7 +127,7 @@ Metrics/BlockNesting:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/spree/payment/processing.rb'
|
- 'app/models/spree/payment/processing.rb'
|
||||||
|
|
||||||
# Offense count: 47
|
# Offense count: 46
|
||||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -178,7 +163,6 @@ Metrics/ClassLength:
|
|||||||
- 'app/models/spree/variant.rb'
|
- 'app/models/spree/variant.rb'
|
||||||
- 'app/models/spree/zone.rb'
|
- 'app/models/spree/zone.rb'
|
||||||
- 'app/reflexes/admin/orders_reflex.rb'
|
- 'app/reflexes/admin/orders_reflex.rb'
|
||||||
- 'app/reflexes/products_reflex.rb'
|
|
||||||
- 'app/serializers/api/cached_enterprise_serializer.rb'
|
- 'app/serializers/api/cached_enterprise_serializer.rb'
|
||||||
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
|
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
|
||||||
- 'app/services/cart_service.rb'
|
- 'app/services/cart_service.rb'
|
||||||
@@ -408,7 +392,6 @@ RSpecRails/HaveHttpStatus:
|
|||||||
- 'spec/controllers/stripe/webhooks_controller_spec.rb'
|
- 'spec/controllers/stripe/webhooks_controller_spec.rb'
|
||||||
- 'spec/controllers/user_passwords_controller_spec.rb'
|
- 'spec/controllers/user_passwords_controller_spec.rb'
|
||||||
- 'spec/controllers/user_registrations_controller_spec.rb'
|
- 'spec/controllers/user_registrations_controller_spec.rb'
|
||||||
- 'spec/requests/admin/images_spec.rb'
|
|
||||||
- 'spec/requests/api/routes_spec.rb'
|
- 'spec/requests/api/routes_spec.rb'
|
||||||
- 'spec/requests/checkout/stripe_sca_spec.rb'
|
- 'spec/requests/checkout/stripe_sca_spec.rb'
|
||||||
- 'spec/requests/home_controller_spec.rb'
|
- 'spec/requests/home_controller_spec.rb'
|
||||||
@@ -638,12 +621,6 @@ Rails/ResponseParsedBody:
|
|||||||
- 'spec/controllers/spree/credit_cards_controller_spec.rb'
|
- 'spec/controllers/spree/credit_cards_controller_spec.rb'
|
||||||
- 'spec/controllers/user_registrations_controller_spec.rb'
|
- 'spec/controllers/user_registrations_controller_spec.rb'
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
||||||
Rails/RootPathnameMethods:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/lib/reports/orders_and_fulfillment/order_cycle_customer_totals_report_spec.rb'
|
|
||||||
|
|
||||||
# Offense count: 7
|
# Offense count: 7
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
@@ -725,7 +702,7 @@ Style/ClassAndModuleChildren:
|
|||||||
- 'lib/open_food_network/locking.rb'
|
- 'lib/open_food_network/locking.rb'
|
||||||
- 'spec/models/spree/payment_method_spec.rb'
|
- 'spec/models/spree/payment_method_spec.rb'
|
||||||
|
|
||||||
# Offense count: 2
|
# Offense count: 1
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
# SupportedStyles: always, always_true, never
|
# SupportedStyles: always, always_true, never
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ SimpleCov.start 'rails' do
|
|||||||
add_filter '/log'
|
add_filter '/log'
|
||||||
add_filter '/db'
|
add_filter '/db'
|
||||||
add_filter '/lib/tasks/sample_data/'
|
add_filter '/lib/tasks/sample_data/'
|
||||||
|
|
||||||
|
formatter SimpleCov::Formatter::SimpleFormatter
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ This project needs specific ruby/bundler versions as well as node/yarn specific
|
|||||||
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
|
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
|
||||||
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/).
|
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/).
|
||||||
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
|
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
|
||||||
- [nodevn](https://github.com/nodenv/nodenv) is recommended as a node version manager.
|
- [nodenv](https://github.com/nodenv/nodenv) is recommended as a node version manager.
|
||||||
* PostgreSQL database
|
* PostgreSQL database
|
||||||
* Redis (for background jobs)
|
* Redis (for background jobs)
|
||||||
* Chrome (for testing)
|
* Chrome (for testing)
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@@ -16,7 +16,6 @@ gem "image_processing"
|
|||||||
|
|
||||||
gem 'activemerchant', '>= 1.78.0'
|
gem 'activemerchant', '>= 1.78.0'
|
||||||
gem 'angular-rails-templates', '>= 0.3.0'
|
gem 'angular-rails-templates', '>= 0.3.0'
|
||||||
gem 'awesome_nested_set'
|
|
||||||
gem 'ransack', '~> 4.1.0'
|
gem 'ransack', '~> 4.1.0'
|
||||||
gem 'responders'
|
gem 'responders'
|
||||||
gem 'webpacker', '~> 5'
|
gem 'webpacker', '~> 5'
|
||||||
@@ -105,6 +104,7 @@ gem 'sidekiq-scheduler'
|
|||||||
gem "cable_ready"
|
gem "cable_ready"
|
||||||
gem "stimulus_reflex"
|
gem "stimulus_reflex"
|
||||||
|
|
||||||
|
gem "turbo_power"
|
||||||
gem "turbo-rails"
|
gem "turbo-rails"
|
||||||
|
|
||||||
gem 'combine_pdf'
|
gem 'combine_pdf'
|
||||||
|
|||||||
19
Gemfile.lock
19
Gemfile.lock
@@ -161,8 +161,6 @@ GEM
|
|||||||
activerecord (>= 3.1.0, < 8)
|
activerecord (>= 3.1.0, < 8)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awesome_nested_set (3.6.0)
|
|
||||||
activerecord (>= 4.0.0, < 7.2)
|
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.929.0)
|
aws-partitions (1.929.0)
|
||||||
aws-sdk-core (3.196.1)
|
aws-sdk-core (3.196.1)
|
||||||
@@ -248,7 +246,7 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
datafoodconsortium-connector (1.0.0.pre.alpha.12)
|
datafoodconsortium-connector (1.0.0.pre.alpha.13)
|
||||||
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
|
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
|
||||||
date (3.3.4)
|
date (3.3.4)
|
||||||
debug (1.9.2)
|
debug (1.9.2)
|
||||||
@@ -358,7 +356,7 @@ GEM
|
|||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
immigrant (0.3.6)
|
immigrant (0.3.6)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
invisible_captcha (2.2.0)
|
invisible_captcha (2.3.0)
|
||||||
rails (>= 5.2)
|
rails (>= 5.2)
|
||||||
io-console (0.7.2)
|
io-console (0.7.2)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
@@ -381,13 +379,14 @@ GEM
|
|||||||
bindata
|
bindata
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
json-ld (3.3.1)
|
json-ld (3.3.2)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 1.0)
|
json-canonicalization (~> 1.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
multi_json (~> 1.15)
|
multi_json (~> 1.15)
|
||||||
rack (>= 2.2, < 4)
|
rack (>= 2.2, < 4)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
|
rexml (~> 3.2)
|
||||||
json-schema (4.1.1)
|
json-schema (4.1.1)
|
||||||
addressable (>= 2.8)
|
addressable (>= 2.8)
|
||||||
json_spec (1.1.5)
|
json_spec (1.1.5)
|
||||||
@@ -417,7 +416,7 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mime-types (3.5.2)
|
mime-types (3.5.2)
|
||||||
@@ -449,7 +448,7 @@ GEM
|
|||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
newrelic_rpm (9.9.0)
|
newrelic_rpm (9.9.0)
|
||||||
nio4r (2.7.0)
|
nio4r (2.7.1)
|
||||||
nokogiri (1.16.5)
|
nokogiri (1.16.5)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
@@ -787,6 +786,8 @@ GEM
|
|||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
turbo_power (0.6.2)
|
||||||
|
turbo-rails (>= 1.3.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
@@ -837,7 +838,7 @@ GEM
|
|||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
whenever (1.0.0)
|
whenever (1.0.0)
|
||||||
chronic (>= 0.6.3)
|
chronic (>= 0.6.3)
|
||||||
wicked_pdf (2.6.3)
|
wicked_pdf (2.8.1)
|
||||||
activesupport
|
activesupport
|
||||||
wkhtmltopdf-binary (0.12.6.7)
|
wkhtmltopdf-binary (0.12.6.7)
|
||||||
xml-simple (1.1.8)
|
xml-simple (1.1.8)
|
||||||
@@ -863,7 +864,6 @@ DEPENDENCIES
|
|||||||
angularjs-file-upload-rails (~> 2.4.1)
|
angularjs-file-upload-rails (~> 2.4.1)
|
||||||
angularjs-rails (= 1.8.0)
|
angularjs-rails (= 1.8.0)
|
||||||
arel-helpers (~> 2.12)
|
arel-helpers (~> 2.12)
|
||||||
awesome_nested_set
|
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
bigdecimal (= 3.0.2)
|
bigdecimal (= 3.0.2)
|
||||||
bootsnap
|
bootsnap
|
||||||
@@ -976,6 +976,7 @@ DEPENDENCIES
|
|||||||
stripe
|
stripe
|
||||||
timecop
|
timecop
|
||||||
turbo-rails
|
turbo-rails
|
||||||
|
turbo_power
|
||||||
valid_email2
|
valid_email2
|
||||||
validates_lengths_from_database
|
validates_lengths_from_database
|
||||||
vcr
|
vcr
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
//= require jquery.ui.all
|
//= require jquery.ui.all
|
||||||
//= require jquery.powertip
|
//= require jquery.powertip
|
||||||
//= require jquery.cookie
|
//= require jquery.cookie
|
||||||
//= require jquery.jstree/jquery.jstree
|
|
||||||
//= require jquery.vAlign
|
//= require jquery.vAlign
|
||||||
//= require angular
|
//= require angular
|
||||||
//= require angular-resource
|
//= require angular-resource
|
||||||
//= require angular-animate
|
//= require angular-animate
|
||||||
|
//= require angular-sanitize
|
||||||
//= require angularjs-file-upload
|
//= require angularjs-file-upload
|
||||||
//= require ../shared/ng-infinite-scroll.min.js
|
//= require ../shared/ng-infinite-scroll.min.js
|
||||||
//= require ../shared/ng-tags-input.min.js
|
//= require ../shared/ng-tags-input.min.js
|
||||||
@@ -61,11 +61,6 @@
|
|||||||
//= require ./variant_overrides/variant_overrides
|
//= require ./variant_overrides/variant_overrides
|
||||||
|
|
||||||
// text, dates and translations
|
// text, dates and translations
|
||||||
//= require textAngular-rangy.min.js
|
|
||||||
// This replaces angular-sanitize. We should include only one.
|
|
||||||
// https://github.com/textAngular/textAngular#where-to-get-it
|
|
||||||
//= require textAngular-sanitize.min.js
|
|
||||||
//= require textAngular.min.js
|
|
||||||
//= require i18n/translations
|
//= require i18n/translations
|
||||||
//= require darkswarm/i18n.translate.js
|
//= require darkswarm/i18n.translate.js
|
||||||
|
|
||||||
|
|||||||
@@ -187,9 +187,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
|||||||
product.variants.length > 0
|
product.variants.length > 0
|
||||||
|
|
||||||
|
|
||||||
$scope.hasUnit = (product) ->
|
$scope.hasUnit = (variant) ->
|
||||||
product.variant_unit_with_scale?
|
variant.variant_unit_with_scale?
|
||||||
|
|
||||||
|
|
||||||
$scope.variantSaved = (variant) ->
|
$scope.variantSaved = (variant) ->
|
||||||
variant.hasOwnProperty('id') && variant.id > 0
|
variant.hasOwnProperty('id') && variant.id > 0
|
||||||
@@ -242,32 +241,28 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
|||||||
$window.location = destination
|
$window.location = destination
|
||||||
|
|
||||||
$scope.packProduct = (product) ->
|
$scope.packProduct = (product) ->
|
||||||
if product.variant_unit_with_scale
|
|
||||||
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
|
||||||
if match
|
|
||||||
product.variant_unit = match[1]
|
|
||||||
product.variant_unit_scale = parseFloat(match[2])
|
|
||||||
else
|
|
||||||
product.variant_unit = product.variant_unit_with_scale
|
|
||||||
product.variant_unit_scale = null
|
|
||||||
else
|
|
||||||
product.variant_unit = product.variant_unit_scale = null
|
|
||||||
|
|
||||||
|
|
||||||
if product.variants
|
if product.variants
|
||||||
for id, variant of product.variants
|
for id, variant of product.variants
|
||||||
$scope.packVariant product, variant
|
$scope.packVariant variant
|
||||||
|
|
||||||
|
|
||||||
$scope.packVariant = (product, variant) ->
|
$scope.packVariant = (variant) ->
|
||||||
|
if variant.variant_unit_with_scale
|
||||||
|
match = variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||||
|
if match
|
||||||
|
variant.variant_unit = match[1]
|
||||||
|
variant.variant_unit_scale = parseFloat(match[2])
|
||||||
|
else
|
||||||
|
variant.variant_unit = variant.variant_unit_with_scale
|
||||||
|
variant.variant_unit_scale = null
|
||||||
|
|
||||||
if variant.hasOwnProperty("unit_value_with_description")
|
if variant.hasOwnProperty("unit_value_with_description")
|
||||||
match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/)
|
match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/)
|
||||||
if match
|
if match
|
||||||
product = BulkProducts.find product.id
|
|
||||||
variant.unit_value = parseFloat(match[1].replace(",", "."))
|
variant.unit_value = parseFloat(match[1].replace(",", "."))
|
||||||
variant.unit_value = null if isNaN(variant.unit_value)
|
variant.unit_value = null if isNaN(variant.unit_value)
|
||||||
if variant.unit_value && product.variant_unit_scale
|
if variant.unit_value && variant.variant_unit_scale
|
||||||
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2))
|
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2))
|
||||||
variant.unit_description = match[3]
|
variant.unit_description = match[3]
|
||||||
|
|
||||||
$scope.incrementLimit = ->
|
$scope.incrementLimit = ->
|
||||||
@@ -321,13 +316,6 @@ filterSubmitProducts = (productsToFilter) ->
|
|||||||
if product.hasOwnProperty("price")
|
if product.hasOwnProperty("price")
|
||||||
filteredProduct.price = product.price
|
filteredProduct.price = product.price
|
||||||
hasUpdatableProperty = true
|
hasUpdatableProperty = true
|
||||||
if product.hasOwnProperty("variant_unit_with_scale")
|
|
||||||
filteredProduct.variant_unit = product.variant_unit
|
|
||||||
filteredProduct.variant_unit_scale = product.variant_unit_scale
|
|
||||||
hasUpdatableProperty = true
|
|
||||||
if product.hasOwnProperty("variant_unit_name")
|
|
||||||
filteredProduct.variant_unit_name = product.variant_unit_name
|
|
||||||
hasUpdatableProperty = true
|
|
||||||
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
|
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
|
||||||
filteredProduct.on_hand = product.on_hand
|
filteredProduct.on_hand = product.on_hand
|
||||||
hasUpdatableProperty = true
|
hasUpdatableProperty = true
|
||||||
@@ -383,6 +371,14 @@ filterSubmitVariant = (variant) ->
|
|||||||
if variant.hasOwnProperty("producer_id")
|
if variant.hasOwnProperty("producer_id")
|
||||||
filteredVariant.supplier_id = variant.producer_id
|
filteredVariant.supplier_id = variant.producer_id
|
||||||
hasUpdatableProperty = true
|
hasUpdatableProperty = true
|
||||||
|
if variant.hasOwnProperty("variant_unit_with_scale")
|
||||||
|
filteredVariant.variant_unit = variant.variant_unit
|
||||||
|
filteredVariant.variant_unit_scale = variant.variant_unit_scale
|
||||||
|
hasUpdatableProperty = true
|
||||||
|
if variant.hasOwnProperty("variant_unit_name")
|
||||||
|
filteredVariant.variant_unit_name = variant.variant_unit_name
|
||||||
|
hasUpdatableProperty = true
|
||||||
|
|
||||||
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}
|
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,31 +4,30 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
|
|||||||
scope.$watchCollection ->
|
scope.$watchCollection ->
|
||||||
return [
|
return [
|
||||||
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
|
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
|
||||||
scope.product.variant_unit_name
|
scope.variant.variant_unit_name
|
||||||
scope.product.variant_unit_with_scale
|
scope.variant.variant_unit_with_scale
|
||||||
]
|
]
|
||||||
, ->
|
, ->
|
||||||
[variant_unit, variant_unit_scale] = productUnitProperties()
|
[variant_unit, variant_unit_scale] = productUnitProperties()
|
||||||
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
|
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
|
||||||
variant_object =
|
variant_object =
|
||||||
unit_value: unit_value
|
unit_value: unit_value
|
||||||
unit_description: unit_description
|
unit_description: unit_description
|
||||||
product:
|
variant_unit_scale: variant_unit_scale
|
||||||
variant_unit_scale: variant_unit_scale
|
variant_unit: variant_unit
|
||||||
variant_unit: variant_unit
|
variant_unit_name: scope.variant.variant_unit_name
|
||||||
variant_unit_name: scope.product.variant_unit_name
|
|
||||||
|
|
||||||
scope.placeholder_text = new OptionValueNamer(variant_object).name()
|
scope.placeholder_text = new OptionValueNamer(variant_object).name()
|
||||||
|
|
||||||
productUnitProperties = ->
|
productUnitProperties = ->
|
||||||
# get relevant product properties
|
# get relevant product properties
|
||||||
if scope.product.variant_unit_with_scale?
|
if scope.variant.variant_unit_with_scale?
|
||||||
match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||||
if match
|
if match
|
||||||
variant_unit = match[1]
|
variant_unit = match[1]
|
||||||
variant_unit_scale = parseFloat(match[2])
|
variant_unit_scale = parseFloat(match[2])
|
||||||
else
|
else
|
||||||
variant_unit = scope.product.variant_unit_with_scale
|
variant_unit = scope.variant.variant_unit_with_scale
|
||||||
variant_unit_scale = null
|
variant_unit_scale = null
|
||||||
else
|
else
|
||||||
variant_unit = variant_unit_scale = null
|
variant_unit = variant_unit_scale = null
|
||||||
@@ -45,4 +44,4 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
|
|||||||
unit_value = null if isNaN(unit_value)
|
unit_value = null if isNaN(unit_value)
|
||||||
unit_value *= variant_unit_scale if unit_value && variant_unit_scale
|
unit_value *= variant_unit_scale if unit_value && variant_unit_scale
|
||||||
unit_description = match[3]
|
unit_description = match[3]
|
||||||
[unit_value, unit_description]
|
[unit_value, unit_description]
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
angular.module("ofn.admin").directive "ofnMaintainUnitScale", ->
|
|
||||||
require: "ngModel"
|
|
||||||
link: (scope, element, attrs, ngModel) ->
|
|
||||||
scope.$watch 'product.variant_unit_with_scale', (newValue, oldValue) ->
|
|
||||||
if not (oldValue == newValue)
|
|
||||||
# Triggers track-variant directive to track the unit_value, so that changes to the unit are passed to the server
|
|
||||||
ngModel.$setViewValue ngModel.$viewValue
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) ->
|
|
||||||
require: "ngModel"
|
|
||||||
link: (scope, element, attrs, ngModel) ->
|
|
||||||
ngModel.$parsers.push (viewValue) ->
|
|
||||||
if ngModel.$dirty
|
|
||||||
DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue
|
|
||||||
scope.displayDirtyProducts()
|
|
||||||
viewValue
|
|
||||||
@@ -1 +1 @@
|
|||||||
angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "textAngular"])
|
angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "ngSanitize"])
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ angular.module("admin.enterprises", [
|
|||||||
"admin.utils",
|
"admin.utils",
|
||||||
"admin.shippingMethods",
|
"admin.shippingMethods",
|
||||||
"admin.users",
|
"admin.users",
|
||||||
"textAngular",
|
|
||||||
"admin.side_menu",
|
"admin.side_menu",
|
||||||
"admin.taxons",
|
"admin.taxons",
|
||||||
'admin.indexUtils',
|
'admin.indexUtils',
|
||||||
@@ -11,16 +10,3 @@ angular.module("admin.enterprises", [
|
|||||||
'admin.dropdown',
|
'admin.dropdown',
|
||||||
'ngSanitize']
|
'ngSanitize']
|
||||||
)
|
)
|
||||||
# For more options: https://github.com/textAngular/textAngular/blob/master/src/textAngularSetup.js
|
|
||||||
.config [
|
|
||||||
'$provide', ($provide) ->
|
|
||||||
$provide.decorator 'taTranslations', [
|
|
||||||
'$delegate'
|
|
||||||
(taTranslations) ->
|
|
||||||
taTranslations.insertLink = {
|
|
||||||
tooltip: t('admin.enterprises.form.shop_preferences.shopfront_message_link_tooltip'),
|
|
||||||
dialogPrompt: t('admin.enterprises.form.shop_preferences.shopfront_message_link_prompt')
|
|
||||||
}
|
|
||||||
taTranslations
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -199,14 +199,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
|
|||||||
$scope.refreshData()
|
$scope.refreshData()
|
||||||
|
|
||||||
$scope.getLineItemScale = (lineItem) ->
|
$scope.getLineItemScale = (lineItem) ->
|
||||||
if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume")
|
if lineItem.units_variant && lineItem.units_variant.variant_unit_scale && (lineItem.units_variant.variant_unit == "weight" || lineItem.units_variant.variant_unit == "volume")
|
||||||
lineItem.units_product.variant_unit_scale
|
lineItem.units_variant.variant_unit_scale
|
||||||
else
|
else
|
||||||
1
|
1
|
||||||
|
|
||||||
$scope.sumUnitValues = ->
|
$scope.sumUnitValues = ->
|
||||||
sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
|
sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
|
||||||
if lineItem.units_product.variant_unit == "items"
|
if lineItem.units_variant.variant_unit == "items"
|
||||||
sum + lineItem.quantity
|
sum + lineItem.quantity
|
||||||
else
|
else
|
||||||
sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
|
sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
|
||||||
@@ -214,7 +214,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
|
|||||||
|
|
||||||
$scope.sumMaxUnitValues = ->
|
$scope.sumMaxUnitValues = ->
|
||||||
sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
|
sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
|
||||||
if lineItem.units_product.variant_unit == "items"
|
if lineItem.units_variant.variant_unit == "items"
|
||||||
sum + lineItem.max_quantity
|
sum + lineItem.max_quantity
|
||||||
else
|
else
|
||||||
sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem))
|
sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem))
|
||||||
@@ -228,39 +228,41 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
|
|||||||
return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0)
|
return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0)
|
||||||
true
|
true
|
||||||
|
|
||||||
$scope.getScale = (unitsProduct, unitsVariant) ->
|
$scope.getScale = (unitsVariant) ->
|
||||||
if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume")
|
if unitsVariant.hasOwnProperty("variant_unit") && (unitsVariant.variant_unit == "weight" || unitsVariant.variant_unit == "volume")
|
||||||
unitsProduct.variant_unit_scale
|
unitsVariant.variant_unit_scale
|
||||||
else if unitsProduct.hasOwnProperty("variant_unit") && unitsProduct.variant_unit == "items"
|
else if unitsVariant.hasOwnProperty("variant_unit") && unitsVariant.variant_unit == "items"
|
||||||
1
|
1
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
|
|
||||||
$scope.getFormattedValueWithUnitName = (value, unitsProduct, unitsVariant, scale) ->
|
$scope.getFormattedValueWithUnitName = (value, unitsVariant, scale) ->
|
||||||
unit_name = VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit)
|
unit_name = VariantUnitManager.getUnitName(scale, unitsVariant.variant_unit)
|
||||||
$scope.roundToThreeDecimals(value) + " " + unit_name
|
$scope.roundToThreeDecimals(value) + " " + unit_name
|
||||||
|
|
||||||
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
|
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsVariant) ->
|
||||||
scale = $scope.getScale(unitsProduct, unitsVariant)
|
scale = $scope.getScale(unitsVariant)
|
||||||
if scale && value
|
if scale && value
|
||||||
value = value / scale if scale != 28.35 && scale != 1 && scale != 453.6 # divide by scale if not smallest unit
|
value = value / scale if scale != 28.35 && scale != 1 && scale != 453.6 # divide by scale if not smallest unit
|
||||||
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
|
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
|
|
||||||
$scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
|
$scope.formattedValueWithUnitName = (value, unitsVariant) ->
|
||||||
scale = $scope.getScale(unitsProduct, unitsVariant)
|
scale = $scope.getScale(unitsVariant)
|
||||||
if scale
|
if scale
|
||||||
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
|
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
|
|
||||||
$scope.fulfilled = (sumOfUnitValues) ->
|
$scope.fulfilled = (sumOfUnitValues) ->
|
||||||
# A Units Variant is an API object which holds unit properies of a variant
|
# A Units Variant is an API object which holds unit properies of a variant
|
||||||
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size")&& $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
|
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
|
||||||
$scope.selectedUnitsProduct.hasOwnProperty("variant_unit")
|
$scope.selectedUnitsVariant.hasOwnProperty("variant_unit")
|
||||||
if $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume"
|
|
||||||
scale = $scope.selectedUnitsProduct.variant_unit_scale
|
if $scope.selectedUnitsVariant.variant_unit == "weight" || $scope.selectedUnitsVariant.variant_unit == "volume"
|
||||||
|
|
||||||
|
scale = $scope.selectedUnitsVariant.variant_unit_scale
|
||||||
sumOfUnitValues = sumOfUnitValues * scale unless scale == 28.35 || scale == 453.6
|
sumOfUnitValues = sumOfUnitValues * scale unless scale == 28.35 || scale == 453.6
|
||||||
$scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size)
|
$scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ angular.module('admin.orderCycles')
|
|||||||
|
|
||||||
$scope.submit = ($event, destination) ->
|
$scope.submit = ($event, destination) ->
|
||||||
$event.preventDefault()
|
$event.preventDefault()
|
||||||
|
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
|
||||||
|
$scope.order_cycle?.confirm = $($event.target).data('confirm');
|
||||||
StatusMessage.display 'progress', t('js.saving')
|
StatusMessage.display 'progress', t('js.saving')
|
||||||
OrderCycle.update(destination, $scope.order_cycle_form)
|
OrderCycle.update(destination, $scope.order_cycle_form)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules, Dereferencer) ->
|
angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules, Dereferencer) ->
|
||||||
$scope.RequestMonitor = RequestMonitor
|
$scope.RequestMonitor = RequestMonitor
|
||||||
$scope.columns = Columns.columns
|
$scope.columns = Columns.columns
|
||||||
$scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form)
|
$scope.saveAll = ($event) ->
|
||||||
|
trigger_action = $($event.target).data('trigger-action')
|
||||||
|
confirm = $($event.target).data('confirm')
|
||||||
|
OrderCycles.saveChanges($scope.order_cycles_form, { trigger_action, confirm })
|
||||||
|
|
||||||
$scope.ordersCloseAtLimit = -31 # days
|
$scope.ordersCloseAtLimit = -31 # days
|
||||||
|
|
||||||
$scope.resetSelectFilters = ->
|
$scope.resetSelectFilters = ->
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
|
|||||||
|
|
||||||
$scope.submit = ($event, destination) ->
|
$scope.submit = ($event, destination) ->
|
||||||
$event.preventDefault()
|
$event.preventDefault()
|
||||||
|
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
|
||||||
|
$scope.order_cycle?.confirm = $($event.target).data('confirm');
|
||||||
StatusMessage.display 'progress', t('js.saving')
|
StatusMessage.display 'progress', t('js.saving')
|
||||||
OrderCycle.mirrorIncomingToOutgoingProducts()
|
OrderCycle.mirrorIncomingToOutgoingProducts()
|
||||||
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()
|
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
|
|||||||
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
|
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
|
||||||
|
|
||||||
update: (destination, form) ->
|
update: (destination, form) ->
|
||||||
oc = new OrderCycleResource({order_cycle: this.dataForSubmit()})
|
oc = new OrderCycleResource({
|
||||||
|
order_cycle: this.dataForSubmit(),
|
||||||
|
confirm: this.order_cycle.confirm,
|
||||||
|
trigger_action: this.order_cycle.trigger_action
|
||||||
|
})
|
||||||
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
|
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
|
||||||
form.$setPristine() if form
|
form.$setPristine() if form
|
||||||
if destination?
|
if destination?
|
||||||
@@ -171,6 +175,8 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
|
|||||||
, (response) ->
|
, (response) ->
|
||||||
if response.data.errors?
|
if response.data.errors?
|
||||||
StatusMessage.display('failure', response.data.errors[0])
|
StatusMessage.display('failure', response.data.errors[0])
|
||||||
|
else if (response.data.trigger_action)
|
||||||
|
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
|
||||||
else
|
else
|
||||||
StatusMessage.display('failure', t('js.order_cycles.update_failure'))
|
StatusMessage.display('failure', t('js.order_cycles.update_failure'))
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
angular.module("admin.products").controller "editUnitsCtrl", ($scope, VariantUnitManager) ->
|
|
||||||
|
|
||||||
$scope.product =
|
|
||||||
variant_unit: angular.element('#variant_unit').val()
|
|
||||||
variant_unit_scale: angular.element('#variant_unit_scale').val()
|
|
||||||
|
|
||||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
|
||||||
|
|
||||||
if $scope.product.variant_unit == 'items'
|
|
||||||
$scope.variant_unit_with_scale = 'items'
|
|
||||||
else
|
|
||||||
$scope.variant_unit_with_scale = $scope.product.variant_unit + '_' + $scope.product.variant_unit_scale.replace(/\.0$/, '');
|
|
||||||
|
|
||||||
$scope.setFields = ->
|
|
||||||
if $scope.variant_unit_with_scale == 'items'
|
|
||||||
variant_unit = 'items'
|
|
||||||
variant_unit_scale = null
|
|
||||||
else
|
|
||||||
options = $scope.variant_unit_with_scale.split('_')
|
|
||||||
variant_unit = options[0]
|
|
||||||
variant_unit_scale = options[1]
|
|
||||||
|
|
||||||
$scope.product.variant_unit = variant_unit
|
|
||||||
$scope.product.variant_unit_scale = variant_unit_scale
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
# Controller for "New Products" form (spree/admin/products/new)
|
# Controller for "New Products" form (spree/admin/products/new)
|
||||||
angular.module("admin.products")
|
angular.module("admin.products")
|
||||||
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
|
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
|
||||||
$scope.product = { master: {} }
|
$scope.product = {}
|
||||||
$scope.product.master.product = $scope.product
|
|
||||||
$scope.placeholder_text = ""
|
$scope.placeholder_text = ""
|
||||||
|
|
||||||
$scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description, product.price, product.variant_unit_name]', ->
|
$scope.$watchCollection '[product.variant_unit_with_scale, product.unit_value_with_description, product.price, product.variant_unit_name]', ->
|
||||||
$scope.processVariantUnitWithScale()
|
$scope.processVariantUnitWithScale()
|
||||||
$scope.processUnitValueWithDescription()
|
$scope.processUnitValueWithDescription()
|
||||||
$scope.processUnitPrice()
|
$scope.processUnitPrice()
|
||||||
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name() if $scope.product.variant_unit_scale
|
$scope.placeholder_text = new OptionValueNamer($scope.product).name() if $scope.product.variant_unit_scale
|
||||||
|
|
||||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||||
|
|
||||||
@@ -38,24 +37,24 @@ angular.module("admin.products")
|
|||||||
# Extract unit_value and unit_description from text field unit_value_with_description,
|
# Extract unit_value and unit_description from text field unit_value_with_description,
|
||||||
# and update hidden variant fields
|
# and update hidden variant fields
|
||||||
$scope.processUnitValueWithDescription = ->
|
$scope.processUnitValueWithDescription = ->
|
||||||
if $scope.product.master.hasOwnProperty("unit_value_with_description")
|
if $scope.product.hasOwnProperty("unit_value_with_description")
|
||||||
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
|
match = $scope.product.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
|
||||||
if match
|
if match
|
||||||
$scope.product.master.unit_value = PriceParser.parse(match[1])
|
$scope.product.unit_value = PriceParser.parse(match[1])
|
||||||
$scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value)
|
$scope.product.unit_value = null if isNaN($scope.product.unit_value)
|
||||||
$scope.product.master.unit_value = window.bigDecimal.multiply($scope.product.master.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
|
$scope.product.unit_value = window.bigDecimal.multiply($scope.product.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
|
||||||
$scope.product.master.unit_description = match[3]
|
$scope.product.unit_description = match[3]
|
||||||
else
|
else
|
||||||
value = $scope.product.master.unit_value
|
value = $scope.product.unit_value
|
||||||
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
|
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
|
||||||
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description
|
$scope.product.unit_value_with_description = value + " " + $scope.product.unit_description
|
||||||
|
|
||||||
# Calculate unit price based on product price and variant_unit_scale
|
# Calculate unit price based on product price and variant_unit_scale
|
||||||
$scope.processUnitPrice = ->
|
$scope.processUnitPrice = ->
|
||||||
price = $scope.product.price
|
price = $scope.product.price
|
||||||
scale = $scope.product.variant_unit_scale
|
scale = $scope.product.variant_unit_scale
|
||||||
unit_type = $scope.product.variant_unit
|
unit_type = $scope.product.variant_unit
|
||||||
unit_value = $scope.product.master.unit_value
|
unit_value = $scope.product.unit_value
|
||||||
variant_unit_name = $scope.product.variant_unit_name
|
variant_unit_name = $scope.product.variant_unit_name
|
||||||
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
|
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, PriceParser) ->
|
|
||||||
|
|
||||||
$scope.unitName = (scale, type) ->
|
|
||||||
VariantUnitManager.getUnitName(scale, type)
|
|
||||||
|
|
||||||
$scope.$watchCollection "[unit_value_human, variant.price]", ->
|
|
||||||
$scope.processUnitPrice()
|
|
||||||
|
|
||||||
$scope.processUnitPrice = ->
|
|
||||||
if ($scope.variant)
|
|
||||||
price = $scope.variant.price
|
|
||||||
scale = $scope.scale
|
|
||||||
unit_type = angular.element("#product_variant_unit").val()
|
|
||||||
if (unit_type != "items")
|
|
||||||
$scope.updateValue()
|
|
||||||
unit_value = $scope.unit_value
|
|
||||||
else
|
|
||||||
unit_value = 1
|
|
||||||
variant_unit_name = angular.element("#product_variant_unit_name").val()
|
|
||||||
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
|
|
||||||
|
|
||||||
$scope.scale = angular.element('#product_variant_unit_scale').val()
|
|
||||||
|
|
||||||
$scope.updateValue = ->
|
|
||||||
unit_value_human = angular.element('#unit_value_human').val()
|
|
||||||
$scope.unit_value = bigDecimal.multiply(PriceParser.parse(unit_value_human), $scope.scale, 2)
|
|
||||||
|
|
||||||
variant_unit_value = angular.element('#variant_unit_value').val()
|
|
||||||
$scope.unit_value_human = parseFloat(bigDecimal.divide(variant_unit_value, $scope.scale, 2))
|
|
||||||
|
|
||||||
$timeout -> $scope.processUnitPrice()
|
|
||||||
$timeout -> $scope.updateValue()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
angular.module("admin.products").directive "setOnDemand", ->
|
|
||||||
link: (scope, element, attr) ->
|
|
||||||
onHand = element.context.querySelector("#variant_on_hand")
|
|
||||||
onDemand = element.context.querySelector("#variant_on_demand")
|
|
||||||
|
|
||||||
disableOnHandIfOnDemand = ->
|
|
||||||
if onDemand.checked
|
|
||||||
onHand.disabled = 'disabled'
|
|
||||||
onHand.dataStock = onHand.value
|
|
||||||
onHand.value = t('admin.products.variants.infinity')
|
|
||||||
|
|
||||||
disableOnHandIfOnDemand()
|
|
||||||
|
|
||||||
onDemand.addEventListener 'change', (event) ->
|
|
||||||
disableOnHandIfOnDemand()
|
|
||||||
|
|
||||||
if !onDemand.checked
|
|
||||||
onHand.removeAttribute('disabled')
|
|
||||||
onHand.value = onHand.dataStock
|
|
||||||
@@ -1 +1 @@
|
|||||||
angular.module("admin.products", ["textAngular", "admin.utils", "OFNShared"])
|
angular.module("admin.products", ["ngSanitize", "admin.utils", "OFNShared"])
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
|
|||||||
name_fields.join ' '
|
name_fields.join ' '
|
||||||
|
|
||||||
value_scaled: ->
|
value_scaled: ->
|
||||||
@variant.product.variant_unit_scale?
|
@variant.variant_unit_scale?
|
||||||
|
|
||||||
option_value_value_unit: ->
|
option_value_value_unit: ->
|
||||||
if @variant.unit_value?
|
if @variant.unit_value?
|
||||||
if @variant.product.variant_unit in ["weight", "volume"]
|
if @variant.variant_unit in ["weight", "volume"]
|
||||||
[value, unit_name] = @option_value_value_unit_scaled()
|
[value, unit_name] = @option_value_value_unit_scaled()
|
||||||
|
|
||||||
else
|
else
|
||||||
value = @variant.unit_value
|
value = @variant.unit_value
|
||||||
unit_name = @pluralize(@variant.product.variant_unit_name, value)
|
unit_name = @pluralize(@variant.variant_unit_name, value)
|
||||||
|
|
||||||
value = parseInt(value, 10) if value == parseInt(value, 10)
|
value = parseInt(value, 10) if value == parseInt(value, 10)
|
||||||
|
|
||||||
@@ -58,14 +58,13 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
|
|||||||
# to >= 1 when expressed in it.
|
# to >= 1 when expressed in it.
|
||||||
# If there is none available where this is true, use the smallest
|
# If there is none available where this is true, use the smallest
|
||||||
# available unit.
|
# available unit.
|
||||||
product = @variant.product
|
scales = VariantUnitManager.compatibleUnitScales(@variant.variant_unit_scale, @variant.variant_unit)
|
||||||
scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit)
|
|
||||||
variantUnitValue = @variant.unit_value
|
variantUnitValue = @variant.unit_value
|
||||||
|
|
||||||
# sets largestScale = last element in filtered scales array
|
# sets largestScale = last element in filtered scales array
|
||||||
[_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1)
|
[_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1)
|
||||||
|
|
||||||
if (largestScale)
|
if (largestScale)
|
||||||
[largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)]
|
[largestScale, VariantUnitManager.getUnitName(largestScale, @variant.variant_unit)]
|
||||||
else
|
else
|
||||||
[scales[0], VariantUnitManager.getUnitName(scales[0], product.variant_unit)]
|
[scales[0], VariantUnitManager.getUnitName(scales[0], @variant.variant_unit)]
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
|
|||||||
deferred.reject(response)
|
deferred.reject(response)
|
||||||
deferred.promise
|
deferred.promise
|
||||||
|
|
||||||
saveChanges: (form) ->
|
saveChanges: (form, params = {}) ->
|
||||||
changed = {}
|
changed = {}
|
||||||
for id, orderCycle of @byID when not @saved(orderCycle)
|
for id, orderCycle of @byID when not @saved(orderCycle)
|
||||||
changed[Object.keys(changed).length] = @changesFor(orderCycle)
|
changed[Object.keys(changed).length] = @changesFor(orderCycle)
|
||||||
if Object.keys(changed).length > 0
|
if Object.keys(changed).length > 0
|
||||||
StatusMessage.display('progress', "Saving...")
|
StatusMessage.display('progress', "Saving...")
|
||||||
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed } }, (data) =>
|
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed }, confirm: params['confirm'], trigger_action: params['trigger_action'] }, (data) =>
|
||||||
for orderCycle in data
|
for orderCycle in data
|
||||||
delete orderCycle.coordinator
|
delete orderCycle.coordinator
|
||||||
delete orderCycle.producers
|
delete orderCycle.producers
|
||||||
@@ -47,8 +47,10 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
|
|||||||
, (response) =>
|
, (response) =>
|
||||||
if response.data.errors?
|
if response.data.errors?
|
||||||
StatusMessage.display('failure', response.data.errors[0])
|
StatusMessage.display('failure', response.data.errors[0])
|
||||||
|
else if (response.data.trigger_action)
|
||||||
|
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
|
||||||
else
|
else
|
||||||
StatusMessage.display('failure', "Oh no! I was unable to save your changes.")
|
StatusMessage.display('failure', t('js.order_cycles.bulk_save_error'))
|
||||||
|
|
||||||
saved: (order_cycle) ->
|
saved: (order_cycle) ->
|
||||||
@diff(order_cycle).length == 0
|
@diff(order_cycle).length == 0
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
|
|||||||
for server_product in serverProducts
|
for server_product in serverProducts
|
||||||
product = @findProductInList(server_product.id, @products)
|
product = @findProductInList(server_product.id, @products)
|
||||||
product.variants = server_product.variants
|
product.variants = server_product.variants
|
||||||
@loadVariantUnitValues product
|
@loadVariantUnitValues product.variants
|
||||||
|
|
||||||
find: (id) ->
|
find: (id) ->
|
||||||
@findProductInList id, @products
|
@findProductInList id, @products
|
||||||
@@ -38,34 +38,32 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
|
|||||||
@products.splice(index + 1, 0, newProduct)
|
@products.splice(index + 1, 0, newProduct)
|
||||||
|
|
||||||
unpackProduct: (product) ->
|
unpackProduct: (product) ->
|
||||||
#$scope.matchProducer product
|
|
||||||
@loadVariantUnit product
|
@loadVariantUnit product
|
||||||
|
|
||||||
loadVariantUnit: (product) ->
|
loadVariantUnit: (product) ->
|
||||||
product.variant_unit_with_scale =
|
@loadVariantUnitValues product.variants if product.variants
|
||||||
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
|
|
||||||
"#{product.variant_unit}_#{product.variant_unit_scale}"
|
loadVariantUnitValues: (variants) ->
|
||||||
else if product.variant_unit
|
for variant in variants
|
||||||
product.variant_unit
|
@loadVariantUnitValue variant
|
||||||
|
|
||||||
|
loadVariantUnitValue: (variant) ->
|
||||||
|
variant.variant_unit_with_scale =
|
||||||
|
if variant.variant_unit && variant.variant_unit_scale && variant.variant_unit != 'items'
|
||||||
|
"#{variant.variant_unit}_#{variant.variant_unit_scale}"
|
||||||
|
else if variant.variant_unit
|
||||||
|
variant.variant_unit
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
|
|
||||||
@loadVariantUnitValues product if product.variants
|
unit_value = @variantUnitValue variant
|
||||||
@loadVariantUnitValue product, product.master if product.master
|
|
||||||
|
|
||||||
loadVariantUnitValues: (product) ->
|
|
||||||
for variant in product.variants
|
|
||||||
@loadVariantUnitValue product, variant
|
|
||||||
|
|
||||||
loadVariantUnitValue: (product, variant) ->
|
|
||||||
unit_value = @variantUnitValue product, variant
|
|
||||||
unit_value = if unit_value? then unit_value else ''
|
unit_value = if unit_value? then unit_value else ''
|
||||||
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
|
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
|
||||||
|
|
||||||
variantUnitValue: (product, variant) ->
|
variantUnitValue: (variant) ->
|
||||||
if variant.unit_value?
|
if variant.unit_value?
|
||||||
if product.variant_unit_scale
|
if variant.variant_unit_scale
|
||||||
variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale
|
variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale
|
||||||
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
|
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
|
||||||
else
|
else
|
||||||
variant.unit_value
|
variant.unit_value
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
root = exports ? this
|
|
||||||
|
|
||||||
root.taxon_tree_menu = (obj, context) ->
|
|
||||||
|
|
||||||
base_url = Spree.url(Spree.routes.taxonomy_taxons)
|
|
||||||
admin_base_url = Spree.url(Spree.routes.admin_taxonomy_taxons)
|
|
||||||
edit_url = Spree.url(Spree.routes.admin_taxonomy_taxons + '/' + obj.attr("id") + "/edit");
|
|
||||||
|
|
||||||
create:
|
|
||||||
label: "<i class='icon-plus'></i> " + Spree.translations.add,
|
|
||||||
action: (obj) -> context.create(obj)
|
|
||||||
rename:
|
|
||||||
label: "<i class='icon-pencil'></i> " + Spree.translations.rename,
|
|
||||||
action: (obj) -> context.rename(obj)
|
|
||||||
remove:
|
|
||||||
label: "<i class='icon-trash'></i> " + Spree.translations.remove,
|
|
||||||
action: (obj) -> context.remove(obj)
|
|
||||||
edit:
|
|
||||||
separator_before: true,
|
|
||||||
label: "<i class='icon-edit'></i> " + Spree.translations.edit,
|
|
||||||
action: (obj) -> window.location = edit_url.toString()
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
handle_ajax_error = (XMLHttpRequest, textStatus, errorThrown) ->
|
|
||||||
$.jstree.rollback(last_rollback)
|
|
||||||
$("#ajax_error").show().html("<strong>" + server_error + "</strong><br />" + taxonomy_tree_error)
|
|
||||||
|
|
||||||
handle_move = (e, data) ->
|
|
||||||
last_rollback = data.rlbk
|
|
||||||
position = data.rslt.cp
|
|
||||||
node = data.rslt.o
|
|
||||||
new_parent = data.rslt.np
|
|
||||||
|
|
||||||
url = new URL(Spree.routes.admin_taxonomy_taxons)
|
|
||||||
url.pathname = url.pathname + '/' + node.attr("id")
|
|
||||||
data = {
|
|
||||||
_method: "put",
|
|
||||||
"taxon[position]": position,
|
|
||||||
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
|
|
||||||
}
|
|
||||||
$.ajax
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
url: url.toString(),
|
|
||||||
data: data,
|
|
||||||
error: handle_ajax_error
|
|
||||||
|
|
||||||
true
|
|
||||||
|
|
||||||
handle_create = (e, data) ->
|
|
||||||
last_rollback = data.rlbk
|
|
||||||
node = data.rslt.obj
|
|
||||||
name = data.rslt.name
|
|
||||||
position = data.rslt.position
|
|
||||||
new_parent = data.rslt.parent
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"taxon[name]": name,
|
|
||||||
"taxon[position]": position
|
|
||||||
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
|
|
||||||
}
|
|
||||||
$.ajax
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
url: base_url.toString(),
|
|
||||||
data: data,
|
|
||||||
error: handle_ajax_error,
|
|
||||||
success: (data,result) ->
|
|
||||||
node.attr('id', data.id)
|
|
||||||
|
|
||||||
handle_rename = (e, data) ->
|
|
||||||
last_rollback = data.rlbk
|
|
||||||
node = data.rslt.obj
|
|
||||||
name = data.rslt.new_name
|
|
||||||
# change the name inside the main input field as well if taxon is the root one
|
|
||||||
document.getElementById("taxonomy_name").value = name if node.parents("[id]").attr("id") == "taxonomy_tree"
|
|
||||||
|
|
||||||
url = new URL(base_url)
|
|
||||||
url.pathname = url.pathname + '/' + node.attr("id")
|
|
||||||
|
|
||||||
$.ajax
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
url: url.toString(),
|
|
||||||
data: {_method: "put", "taxon[name]": name },
|
|
||||||
error: handle_ajax_error
|
|
||||||
|
|
||||||
handle_delete = (e, data) ->
|
|
||||||
last_rollback = data.rlbk
|
|
||||||
node = data.rslt.obj
|
|
||||||
delete_url = new URL(base_url)
|
|
||||||
delete_url.pathname = delete_url.pathname + '/' + node.attr("id")
|
|
||||||
if confirm(Spree.translations.are_you_sure_delete)
|
|
||||||
$.ajax
|
|
||||||
type: "POST",
|
|
||||||
dataType: "json",
|
|
||||||
url: delete_url.toString(),
|
|
||||||
data: {_method: "delete"},
|
|
||||||
error: handle_ajax_error
|
|
||||||
else
|
|
||||||
$.jstree.rollback(last_rollback)
|
|
||||||
last_rollback = null
|
|
||||||
|
|
||||||
root = exports ? this
|
|
||||||
root.setup_taxonomy_tree = (taxonomy_id) ->
|
|
||||||
if taxonomy_id != undefined
|
|
||||||
# this is defined within admin/taxonomies/edit
|
|
||||||
root.base_url = Spree.url(Spree.routes.taxonomy_taxons)
|
|
||||||
|
|
||||||
$.ajax
|
|
||||||
url: base_url.pathname.replace("/taxons", "/jstree"),
|
|
||||||
success: (taxonomy) ->
|
|
||||||
last_rollback = null
|
|
||||||
|
|
||||||
conf =
|
|
||||||
json_data:
|
|
||||||
data: taxonomy,
|
|
||||||
ajax:
|
|
||||||
url: (e) ->
|
|
||||||
base_url.pathname + '/' + e.attr('id') + '/jstree'
|
|
||||||
themes:
|
|
||||||
theme: "apple",
|
|
||||||
url: "/assets/jquery.jstree/themes/apple/style.css"
|
|
||||||
strings:
|
|
||||||
new_node: new_taxon,
|
|
||||||
loading: Spree.translations.loading + "..."
|
|
||||||
crrm:
|
|
||||||
move:
|
|
||||||
check_move: (m) ->
|
|
||||||
position = m.cp
|
|
||||||
node = m.o
|
|
||||||
new_parent = m.np
|
|
||||||
|
|
||||||
# no parent or cant drag and drop
|
|
||||||
if !new_parent || node.attr("rel") == "root"
|
|
||||||
return false
|
|
||||||
|
|
||||||
# can't drop before root
|
|
||||||
if new_parent.attr("id") == "taxonomy_tree" && position == 0
|
|
||||||
return false
|
|
||||||
|
|
||||||
true
|
|
||||||
contextmenu:
|
|
||||||
items: (obj) ->
|
|
||||||
taxon_tree_menu(obj, this)
|
|
||||||
plugins: ["themes", "json_data", "dnd", "crrm", "contextmenu"]
|
|
||||||
|
|
||||||
$("#taxonomy_tree").jstree(conf)
|
|
||||||
.bind("move_node.jstree", handle_move)
|
|
||||||
.bind("remove.jstree", handle_delete)
|
|
||||||
.bind("create.jstree", handle_create)
|
|
||||||
.bind("rename.jstree", handle_rename)
|
|
||||||
.bind "loaded.jstree", ->
|
|
||||||
$(this).jstree("core").toggle_node($('.jstree-icon').first())
|
|
||||||
|
|
||||||
$("#taxonomy_tree a").on "dblclick", (e) ->
|
|
||||||
$("#taxonomy_tree").jstree("rename", this)
|
|
||||||
|
|
||||||
# surpress form submit on enter/return
|
|
||||||
$(document).keypress (e) ->
|
|
||||||
if e.keyCode == 13
|
|
||||||
e.preventDefault()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
angular.module("admin.utils").directive "textangularLinksTargetBlank", () ->
|
|
||||||
restrict: 'CA'
|
|
||||||
link: (scope, element, attrs) ->
|
|
||||||
setTimeout ->
|
|
||||||
element.find(".ta-editor").scope().defaultTagAttributes.a.target = '_blank'
|
|
||||||
, 500
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
angular.module("admin.utils").directive "textangularStrip", () ->
|
|
||||||
restrict: 'CA'
|
|
||||||
link: (scope, element, attrs) ->
|
|
||||||
scope.stripFormatting = ($html) ->
|
|
||||||
element = document.createElement("div")
|
|
||||||
element.innerHTML = String($html)
|
|
||||||
allTags = element.getElementsByTagName("*")
|
|
||||||
for child in allTags
|
|
||||||
child.removeAttribute("style")
|
|
||||||
child.removeAttribute("class")
|
|
||||||
return element.innerHTML
|
|
||||||
@@ -9,6 +9,7 @@ angular.module("admin.utils")
|
|||||||
$window.onbeforeunload = @onBeforeUnloadHandler
|
$window.onbeforeunload = @onBeforeUnloadHandler
|
||||||
|
|
||||||
$rootScope.$on "$locationChangeStart", @locationChangeStartHandler
|
$rootScope.$on "$locationChangeStart", @locationChangeStartHandler
|
||||||
|
$window.onBeforeUnloadHandler = @onBeforeUnloadHandler
|
||||||
|
|
||||||
# Action for regular browser navigation.
|
# Action for regular browser navigation.
|
||||||
onBeforeUnloadHandler: ($event) =>
|
onBeforeUnloadHandler: ($event) =>
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ angular.module("admin.utils").factory "StatusMessage", ->
|
|||||||
|
|
||||||
statusMessage:
|
statusMessage:
|
||||||
text: ""
|
text: ""
|
||||||
style: {}
|
style: {},
|
||||||
|
type: null,
|
||||||
|
actionName: null
|
||||||
|
|
||||||
invalidMessage: ""
|
invalidMessage: ""
|
||||||
|
|
||||||
@@ -23,11 +25,15 @@ angular.module("admin.utils").factory "StatusMessage", ->
|
|||||||
active: ->
|
active: ->
|
||||||
@statusMessage.text != ''
|
@statusMessage.text != ''
|
||||||
|
|
||||||
display: (type, text) ->
|
display: (type, text, actionName = null) ->
|
||||||
@statusMessage.text = text
|
@statusMessage.text = text
|
||||||
|
@statusMessage.type = type
|
||||||
|
@statusMessage.actionName = actionName
|
||||||
@statusMessage.style = @types[type].style
|
@statusMessage.style = @types[type].style
|
||||||
null
|
null
|
||||||
|
|
||||||
clear: ->
|
clear: ->
|
||||||
@statusMessage.text = ''
|
@statusMessage.text = ''
|
||||||
@statusMessage.style = {}
|
@statusMessage.style = {}
|
||||||
|
@statusMessage.type = null
|
||||||
|
@statusMessage.actionName = null
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
|
||||||
%li{ "ng-class": "{active: selector.active}" }
|
%li{ "ng-class": "{active: selector.active}" }
|
||||||
%a{ tooltip: "{{selector.object.value}}", "tooltip-placement": "bottom", "ng-transclude": true, "ng-class": "{active: selector.active, 'has-tip': selector.object.value}" }
|
%a{ tooltip: "{{selector.object.value}}", "tooltip-placement": "bottom", "ng-transclude": true, "ng-class": "{active: selector.active, 'has-tip': selector.object.value}" }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#save-bar.animate-show{ "ng-show": 'dirty || persist || StatusMessage.active()' }
|
#save-bar.animate-show{ "ng-show": 'dirty || persist || StatusMessage.active()' }
|
||||||
.container
|
.container
|
||||||
.seven.columns.alpha
|
.seven.columns.alpha
|
||||||
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style' }
|
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style', data: { 'order-cycle-form-target': 'statusMessage' }, "ng-attr-data-type": "{{StatusMessage.statusMessage.type}}", "ng-attr-data-action-name": "{{StatusMessage.statusMessage.actionName}}" }
|
||||||
{{ StatusMessage.statusMessage.text || " " }}
|
{{ StatusMessage.statusMessage.text || " " }}
|
||||||
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
|
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
|
||||||
{{ StatusMessage.invalidMessage || " " }}
|
{{ StatusMessage.invalidMessage || " " }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
|
||||||
%ul
|
%ul
|
||||||
%active-selector{ "ng-repeat": "selector in allSelectors", "ng-show": "ifDefined(selector.fits, true)" }
|
%active-selector{ "ng-repeat": "selector in allSelectors", "ng-show": "ifDefined(selector.fits, true)" }
|
||||||
%span{"ng-bind" => "::selector.object.name"}
|
%span{"ng-bind" => "::selector.object.name"}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
- # NOTE: make sure that any changes in this template are reflected in app/views/admin/products_v3/product_preview.turbo_stream.haml
|
||||||
.row
|
.row
|
||||||
.columns.small-12.medium-6.large-6.product-header
|
.columns.small-12.medium-6.large-6.product-header
|
||||||
%h3{"ng-bind" => "::product.name"}
|
%h3{"ng-bind" => "::product.name"}
|
||||||
|
|||||||
1
app/assets/stylesheets/mail.scss
Normal file
1
app/assets/stylesheets/mail.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import './mail/all.scss';
|
||||||
3
app/assets/stylesheets/mail/all.scss
Normal file
3
app/assets/stylesheets/mail/all.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@import '../../../webpacker/css/admin/globals/palette.scss';
|
||||||
|
@import 'email';
|
||||||
|
@import 'payments_list';
|
||||||
11
app/components/admin_tooltip_component.rb
Normal file
11
app/components/admin_tooltip_component.rb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AdminTooltipComponent < ViewComponent::Base
|
||||||
|
def initialize(text:, link_text:, placement: "top", link: "", link_class: "")
|
||||||
|
@text = text
|
||||||
|
@link_text = link_text
|
||||||
|
@placement = placement
|
||||||
|
@link = link
|
||||||
|
@link_class = link_class
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
%div{"data-controller": "tooltip", "data-tooltip-placement-value": @placement }
|
||||||
|
%a{"data-tooltip-target": "element", href: @link, class: @link_class}
|
||||||
|
= @link_text
|
||||||
|
.tooltip-container
|
||||||
|
.tooltip{"data-tooltip-target": "tooltip"}
|
||||||
|
= sanitize @text
|
||||||
|
.arrow{"data-tooltip-target": "arrow"}
|
||||||
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ModalComponent < ViewComponent::Base
|
class ModalComponent < ViewComponent::Base
|
||||||
def initialize(id:, close_button: true, instant: false, modal_class: :small)
|
def initialize(id:, close_button: true, instant: false, modal_class: :small, **options)
|
||||||
@id = id
|
@id = id
|
||||||
@close_button = close_button
|
@close_button = close_button
|
||||||
@instant = instant
|
@instant = instant
|
||||||
@modal_class = modal_class
|
@modal_class = modal_class
|
||||||
|
@options = options
|
||||||
|
@data_controller = "modal #{@options.delete(:'data-controller')}".squish
|
||||||
|
@data_action =
|
||||||
|
"keyup@document->modal#closeIfEscapeKey #{@options.delete(:'data-action')}".squish
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
%div{ id: @id, "data-controller": "modal", "data-action": "keyup@document->modal#closeIfEscapeKey", "data-modal-instant-value": @instant }
|
%div{ id: @id, "data-controller": @data_controller, "data-action": @data_action, "data-modal-instant-value": @instant, **@options }
|
||||||
.reveal-modal-bg.fade{ "data-modal-target": "background", "data-action": "click->modal#close" }
|
.reveal-modal-bg.fade{ "data-modal-target": "background", "data-action": "click->modal#close" }
|
||||||
.reveal-modal.fade.modal-component{ "data-modal-target": "modal", class: @modal_class }
|
.reveal-modal.fade.modal-component{ "data-modal-target": "modal", class: @modal_class }
|
||||||
= content
|
= content
|
||||||
|
|||||||
@@ -24,6 +24,19 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-1 {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* prevent arrow on selected admin menu item appearing above modal */
|
/* prevent arrow on selected admin menu item appearing above modal */
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class SearchableDropdownComponent < ViewComponent::Base
|
|||||||
selected_option:,
|
selected_option:,
|
||||||
placeholder_value:,
|
placeholder_value:,
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
aria_label: ''
|
aria_label: '',
|
||||||
|
other_attrs: {}
|
||||||
)
|
)
|
||||||
@f = form
|
@f = form
|
||||||
@name = name
|
@name = name
|
||||||
@@ -20,11 +21,13 @@ class SearchableDropdownComponent < ViewComponent::Base
|
|||||||
@placeholder_value = placeholder_value
|
@placeholder_value = placeholder_value
|
||||||
@include_blank = include_blank
|
@include_blank = include_blank
|
||||||
@aria_label = aria_label
|
@aria_label = aria_label
|
||||||
|
@other_attrs = other_attrs
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank, :aria_label
|
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank,
|
||||||
|
:aria_label, :other_attrs
|
||||||
|
|
||||||
def classes
|
def classes
|
||||||
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"
|
"fullwidth #{remove_search_plugin? ? 'no-input' : ''}"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label
|
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label, **other_attrs
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export default class extends Controller {
|
|||||||
window.addEventListener("click", this.#hideIfClickedOutside);
|
window.addEventListener("click", this.#hideIfClickedOutside);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
window.removeEventListener("click", this.#hideIfClickedOutside);
|
||||||
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.contentTarget.classList.toggle("show");
|
this.contentTarget.classList.toggle("show");
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/controllers/admin/connected_app_settings_controller.rb
Normal file
22
app/controllers/admin/connected_app_settings_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ConnectedAppSettingsController < Spree::Admin::BaseController
|
||||||
|
def update
|
||||||
|
Spree::Config.set(connected_apps_enabled:)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html {
|
||||||
|
flash[:success] = t(:successfully_updated, resource: t('.resource'))
|
||||||
|
redirect_to main_app.edit_admin_connected_app_settings_path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def connected_apps_enabled
|
||||||
|
params.require(:preferences).require(:connected_apps_enabled).compact_blank.join(",")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,12 +5,7 @@ module Admin
|
|||||||
def create
|
def create
|
||||||
authorize! :admin, enterprise
|
authorize! :admin, enterprise
|
||||||
|
|
||||||
attributes = {}
|
connect
|
||||||
attributes[:type] = connected_app_params[:type] if connected_app_params[:type]
|
|
||||||
|
|
||||||
app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes)
|
|
||||||
app.connect(api_key: spree_current_user.spree_api_key,
|
|
||||||
channel: SessionChannel.for_request(request))
|
|
||||||
|
|
||||||
render_panel
|
render_panel
|
||||||
end
|
end
|
||||||
@@ -26,6 +21,47 @@ module Admin
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_connected_app
|
||||||
|
attributes = {}
|
||||||
|
attributes[:type] = connected_app_params[:type] if connected_app_params[:type]
|
||||||
|
|
||||||
|
@app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect
|
||||||
|
return connect_vine if connected_app_params[:type] == "ConnectedApps::Vine"
|
||||||
|
|
||||||
|
create_connected_app
|
||||||
|
@app.connect(api_key: spree_current_user.spree_api_key,
|
||||||
|
channel: SessionChannel.for_request(request))
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect_vine
|
||||||
|
if vine_params_empty?
|
||||||
|
return flash[:error] =
|
||||||
|
I18n.t("admin.enterprises.form.connected_apps.vine.api_parameters_empty")
|
||||||
|
end
|
||||||
|
|
||||||
|
create_connected_app
|
||||||
|
|
||||||
|
jwt_service = VineJwtService.new(secret: connected_app_params[:vine_secret])
|
||||||
|
vine_api = VineApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||||
|
jwt_generator: jwt_service)
|
||||||
|
|
||||||
|
if !@app.connect(api_key: connected_app_params[:vine_api_key],
|
||||||
|
secret: connected_app_params[:vine_secret], vine_api:)
|
||||||
|
error_message = "#{@app.errors.full_messages.to_sentence}. \
|
||||||
|
#{I18n.t('admin.enterprises.form.connected_apps.vine.api_parameters_error')}".squish
|
||||||
|
handle_error(error_message)
|
||||||
|
end
|
||||||
|
rescue Faraday::Error => e
|
||||||
|
log_and_notify_exception(e)
|
||||||
|
handle_error(I18n.t("admin.enterprises.form.connected_apps.vine.connection_error"))
|
||||||
|
rescue KeyError => e
|
||||||
|
log_and_notify_exception(e)
|
||||||
|
handle_error(I18n.t("admin.enterprises.form.connected_apps.vine.setup_error"))
|
||||||
|
end
|
||||||
|
|
||||||
def enterprise
|
def enterprise
|
||||||
@enterprise ||= Enterprise.find(params.require(:enterprise_id))
|
@enterprise ||= Enterprise.find(params.require(:enterprise_id))
|
||||||
end
|
end
|
||||||
@@ -34,8 +70,22 @@ module Admin
|
|||||||
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
|
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_error(message)
|
||||||
|
flash[:error] = message
|
||||||
|
@app.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_and_notify_exception(exception)
|
||||||
|
Rails.logger.error exception.inspect
|
||||||
|
Bugsnag.notify(exception)
|
||||||
|
end
|
||||||
|
|
||||||
|
def vine_params_empty?
|
||||||
|
connected_app_params[:vine_api_key].empty? || connected_app_params[:vine_secret].empty?
|
||||||
|
end
|
||||||
|
|
||||||
def connected_app_params
|
def connected_app_params
|
||||||
params.permit(:type)
|
params.permit(:type, :vine_api_key, :vine_secret)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,35 +26,29 @@ module Admin
|
|||||||
# * First step: import all products for given enterprise.
|
# * First step: import all products for given enterprise.
|
||||||
# * Second step: render table and let user decide which ones to import.
|
# * Second step: render table and let user decide which ones to import.
|
||||||
imported = graph.map do |subject|
|
imported = graph.map do |subject|
|
||||||
import_product(subject, enterprise)
|
next unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||||
|
|
||||||
|
existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId)
|
||||||
|
|
||||||
|
if existing_variant
|
||||||
|
SuppliedProductBuilder.update_product(subject, existing_variant)
|
||||||
|
else
|
||||||
|
SuppliedProductBuilder.store_product(subject, enterprise)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@count = imported.compact.count
|
@count = imported.compact.count
|
||||||
|
rescue Faraday::Error,
|
||||||
|
Addressable::URI::InvalidURIError,
|
||||||
|
ActionController::ParameterMissing => e
|
||||||
|
flash[:error] = e.message
|
||||||
|
redirect_to admin_product_import_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_catalog(url)
|
def fetch_catalog(url)
|
||||||
if url =~ /food-data-collaboration/
|
DfcRequest.new(spree_current_user).call(url)
|
||||||
fdc_json = FdcRequest.new(spree_current_user).call(url)
|
|
||||||
fdc_message = JSON.parse(fdc_json)
|
|
||||||
fdc_message["products"]
|
|
||||||
else
|
|
||||||
DfcRequest.new(spree_current_user).call(url)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
|
|
||||||
def import_product(subject, enterprise)
|
|
||||||
return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
|
||||||
|
|
||||||
variant = SuppliedProductBuilder.import_variant(subject, enterprise)
|
|
||||||
product = variant.product
|
|
||||||
|
|
||||||
product.save! if product.new_record?
|
|
||||||
variant.save! if variant.new_record?
|
|
||||||
|
|
||||||
variant
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class OrderCyclesController < Admin::ResourceController
|
class OrderCyclesController < Admin::ResourceController
|
||||||
|
class DateTimeChangeError < StandardError; end
|
||||||
|
|
||||||
include ::OrderCyclesHelper
|
include ::OrderCyclesHelper
|
||||||
include PaperTrailLogging
|
include PaperTrailLogging
|
||||||
|
|
||||||
@@ -62,9 +64,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@order_cycle_form = OrderCycles::FormService.new(@order_cycle, order_cycle_params,
|
@order_cycle_form = set_order_cycle_form
|
||||||
spree_current_user)
|
|
||||||
|
|
||||||
if @order_cycle_form.save
|
if @order_cycle_form.save
|
||||||
update_nil_subscription_line_items_price_estimate(@order_cycle)
|
update_nil_subscription_line_items_price_estimate(@order_cycle)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
@@ -77,6 +77,9 @@ module Admin
|
|||||||
elsif request.format.json?
|
elsif request.format.json?
|
||||||
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: @order_cycle.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
rescue DateTimeChangeError
|
||||||
|
render json: { trigger_action: params[:trigger_action] },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_update
|
def bulk_update
|
||||||
@@ -90,6 +93,9 @@ module Admin
|
|||||||
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
|
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
|
||||||
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
rescue DateTimeChangeError
|
||||||
|
render json: { trigger_action: params[:trigger_action] },
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_update_nil_subscription_line_items_price_estimate
|
def bulk_update_nil_subscription_line_items_price_estimate
|
||||||
@@ -235,7 +241,7 @@ module Admin
|
|||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
yield
|
yield
|
||||||
rescue ActiveRecord::InvalidForeignKey
|
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::DeleteRestrictionError
|
||||||
redirect_to main_app.admin_order_cycles_url
|
redirect_to main_app.admin_order_cycles_url
|
||||||
flash[:error] = I18n.t('admin.order_cycles.destroy_errors.orders_present')
|
flash[:error] = I18n.t('admin.order_cycles.destroy_errors.orders_present')
|
||||||
end
|
end
|
||||||
@@ -270,7 +276,10 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def order_cycle_set
|
def order_cycle_set
|
||||||
@order_cycle_set ||= Sets::OrderCycleSet.new(@order_cycles, order_cycle_bulk_params)
|
@order_cycle_set ||= Sets::OrderCycleSet.new(
|
||||||
|
@order_cycles, { **order_cycle_bulk_params,
|
||||||
|
confirm_datetime_change: params[:confirm], error_class: DateTimeChangeError }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_order_cycle_set_params
|
def require_order_cycle_set_params
|
||||||
@@ -294,5 +303,14 @@ module Admin
|
|||||||
collection_attributes: [:id] + PermittedAttributes::OrderCycle.basic_attributes
|
collection_attributes: [:id] + PermittedAttributes::OrderCycle.basic_attributes
|
||||||
).to_h.with_indifferent_access
|
).to_h.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_order_cycle_form
|
||||||
|
OrderCycles::FormService.new(
|
||||||
|
@order_cycle, order_cycle_params.merge(
|
||||||
|
{ confirm_datetime_change: params[:order_cycle][:confirm],
|
||||||
|
error_class: DateTimeChangeError }
|
||||||
|
), spree_current_user
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ module Admin
|
|||||||
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user,
|
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user,
|
||||||
params[:settings])
|
params[:settings])
|
||||||
@original_filename = params[:file].try(:original_filename)
|
@original_filename = params[:file].try(:original_filename)
|
||||||
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_fields
|
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_variant_fields
|
||||||
|
|
||||||
return if contains_errors? @importer
|
return if contains_errors? @importer
|
||||||
|
|
||||||
@ams_data = ams_data
|
@ams_data = ams_data
|
||||||
|
|||||||
22
app/controllers/admin/product_preview_controller.rb
Normal file
22
app/controllers/admin/product_preview_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ProductPreviewController < Spree::Admin::BaseController
|
||||||
|
def show
|
||||||
|
@product = Spree::Product.find(params[:id])
|
||||||
|
authorize! :show, @product
|
||||||
|
|
||||||
|
respond_with do |format|
|
||||||
|
format.turbo_stream {
|
||||||
|
render "admin/products_v3/product_preview", status: :ok
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def model_class
|
||||||
|
Spree::Product
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,6 +11,8 @@ module Admin
|
|||||||
def index
|
def index
|
||||||
fetch_products
|
fetch_products
|
||||||
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
|
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
|
||||||
|
|
||||||
|
session[:products_return_to_url] = request.url
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_update
|
def bulk_update
|
||||||
@@ -38,6 +40,8 @@ module Admin
|
|||||||
{ id: params[:id] }
|
{ id: params[:id] }
|
||||||
).find_product
|
).find_product
|
||||||
|
|
||||||
|
authorize! :delete, @record
|
||||||
|
|
||||||
@record.destroyed_by = spree_current_user
|
@record.destroyed_by = spree_current_user
|
||||||
status = :ok
|
status = :ok
|
||||||
|
|
||||||
@@ -72,6 +76,8 @@ module Admin
|
|||||||
|
|
||||||
def clone
|
def clone
|
||||||
@product = Spree::Product.find(params[:id])
|
@product = Spree::Product.find(params[:id])
|
||||||
|
authorize! :clone, @product
|
||||||
|
|
||||||
status = :ok
|
status = :ok
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@@ -82,8 +88,8 @@ module Admin
|
|||||||
@producer_options = producers
|
@producer_options = producers
|
||||||
@category_options = categories
|
@category_options = categories
|
||||||
@tax_category_options = tax_category_options
|
@tax_category_options = tax_category_options
|
||||||
rescue ActiveRecord::ActiveRecordError => _e
|
rescue ActiveRecord::ActiveRecordError => e
|
||||||
flash.now[:error] = t('.error')
|
flash.now[:error] = clone_error_message(e)
|
||||||
status = :unprocessable_entity
|
status = :unprocessable_entity
|
||||||
@product_index = "-1" # Create a unique enough index
|
@product_index = "-1" # Create a unique enough index
|
||||||
end
|
end
|
||||||
@@ -207,6 +213,15 @@ module Admin
|
|||||||
params.permit(products: ::PermittedAttributes::Product.attributes)
|
params.permit(products: ::PermittedAttributes::Product.attributes)
|
||||||
.to_h.with_indifferent_access
|
.to_h.with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clone_error_message(error)
|
||||||
|
case error
|
||||||
|
when ActiveRecord::RecordInvalid
|
||||||
|
error.record.errors.full_messages.to_sentence
|
||||||
|
else
|
||||||
|
t('.error')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/ClassLength
|
# rubocop:enable Metrics/ClassLength
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module Admin
|
|||||||
include ReportsActions
|
include ReportsActions
|
||||||
helper ReportsHelper
|
helper ReportsHelper
|
||||||
|
|
||||||
before_action :authorize_report, only: [:show]
|
before_action :authorize_report, only: [:show, :create]
|
||||||
|
|
||||||
# Define model class for Can? permissions
|
# Define model class for Can? permissions
|
||||||
def model_class
|
def model_class
|
||||||
@@ -20,14 +20,17 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@report = report_class.new(spree_current_user, params, render: render_data?)
|
@report = report_class.new(spree_current_user, params, render: false)
|
||||||
@rendering_options = rendering_options # also stores user preferences
|
@rendering_options = rendering_options
|
||||||
|
|
||||||
if render_data?
|
show_report
|
||||||
render_in_background
|
end
|
||||||
else
|
|
||||||
show_report
|
def create
|
||||||
end
|
@report = report_class.new(spree_current_user, params, render: true)
|
||||||
|
update_rendering_options
|
||||||
|
|
||||||
|
render_in_background
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -54,31 +57,15 @@ module Admin
|
|||||||
@variant_serialized = Api::Admin::VariantSerializer.new(variant)
|
@variant_serialized = Api::Admin::VariantSerializer.new(variant)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_data?
|
|
||||||
request.post?
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_in_background
|
def render_in_background
|
||||||
cable_ready[ScopedChannel.for_id(params[:uuid])]
|
@blob = ReportBlob.create_for_upload_later!(report_filename)
|
||||||
.inner_html(
|
|
||||||
selector: "#report-go",
|
|
||||||
html: helpers.button(t(:go), "report__submit-btn", "submit", disabled: true)
|
|
||||||
).inner_html(
|
|
||||||
selector: "#report-table",
|
|
||||||
html: render_to_string(partial: "admin/reports/loading")
|
|
||||||
).scroll_into_view(
|
|
||||||
selector: "#report-table",
|
|
||||||
block: "start"
|
|
||||||
).broadcast
|
|
||||||
|
|
||||||
ReportJob.perform_later(
|
ReportJob.perform_later(
|
||||||
report_class:, user: spree_current_user, params:,
|
report_class:, user: spree_current_user, params:,
|
||||||
format: report_format,
|
format: report_format,
|
||||||
filename: report_filename,
|
blob: @blob,
|
||||||
channel: ScopedChannel.for_id(params[:uuid]),
|
channel: ScopedChannel.for_id(params[:uuid]),
|
||||||
)
|
)
|
||||||
|
|
||||||
head :no_content
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module Api
|
|||||||
authorize! :create, Spree::Product
|
authorize! :create, Spree::Product
|
||||||
@product = Spree::Product.new(product_params)
|
@product = Spree::Product.new(product_params)
|
||||||
|
|
||||||
if @product.save
|
if @product.save(context: :create_and_create_standard_variant)
|
||||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: :created
|
render json: @product, serializer: Api::Admin::ProductSerializer, status: :created
|
||||||
else
|
else
|
||||||
invalid_resource!(@product)
|
invalid_resource!(@product)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
module V0
|
|
||||||
class TaxonomiesController < Api::V0::BaseController
|
|
||||||
respond_to :json
|
|
||||||
|
|
||||||
skip_authorization_check only: :jstree
|
|
||||||
|
|
||||||
def jstree
|
|
||||||
@taxonomy = Spree::Taxonomy.find(params[:id])
|
|
||||||
render json: @taxonomy.root, serializer: Api::TaxonJstreeSerializer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -5,12 +5,10 @@ module Api
|
|||||||
class TaxonsController < Api::V0::BaseController
|
class TaxonsController < Api::V0::BaseController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
skip_authorization_check only: [:index, :show, :jstree]
|
skip_authorization_check only: [:index, :show]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@taxons = if taxonomy
|
@taxons = if params[:ids]
|
||||||
taxonomy.root.children
|
|
||||||
elsif params[:ids]
|
|
||||||
Spree::Taxon.where(id: raw_params[:ids].split(","))
|
Spree::Taxon.where(id: raw_params[:ids].split(","))
|
||||||
else
|
else
|
||||||
Spree::Taxon.ransack(raw_params[:q]).result
|
Spree::Taxon.ransack(raw_params[:q]).result
|
||||||
@@ -18,23 +16,9 @@ module Api
|
|||||||
render json: @taxons, each_serializer: Api::TaxonSerializer
|
render json: @taxons, each_serializer: Api::TaxonSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def jstree
|
|
||||||
@taxon = taxon
|
|
||||||
render json: @taxon.children, each_serializer: Api::TaxonJstreeSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize! :create, Spree::Taxon
|
authorize! :create, Spree::Taxon
|
||||||
@taxon = Spree::Taxon.new(taxon_params)
|
@taxon = Spree::Taxon.new(taxon_params)
|
||||||
@taxon.taxonomy_id = params[:taxonomy_id]
|
|
||||||
taxonomy = Spree::Taxonomy.find_by(id: params[:taxonomy_id])
|
|
||||||
|
|
||||||
if taxonomy.nil?
|
|
||||||
@taxon.errors.add(:taxonomy_id, I18n.t(:invalid_taxonomy_id, scope: 'spree.api'))
|
|
||||||
invalid_resource!(@taxon) && return
|
|
||||||
end
|
|
||||||
|
|
||||||
@taxon.parent_id = taxonomy.root.id unless params.dig(:taxon, :parent_id)
|
|
||||||
|
|
||||||
if @taxon.save
|
if @taxon.save
|
||||||
render json: @taxon, serializer: Api::TaxonSerializer, status: :created
|
render json: @taxon, serializer: Api::TaxonSerializer, status: :created
|
||||||
@@ -60,20 +44,14 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def taxonomy
|
|
||||||
return if params[:taxonomy_id].blank?
|
|
||||||
|
|
||||||
@taxonomy ||= Spree::Taxonomy.find(params[:taxonomy_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def taxon
|
def taxon
|
||||||
@taxon ||= taxonomy.taxons.find(params[:id])
|
@taxon = Spree::Taxon.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def taxon_params
|
def taxon_params
|
||||||
return if params[:taxon].blank?
|
return if params[:taxon].blank?
|
||||||
|
|
||||||
params.require(:taxon).permit([:name, :parent_id, :position])
|
params.require(:taxon).permit([:name, :position])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class CartController < BaseController
|
|||||||
order.cap_quantity_at_stock!
|
order.cap_quantity_at_stock!
|
||||||
order.recreate_all_fees!
|
order.recreate_all_fees!
|
||||||
|
|
||||||
|
StockSyncJob.sync_linked_catalogs(order)
|
||||||
|
|
||||||
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
|
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
|
||||||
else
|
else
|
||||||
render json: { error: cart_service.errors.full_messages.join(",") },
|
render json: { error: cart_service.errors.full_messages.join(",") },
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ module CheckoutCallbacks
|
|||||||
# Otherwise we fail on duplicate indexes or end up with negative stock.
|
# Otherwise we fail on duplicate indexes or end up with negative stock.
|
||||||
prepend_around_action CurrentOrderLocker, only: [:edit, :update]
|
prepend_around_action CurrentOrderLocker, only: [:edit, :update]
|
||||||
|
|
||||||
|
# We want to download the latest stock data before anything else happens.
|
||||||
|
# We don't want it to be in the same database transaction as the order
|
||||||
|
# locking because this action locks a different set of variants and it
|
||||||
|
# could cause race conditions.
|
||||||
|
prepend_around_action :sync_stock, only: :update
|
||||||
|
|
||||||
prepend_before_action :check_hub_ready_for_checkout
|
prepend_before_action :check_hub_ready_for_checkout
|
||||||
prepend_before_action :check_order_cycle_expiry
|
prepend_before_action :check_order_cycle_expiry
|
||||||
prepend_before_action :require_order_cycle
|
prepend_before_action :require_order_cycle
|
||||||
@@ -25,6 +31,14 @@ module CheckoutCallbacks
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def sync_stock
|
||||||
|
if current_order&.state == "confirmation"
|
||||||
|
StockSyncJob.sync_linked_catalogs_now(current_order)
|
||||||
|
end
|
||||||
|
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
|
||||||
def load_order
|
def load_order
|
||||||
@order = current_order
|
@order = current_order
|
||||||
@order.manual_shipping_selection = true
|
@order.manual_shipping_selection = true
|
||||||
@@ -63,12 +77,6 @@ module CheckoutCallbacks
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_order_line_items?
|
|
||||||
@order.insufficient_stock_lines.empty? &&
|
|
||||||
OrderCycles::DistributedVariantsService.new(@order.order_cycle, @order.distributor).
|
|
||||||
distributes_order_variants?(@order)
|
|
||||||
end
|
|
||||||
|
|
||||||
def ensure_order_not_completed
|
def ensure_order_not_completed
|
||||||
redirect_to main_app.cart_path if @order.completed?
|
redirect_to main_app.cart_path if @order.completed?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ module OrderCompletion
|
|||||||
|
|
||||||
def order_invalid!
|
def order_invalid!
|
||||||
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
|
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
|
||||||
payload.add_metadata :order, @order
|
payload.add_metadata :order, :order, @order
|
||||||
end
|
end
|
||||||
|
|
||||||
flash[:error] = t('checkout.order_not_loaded')
|
flash[:error] = t('checkout.order_not_loaded')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module OrderStockCheck
|
|||||||
return unless current_order_cycle&.closed?
|
return unless current_order_cycle&.closed?
|
||||||
|
|
||||||
Bugsnag.notify("Notice: order cycle closed during checkout completion") do |payload|
|
Bugsnag.notify("Notice: order cycle closed during checkout completion") do |payload|
|
||||||
payload.add_metadata :order, current_order
|
payload.add_metadata :order, :order, current_order
|
||||||
end
|
end
|
||||||
current_order.empty!
|
current_order.empty!
|
||||||
current_order.set_order_cycle! nil
|
current_order.set_order_cycle! nil
|
||||||
|
|||||||
@@ -88,14 +88,10 @@ module ReportsActions
|
|||||||
display_header_row: false
|
display_header_row: false
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
update_rendering_options
|
|
||||||
@rendering_options
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_rendering_options
|
def update_rendering_options
|
||||||
return unless request.post?
|
rendering_options.update(
|
||||||
|
|
||||||
@rendering_options.update(
|
|
||||||
options: {
|
options: {
|
||||||
fields_to_show: params[:fields_to_show],
|
fields_to_show: params[:fields_to_show],
|
||||||
display_summary_row: params[:display_summary_row].present?,
|
display_summary_row: params[:display_summary_row].present?,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ErrorsController < ApplicationController
|
|||||||
Bugsnag.notify("404") do |event|
|
Bugsnag.notify("404") do |event|
|
||||||
event.severity = "info"
|
event.severity = "info"
|
||||||
|
|
||||||
event.add_metadata(:request, request.env)
|
event.add_metadata(:request, :env, request.env)
|
||||||
end
|
end
|
||||||
render status: :not_found, formats: :html
|
render status: :not_found, formats: :html
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,15 +19,18 @@ module Spree
|
|||||||
|
|
||||||
before_action :authorize_admin
|
before_action :authorize_admin
|
||||||
before_action :set_locale
|
before_action :set_locale
|
||||||
before_action :warn_invalid_order_cycles, if: :html_request?
|
before_action :warn_invalid_order_cycles, if: :page_load_request?
|
||||||
|
|
||||||
# Warn the user when they have an active order cycle with hubs that are not ready
|
# Warn the user when they have an active order cycle with hubs that are not ready
|
||||||
# for checkout (ie. does not have valid shipping and payment methods).
|
# for checkout (ie. does not have valid shipping and payment methods).
|
||||||
def warn_invalid_order_cycles
|
def warn_invalid_order_cycles
|
||||||
return if flash[:notice].present?
|
return if flash[:notice].present? || session[:displayed_order_cycle_warning]
|
||||||
|
|
||||||
warning = OrderCycles::WarningService.new(spree_current_user).call
|
warning = OrderCycles::WarningService.new(spree_current_user).call
|
||||||
flash[:notice] = warning if warning.present?
|
return if warning.blank?
|
||||||
|
|
||||||
|
flash.now[:notice] = warning
|
||||||
|
session[:displayed_order_cycle_warning] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@@ -81,6 +84,12 @@ module Spree
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def page_load_request?
|
||||||
|
return false if request.format.include?('turbo')
|
||||||
|
|
||||||
|
html_request?
|
||||||
|
end
|
||||||
|
|
||||||
def html_request?
|
def html_request?
|
||||||
request.format.html?
|
request.format.html?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module Spree
|
|||||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||||
include OrderCyclesHelper
|
include OrderCyclesHelper
|
||||||
include EnterprisesHelper
|
include EnterprisesHelper
|
||||||
|
helper ::Admin::ProductsHelper
|
||||||
|
|
||||||
before_action :load_data
|
before_action :load_data
|
||||||
before_action :load_producers, only: [:index, :new]
|
before_action :load_producers, only: [:index, :new]
|
||||||
@@ -38,7 +39,7 @@ module Spree
|
|||||||
def create
|
def create
|
||||||
delete_stock_params_and_set_after do
|
delete_stock_params_and_set_after do
|
||||||
@object.attributes = permitted_resource_params
|
@object.attributes = permitted_resource_params
|
||||||
if @object.save
|
if @object.save(context: :create_and_create_standard_variant)
|
||||||
flash[:success] = flash_message_for(@object, :successfully_created)
|
flash[:success] = flash_message_for(@object, :successfully_created)
|
||||||
redirect_after_save
|
redirect_after_save
|
||||||
else
|
else
|
||||||
@@ -213,10 +214,10 @@ module Spree
|
|||||||
|
|
||||||
def notify_bugsnag(error, product, variant)
|
def notify_bugsnag(error, product, variant)
|
||||||
Bugsnag.notify(error) do |report|
|
Bugsnag.notify(error) do |report|
|
||||||
report.add_metadata(:product, product.attributes)
|
report.add_metadata(:product,
|
||||||
report.add_metadata(:product_error, product.errors.first) unless product.valid?
|
{ product: product.attributes, variant: variant.attributes })
|
||||||
report.add_metadata(:variant, variant.attributes)
|
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
|
||||||
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
|
report.add_metadata(:product, :variant_error, variant.errors.first) unless variant.valid?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ module Spree
|
|||||||
|
|
||||||
def permitted_resource_params
|
def permitted_resource_params
|
||||||
params.require(:return_authorization).
|
params.require(:return_authorization).
|
||||||
permit(:amount, :reason, :stock_location_id)
|
permit(:amount, :reason)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Spree
|
|
||||||
module Admin
|
|
||||||
class TaxonomiesController < ::Admin::ResourceController
|
|
||||||
respond_to :json, only: [:get_children]
|
|
||||||
|
|
||||||
def get_children
|
|
||||||
@taxons = Taxon.find(params[:parent_id]).children
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def location_after_save
|
|
||||||
if @taxonomy.created_at == @taxonomy.updated_at
|
|
||||||
spree.edit_admin_taxonomy_url(@taxonomy)
|
|
||||||
else
|
|
||||||
spree.admin_taxonomies_url
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_resource_params
|
|
||||||
params.require(:taxonomy).permit(:name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,122 +2,70 @@
|
|||||||
|
|
||||||
module Spree
|
module Spree
|
||||||
module Admin
|
module Admin
|
||||||
class TaxonsController < Spree::Admin::BaseController
|
class TaxonsController < ::Admin::ResourceController
|
||||||
respond_to :html, :json, :js
|
before_action :set_taxon, except: %i[create index new]
|
||||||
|
|
||||||
def edit
|
def index
|
||||||
@taxonomy = Taxonomy.find(params[:taxonomy_id])
|
@taxons = Taxon.order(:name)
|
||||||
@taxon = @taxonomy.taxons.find(params[:id])
|
|
||||||
@permalink_part = @taxon.permalink.split("/").last
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@taxon = Taxon.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@taxonomy = Taxonomy.find(params[:taxonomy_id])
|
@taxon = Spree::Taxon.new(taxon_params)
|
||||||
@taxon = @taxonomy.taxons.build(params[:taxon])
|
|
||||||
if @taxon.save
|
if @taxon.save
|
||||||
respond_with(@taxon) do |format|
|
flash[:success] = flash_message_for(@taxon, :successfully_created)
|
||||||
format.json { render json: @taxon.to_json }
|
redirect_to edit_admin_taxon_path(@taxon.id)
|
||||||
end
|
|
||||||
else
|
else
|
||||||
flash[:error] = Spree.t('errors.messages.could_not_create_taxon')
|
render :new, status: :unprocessable_entity
|
||||||
respond_with(@taxon) do |format|
|
|
||||||
format.html do
|
|
||||||
if redirect_to @taxonomy
|
|
||||||
spree.edit_admin_taxonomy_url(@taxonomy)
|
|
||||||
else
|
|
||||||
spree.admin_taxonomies_url
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@taxonomy = Taxonomy.find(params[:taxonomy_id])
|
|
||||||
@taxon = @taxonomy.taxons.find(params[:id])
|
|
||||||
parent_id = params[:taxon][:parent_id]
|
|
||||||
new_position = params[:taxon][:position]
|
|
||||||
|
|
||||||
if parent_id || new_position # taxon is being moved
|
|
||||||
new_parent = parent_id.nil? ? @taxon.parent : Taxon.find(parent_id.to_i)
|
|
||||||
new_position = new_position.nil? ? -1 : new_position.to_i
|
|
||||||
|
|
||||||
# Bellow is a very complicated way of finding where in nested set we
|
|
||||||
# should actually move the taxon to achieve sane results,
|
|
||||||
# JS is giving us the desired position, which was awesome for previous setup,
|
|
||||||
# but now it's quite complicated to find where we should put it as we have
|
|
||||||
# to differenciate between moving to the same branch, up down and into
|
|
||||||
# first position.
|
|
||||||
new_siblings = new_parent.children
|
|
||||||
if new_position <= 0 && new_siblings.empty?
|
|
||||||
@taxon.move_to_child_of(new_parent)
|
|
||||||
elsif new_parent.id != @taxon.parent_id
|
|
||||||
if new_position.zero?
|
|
||||||
@taxon.move_to_left_of(new_siblings.first)
|
|
||||||
else
|
|
||||||
@taxon.move_to_right_of(new_siblings[new_position - 1])
|
|
||||||
end
|
|
||||||
elsif new_position < new_siblings.index(@taxon)
|
|
||||||
@taxon.move_to_left_of(new_siblings[new_position]) # we move up
|
|
||||||
else
|
|
||||||
@taxon.move_to_right_of(new_siblings[new_position - 1]) # we move down
|
|
||||||
end
|
|
||||||
# Reset legacy position, if any extensions still rely on it
|
|
||||||
new_parent.children.reload.each do |t|
|
|
||||||
t.update_columns(
|
|
||||||
position: t.position,
|
|
||||||
updated_at: Time.zone.now
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
if parent_id
|
|
||||||
@taxon.reload
|
|
||||||
@taxon.set_permalink
|
|
||||||
@taxon.save!
|
|
||||||
@update_children = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if params.key? "permalink_part"
|
|
||||||
parent_permalink = @taxon.permalink.split("/")[0...-1].join("/")
|
|
||||||
parent_permalink += "/" if parent_permalink.present?
|
|
||||||
params[:taxon][:permalink] = parent_permalink + params[:permalink_part]
|
|
||||||
end
|
|
||||||
# check if we need to rename child taxons if parent name or permalink changes
|
|
||||||
if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink
|
|
||||||
@update_children = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if @taxon.update(taxon_params)
|
if @taxon.update(taxon_params)
|
||||||
flash[:success] = flash_message_for(@taxon, :successfully_updated)
|
flash[:success] = flash_message_for(@taxon, :successfully_updated)
|
||||||
end
|
redirect_to edit_admin_taxon_path(@taxon.id)
|
||||||
|
else
|
||||||
# rename child taxons
|
render :edit, status: :unprocessable_entity
|
||||||
if @update_children
|
|
||||||
@taxon.descendants.each do |taxon|
|
|
||||||
taxon.reload
|
|
||||||
taxon.set_permalink
|
|
||||||
taxon.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
respond_with(@taxon) do |format|
|
|
||||||
format.html { redirect_to spree.edit_admin_taxonomy_url(@taxonomy) }
|
|
||||||
format.json { render json: @taxon.to_json }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@taxon = Taxon.find(params[:id])
|
status = if @taxon.destroy
|
||||||
@taxon.destroy
|
flash_message = t('.delete_taxon.success')
|
||||||
respond_with(@taxon) { |format| format.json { render json: '' } }
|
status = :ok
|
||||||
|
else
|
||||||
|
flash_message = t('.delete_taxon.error')
|
||||||
|
status = :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html {
|
||||||
|
flash[:success] = flash_message if status == :ok
|
||||||
|
flash[:error] = flash_message if status == :unprocessable_entity
|
||||||
|
redirect_to admin_taxons_path
|
||||||
|
}
|
||||||
|
format.turbo_stream {
|
||||||
|
flash[:success] = flash_message if status == :ok
|
||||||
|
flash[:error] = flash_message if status == :unprocessable_entity
|
||||||
|
render :destroy_taxon, status:
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_taxon
|
||||||
|
@taxon = Taxon.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
def taxon_params
|
def taxon_params
|
||||||
params.require(:taxon).permit(
|
params.require(:taxon).permit(
|
||||||
:name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id,
|
:name, :position, :icon, :description, :permalink,
|
||||||
:meta_description, :meta_keywords, :meta_title, :dfc_id
|
:meta_description, :meta_keywords, :meta_title, :dfc_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ require 'open_food_network/scope_variants_for_search'
|
|||||||
module Spree
|
module Spree
|
||||||
module Admin
|
module Admin
|
||||||
class VariantsController < ::Admin::ResourceController
|
class VariantsController < ::Admin::ResourceController
|
||||||
|
helper ::Admin::ProductsHelper
|
||||||
|
|
||||||
belongs_to 'spree/product'
|
belongs_to 'spree/product'
|
||||||
|
|
||||||
before_action :load_data, only: [:new, :edit]
|
before_action :load_data, only: [:new, :edit]
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ module Admin
|
|||||||
show_enterprise_fees = can?(:manage_enterprise_fees,
|
show_enterprise_fees = can?(:manage_enterprise_fees,
|
||||||
enterprise) && (is_shop || enterprise.is_primary_producer)
|
enterprise) && (is_shop || enterprise.is_primary_producer)
|
||||||
show_connected_apps = can?(:manage_connected_apps, enterprise) &&
|
show_connected_apps = can?(:manage_connected_apps, enterprise) &&
|
||||||
feature?(:connected_apps, spree_current_user, enterprise)
|
feature?(:connected_apps, spree_current_user, enterprise) &&
|
||||||
|
Spree::Config.connected_apps_enabled.present?
|
||||||
|
|
||||||
build_enterprise_side_menu_items(
|
build_enterprise_side_menu_items(
|
||||||
is_shop:,
|
is_shop:,
|
||||||
@@ -38,6 +39,11 @@ module Admin
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def connected_apps_enabled
|
||||||
|
connected_apps_enabled = Spree::Config.connected_apps_enabled&.split(',') || []
|
||||||
|
ConnectedApp::TYPES & connected_apps_enabled
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_enterprise_side_menu_items(
|
def build_enterprise_side_menu_items(
|
||||||
|
|||||||
@@ -10,22 +10,39 @@ module Admin
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def prepare_new_variant(product)
|
def prepare_new_variant(product, producer_options)
|
||||||
product.variants.build do |variant|
|
# e.g producer_options = [['producer name', id]]
|
||||||
variant.unit_value = 1.0 * (product.variant_unit_scale || 1)
|
product.variants.build do |new_variant|
|
||||||
variant.unit_presentation = VariantUnits::OptionValueNamer.new(variant).name
|
new_variant.supplier_id = producer_options.first.second if producer_options.one?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unit_value_with_description(variant)
|
def unit_value_with_description(variant)
|
||||||
scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1)
|
return variant.unit_description.to_s if variant.unit_value.nil?
|
||||||
|
|
||||||
|
scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1)
|
||||||
precised_unit_value = number_with_precision(
|
precised_unit_value = number_with_precision(
|
||||||
scaled_unit_value,
|
scaled_unit_value,
|
||||||
precision: nil,
|
precision: nil,
|
||||||
strip_insignificant_zeros: true
|
strip_insignificant_zeros: true,
|
||||||
|
significant: false,
|
||||||
)
|
)
|
||||||
|
|
||||||
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
|
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def products_return_to_url(url_filters)
|
||||||
|
if feature?(:admin_style_v3, spree_current_user)
|
||||||
|
return session[:products_return_to_url] || admin_products_url
|
||||||
|
end
|
||||||
|
|
||||||
|
"#{admin_products_path}#{url_filters.empty? ? '' : "#?#{url_filters.to_query}"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# if user hasn't saved any preferences on products page and there's only one producer;
|
||||||
|
# we need to hide producer column
|
||||||
|
def hide_producer_column?(producer_options)
|
||||||
|
spree_current_user.column_preferences.bulk_edit_product.empty? && producer_options.one?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,15 +61,6 @@ module ApplicationHelper
|
|||||||
classes << shopfront_layout
|
classes << shopfront_layout
|
||||||
end
|
end
|
||||||
|
|
||||||
def pdf_stylesheet_pack_tag(source)
|
|
||||||
if running_in_development?
|
|
||||||
options = { media: "all", host: "#{Webpacker.dev_server.host}:#{Webpacker.dev_server.port}" }
|
|
||||||
stylesheet_pack_tag(source, **options)
|
|
||||||
else
|
|
||||||
wicked_pdf_stylesheet_pack_tag(source)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_with_locale(key = nil, options = {}, &block)
|
def cache_with_locale(key = nil, options = {}, &block)
|
||||||
cache(cache_key_with_locale(key, I18n.locale), options) do
|
cache(cache_key_with_locale(key, I18n.locale), options) do
|
||||||
yield(block)
|
yield(block)
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ module InjectionHelper
|
|||||||
include OrderCyclesHelper
|
include OrderCyclesHelper
|
||||||
|
|
||||||
def inject_enterprises(enterprises = nil)
|
def inject_enterprises(enterprises = nil)
|
||||||
|
enterprises ||= default_enterprise_query
|
||||||
|
|
||||||
inject_json_array(
|
inject_json_array(
|
||||||
"enterprises",
|
"enterprises",
|
||||||
enterprises || default_enterprise_query,
|
enterprises,
|
||||||
Api::EnterpriseSerializer,
|
Api::EnterpriseSerializer,
|
||||||
enterprise_injection_data,
|
enterprise_injection_data(enterprises.map(&:id)),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,15 +59,16 @@ module InjectionHelper
|
|||||||
inject_json_array "enterprises",
|
inject_json_array "enterprises",
|
||||||
enterprises_and_relatives,
|
enterprises_and_relatives,
|
||||||
Api::EnterpriseSerializer,
|
Api::EnterpriseSerializer,
|
||||||
enterprise_injection_data
|
enterprise_injection_data(enterprises_and_relatives.map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def inject_group_enterprises(group)
|
def inject_group_enterprises(group)
|
||||||
|
enterprises = group.enterprises.activated.visible.all
|
||||||
inject_json_array(
|
inject_json_array(
|
||||||
"enterprises",
|
"enterprises",
|
||||||
group.enterprises.activated.visible.all,
|
enterprises,
|
||||||
Api::EnterpriseSerializer,
|
Api::EnterpriseSerializer,
|
||||||
enterprise_injection_data,
|
enterprise_injection_data(enterprises.map(&:id)),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -73,7 +76,7 @@ module InjectionHelper
|
|||||||
inject_json "currentHub",
|
inject_json "currentHub",
|
||||||
current_distributor,
|
current_distributor,
|
||||||
Api::EnterpriseSerializer,
|
Api::EnterpriseSerializer,
|
||||||
enterprise_injection_data
|
enterprise_injection_data(current_distributor ? [current_distributor.id] : nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def inject_current_order
|
def inject_current_order
|
||||||
@@ -153,7 +156,9 @@ module InjectionHelper
|
|||||||
Enterprise.activated.includes(address: [:state, :country]).all
|
Enterprise.activated.includes(address: [:state, :country]).all
|
||||||
end
|
end
|
||||||
|
|
||||||
def enterprise_injection_data
|
def enterprise_injection_data(enterprise_ids)
|
||||||
@enterprise_injection_data ||= { data: OpenFoodNetwork::EnterpriseInjectionData.new }
|
{
|
||||||
|
data: OpenFoodNetwork::EnterpriseInjectionData.new(enterprise_ids)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -58,4 +58,9 @@ module ReportsHelper
|
|||||||
.where(order_id: orders.map(&:id))
|
.where(order_id: orders.map(&:id))
|
||||||
.pluck(:originator_id)
|
.pluck(:originator_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def datepicker_time(datetime)
|
||||||
|
datetime = Time.zone.parse(datetime) if datetime.is_a? String
|
||||||
|
datetime.strftime('%Y-%m-%d %H:%M')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Spree
|
|
||||||
module Admin
|
|
||||||
module TaxonsHelper
|
|
||||||
def taxon_path(taxon)
|
|
||||||
taxon.ancestors.reverse.collect(&:name).join( " >> ")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
98
app/jobs/amend_backorder_job.rb
Normal file
98
app/jobs/amend_backorder_job.rb
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# When orders are cancelled, we need to amend
|
||||||
|
# an existing backorder as well.
|
||||||
|
# We're not dealing with line item changes just yet.
|
||||||
|
class AmendBackorderJob < ApplicationJob
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
def perform(order)
|
||||||
|
OrderLocker.lock_order_and_variants(order) do
|
||||||
|
amend_backorder(order)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The following is a mix of the BackorderJob and the CompleteBackorderJob.
|
||||||
|
# TODO: Move the common code into a re-usable service class.
|
||||||
|
def amend_backorder(order)
|
||||||
|
order_cycle = order.order_cycle
|
||||||
|
distributor = order.distributor
|
||||||
|
user = distributor.owner
|
||||||
|
items = backorderable_items(order)
|
||||||
|
|
||||||
|
return if items.empty?
|
||||||
|
|
||||||
|
# We are assuming that all variants are linked to the same wholesale
|
||||||
|
# shop and its catalog:
|
||||||
|
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||||
|
urls = FdcUrlBuilder.new(reference_link)
|
||||||
|
orderer = FdcBackorderer.new(user, urls)
|
||||||
|
|
||||||
|
backorder = orderer.find_open_order(order)
|
||||||
|
|
||||||
|
variants = order_cycle.variants_distributed_by(distributor)
|
||||||
|
adjust_quantities(order_cycle, user, backorder, urls, variants)
|
||||||
|
|
||||||
|
FdcBackorderer.new(user, urls).send_order(backorder)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if we have enough stock to reduce the backorder.
|
||||||
|
#
|
||||||
|
# Our local stock can increase when users cancel their orders.
|
||||||
|
# But stock levels could also have been adjusted manually. So we review all
|
||||||
|
# quantities before finalising the order.
|
||||||
|
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
broker = FdcOfferBroker.new(user, urls)
|
||||||
|
|
||||||
|
order.lines.each do |line|
|
||||||
|
line.quantity = line.quantity.to_i
|
||||||
|
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||||
|
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||||
|
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||||
|
|
||||||
|
# Assumption: If a transformation is present then we only sell the retail
|
||||||
|
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||||
|
# for now.
|
||||||
|
next if linked_variant.nil?
|
||||||
|
|
||||||
|
# Find all line items for this order cycle
|
||||||
|
# Update quantity accordingly
|
||||||
|
if linked_variant.on_demand
|
||||||
|
release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
else
|
||||||
|
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean up empty lines:
|
||||||
|
order.lines.reject! { |line| line.quantity.zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
# We look at all linked variants.
|
||||||
|
def backorderable_items(order)
|
||||||
|
order.line_items.select do |item|
|
||||||
|
# TODO: scope variants to hub.
|
||||||
|
# We are only supporting producer stock at the moment.
|
||||||
|
item.variant.semantic_links.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||||
|
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||||
|
|
||||||
|
# But maybe we didn't actually order that much:
|
||||||
|
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||||
|
line.quantity -= deductable_quantity
|
||||||
|
|
||||||
|
retail_stock_changes = deductable_quantity * transformation.factor
|
||||||
|
linked_variant.on_hand -= retail_stock_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||||
|
orders = order_cycle.orders.invoiceable
|
||||||
|
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||||
|
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||||
|
line.quantity = wholesale_quantity
|
||||||
|
end
|
||||||
|
end
|
||||||
139
app/jobs/backorder_job.rb
Normal file
139
app/jobs/backorder_job.rb
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BackorderJob < ApplicationJob
|
||||||
|
# In the current FDC project, one shop wants to review and adjust orders
|
||||||
|
# before finalising. They also run a market stall and need to adjust stock
|
||||||
|
# levels after the market. This should be done within four hours.
|
||||||
|
SALE_SESSION_DELAYS = {
|
||||||
|
# https://openfoodnetwork.org.uk/handleyfarm/shop
|
||||||
|
"https://openfoodnetwork.org.uk/api/dfc/enterprises/203468" => 4.hours,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
queue_as :default
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
def self.check_stock(order)
|
||||||
|
links = SemanticLink.where(subject: order.variants)
|
||||||
|
|
||||||
|
perform_later(order) if links.exists?
|
||||||
|
rescue StandardError => e
|
||||||
|
# Errors here shouldn't affect the checkout. So let's report them
|
||||||
|
# separately:
|
||||||
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(order)
|
||||||
|
OrderLocker.lock_order_and_variants(order) do
|
||||||
|
place_backorder(order)
|
||||||
|
end
|
||||||
|
rescue StandardError
|
||||||
|
# If the backordering fails, we need to tell the shop owner because they
|
||||||
|
# need to organgise more stock.
|
||||||
|
BackorderMailer.backorder_failed(order).deliver_later
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
def place_backorder(order)
|
||||||
|
user = order.distributor.owner
|
||||||
|
items = backorderable_items(order)
|
||||||
|
|
||||||
|
return if items.empty?
|
||||||
|
|
||||||
|
# We are assuming that all variants are linked to the same wholesale
|
||||||
|
# shop and its catalog:
|
||||||
|
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||||
|
urls = FdcUrlBuilder.new(reference_link)
|
||||||
|
orderer = FdcBackorderer.new(user, urls)
|
||||||
|
|
||||||
|
backorder = orderer.find_or_build_order(order)
|
||||||
|
broker = load_broker(order.distributor.owner, urls)
|
||||||
|
ordered_quantities = {}
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
retail_quantity = add_item_to_backorder(item, broker, backorder, orderer)
|
||||||
|
ordered_quantities[item] = retail_quantity
|
||||||
|
end
|
||||||
|
|
||||||
|
place_order(user, order, orderer, backorder)
|
||||||
|
|
||||||
|
items.each do |item|
|
||||||
|
variant = item.variant
|
||||||
|
variant.on_hand += ordered_quantities[item] if variant.on_demand
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# We look at linked variants which are either stock controlled or
|
||||||
|
# are on demand with negative stock.
|
||||||
|
def backorderable_items(order)
|
||||||
|
order.line_items.select do |item|
|
||||||
|
# TODO: scope variants to hub.
|
||||||
|
# We are only supporting producer stock at the moment.
|
||||||
|
variant = item.variant
|
||||||
|
variant.semantic_links.present? &&
|
||||||
|
(variant.on_demand == false || variant.on_hand&.negative?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_item_to_backorder(line_item, broker, backorder, orderer)
|
||||||
|
variant = line_item.variant
|
||||||
|
needed_quantity = needed_quantity(line_item)
|
||||||
|
solution = broker.best_offer(variant.semantic_links[0].semantic_id)
|
||||||
|
|
||||||
|
# The number of wholesale packs we need to order to fulfill the
|
||||||
|
# needed quantity.
|
||||||
|
# For example, we order 2 packs of 12 cans if we need 15 cans.
|
||||||
|
wholesale_quantity = (needed_quantity.to_f / solution.factor).ceil
|
||||||
|
|
||||||
|
# The number of individual retail items we get with the wholesale order.
|
||||||
|
# For example, if we order 2 packs of 12 cans, we will get 24 cans
|
||||||
|
# and we'll account for that in our stock levels.
|
||||||
|
retail_quantity = wholesale_quantity * solution.factor
|
||||||
|
|
||||||
|
line = orderer.find_or_build_order_line(backorder, solution.offer)
|
||||||
|
line.quantity = line.quantity.to_i + wholesale_quantity
|
||||||
|
|
||||||
|
retail_quantity
|
||||||
|
end
|
||||||
|
|
||||||
|
# We have two different types of stock management:
|
||||||
|
#
|
||||||
|
# 1. on demand
|
||||||
|
# We don't restrict sales but account for the quantity sold in our local
|
||||||
|
# stock level. If it goes negative, we need more stock and trigger a
|
||||||
|
# backorder.
|
||||||
|
# 2. limited stock
|
||||||
|
# The local stock level is a copy from another catalog. We limit sales
|
||||||
|
# according to that stock level. Every order reduces the local stock level
|
||||||
|
# and needs to trigger a backorder of the same quantity to stay in sync.
|
||||||
|
def needed_quantity(line_item)
|
||||||
|
variant = line_item.variant
|
||||||
|
|
||||||
|
if variant.on_demand
|
||||||
|
-1 * variant.on_hand # on_hand is negative and we need to replenish it.
|
||||||
|
else
|
||||||
|
line_item.quantity # We need to order exactly what's we sold.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_broker(user, urls)
|
||||||
|
FdcOfferBroker.new(user, urls)
|
||||||
|
end
|
||||||
|
|
||||||
|
def place_order(user, order, orderer, backorder)
|
||||||
|
placed_order = orderer.send_order(backorder)
|
||||||
|
|
||||||
|
return unless orderer.new?(backorder)
|
||||||
|
|
||||||
|
delay = SALE_SESSION_DELAYS.fetch(backorder.client, 1.minute)
|
||||||
|
wait_until = order.order_cycle.orders_close_at + delay
|
||||||
|
CompleteBackorderJob.set(wait_until:)
|
||||||
|
.perform_later(
|
||||||
|
user, order.distributor, order.order_cycle, placed_order.semanticId
|
||||||
|
)
|
||||||
|
|
||||||
|
order.exchange.semantic_links.create!(semantic_id: placed_order.semanticId)
|
||||||
|
end
|
||||||
|
end
|
||||||
90
app/jobs/complete_backorder_job.rb
Normal file
90
app/jobs/complete_backorder_job.rb
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# After an order cycle closed, we need to finalise open draft orders placed
|
||||||
|
# to replenish stock.
|
||||||
|
class CompleteBackorderJob < ApplicationJob
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
# Required parameters:
|
||||||
|
#
|
||||||
|
# * user: to authenticate DFC requests
|
||||||
|
# * distributor: to reconile with its catalog
|
||||||
|
# * order_cycle: to scope the catalog when looking up variants
|
||||||
|
# Multiple variants can be linked to the same remote product.
|
||||||
|
# To reduce ambiguity, we'll reconcile only with products
|
||||||
|
# from the given distributor in a given order cycle for which
|
||||||
|
# the remote backorder was placed.
|
||||||
|
# * order_id: the remote semantic id of a draft order
|
||||||
|
# Having the id makes sure that we don't accidentally finalise
|
||||||
|
# someone else's order.
|
||||||
|
def perform(user, distributor, order_cycle, order_id)
|
||||||
|
order = FdcBackorderer.new(user, nil).find_order(order_id)
|
||||||
|
|
||||||
|
return if order&.lines.blank?
|
||||||
|
|
||||||
|
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
||||||
|
|
||||||
|
variants = order_cycle.variants_distributed_by(distributor)
|
||||||
|
adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
|
||||||
|
FdcBackorderer.new(user, urls).complete_order(order)
|
||||||
|
|
||||||
|
exchange = order_cycle.exchanges.outgoing.find_by(receiver: distributor)
|
||||||
|
exchange.semantic_links.find_by(semantic_id: order_id)&.destroy!
|
||||||
|
rescue StandardError
|
||||||
|
BackorderMailer.backorder_incomplete(user, distributor, order_cycle, order_id).deliver_later
|
||||||
|
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if we have enough stock to reduce the backorder.
|
||||||
|
#
|
||||||
|
# Our local stock can increase when users cancel their orders.
|
||||||
|
# But stock levels could also have been adjusted manually. So we review all
|
||||||
|
# quantities before finalising the order.
|
||||||
|
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
broker = FdcOfferBroker.new(user, urls)
|
||||||
|
|
||||||
|
order.lines.each do |line|
|
||||||
|
line.quantity = line.quantity.to_i
|
||||||
|
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||||
|
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||||
|
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||||
|
|
||||||
|
# Assumption: If a transformation is present then we only sell the retail
|
||||||
|
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||||
|
# for now.
|
||||||
|
next if linked_variant.nil?
|
||||||
|
|
||||||
|
# Find all line items for this order cycle
|
||||||
|
# Update quantity accordingly
|
||||||
|
if linked_variant.on_demand
|
||||||
|
release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
else
|
||||||
|
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean up empty lines:
|
||||||
|
order.lines.reject! { |line| line.quantity.zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||||
|
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||||
|
|
||||||
|
# But maybe we didn't actually order that much:
|
||||||
|
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||||
|
line.quantity -= deductable_quantity
|
||||||
|
|
||||||
|
retail_stock_changes = deductable_quantity * transformation.factor
|
||||||
|
linked_variant.on_hand -= retail_stock_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||||
|
orders = order_cycle.orders.invoiceable
|
||||||
|
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||||
|
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||||
|
line.quantity = wholesale_quantity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,12 +9,12 @@ class ReportJob < ApplicationJob
|
|||||||
|
|
||||||
NOTIFICATION_TIME = 5.seconds
|
NOTIFICATION_TIME = 5.seconds
|
||||||
|
|
||||||
def perform(report_class:, user:, params:, format:, filename:, channel: nil)
|
def perform(report_class:, user:, params:, format:, blob:, channel: nil)
|
||||||
start_time = Time.zone.now
|
start_time = Time.zone.now
|
||||||
|
|
||||||
report = report_class.new(user, params, render: true)
|
report = report_class.new(user, params, render: true)
|
||||||
result = report.render_as(format)
|
result = report.render_as(format)
|
||||||
blob = ReportBlob.create!(filename, result)
|
blob.store(result)
|
||||||
|
|
||||||
execution_time = Time.zone.now - start_time
|
execution_time = Time.zone.now - start_time
|
||||||
|
|
||||||
|
|||||||
79
app/jobs/stock_sync_job.rb
Normal file
79
app/jobs/stock_sync_job.rb
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class StockSyncJob < ApplicationJob
|
||||||
|
# No retry but stay as failed job:
|
||||||
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
|
# We synchronise stock of stock-controlled variants linked to a remote
|
||||||
|
# product. These variants are rare though and we check first before we
|
||||||
|
# enqueue a new job. That should save some time loading the order with
|
||||||
|
# all the stock data to make this decision.
|
||||||
|
def self.sync_linked_catalogs(order)
|
||||||
|
user = order.distributor.owner
|
||||||
|
catalog_ids(order).each do |catalog_id|
|
||||||
|
perform_later(user, catalog_id)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
|
# separately:
|
||||||
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sync_linked_catalogs_now(order)
|
||||||
|
user = order.distributor.owner
|
||||||
|
catalog_ids(order).each do |catalog_id|
|
||||||
|
perform_now(user, catalog_id)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
|
# separately:
|
||||||
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.catalog_ids(order)
|
||||||
|
stock_controlled_variants = order.variants.reject(&:on_demand)
|
||||||
|
links = SemanticLink.where(subject: stock_controlled_variants)
|
||||||
|
semantic_ids = links.pluck(:semantic_id)
|
||||||
|
semantic_ids.map do |product_id|
|
||||||
|
FdcUrlBuilder.new(product_id).catalog_url
|
||||||
|
end.uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(user, catalog_id)
|
||||||
|
products = load_products(user, catalog_id)
|
||||||
|
products_by_id = products.index_by(&:semanticId)
|
||||||
|
product_ids = products_by_id.keys
|
||||||
|
variants = linked_variants(user.enterprises, product_ids)
|
||||||
|
|
||||||
|
# Avoid race condition between checkout and stock sync.
|
||||||
|
Spree::Variant.transaction do
|
||||||
|
variants.order(:id).lock.each do |variant|
|
||||||
|
next if variant.on_demand
|
||||||
|
|
||||||
|
product = products_by_id[variant.semantic_links[0].semantic_id]
|
||||||
|
catalog_item = product&.catalogItems&.first
|
||||||
|
CatalogItemBuilder.apply_stock(catalog_item, variant)
|
||||||
|
variant.stock_items[0].save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_products(user, catalog_id)
|
||||||
|
json_catalog = DfcRequest.new(user).call(catalog_id)
|
||||||
|
graph = DfcIo.import(json_catalog)
|
||||||
|
|
||||||
|
graph.select do |subject|
|
||||||
|
subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def linked_variants(enterprises, product_ids)
|
||||||
|
Spree::Variant.where(supplier: enterprises)
|
||||||
|
.includes(:semantic_links).references(:semantic_links)
|
||||||
|
.where(semantic_links: { semantic_id: product_ids })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -56,7 +56,7 @@ class SubscriptionConfirmJob < ApplicationJob
|
|||||||
send_failed_payment_email(order)
|
send_failed_payment_email(order)
|
||||||
else
|
else
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata :order, order
|
payload.add_metadata :order, :order, order
|
||||||
end
|
end
|
||||||
send_failed_payment_email(order, e.message)
|
send_failed_payment_email(order, e.message)
|
||||||
end
|
end
|
||||||
@@ -109,8 +109,7 @@ class SubscriptionConfirmJob < ApplicationJob
|
|||||||
SubscriptionMailer.failed_payment_email(order).deliver_now
|
SubscriptionMailer.failed_payment_email(order).deliver_now
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata :order, order
|
payload.add_metadata :subscription_data, { order:, error_message: }
|
||||||
payload.add_metadata :error_message, error_message
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
24
app/mailers/backorder_mailer.rb
Normal file
24
app/mailers/backorder_mailer.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class BackorderMailer < ApplicationMailer
|
||||||
|
include I18nHelper
|
||||||
|
|
||||||
|
def backorder_failed(order)
|
||||||
|
@order = order
|
||||||
|
@linked_variants = order.variants
|
||||||
|
|
||||||
|
I18n.with_locale valid_locale(order.distributor.owner) do
|
||||||
|
mail(to: order.distributor.owner.email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def backorder_incomplete(user, distributor, order_cycle, order_id)
|
||||||
|
@distributor = distributor
|
||||||
|
@order_cycle = order_cycle
|
||||||
|
@order_id = order_id
|
||||||
|
|
||||||
|
I18n.with_locale valid_locale(user) do
|
||||||
|
mail(to: user.email)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -15,10 +15,11 @@ class ColumnPreference < ApplicationRecord
|
|||||||
validates :column_name, presence: true, inclusion: { in: proc { |p|
|
validates :column_name, presence: true, inclusion: { in: proc { |p|
|
||||||
valid_columns_for(p.action_name)
|
valid_columns_for(p.action_name)
|
||||||
} }
|
} }
|
||||||
|
scope :bulk_edit_product, -> { where(action_name: 'products_v3_index') }
|
||||||
|
|
||||||
def self.for(user, action_name)
|
def self.for(user, action_name)
|
||||||
stored_preferences = where(user_id: user.id, action_name:)
|
stored_preferences = where(user_id: user.id, action_name:)
|
||||||
default_preferences = __send__("#{action_name}_columns")
|
default_preferences = get_default_preferences(action_name, user)
|
||||||
filter(default_preferences, user, action_name)
|
filter(default_preferences, user, action_name)
|
||||||
default_preferences.each_with_object([]) do |(column_name, default_attributes), preferences|
|
default_preferences.each_with_object([]) do |(column_name, default_attributes), preferences|
|
||||||
stored_preference = stored_preferences.find_by(column_name:)
|
stored_preference = stored_preferences.find_by(column_name:)
|
||||||
@@ -36,7 +37,7 @@ class ColumnPreference < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.valid_columns_for(action_name)
|
def self.valid_columns_for(action_name)
|
||||||
__send__("#{action_name}_columns").keys.map(&:to_s)
|
get_default_preferences(action_name, Spree::User.new).keys.map(&:to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.known_actions
|
def self.known_actions
|
||||||
@@ -52,4 +53,13 @@ class ColumnPreference < ApplicationRecord
|
|||||||
|
|
||||||
default_preferences.delete(:schedules)
|
default_preferences.delete(:schedules)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.get_default_preferences(action_name, user)
|
||||||
|
case action_name
|
||||||
|
when 'products_v3_index'
|
||||||
|
products_v3_index_columns(user)
|
||||||
|
else
|
||||||
|
__send__("#{action_name}_columns")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
# `count_on_hand` can either be: nil or a number
|
# `count_on_hand` can either be: nil or a number
|
||||||
#
|
#
|
||||||
# This means that a variant override can be in six different stock states
|
# This means that a variant override can be in six different stock states
|
||||||
# but only three of them are valid.
|
# but only four of them are valid.
|
||||||
#
|
#
|
||||||
# | on_demand | count_on_hand | stock_overridden? | use_producer_stock_settings? | valid? |
|
# | on_demand | count_on_hand | stock_overridden? | use_producer_stock_settings? | valid? |
|
||||||
# |-----------|---------------|-------------------|------------------------------|--------|
|
# |-----------|---------------|-------------------|------------------------------|--------|
|
||||||
# | 1 | nil | false | false | true |
|
# | 1 | nil | true | false | true |
|
||||||
# | 0 | x | true | false | true |
|
# | 0 | x | true | false | true |
|
||||||
# | nil | nil | false | true | true |
|
# | nil | nil | false | true | true |
|
||||||
# | 1 | x | ? | ? | false |
|
# | 1 | x | true | false | true |
|
||||||
# | 0 | nil | ? | ? | false |
|
# | 0 | nil | ? | ? | false |
|
||||||
# | nil | x | ? | ? | false |
|
# | nil | x | ? | ? | false |
|
||||||
#
|
#
|
||||||
@@ -27,7 +27,6 @@ module StockSettingsOverrideValidation
|
|||||||
|
|
||||||
def require_compatible_on_demand_and_count_on_hand
|
def require_compatible_on_demand_and_count_on_hand
|
||||||
disallow_count_on_hand_if_using_producer_stock_settings
|
disallow_count_on_hand_if_using_producer_stock_settings
|
||||||
disallow_count_on_hand_if_on_demand
|
|
||||||
require_count_on_hand_if_limited_stock
|
require_count_on_hand_if_limited_stock
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,14 +38,6 @@ module StockSettingsOverrideValidation
|
|||||||
errors.add(:count_on_hand, error_message)
|
errors.add(:count_on_hand, error_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def disallow_count_on_hand_if_on_demand
|
|
||||||
return unless on_demand? && count_on_hand.present?
|
|
||||||
|
|
||||||
error_message = I18n.t("count_on_hand.on_demand_but_count_on_hand_set",
|
|
||||||
scope: i18n_scope_for_stock_settings_override_validation_error)
|
|
||||||
errors.add(:count_on_hand, error_message)
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_count_on_hand_if_limited_stock
|
def require_count_on_hand_if_limited_stock
|
||||||
return unless on_demand == false && count_on_hand.blank?
|
return unless on_demand == false && count_on_hand.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -43,19 +43,9 @@ module VariantStock
|
|||||||
def on_demand
|
def on_demand
|
||||||
# A variant that has not been saved yet or has been soft-deleted doesn't have a stock item
|
# A variant that has not been saved yet or has been soft-deleted doesn't have a stock item
|
||||||
# This provides a default value for variant.on_demand
|
# This provides a default value for variant.on_demand
|
||||||
# using Spree::StockLocation.backorderable_default
|
return false if new_record? || deleted?
|
||||||
return Spree::StockLocation.first.backorderable_default if new_record? || deleted?
|
|
||||||
|
|
||||||
# This can be removed unless we have seen this error in Bugsnag recently
|
stock_item&.backorderable?
|
||||||
if stock_item.nil?
|
|
||||||
Bugsnag.notify(
|
|
||||||
RuntimeError.new("Variant #stock_item called, but the stock_item does not exist!"),
|
|
||||||
object: as_json
|
|
||||||
)
|
|
||||||
return Spree::StockLocation.first.backorderable_default
|
|
||||||
end
|
|
||||||
|
|
||||||
stock_item.backorderable?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets whether the variant can be ordered on demand or not. Note that
|
# Sets whether the variant can be ordered on demand or not. Note that
|
||||||
@@ -96,7 +86,7 @@ module VariantStock
|
|||||||
# Here we depend only on variant.total_on_hand and variant.on_demand.
|
# Here we depend only on variant.total_on_hand and variant.on_demand.
|
||||||
# This way, variant_overrides only need to override variant.total_on_hand and variant.on_demand.
|
# This way, variant_overrides only need to override variant.total_on_hand and variant.on_demand.
|
||||||
def fill_status(quantity)
|
def fill_status(quantity)
|
||||||
on_hand = if total_on_hand >= quantity || on_demand
|
on_hand = if total_on_hand.to_i >= quantity || on_demand
|
||||||
quantity
|
quantity
|
||||||
else
|
else
|
||||||
[0, total_on_hand].max
|
[0, total_on_hand].max
|
||||||
@@ -112,8 +102,7 @@ module VariantStock
|
|||||||
#
|
#
|
||||||
# This enables us to override this behaviour for variant overrides
|
# This enables us to override this behaviour for variant overrides
|
||||||
def move(quantity, originator = nil)
|
def move(quantity, originator = nil)
|
||||||
# Don't change variant stock if variant is on_demand or has been deleted
|
return if deleted_at
|
||||||
return if on_demand || deleted_at
|
|
||||||
|
|
||||||
raise_error_if_no_stock_item_available
|
raise_error_if_no_stock_item_available
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
#
|
#
|
||||||
# Here we store keys and links to access the app.
|
# Here we store keys and links to access the app.
|
||||||
class ConnectedApp < ApplicationRecord
|
class ConnectedApp < ApplicationRecord
|
||||||
|
TYPES = ['discover_regen', 'affiliate_sales_data', 'vine'].freeze
|
||||||
|
|
||||||
belongs_to :enterprise
|
belongs_to :enterprise
|
||||||
after_destroy :disconnect
|
after_destroy :disconnect
|
||||||
|
|
||||||
scope :discover_regen, -> { where(type: "ConnectedApp") }
|
scope :discover_regen, -> { where(type: "ConnectedApp") }
|
||||||
scope :affiliate_sales_data, -> { where(type: "ConnectedApps::AffiliateSalesData") }
|
scope :affiliate_sales_data, -> { where(type: "ConnectedApps::AffiliateSalesData") }
|
||||||
|
scope :vine, -> { where(type: "ConnectedApps::Vine") }
|
||||||
|
|
||||||
scope :connecting, -> { where(data: nil) }
|
scope :connecting, -> { where(data: nil) }
|
||||||
scope :ready, -> { where.not(data: nil) }
|
scope :ready, -> { where.not(data: nil) }
|
||||||
|
|||||||
21
app/models/connected_apps/vine.rb
Normal file
21
app/models/connected_apps/vine.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# An enterprise can opt-in to use VINE API to manage vouchers
|
||||||
|
#
|
||||||
|
module ConnectedApps
|
||||||
|
class Vine < ConnectedApp
|
||||||
|
encrypts :data
|
||||||
|
|
||||||
|
def connect(api_key:, secret:, vine_api:, **_opts)
|
||||||
|
response = vine_api.my_team
|
||||||
|
|
||||||
|
return update data: { api_key:, secret: } if response.success?
|
||||||
|
|
||||||
|
errors.add(:base, I18n.t("activerecord.errors.models.connected_apps.vine.api_request_error"))
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect; end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,11 +5,6 @@ class CustomTab < ApplicationRecord
|
|||||||
|
|
||||||
validates :title, presence: true, length: { maximum: 20 }
|
validates :title, presence: true, length: { maximum: 20 }
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
|
||||||
def content
|
|
||||||
HtmlSanitizer.sanitize(super)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
# Remove any unsupported HTML.
|
||||||
def content=(html)
|
def content=(html)
|
||||||
super(HtmlSanitizer.sanitize(html))
|
super(HtmlSanitizer.sanitize(html))
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
class Customer < ApplicationRecord
|
class Customer < ApplicationRecord
|
||||||
include SetUnusedAddressFields
|
include SetUnusedAddressFields
|
||||||
|
|
||||||
|
self.ignored_columns += ['name']
|
||||||
|
|
||||||
acts_as_taggable
|
acts_as_taggable
|
||||||
|
|
||||||
searchable_attributes :first_name, :last_name, :email, :code
|
searchable_attributes :first_name, :last_name, :email, :code
|
||||||
|
|||||||
@@ -247,14 +247,17 @@ class Enterprise < ApplicationRecord
|
|||||||
count(distinct: true)
|
count(distinct: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
def long_description=(html)
|
||||||
def long_description
|
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
|
||||||
HtmlSanitizer.sanitize(super)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
def preferred_shopfront_message=(html)
|
||||||
def long_description=(html)
|
self.prefers_shopfront_message = HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)
|
||||||
super(HtmlSanitizer.sanitize(html))
|
end
|
||||||
|
|
||||||
|
def preferred_shopfront_closed_message=(html)
|
||||||
|
self.prefers_shopfront_closed_message =
|
||||||
|
HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact
|
def contact
|
||||||
@@ -477,7 +480,7 @@ class Enterprise < ApplicationRecord
|
|||||||
return unless image.variable?
|
return unless image.variable?
|
||||||
|
|
||||||
image_variant_url_for(image.variant(name))
|
image_variant_url_for(image.variant(name))
|
||||||
rescue ActiveStorage::Error, MiniMagick::Error, ActionView::Template::Error => e
|
rescue StandardError => e
|
||||||
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
||||||
Rails.logger.error(e.message)
|
Rails.logger.error(e.message)
|
||||||
|
|
||||||
|
|||||||
@@ -74,14 +74,9 @@ class EnterpriseGroup < ApplicationRecord
|
|||||||
permalink
|
permalink
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
|
||||||
def long_description
|
|
||||||
HtmlSanitizer.sanitize(super)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
# Remove any unsupported HTML.
|
||||||
def long_description=(html)
|
def long_description=(html)
|
||||||
super(HtmlSanitizer.sanitize(html))
|
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class Exchange < ApplicationRecord
|
|||||||
has_many :exchange_fees, dependent: :destroy
|
has_many :exchange_fees, dependent: :destroy
|
||||||
has_many :enterprise_fees, through: :exchange_fees
|
has_many :enterprise_fees, through: :exchange_fees
|
||||||
|
|
||||||
|
# Links to open backorders of a distributor (outgoing exchanges only)
|
||||||
|
# Don't allow removal of distributor from OC while we have an open backorder.
|
||||||
|
has_many :semantic_links, as: :subject, dependent: :restrict_with_error
|
||||||
|
|
||||||
validates :sender_id, uniqueness: { scope: [:order_cycle_id, :receiver_id, :incoming] }
|
validates :sender_id, uniqueness: { scope: [:order_cycle_id, :receiver_id, :incoming] }
|
||||||
|
|
||||||
before_destroy :delete_related_exchange_variants, prepend: true
|
before_destroy :delete_related_exchange_variants, prepend: true
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user