mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-04 02:31:33 +00:00
Compare commits
228 Commits
v4.6.10
...
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 | ||
|
|
bc87c98e92 | ||
|
|
5b8e0d734f | ||
|
|
216883101e | ||
|
|
08308ba08e | ||
|
|
df67b53971 | ||
|
|
a3d8ae693d | ||
|
|
b14a1e72f3 | ||
|
|
224738e0a1 | ||
|
|
10c3c53aad | ||
|
|
22428fc78d | ||
|
|
f980cb45f6 | ||
|
|
097c6dee2f | ||
|
|
1a30cf6495 | ||
|
|
f7708d69a7 | ||
|
|
aa5feb6605 | ||
|
|
b2b6847882 | ||
|
|
d01d312b4f | ||
|
|
a74cf97083 | ||
|
|
03dbd54b25 | ||
|
|
fafd86a2db | ||
|
|
eb8050d61d | ||
|
|
0824430da5 | ||
|
|
ef6e37e7ca | ||
|
|
ffc2fed9b5 |
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"
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/release.md
vendored
9
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -7,7 +7,7 @@ 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`
|
||||||
@@ -26,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.
|
||||||
@@ -40,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.
|
||||||
|
|
||||||
@@ -53,5 +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
|
[Transifex Client]: https://developers.transifex.com/docs/cli
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -39,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
|
||||||
@@ -214,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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -18,17 +18,15 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unit_value_with_description(variant)
|
def unit_value_with_description(variant)
|
||||||
precised_unit_value = nil
|
return variant.unit_description.to_s if variant.unit_value.nil?
|
||||||
|
|
||||||
if variant.unit_value
|
scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1)
|
||||||
scaled_unit_value = variant.unit_value / (variant.product.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,
|
||||||
significant: false,
|
)
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
|
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
|
||||||
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
|
||||||
|
|||||||
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
|
||||||
@@ -13,14 +13,14 @@ class BackorderJob < ApplicationJob
|
|||||||
sidekiq_options retry: 0
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
def self.check_stock(order)
|
def self.check_stock(order)
|
||||||
links = SemanticLink.where(variant_id: order.line_items.select(:variant_id))
|
links = SemanticLink.where(subject: order.variants)
|
||||||
|
|
||||||
perform_later(order) if links.exists?
|
perform_later(order) if links.exists?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
# Errors here shouldn't affect the checkout. So let's report them
|
# Errors here shouldn't affect the checkout. So let's report them
|
||||||
# separately:
|
# separately:
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata(:order, order)
|
payload.add_metadata(:order, :order, order)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ class BackorderJob < ApplicationJob
|
|||||||
user = order.distributor.owner
|
user = order.distributor.owner
|
||||||
items = backorderable_items(order)
|
items = backorderable_items(order)
|
||||||
|
|
||||||
|
return if items.empty?
|
||||||
|
|
||||||
# We are assuming that all variants are linked to the same wholesale
|
# We are assuming that all variants are linked to the same wholesale
|
||||||
# shop and its catalog:
|
# shop and its catalog:
|
||||||
reference_link = items[0].variant.semantic_links[0].semantic_id
|
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||||
@@ -131,5 +133,7 @@ class BackorderJob < ApplicationJob
|
|||||||
.perform_later(
|
.perform_later(
|
||||||
user, order.distributor, order.order_cycle, placed_order.semanticId
|
user, order.distributor, order.order_cycle, placed_order.semanticId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
order.exchange.semantic_links.create!(semantic_id: placed_order.semanticId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,12 +19,18 @@ class CompleteBackorderJob < ApplicationJob
|
|||||||
# someone else's order.
|
# someone else's order.
|
||||||
def perform(user, distributor, order_cycle, order_id)
|
def perform(user, distributor, order_cycle, order_id)
|
||||||
order = FdcBackorderer.new(user, nil).find_order(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)
|
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
||||||
|
|
||||||
variants = order_cycle.variants_distributed_by(distributor)
|
variants = order_cycle.variants_distributed_by(distributor)
|
||||||
adjust_quantities(order_cycle, user, order, urls, variants)
|
adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
|
||||||
FdcBackorderer.new(user, urls).complete_order(order)
|
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
|
rescue StandardError
|
||||||
BackorderMailer.backorder_incomplete(user, distributor, order_cycle, order_id).deliver_later
|
BackorderMailer.backorder_incomplete(user, distributor, order_cycle, order_id).deliver_later
|
||||||
|
|
||||||
@@ -45,6 +51,11 @@ class CompleteBackorderJob < ApplicationJob
|
|||||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||||
linked_variant = variants.linked_to(transformation.retail_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
|
# Find all line items for this order cycle
|
||||||
# Update quantity accordingly
|
# Update quantity accordingly
|
||||||
if linked_variant.on_demand
|
if linked_variant.on_demand
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class StockSyncJob < ApplicationJob
|
|||||||
# Errors here shouldn't affect the shopping. So let's report them
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
# separately:
|
# separately:
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata(:order, order)
|
payload.add_metadata(:order, :order, order)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ class StockSyncJob < ApplicationJob
|
|||||||
# Errors here shouldn't affect the shopping. So let's report them
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
# separately:
|
# separately:
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata(:order, order)
|
payload.add_metadata(:order, :order, order)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.catalog_ids(order)
|
def self.catalog_ids(order)
|
||||||
stock_controlled_variants = order.variants.reject(&:on_demand)
|
stock_controlled_variants = order.variants.reject(&:on_demand)
|
||||||
links = SemanticLink.where(variant_id: stock_controlled_variants.map(&:id))
|
links = SemanticLink.where(subject: stock_controlled_variants)
|
||||||
semantic_ids = links.pluck(:semantic_id)
|
semantic_ids = links.pluck(:semantic_id)
|
||||||
semantic_ids.map do |product_id|
|
semantic_ids.map do |product_id|
|
||||||
FdcUrlBuilder.new(product_id).catalog_url
|
FdcUrlBuilder.new(product_id).catalog_url
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -4,13 +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'].freeze
|
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
|
||||||
|
|||||||
@@ -74,11 +74,6 @@ class EnterpriseGroup < ApplicationRecord
|
|||||||
permalink
|
permalink
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
|
||||||
def long_description
|
|
||||||
HtmlSanitizer.sanitize_and_enforce_link_target_blank(super)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
# Remove any unsupported HTML.
|
||||||
def long_description=(html)
|
def long_description=(html)
|
||||||
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
|
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -224,6 +224,9 @@ module ProductImport
|
|||||||
# Ensure attributes are correctly copied to a new product's variant
|
# Ensure attributes are correctly copied to a new product's variant
|
||||||
variant = product.variants.first
|
variant = product.variants.first
|
||||||
variant.display_name = entry.display_name if entry.display_name
|
variant.display_name = entry.display_name if entry.display_name
|
||||||
|
variant.variant_unit = entry.variant_unit if entry.variant_unit
|
||||||
|
variant.variant_unit_name = entry.variant_unit_name if entry.variant_unit_name
|
||||||
|
variant.variant_unit_scale = entry.variant_unit_scale if entry.variant_unit_scale
|
||||||
variant.import_date = @import_time
|
variant.import_date = @import_time
|
||||||
variant.supplier_id = entry.producer_id
|
variant.supplier_id = entry.producer_id
|
||||||
variant.save
|
variant.save
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
|
|
||||||
module ProductImport
|
module ProductImport
|
||||||
class EntryValidator
|
class EntryValidator
|
||||||
SKIP_VALIDATE_ON_UPDATE = [:description].freeze
|
|
||||||
|
|
||||||
# rubocop:disable Metrics/ParameterLists
|
# rubocop:disable Metrics/ParameterLists
|
||||||
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises,
|
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises,
|
||||||
inventory_permissions, reset_counts, import_settings, all_entries)
|
inventory_permissions, reset_counts, import_settings, all_entries)
|
||||||
@@ -22,9 +20,8 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics/ParameterLists
|
# rubocop:enable Metrics/ParameterLists
|
||||||
|
|
||||||
def self.non_updatable_fields
|
def self.non_updatable_variant_fields
|
||||||
{
|
{
|
||||||
description: :description,
|
|
||||||
unit_type: :variant_unit_scale,
|
unit_type: :variant_unit_scale,
|
||||||
variant_unit_name: :variant_unit_name,
|
variant_unit_name: :variant_unit_name,
|
||||||
}
|
}
|
||||||
@@ -67,8 +64,7 @@ module ProductImport
|
|||||||
|
|
||||||
def mark_as_new_variant(entry, product_id)
|
def mark_as_new_variant(entry, product_id)
|
||||||
variant_attributes = entry.assignable_attributes.except(
|
variant_attributes = entry.assignable_attributes.except(
|
||||||
'id', 'product_id', 'on_hand', 'on_demand', 'variant_unit', 'variant_unit_name',
|
'id', 'product_id', 'on_hand', 'on_demand'
|
||||||
'variant_unit_scale'
|
|
||||||
)
|
)
|
||||||
# Variant needs a product. Product needs to be assigned first in order for
|
# Variant needs a product. Product needs to be assigned first in order for
|
||||||
# delegate to work. name= will fail otherwise.
|
# delegate to work. name= will fail otherwise.
|
||||||
@@ -297,11 +293,11 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
|
|
||||||
products.flat_map(&:variants).each do |existing_variant|
|
products.flat_map(&:variants).each do |existing_variant|
|
||||||
unit_scale = existing_variant.product.variant_unit_scale
|
unit_scale = existing_variant.variant_unit_scale
|
||||||
unscaled_units = entry.unscaled_units.to_f || 0
|
unscaled_units = entry.unscaled_units.to_f || 0
|
||||||
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
||||||
|
|
||||||
if entry_matches_existing_variant?(entry, existing_variant)
|
if entry.match_inventory_variant?(existing_variant)
|
||||||
variant_override = create_inventory_item(entry, existing_variant)
|
variant_override = create_inventory_item(entry, existing_variant)
|
||||||
return validate_inventory_item(entry, variant_override)
|
return validate_inventory_item(entry, variant_override)
|
||||||
end
|
end
|
||||||
@@ -311,17 +307,6 @@ module ProductImport
|
|||||||
error: I18n.t('admin.product_import.model.not_found'))
|
error: I18n.t('admin.product_import.model.not_found'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def entry_matches_existing_variant?(entry, existing_variant)
|
|
||||||
display_name_are_the_same?(entry, existing_variant) &&
|
|
||||||
existing_variant.unit_value == entry.unit_value.to_f
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_name_are_the_same?(entry, existing_variant)
|
|
||||||
return true if entry.display_name.blank? && existing_variant.display_name.blank?
|
|
||||||
|
|
||||||
existing_variant.display_name == entry.display_name
|
|
||||||
end
|
|
||||||
|
|
||||||
def category_validation(entry)
|
def category_validation(entry)
|
||||||
category_name = entry.category
|
category_name = entry.category
|
||||||
|
|
||||||
@@ -364,13 +349,13 @@ module ProductImport
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
products.each { |product| product_field_errors(entry, product) }
|
|
||||||
|
|
||||||
products.flat_map(&:variants).each do |existing_variant|
|
products.flat_map(&:variants).each do |existing_variant|
|
||||||
if entry_matches_existing_variant?(entry, existing_variant) &&
|
next unless entry.match_variant?(existing_variant) &&
|
||||||
existing_variant.deleted_at.nil?
|
existing_variant.deleted_at.nil?
|
||||||
return mark_as_existing_variant(entry, existing_variant)
|
|
||||||
end
|
variant_field_errors(entry, existing_variant)
|
||||||
|
|
||||||
|
return mark_as_existing_variant(entry, existing_variant)
|
||||||
end
|
end
|
||||||
|
|
||||||
mark_as_new_variant(entry, products.first.id)
|
mark_as_new_variant(entry, products.first.id)
|
||||||
@@ -392,8 +377,7 @@ module ProductImport
|
|||||||
|
|
||||||
def mark_as_existing_variant(entry, existing_variant)
|
def mark_as_existing_variant(entry, existing_variant)
|
||||||
existing_variant.assign_attributes(
|
existing_variant.assign_attributes(
|
||||||
entry.assignable_attributes.except('id', 'product_id', 'variant_unit', 'variant_unit_name',
|
entry.assignable_attributes.except('id', 'product_id')
|
||||||
'variant_unit_scale')
|
|
||||||
)
|
)
|
||||||
check_on_hand_nil(entry, existing_variant)
|
check_on_hand_nil(entry, existing_variant)
|
||||||
|
|
||||||
@@ -406,11 +390,10 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def product_field_errors(entry, existing_product)
|
def variant_field_errors(entry, existing_variant)
|
||||||
EntryValidator.non_updatable_fields.each do |display_name, attribute|
|
EntryValidator.non_updatable_variant_fields.each do |display_name, attribute|
|
||||||
next if attributes_match?(attribute, existing_product, entry) ||
|
next if attributes_match?(attribute, existing_variant, entry) ||
|
||||||
attributes_blank?(attribute, existing_product, entry)
|
attributes_blank?(attribute, existing_variant, entry)
|
||||||
next if ignore_when_updating_product?(attribute)
|
|
||||||
|
|
||||||
mark_as_invalid(entry, attribute: display_name,
|
mark_as_invalid(entry, attribute: display_name,
|
||||||
error: I18n.t('admin.product_import.model.not_updatable'))
|
error: I18n.t('admin.product_import.model.not_updatable'))
|
||||||
@@ -423,10 +406,6 @@ module ProductImport
|
|||||||
existing_product_value == convert_to_trusted_type(entry_value, existing_product_value)
|
existing_product_value == convert_to_trusted_type(entry_value, existing_product_value)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ignore_when_updating_product?(attribute)
|
|
||||||
SKIP_VALIDATE_ON_UPDATE.include? attribute
|
|
||||||
end
|
|
||||||
|
|
||||||
def convert_to_trusted_type(untrusted_attribute, trusted_attribute)
|
def convert_to_trusted_type(untrusted_attribute, trusted_attribute)
|
||||||
case trusted_attribute
|
case trusted_attribute
|
||||||
when Integer
|
when Integer
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ module ProductImport
|
|||||||
invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES)
|
invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_variant?(variant)
|
||||||
|
match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_inventory_variant?(variant)
|
||||||
|
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def remove_empty_skus(attrs)
|
def remove_empty_skus(attrs)
|
||||||
@@ -99,5 +107,11 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def match_display_name?(variant)
|
||||||
|
return true if display_name.blank? && variant.display_name.blank?
|
||||||
|
|
||||||
|
variant.display_name == display_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
# Link a Spree::Variant to an external DFC SuppliedProduct.
|
# Link a Spree::Variant to an external DFC SuppliedProduct.
|
||||||
class SemanticLink < ApplicationRecord
|
class SemanticLink < ApplicationRecord
|
||||||
belongs_to :variant, class_name: "Spree::Variant"
|
self.ignored_columns += [:variant_id]
|
||||||
|
|
||||||
|
belongs_to :subject, polymorphic: true
|
||||||
|
|
||||||
validates :semantic_id, presence: true
|
validates :semantic_id, presence: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ module Spree
|
|||||||
can [:admin, :index, :show, :create], ::Admin::ReportsController
|
can [:admin, :index, :show, :create], ::Admin::ReportsController
|
||||||
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
|
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
|
||||||
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
|
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
|
||||||
:packing, :enterprise_fee_summary, :bulk_coop], :report
|
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_order_cycle_management_abilities(user)
|
def add_order_cycle_management_abilities(user)
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ module Spree
|
|||||||
after_destroy :update_order
|
after_destroy :update_order
|
||||||
after_save :update_order
|
after_save :update_order
|
||||||
|
|
||||||
delegate :product, :variant_unit, :unit_description, :display_name, :display_as, to: :variant
|
delegate :product, :variant_unit, :unit_description, :display_name, :display_as,
|
||||||
|
:variant_unit_scale, :variant_unit_name, to: :variant
|
||||||
|
|
||||||
# Allows manual skipping of Stock::AvailabilityValidator
|
# Allows manual skipping of Stock::AvailabilityValidator
|
||||||
attr_accessor :skip_stock_check, :target_shipment
|
attr_accessor :skip_stock_check, :target_shipment
|
||||||
|
|||||||
@@ -67,8 +67,12 @@ module Spree
|
|||||||
class_name: 'Spree::Adjustment',
|
class_name: 'Spree::Adjustment',
|
||||||
dependent: :destroy
|
dependent: :destroy
|
||||||
has_many :invoices, dependent: :restrict_with_exception
|
has_many :invoices, dependent: :restrict_with_exception
|
||||||
|
|
||||||
belongs_to :order_cycle, optional: true
|
belongs_to :order_cycle, optional: true
|
||||||
|
has_one :exchange, ->(order) {
|
||||||
|
outgoing.to_enterprise(order.distributor)
|
||||||
|
}, through: :order_cycle, source: :exchanges
|
||||||
|
has_many :semantic_links, through: :exchange
|
||||||
|
|
||||||
belongs_to :distributor, class_name: 'Enterprise', optional: true
|
belongs_to :distributor, class_name: 'Enterprise', optional: true
|
||||||
belongs_to :customer, optional: true
|
belongs_to :customer, optional: true
|
||||||
has_one :proxy_order, dependent: :destroy
|
has_one :proxy_order, dependent: :destroy
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ module Spree
|
|||||||
|
|
||||||
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
||||||
update(payment_state: updater.update_payment_state)
|
update(payment_state: updater.update_payment_state)
|
||||||
|
|
||||||
|
AmendBackorderJob.perform_later(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_resume
|
def after_resume
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ module Spree
|
|||||||
if adjustment
|
if adjustment
|
||||||
adjustment.originator = payment_method
|
adjustment.originator = payment_method
|
||||||
adjustment.label = adjustment_label
|
adjustment.label = adjustment_label
|
||||||
adjustment.amount = payment_method.compute_amount(self)
|
|
||||||
adjustment.save
|
adjustment.save
|
||||||
elsif !processing_refund? && payment_method.present?
|
elsif !processing_refund? && payment_method.present?
|
||||||
payment_method.create_adjustment(adjustment_label, self, true)
|
payment_method.create_adjustment(adjustment_label, self, true)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ module Spree
|
|||||||
|
|
||||||
# strips all non-price-like characters from the price, taking into account locale settings
|
# strips all non-price-like characters from the price, taking into account locale settings
|
||||||
def parse_price(price)
|
def parse_price(price)
|
||||||
|
return nil if price.blank?
|
||||||
return price unless price.is_a?(String)
|
return price unless price.is_a?(String)
|
||||||
|
|
||||||
separator, _delimiter = I18n.t([:'number.currency.format.separator',
|
separator, _delimiter = I18n.t([:'number.currency.format.separator',
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ module Spree
|
|||||||
include LogDestroyPerformer
|
include LogDestroyPerformer
|
||||||
|
|
||||||
self.belongs_to_required_by_default = false
|
self.belongs_to_required_by_default = false
|
||||||
self.ignored_columns += [:supplier_id]
|
# These columns have been moved to variant. Currently this is only for documentation purposes,
|
||||||
|
# because they are declared as attr_accessor below, declaring them as ignored columns has no
|
||||||
|
# effect
|
||||||
|
self.ignored_columns += [
|
||||||
|
:supplier_id, :primary_taxon_id, :variant_unit, :variant_unit_scale, :variant_unit_name
|
||||||
|
]
|
||||||
|
|
||||||
acts_as_paranoid
|
acts_as_paranoid
|
||||||
|
|
||||||
@@ -45,20 +50,30 @@ module Spree
|
|||||||
|
|
||||||
validates_lengths_from_database
|
validates_lengths_from_database
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
validates :variant_unit, presence: true
|
|
||||||
validates :unit_value, numericality: {
|
|
||||||
greater_than: 0,
|
|
||||||
if: ->(p) { p.variant_unit.in?(%w(weight volume)) && new_record? }
|
|
||||||
}
|
|
||||||
validates :variant_unit_scale,
|
|
||||||
presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } }
|
|
||||||
validates :variant_unit_name,
|
|
||||||
presence: { if: ->(p) { p.variant_unit == 'items' } }
|
|
||||||
validate :validate_image
|
validate :validate_image
|
||||||
validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } }
|
validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } }
|
||||||
|
|
||||||
accepts_nested_attributes_for :variants, allow_destroy: true
|
# These validators are used to make sure the standard variant created via
|
||||||
|
# `ensure_standard_variant` will be valid. The are only used when creating a new product
|
||||||
|
with_options on: :create_and_create_standard_variant do
|
||||||
|
validates :supplier_id, presence: true
|
||||||
|
validates :primary_taxon_id, presence: true
|
||||||
|
validates :variant_unit, presence: true
|
||||||
|
validates :unit_value, presence: true, if: ->(product) {
|
||||||
|
%w(weight volume).include?(product.variant_unit)
|
||||||
|
}
|
||||||
|
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
|
||||||
|
validates :unit_description, presence: true, if: ->(product) {
|
||||||
|
product.variant_unit.present? && product.unit_value.nil?
|
||||||
|
}
|
||||||
|
validates :variant_unit_scale, presence: true, if: ->(product) {
|
||||||
|
%w(weight volume).include?(product.variant_unit)
|
||||||
|
}
|
||||||
|
validates :variant_unit_name, presence: true, if: ->(product) {
|
||||||
|
product.variant_unit == 'items'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
accepts_nested_attributes_for :image
|
accepts_nested_attributes_for :image
|
||||||
accepts_nested_attributes_for :product_properties,
|
accepts_nested_attributes_for :product_properties,
|
||||||
allow_destroy: true,
|
allow_destroy: true,
|
||||||
@@ -66,14 +81,12 @@ module Spree
|
|||||||
|
|
||||||
# Transient attributes used temporarily when creating a new product,
|
# Transient attributes used temporarily when creating a new product,
|
||||||
# these values are persisted on the product's variant
|
# these values are persisted on the product's variant
|
||||||
attr_accessor :price, :display_as, :unit_value, :unit_description, :tax_category_id,
|
attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit,
|
||||||
:shipping_category_id, :primary_taxon_id, :supplier_id
|
:variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id,
|
||||||
|
:primary_taxon_id, :supplier_id
|
||||||
|
|
||||||
after_validation :validate_variant_attrs, on: :create
|
|
||||||
after_create :ensure_standard_variant
|
after_create :ensure_standard_variant
|
||||||
after_update :touch_supplier, if: :saved_change_to_primary_taxon_id?
|
|
||||||
around_destroy :destruction
|
around_destroy :destruction
|
||||||
after_save :update_units
|
|
||||||
after_touch :touch_supplier
|
after_touch :touch_supplier
|
||||||
|
|
||||||
# -- Scopes
|
# -- Scopes
|
||||||
@@ -198,10 +211,6 @@ module Spree
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_on_hand
|
|
||||||
stock_items.sum(&:count_on_hand)
|
|
||||||
end
|
|
||||||
|
|
||||||
def properties_including_inherited
|
def properties_including_inherited
|
||||||
# Product properties override producer properties
|
# Product properties override producer properties
|
||||||
ps = product_properties.all
|
ps = product_properties.all
|
||||||
@@ -245,6 +254,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize
|
||||||
def ensure_standard_variant
|
def ensure_standard_variant
|
||||||
return unless variants.empty?
|
return unless variants.empty?
|
||||||
|
|
||||||
@@ -254,36 +264,16 @@ module Spree
|
|||||||
variant.display_as = display_as
|
variant.display_as = display_as
|
||||||
variant.unit_value = unit_value
|
variant.unit_value = unit_value
|
||||||
variant.unit_description = unit_description
|
variant.unit_description = unit_description
|
||||||
|
variant.variant_unit = variant_unit
|
||||||
|
variant.variant_unit_name = variant_unit_name
|
||||||
|
variant.variant_unit_scale = variant_unit_scale
|
||||||
variant.tax_category_id = tax_category_id
|
variant.tax_category_id = tax_category_id
|
||||||
variant.shipping_category_id = shipping_category_id
|
variant.shipping_category_id = shipping_category_id
|
||||||
variant.primary_taxon_id = primary_taxon_id
|
variant.primary_taxon_id = primary_taxon_id
|
||||||
variant.supplier_id = supplier_id
|
variant.supplier_id = supplier_id
|
||||||
variants << variant
|
variants << variant
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize
|
||||||
# Format as per WeightsAndMeasures (todo: re-orgnaise maybe after product/variant refactor)
|
|
||||||
def variant_unit_with_scale
|
|
||||||
# Our code is based upon English based number formatting with a period `.`
|
|
||||||
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
|
|
||||||
precision: nil,
|
|
||||||
significant: false,
|
|
||||||
strip_insignificant_zeros: true,
|
|
||||||
locale: :en)
|
|
||||||
[variant_unit, scale_clean].compact_blank.join("_")
|
|
||||||
end
|
|
||||||
|
|
||||||
def variant_unit_with_scale=(variant_unit_with_scale)
|
|
||||||
values = variant_unit_with_scale.split("_")
|
|
||||||
assign_attributes(
|
|
||||||
variant_unit: values[0],
|
|
||||||
variant_unit_scale: values[1] || nil
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
|
||||||
def description
|
|
||||||
HtmlSanitizer.sanitize(super)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove any unsupported HTML.
|
# Remove any unsupported HTML.
|
||||||
def description=(html)
|
def description=(html)
|
||||||
@@ -292,27 +282,6 @@ module Spree
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_variant_attrs
|
|
||||||
# Avoid running validation when we can't set variant attrs
|
|
||||||
# eg clone product. Will raise error if clonning a product with no variant
|
|
||||||
return if variants.first&.valid?
|
|
||||||
|
|
||||||
errors.add(:primary_taxon_id, :blank) unless Spree::Taxon.find_by(id: primary_taxon_id)
|
|
||||||
errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_units
|
|
||||||
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
|
|
||||||
|
|
||||||
variants.each do |v|
|
|
||||||
if v.persisted?
|
|
||||||
v.update_units
|
|
||||||
else
|
|
||||||
v.assign_units
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def touch_supplier
|
def touch_supplier
|
||||||
return if variants.empty?
|
return if variants.empty?
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
module Spree
|
module Spree
|
||||||
class ReturnAuthorization < ApplicationRecord
|
class ReturnAuthorization < ApplicationRecord
|
||||||
|
self.ignored_columns += [:stock_location_id]
|
||||||
acts_as_paranoid
|
acts_as_paranoid
|
||||||
|
|
||||||
belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
|
belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
|
||||||
|
|
||||||
has_many :inventory_units, inverse_of: :return_authorization, dependent: :nullify
|
has_many :inventory_units, inverse_of: :return_authorization, dependent: :nullify
|
||||||
has_one :stock_location, dependent: nil
|
|
||||||
before_save :force_positive_amount
|
before_save :force_positive_amount
|
||||||
before_create :generate_number
|
before_create :generate_number
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,15 @@ module Spree
|
|||||||
if default_zone_or_zone_match?(item.order)
|
if default_zone_or_zone_match?(item.order)
|
||||||
calculator.compute(item)
|
calculator.compute(item)
|
||||||
else
|
else
|
||||||
|
# Tax refund should not be possible with the way our production server are configured
|
||||||
|
Bugsnag.notify(
|
||||||
|
"Notice: Tax refund should not be possible, please check the default zone and " \
|
||||||
|
"the tax rate zone configuration"
|
||||||
|
) do |payload|
|
||||||
|
payload.add_metadata :order_tax_zone, item.order.tax_zone
|
||||||
|
payload.add_metadata :tax_rate_zone, zone
|
||||||
|
payload.add_metadata :default_zone, Zone.default_tax
|
||||||
|
end
|
||||||
# In this case, it's a refund.
|
# In this case, it's a refund.
|
||||||
calculator.compute(item) * - 1
|
calculator.compute(item) * - 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ module Spree
|
|||||||
has_many :exchanges, through: :exchange_variants
|
has_many :exchanges, through: :exchange_variants
|
||||||
has_many :variant_overrides, dependent: :destroy
|
has_many :variant_overrides, dependent: :destroy
|
||||||
has_many :inventory_items, dependent: :destroy
|
has_many :inventory_items, dependent: :destroy
|
||||||
has_many :semantic_links, dependent: :delete_all
|
has_many :semantic_links, as: :subject, dependent: :delete_all
|
||||||
has_many :supplier_properties, through: :supplier, source: :properties
|
has_many :supplier_properties, through: :supplier, source: :properties
|
||||||
|
|
||||||
localize_number :price, :weight
|
localize_number :price, :weight
|
||||||
@@ -71,21 +71,25 @@ module Spree
|
|||||||
validates :tax_category, presence: true,
|
validates :tax_category, presence: true,
|
||||||
if: proc { Spree::Config.products_require_tax_category }
|
if: proc { Spree::Config.products_require_tax_category }
|
||||||
|
|
||||||
|
validates :variant_unit, presence: true
|
||||||
validates :unit_value, presence: true, if: ->(variant) {
|
validates :unit_value, presence: true, if: ->(variant) {
|
||||||
%w(weight volume).include?(variant.product&.variant_unit)
|
%w(weight volume).include?(variant.variant_unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
|
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
|
||||||
validates :price, numericality: { greater_than_or_equal_to: 0 }
|
|
||||||
|
|
||||||
validates :unit_description, presence: true, if: ->(variant) {
|
validates :unit_description, presence: true, if: ->(variant) {
|
||||||
variant.product&.variant_unit.present? && variant.unit_value.nil?
|
variant.variant_unit.present? && variant.unit_value.nil?
|
||||||
|
}
|
||||||
|
validates :variant_unit_scale, presence: true, if: ->(variant) {
|
||||||
|
%w(weight volume).include?(variant.variant_unit)
|
||||||
|
}
|
||||||
|
validates :variant_unit_name, presence: true, if: ->(variant) {
|
||||||
|
variant.variant_unit == 'items'
|
||||||
}
|
}
|
||||||
|
|
||||||
before_validation :set_cost_currency
|
before_validation :set_cost_currency
|
||||||
before_validation :ensure_shipping_category
|
before_validation :ensure_shipping_category
|
||||||
before_validation :ensure_unit_value
|
before_validation :ensure_unit_value
|
||||||
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
|
before_validation :update_weight_from_unit_value
|
||||||
before_validation :convert_variant_weight_to_decimal
|
before_validation :convert_variant_weight_to_decimal
|
||||||
|
|
||||||
before_save :assign_units, if: ->(variant) {
|
before_save :assign_units, if: ->(variant) {
|
||||||
@@ -95,6 +99,9 @@ module Spree
|
|||||||
after_create :create_stock_items
|
after_create :create_stock_items
|
||||||
around_destroy :destruction
|
around_destroy :destruction
|
||||||
after_save :save_default_price
|
after_save :save_default_price
|
||||||
|
after_save :update_units, if: -> {
|
||||||
|
saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
|
||||||
|
}
|
||||||
|
|
||||||
# default variant scope only lists non-deleted variants
|
# default variant scope only lists non-deleted variants
|
||||||
scope :deleted, -> { where.not(deleted_at: nil) }
|
scope :deleted, -> { where.not(deleted_at: nil) }
|
||||||
@@ -219,6 +226,25 @@ module Spree
|
|||||||
Spree::Stock::Quantifier.new(self).total_on_hand
|
Spree::Stock::Quantifier.new(self).total_on_hand
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Format as per WeightsAndMeasures
|
||||||
|
def variant_unit_with_scale
|
||||||
|
# Our code is based upon English based number formatting with a period `.`
|
||||||
|
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
|
||||||
|
precision: nil,
|
||||||
|
significant: false,
|
||||||
|
strip_insignificant_zeros: true,
|
||||||
|
locale: :en)
|
||||||
|
[variant_unit, scale_clean].compact_blank.join("_")
|
||||||
|
end
|
||||||
|
|
||||||
|
def variant_unit_with_scale=(variant_unit_with_scale)
|
||||||
|
values = variant_unit_with_scale.split("_")
|
||||||
|
assign_attributes(
|
||||||
|
variant_unit: values[0],
|
||||||
|
variant_unit_scale: values[1] || nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_currency
|
def check_currency
|
||||||
@@ -248,7 +274,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_weight_from_unit_value
|
def update_weight_from_unit_value
|
||||||
return unless product.variant_unit == 'weight' && unit_value.present?
|
return unless variant_unit == 'weight' && unit_value.present?
|
||||||
|
|
||||||
self.weight = weight_from_unit_value
|
self.weight = weight_from_unit_value
|
||||||
end
|
end
|
||||||
@@ -268,7 +294,7 @@ module Spree
|
|||||||
|
|
||||||
def ensure_unit_value
|
def ensure_unit_value
|
||||||
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
|
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
|
||||||
return unless (product&.variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
|
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
|
||||||
|
|
||||||
self.unit_value = 1.0
|
self.unit_value = 1.0
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
module Admin
|
module Admin
|
||||||
class ProductSerializer < ActiveModel::Serializer
|
class ProductSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name,
|
attributes :id, :name, :sku, :inherits_properties, :on_hand, :price, :import_date, :image_url,
|
||||||
:inherits_properties, :on_hand, :price, :import_date, :image_url,
|
|
||||||
:thumb_url, :variants
|
:thumb_url, :variants
|
||||||
|
|
||||||
def variants
|
def variants
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
module Admin
|
module Admin
|
||||||
class UnitsProductSerializer < ActiveModel::Serializer
|
class UnitsProductSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :name, :group_buy_unit_size, :variant_unit, :variant_unit_scale
|
attributes :id, :name, :group_buy_unit_size
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
module Admin
|
module Admin
|
||||||
class UnitsVariantSerializer < ActiveModel::Serializer
|
class UnitsVariantSerializer < ActiveModel::Serializer
|
||||||
attributes :id, :full_name, :unit_value
|
attributes :id, :full_name, :unit_value, :variant_unit, :variant_unit_scale
|
||||||
|
|
||||||
def full_name
|
def full_name
|
||||||
full_name = object.full_name
|
full_name = object.full_name
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ module Api
|
|||||||
attributes :id, :name, :producer_name, :image, :sku, :import_date, :tax_category_id,
|
attributes :id, :name, :producer_name, :image, :sku, :import_date, :tax_category_id,
|
||||||
:options_text, :unit_value, :unit_description, :unit_to_display,
|
:options_text, :unit_value, :unit_description, :unit_to_display,
|
||||||
:display_as, :display_name, :name_to_display, :variant_overrides_count,
|
:display_as, :display_name, :name_to_display, :variant_overrides_count,
|
||||||
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name
|
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name,
|
||||||
|
:variant_unit, :variant_unit_scale, :variant_unit_name, :variant_unit_with_scale
|
||||||
|
|
||||||
has_one :primary_taxon, key: :category_id, embed: :id
|
has_one :primary_taxon, key: :category_id, embed: :id
|
||||||
has_one :supplier, key: :producer_id, embed: :id
|
has_one :supplier, key: :producer_id, embed: :id
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class FdcBackorderer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def find_or_build_order(ofn_order)
|
def find_or_build_order(ofn_order)
|
||||||
find_open_order || build_new_order(ofn_order)
|
find_open_order(ofn_order) || build_new_order(ofn_order)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_order(ofn_order)
|
def build_new_order(ofn_order)
|
||||||
@@ -19,7 +19,37 @@ class FdcBackorderer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_open_order
|
# Try the new method and fall back to old method.
|
||||||
|
def find_open_order(ofn_order)
|
||||||
|
lookup_open_order(ofn_order) || find_last_open_order
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup_open_order(ofn_order)
|
||||||
|
# There should be only one link at the moment but we may support
|
||||||
|
# ordering from multiple suppliers one day.
|
||||||
|
semantic_ids = ofn_order.semantic_links.pluck(:semantic_id)
|
||||||
|
|
||||||
|
semantic_ids.lazy
|
||||||
|
# Make sure we select an order from the right supplier:
|
||||||
|
.select { |id| id.starts_with?(urls.orders_url) }
|
||||||
|
# Fetch the order from the remote DFC server, lazily:
|
||||||
|
.map { |id| find_order(id) }
|
||||||
|
.compact
|
||||||
|
# Just in case someone completed the order without updating our database:
|
||||||
|
.select { |o| o.orderStatus[:path] == "Held" }
|
||||||
|
.first
|
||||||
|
# The DFC Connector doesn't recognise status values properly yet.
|
||||||
|
# So we are overriding the value with something that can be exported.
|
||||||
|
&.tap { |o| o.orderStatus = "dfc-v:Held" }
|
||||||
|
end
|
||||||
|
|
||||||
|
# DEPRECATED
|
||||||
|
#
|
||||||
|
# We now store links to orders we placed. So we don't need to search
|
||||||
|
# through all orders and pick a random open one.
|
||||||
|
# But for compatibility with currently open order cycles that don't have
|
||||||
|
# a stored link yet, we keep this method as well.
|
||||||
|
def find_last_open_order
|
||||||
graph = import(urls.orders_url)
|
graph = import(urls.orders_url)
|
||||||
open_orders = graph&.select do |o|
|
open_orders = graph&.select do |o|
|
||||||
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
|
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ module PermittedAttributes
|
|||||||
class Variant
|
class Variant
|
||||||
def self.attributes
|
def self.attributes
|
||||||
[
|
[
|
||||||
:id, :sku, :on_hand, :on_demand, :shipping_category_id,
|
:id, :sku, :on_hand, :on_demand, :shipping_category_id, :price, :unit_value,
|
||||||
:price, :unit_value, :unit_description,
|
:unit_description, :variant_unit, :variant_unit_name, :variant_unit_scale, :display_name,
|
||||||
:display_name, :display_as, :tax_category_id,
|
:display_as, :tax_category_id, :weight, :height, :width, :depth, :taxon_ids,
|
||||||
:weight, :height, :width, :depth, :taxon_ids, :primary_taxon_id,
|
:primary_taxon_id, :supplier_id
|
||||||
:supplier_id
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class PlaceProxyOrder
|
|||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
summarizer.record_and_log_error(:processing, order, e.message)
|
summarizer.record_and_log_error(:processing, order, e.message)
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata :order, order
|
payload.add_metadata :order, :order, order
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,8 +57,7 @@ class PlaceProxyOrder
|
|||||||
true
|
true
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Bugsnag.notify(e) do |payload|
|
Bugsnag.notify(e) do |payload|
|
||||||
payload.add_metadata :subscription, subscription
|
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
|
||||||
payload.add_metadata :proxy_order, proxy_order
|
|
||||||
end
|
end
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -146,11 +146,11 @@ module Sets
|
|||||||
|
|
||||||
def notify_bugsnag(error, product, variant, variant_attributes)
|
def notify_bugsnag(error, product, variant, variant_attributes)
|
||||||
Bugsnag.notify(error) do |report|
|
Bugsnag.notify(error) do |report|
|
||||||
report.add_metadata(:product, product.attributes)
|
report.add_metadata( :product_set,
|
||||||
report.add_metadata(:product_error, product.errors.first) unless product.valid?
|
{ product: product.attributes, variant_attributes:,
|
||||||
report.add_metadata(:variant_attributes, variant_attributes)
|
variant: variant.attributes } )
|
||||||
report.add_metadata(:variant, variant.attributes)
|
report.add_metadata(:product_set, :product_error, product.errors.first) if !product.valid?
|
||||||
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
|
report.add_metadata(:product_set, :variant_error, variant.errors.first) if !variant.valid?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
class UnitPrice
|
class UnitPrice
|
||||||
def initialize(variant)
|
def initialize(variant)
|
||||||
@variant = variant
|
@variant = variant
|
||||||
@product = variant.product
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def denominator
|
def denominator
|
||||||
# catches any case where unit is not kg, lb, or L.
|
# catches any case where unit is not kg, lb, or L.
|
||||||
return @variant.unit_value if @product&.variant_unit == "items"
|
return @variant.unit_value if @variant.variant_unit == "items"
|
||||||
|
|
||||||
case unit
|
case unit
|
||||||
when "lb"
|
when "lb"
|
||||||
@@ -23,13 +22,13 @@ class UnitPrice
|
|||||||
def unit
|
def unit
|
||||||
return "lb" if WeightsAndMeasures.new(@variant).system == "imperial"
|
return "lb" if WeightsAndMeasures.new(@variant).system == "imperial"
|
||||||
|
|
||||||
case @product&.variant_unit
|
case @variant.variant_unit
|
||||||
when "weight"
|
when "weight"
|
||||||
"kg"
|
"kg"
|
||||||
when "volume"
|
when "volume"
|
||||||
"L"
|
"L"
|
||||||
else
|
else
|
||||||
@product.variant_unit_name.presence || I18n.t("item")
|
@variant.variant_unit_name.presence || I18n.t("item")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ module VariantUnits
|
|||||||
private
|
private
|
||||||
|
|
||||||
def value_scaled?
|
def value_scaled?
|
||||||
@nameable.product.variant_unit_scale.present?
|
@nameable.variant_unit_scale.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def option_value_value_unit
|
def option_value_value_unit
|
||||||
if @nameable.unit_value.present? && @nameable.product&.persisted?
|
if @nameable.unit_value.present?
|
||||||
if %w(weight volume).include? @nameable.product.variant_unit
|
if %w(weight volume).include? @nameable.variant_unit
|
||||||
value, unit_name = option_value_value_unit_scaled
|
value, unit_name = option_value_value_unit_scaled
|
||||||
else
|
else
|
||||||
value = @nameable.unit_value
|
value = @nameable.unit_value
|
||||||
unit_name = pluralize(@nameable.product.variant_unit_name, value)
|
|
||||||
|
unit_name = @nameable.variant_unit_name
|
||||||
|
unit_name = pluralize(unit_name, value) if unit_name.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
value = value.to_i if value == value.to_i
|
value = value.to_i if value == value.to_i
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ module VariantUnits
|
|||||||
|
|
||||||
def unit_value_attributes
|
def unit_value_attributes
|
||||||
units = { unit_presentation: option_value_name }
|
units = { unit_presentation: option_value_name }
|
||||||
units.merge!(variant_unit: product.variant_unit) if has_attribute?(:variant_unit)
|
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
|
||||||
units
|
units
|
||||||
end
|
end
|
||||||
|
|
||||||
def weight_from_unit_value
|
def weight_from_unit_value
|
||||||
(unit_value || 0) / 1000 if product.variant_unit == 'weight'
|
(unit_value || 0) / 1000 if variant_unit == 'weight'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
39
app/services/vine_api_service.rb
Normal file
39
app/services/vine_api_service.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "faraday"
|
||||||
|
|
||||||
|
class VineApiService
|
||||||
|
attr_reader :api_key, :jwt_generator
|
||||||
|
|
||||||
|
def initialize(api_key:, jwt_generator:)
|
||||||
|
@vine_api_url = ENV.fetch("VINE_API_URL")
|
||||||
|
@api_key = api_key
|
||||||
|
@jwt_generator = jwt_generator
|
||||||
|
end
|
||||||
|
|
||||||
|
def my_team
|
||||||
|
my_team_url = "#{@vine_api_url}/my-team"
|
||||||
|
|
||||||
|
jwt = jwt_generator.generate_token
|
||||||
|
connection = Faraday.new(
|
||||||
|
request: { timeout: 30 },
|
||||||
|
headers: {
|
||||||
|
'X-Authorization': "JWT #{jwt}",
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
) do |f|
|
||||||
|
f.request :json
|
||||||
|
f.response :json
|
||||||
|
f.request :authorization, 'Bearer', api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
response = connection.get(my_team_url)
|
||||||
|
|
||||||
|
if !response.success?
|
||||||
|
Rails.logger.error "VineApiService#my_team -- response_status: #{response.status}"
|
||||||
|
Rails.logger.error "VineApiService#my_team -- response: #{response.body}"
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
21
app/services/vine_jwt_service.rb
Normal file
21
app/services/vine_jwt_service.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VineJwtService
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
ISSUER = "openfoodnetwork"
|
||||||
|
|
||||||
|
def initialize(secret: )
|
||||||
|
@secret = secret
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_token
|
||||||
|
generation_time = Time.zone.now
|
||||||
|
payload = {
|
||||||
|
iss: ISSUER,
|
||||||
|
iat: generation_time.to_i,
|
||||||
|
exp: (generation_time + 1.minute).to_i,
|
||||||
|
}
|
||||||
|
|
||||||
|
JWT.encode(payload, @secret, ALGORITHM)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -16,10 +16,10 @@ class WeightsAndMeasures
|
|||||||
def system
|
def system
|
||||||
return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true)
|
return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true)
|
||||||
|
|
||||||
product_scale = @variant.product.variant_unit_scale&.to_f
|
variant_scale = @variant.variant_unit_scale&.to_f
|
||||||
return "custom" unless product_scale.present? && product_scale.positive?
|
return "custom" unless variant_scale.present? && variant_scale.positive?
|
||||||
|
|
||||||
scales[product_scale]['system']
|
scales[variant_scale]['system']
|
||||||
end
|
end
|
||||||
|
|
||||||
# @returns enumerable with label and value for select
|
# @returns enumerable with label and value for select
|
||||||
@@ -92,9 +92,9 @@ class WeightsAndMeasures
|
|||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def scales_for_variant_unit(ignore_available_units: false)
|
def scales_for_variant_unit(ignore_available_units: false)
|
||||||
return @units[@variant.product.variant_unit] if ignore_available_units
|
return @units[@variant.variant_unit] if ignore_available_units
|
||||||
|
|
||||||
@units[@variant.product.variant_unit]&.reject { |_scale, unit_info|
|
@units[@variant.variant_unit]&.reject { |_scale, unit_info|
|
||||||
self.class.available_units.exclude?(unit_info['name'])
|
self.class.available_units.exclude?(unit_info['name'])
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
%section.connected_app
|
||||||
|
.connected-app__head
|
||||||
|
%div
|
||||||
|
%h3= t ".title"
|
||||||
|
%p= t ".tagline"
|
||||||
|
.connected-app__vine
|
||||||
|
- if connected_app.nil?
|
||||||
|
= form_with url: admin_enterprise_connected_apps_path(enterprise.id) do |f|
|
||||||
|
.connected-app__vine-content
|
||||||
|
.vine-api-key
|
||||||
|
= f.hidden_field :type, value: "ConnectedApps::Vine"
|
||||||
|
= f.label :vine_api_key, t(".vine_api_key")
|
||||||
|
%span.required *
|
||||||
|
= f.text_field :vine_api_key, { disabled: !managed_by_user?(enterprise) }
|
||||||
|
= f.label :vine_secret, t(".vine_secret")
|
||||||
|
%span.required *
|
||||||
|
= f.text_field :vine_secret, { disabled: !managed_by_user?(enterprise) }
|
||||||
|
%div
|
||||||
|
- disabled = managed_by_user?(enterprise) ? {} : { disabled: true, "data-disable-with": false }
|
||||||
|
= f.submit t(".enable"), disabled
|
||||||
|
|
||||||
|
-# This is only seen by super-admins:
|
||||||
|
%em= t(".need_to_be_manager") unless managed_by_user?(enterprise)
|
||||||
|
- else
|
||||||
|
.connected-app__vine-content
|
||||||
|
.vine-disable
|
||||||
|
= button_to t(".disable"), admin_enterprise_connected_app_path(connected_app.id, enterprise_id: enterprise.id), method: :delete
|
||||||
|
%hr
|
||||||
|
.connected-app__description
|
||||||
|
= t ".description_html"
|
||||||
@@ -7,18 +7,8 @@
|
|||||||
%td.col-sku.field.naked_inputs
|
%td.col-sku.field.naked_inputs
|
||||||
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
||||||
= error_message_on product, :sku
|
= error_message_on product, :sku
|
||||||
%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
%td.col-unit_scale.align-right
|
||||||
= f.hidden_field :variant_unit
|
-# empty
|
||||||
= f.hidden_field :variant_unit_scale
|
|
||||||
= f.select :variant_unit_with_scale,
|
|
||||||
options_for_select(WeightsAndMeasures.variant_unit_options, product.variant_unit_with_scale),
|
|
||||||
{},
|
|
||||||
class: "fullwidth no-input",
|
|
||||||
'aria-label': t('admin.products_page.columns.unit_scale'),
|
|
||||||
data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch"}
|
|
||||||
.field
|
|
||||||
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (product.variant_unit == "items" ? "" : "display: none")
|
|
||||||
= error_message_on product, :variant_unit_name, 'data-toggle-control-target': 'control'
|
|
||||||
%td.col-unit.align-right
|
%td.col-unit.align-right
|
||||||
-# empty
|
-# empty
|
||||||
%td.col-price.align-right
|
%td.col-price.align-right
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
.form-buttons
|
.form-buttons
|
||||||
%a.button.reset.medium{ href: admin_products_path(page: @page, per_page: @per_page, search_term: @search_term, producer_id: @producer_id, category_id: @category_id), 'data-turbo': "false" }
|
%a.button.reset.medium{ href: admin_products_path(page: @page, per_page: @per_page, search_term: @search_term, producer_id: @producer_id, category_id: @category_id), 'data-turbo': "false" }
|
||||||
= t('.reset')
|
= t('.reset')
|
||||||
= form.submit t('.save'), class: "medium"
|
= form.submit t('.save'), { class: "medium", data: { action: "click->bulk-form#popoutEmptyVariantUnit" }}
|
||||||
%tr
|
%tr
|
||||||
%th.col-image.align-left= # image
|
%th.col-image.align-left= # image
|
||||||
= render partial: 'spree/admin/shared/stimulus_sortable_header',
|
= render partial: 'spree/admin/shared/stimulus_sortable_header',
|
||||||
|
|||||||
@@ -7,8 +7,17 @@
|
|||||||
%td.col-sku.field.naked_inputs
|
%td.col-sku.field.naked_inputs
|
||||||
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
||||||
= error_message_on variant, :sku
|
= error_message_on variant, :sku
|
||||||
%td.col-unit_scale
|
%td.col-unir_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||||
-# empty
|
= f.hidden_field :variant_unit
|
||||||
|
= f.hidden_field :variant_unit_scale
|
||||||
|
= f.select :variant_unit_with_scale,
|
||||||
|
options_for_select(WeightsAndMeasures.variant_unit_options, variant.variant_unit_with_scale),
|
||||||
|
{ include_blank: true },
|
||||||
|
{ class: "fullwidth no-input", 'aria-label': t('admin.products_page.columns.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" }, required: true }
|
||||||
|
= error_message_on variant, :variant_unit, 'data-toggle-control-target': 'control'
|
||||||
|
.field
|
||||||
|
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (variant.variant_unit == "items" ? "" : "display: none")
|
||||||
|
= error_message_on variant, :variant_unit_name, 'data-toggle-control-target': 'control'
|
||||||
%td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
|
%td.col-unit.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
|
||||||
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do
|
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('admin.products_page.columns.unit'), 'data-popout-target': "button" do
|
||||||
= variant.unit_to_display # Show the generated summary of unit values
|
= variant.unit_to_display # Show the generated summary of unit values
|
||||||
@@ -18,7 +27,7 @@
|
|||||||
= f.hidden_field :unit_value
|
= f.hidden_field :unit_value
|
||||||
= f.hidden_field :unit_description
|
= f.hidden_field :unit_description
|
||||||
= f.text_field :unit_value_with_description,
|
= f.text_field :unit_value_with_description,
|
||||||
value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value')
|
value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true
|
||||||
.field
|
.field
|
||||||
= f.label :display_as, t('admin.products_page.columns.display_as')
|
= f.label :display_as, t('admin.products_page.columns.display_as')
|
||||||
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name
|
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||||
|
|
||||||
#products_v3_page{ "data-controller": "products", 'data-turbo': true }
|
#products_v3_page{ 'data-turbo': true }
|
||||||
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
|
= render partial: "content", locals: { products: @products, pagy: @pagy, search_term: @search_term,
|
||||||
producer_options: producers, producer_id: @producer_id,
|
producer_options: producers, producer_id: @producer_id,
|
||||||
category_options: categories, category_id: @category_id,
|
category_options: categories, category_id: @category_id,
|
||||||
|
|||||||
@@ -59,18 +59,18 @@
|
|||||||
.variant-unit
|
.variant-unit
|
||||||
= variant.unit_to_display
|
= variant.unit_to_display
|
||||||
.small-4.medium-3.columns.variant-price
|
.small-4.medium-3.columns.variant-price
|
||||||
= number_to_currency(variant.price)
|
= Spree::Money.new(variant.price)
|
||||||
.unit-price.variant-unit-price
|
.unit-price.variant-unit-price
|
||||||
= render AdminTooltipComponent.new(text: t("js.shopfront.unit_price_tooltip"), link_text: "", placement: "top", link_class: "question-mark-icon")
|
= render AdminTooltipComponent.new(text: t("js.shopfront.unit_price_tooltip"), link_text: "", placement: "top", link_class: "question-mark-icon")
|
||||||
- # TODO use an helper
|
- # TODO use an helper
|
||||||
- unit_price = UnitPrice.new(variant)
|
- unit_price = UnitPrice.new(variant)
|
||||||
- price_per_unit = variant.price / (unit_price.denominator || 1)
|
- price_per_unit = variant.price / (unit_price.denominator || 1)
|
||||||
= "#{number_to_currency(price_per_unit)} / #{unit_price.unit}".html_safe
|
= "#{Spree::Money.new(price_per_unit)} / #{unit_price.unit}".html_safe
|
||||||
|
|
||||||
|
|
||||||
.medium-3.columns.total-price
|
.medium-3.columns.total-price
|
||||||
%span
|
%span
|
||||||
= number_to_currency(0.00)
|
= Spree::Money.new(0.00)
|
||||||
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right
|
.small-5.medium-3.large-3.columns.variant-quantity-column.text-right
|
||||||
.variant-quantity-inputs
|
.variant-quantity-inputs
|
||||||
%button.add-variant
|
%button.add-variant
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
-# Field used for ransack search. This date range is mostly used for Spree::Order
|
-# Field used for ransack search. This date range is mostly used for Spree::Order
|
||||||
-# so default field is 'completed_at'
|
-# so default field is 'completed_at'
|
||||||
- field ||= 'completed_at'
|
- field ||= 'completed_at'
|
||||||
- start_date ||= params[:q].try(:[], :completed_at_gt)
|
- start_field = "#{field}_gt"
|
||||||
- end_date ||= params[:q].try(:[], :completed_at_lt)
|
- end_field = "#{field}_lt"
|
||||||
|
- query = params[:q].to_h
|
||||||
|
- start_date = datepicker_time(query[start_field].presence || 3.months.ago.beginning_of_day)
|
||||||
|
- end_date = datepicker_time(query[end_field].presence || Time.zone.tomorrow.beginning_of_day)
|
||||||
|
|
||||||
.row.date-range-filter
|
.row.date-range-filter
|
||||||
.alpha.two.columns= label_tag nil, t(:date_range)
|
.alpha.two.columns= label_tag nil, t(:date_range)
|
||||||
.omega.fourteen.columns
|
.omega.fourteen.columns
|
||||||
.field-block.omega.four.columns
|
.field-block.omega.four.columns
|
||||||
.date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-enable-time-value": true , "flatpickr-default-hour": 0 } }
|
.date-range-fields{ data: { controller: "flatpickr", "flatpickr-mode-value": "range", "flatpickr-enable-time-value": true , "flatpickr-default-hour": 0, "flatpickr-default-date": [start_date, end_date] } }
|
||||||
= text_field_tag nil, nil, class: "datepicker fullwidth", data: { "flatpickr-target": "instance", action: "flatpickr_clear@window->flatpickr#clear" }
|
= text_field_tag nil, nil, class: "datepicker fullwidth", data: { "flatpickr-target": "instance", action: "flatpickr_clear@window->flatpickr#clear" }
|
||||||
= text_field_tag "q[#{field}_gt]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_date
|
= text_field_tag "q[#{start_field}]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_date
|
||||||
= text_field_tag "q[#{field}_lt]", nil, data: { "flatpickr-target": "end" }, style: "display: none", value: end_date
|
= text_field_tag "q[#{end_field}]", nil, data: { "flatpickr-target": "end" }, style: "display: none", value: end_date
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
= render partial: 'admin/reports/date_range_form',
|
= render partial: 'admin/reports/date_range_form',
|
||||||
locals: { f: f, field: 'order_completed_at', start_date: params[:q].try(:[], :order_completed_at_gt), end_date: params[:q].try(:[], :order_completed_at_lt) }
|
locals: { f: f, field: 'order_completed_at' }
|
||||||
|
|
||||||
.row
|
.row
|
||||||
.alpha.two.columns= label_tag nil, t(:report_hubs)
|
.alpha.two.columns= label_tag nil, t(:report_hubs)
|
||||||
@@ -14,4 +14,4 @@
|
|||||||
.row
|
.row
|
||||||
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
|
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
|
||||||
.omega.fourteen.columns
|
.omega.fourteen.columns
|
||||||
= select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true})
|
= select_tag("q[order_cycle_id_in]", options_for_select(report_order_cycle_options(@data.order_cycles), params.dig(:q, :order_cycle_id_in)), {class: "select2 fullwidth", multiple: true})
|
||||||
|
|||||||
14
app/views/admin/reports/filters/_suppliers.html.haml
Normal file
14
app/views/admin/reports/filters/_suppliers.html.haml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
= render 'admin/reports/date_range_form', f: f
|
||||||
|
|
||||||
|
.row
|
||||||
|
.alpha.two.columns= label_tag nil, t(:report_hubs)
|
||||||
|
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.orders_distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
|
||||||
|
|
||||||
|
.row
|
||||||
|
.alpha.two.columns= label_tag nil, t(:report_producers)
|
||||||
|
.omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true})
|
||||||
|
|
||||||
|
.row
|
||||||
|
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
|
||||||
|
.omega.fourteen.columns
|
||||||
|
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
|
||||||
@@ -80,15 +80,15 @@
|
|||||||
.three.columns
|
.three.columns
|
||||||
.text-center
|
.text-center
|
||||||
= t("admin.orders.bulk_management.group_buy_unit_size")
|
= t("admin.orders.bulk_management.group_buy_unit_size")
|
||||||
.text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsProduct, selectedUnitsVariant ) }}
|
.text-center {{ getGroupBySizeFormattedValueWithUnitName(selectedUnitsProduct.group_buy_unit_size , selectedUnitsVariant ) }}
|
||||||
.three.columns
|
.three.columns
|
||||||
.text-center
|
.text-center
|
||||||
= t("admin.orders.bulk_management.total_qtt_ordered")
|
= t("admin.orders.bulk_management.total_qtt_ordered")
|
||||||
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
|
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsVariant ) }}
|
||||||
.three.columns
|
.three.columns
|
||||||
.text-center
|
.text-center
|
||||||
= t("admin.orders.bulk_management.max_qtt_ordered")
|
= t("admin.orders.bulk_management.max_qtt_ordered")
|
||||||
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
|
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsVariant ) }}
|
||||||
.three.columns
|
.three.columns
|
||||||
.text-center
|
.text-center
|
||||||
= t("admin.orders.bulk_management.current_fulfilled_units")
|
= t("admin.orders.bulk_management.current_fulfilled_units")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
%div.admin-product-form-fields
|
%div.admin-product-form-fields
|
||||||
.left.twelve.columns.alpha
|
.left.sixteen.columns.alpha
|
||||||
= f.field_container :name do
|
= f.field_container :name do
|
||||||
= f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required'))
|
= f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required'))
|
||||||
= f.text_field :name, :class => 'fullwidth title'
|
= f.text_field :name, :class => 'fullwidth title'
|
||||||
@@ -10,25 +10,6 @@
|
|||||||
= f.hidden_field :description, id: "product_description", value: @product.description
|
= f.hidden_field :description, id: "product_description", value: @product.description
|
||||||
%trix-editor{ input: "product_description", "data-controller": "trixeditor" }
|
%trix-editor{ input: "product_description", "data-controller": "trixeditor" }
|
||||||
|
|
||||||
.right.four.columns.omega
|
|
||||||
.variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' }
|
|
||||||
|
|
||||||
= f.field_container :units do
|
|
||||||
= f.label :variant_unit_with_scale, t(:spree_admin_variant_unit_scale)
|
|
||||||
%select.select2.fullwidth{ id: 'product_variant_unit_with_scale', 'ng-model' => 'variant_unit_with_scale', 'ng-change' => 'setFields()', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
|
|
||||||
%option{'value' => ''}
|
|
||||||
|
|
||||||
= f.text_field :variant_unit, {'id' => 'variant_unit', 'ng-value' => 'product.variant_unit', 'hidden' => true}
|
|
||||||
= f.text_field :variant_unit_scale, {'id' => 'variant_unit_scale', 'ng-value' => 'product.variant_unit_scale', 'hidden' => true}
|
|
||||||
|
|
||||||
.variant_unit_name{'ng-show' => 'product.variant_unit == "items"'}
|
|
||||||
= f.field_container :variant_unit_name do
|
|
||||||
= f.label :variant_unit_name, t(:spree_admin_variant_unit_name)
|
|
||||||
= f.text_field :variant_unit_name, {placeholder: t('admin.products.unit_name_placeholder')}
|
|
||||||
= f.error_message_on :variant_unit_name
|
|
||||||
|
|
||||||
.clear
|
|
||||||
|
|
||||||
.clear
|
.clear
|
||||||
|
|
||||||
%div
|
%div
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
%td.name{ 'ng-show' => 'columns.name.visible' }
|
%td.name{ 'ng-show' => 'columns.name.visible' }
|
||||||
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
|
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
|
||||||
%td.unit{ 'ng-show' => 'columns.unit.visible' }
|
%td.unit{ 'ng-show' => 'columns.unit.visible' }
|
||||||
%select.no-search{ "data-controller": "tom-select", 'ng-model' => 'product.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-product' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
|
|
||||||
%input{ 'ng-model' => 'product.master.unit_value_with_description', :name => 'master_unit_value_with_description', 'ofn-track-master' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "!hasVariants(product) && hasUnit(product)", 'ofn-maintain-unit-scale' => true }
|
|
||||||
%input{ 'ng-model' => 'product.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-product' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "product.variant_unit_with_scale == 'items'", :type => 'text' }
|
|
||||||
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
|
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
|
||||||
%td.price{ 'ng-show' => 'columns.price.visible' }
|
%td.price{ 'ng-show' => 'columns.price.visible' }
|
||||||
%input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' }
|
%input{ 'ng-model' => 'product.price', 'ofn-decimal' => :true, :name => 'price', 'ofn-track-product' => 'price', :type => 'text', 'ng-hide' => 'hasVariants(product)' }
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
%td{ 'ng-show' => 'columns.name.visible' }
|
%td{ 'ng-show' => 'columns.name.visible' }
|
||||||
%input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" }
|
%input{ 'ng-model' => 'variant.display_name', :name => 'variant_display_name', 'ofn-track-variant' => 'display_name', :type => 'text', placeholder: "{{ product.name }}" }
|
||||||
%td.unit_value{ 'ng-show' => 'columns.unit.visible' }
|
%td.unit_value{ 'ng-show' => 'columns.unit.visible' }
|
||||||
%input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', 'ofn-maintain-unit-scale' => true }
|
%select.no-search{ "data-controller": "tom-select", 'ng-model' => 'variant.variant_unit_with_scale', :name => 'variant_unit_with_scale', 'ofn-track-variant' => 'variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' }
|
||||||
|
%input{ 'ng-model' => 'variant.unit_value_with_description', :name => 'variant_unit_value_with_description', 'ofn-track-variant' => 'unit_value_with_description', :type => 'text', :placeholder => 'value', 'ng-show' => "hasUnit(variant)" }
|
||||||
|
%input{ 'ng-model' => 'variant.variant_unit_name', :name => 'variant_unit_name', 'ofn-track-variant' => 'variant_unit_name', :placeholder => 'unit', 'ng-show' => "variant.variant_unit_with_scale == 'items'", :type => 'text' }
|
||||||
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
|
%td.display_as{ 'ng-show' => 'columns.unit.visible' }
|
||||||
%input{ 'ofn-display-as' => 'variant', 'ng-model' => 'variant.display_as', name: 'variant_display_as', 'ofn-track-variant' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}' }
|
%input{ 'ofn-display-as' => 'variant', 'ng-model' => 'variant.display_as', name: 'variant_display_as', 'ofn-track-variant' => 'display_as', type: 'text', placeholder: '{{ placeholder_text }}' }
|
||||||
%td{ 'ng-show' => 'columns.price.visible' }
|
%td{ 'ng-show' => 'columns.price.visible' }
|
||||||
|
|||||||
@@ -48,9 +48,9 @@
|
|||||||
= f.field_container :unit_value do
|
= f.field_container :unit_value do
|
||||||
= f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)"
|
= f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)"
|
||||||
%span.required *
|
%span.required *
|
||||||
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.master.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)"
|
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.unit_value_with_description', class: 'fullwidth', 'ng-disabled' => "!hasUnit(product)"
|
||||||
%input{ type: 'hidden', 'ng-value': 'product.master.unit_value', "ng-init": "product.master.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
|
%input{ type: 'hidden', 'ng-value': 'product.unit_value', "ng-init": "product.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
|
||||||
%input{ type: 'hidden', 'ng-value': 'product.master.unit_description', "ng-init": "product.master.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
|
%input{ type: 'hidden', 'ng-value': 'product.unit_description', "ng-init": "product.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
|
||||||
= f.error_message_on :unit_value
|
= f.error_message_on :unit_value
|
||||||
= render 'display_as', f: f
|
= render 'display_as', f: f
|
||||||
.six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }
|
.six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }
|
||||||
|
|||||||
@@ -41,8 +41,3 @@
|
|||||||
= f.label :reason, t('.reason')
|
= f.label :reason, t('.reason')
|
||||||
= f.text_area :reason, { style: 'height:100px;', class: 'fullwidth' }
|
= f.text_area :reason, { style: 'height:100px;', class: 'fullwidth' }
|
||||||
= f.error_message_on :reason
|
= f.error_message_on :reason
|
||||||
|
|
||||||
= f.field_container :stock_location do
|
|
||||||
= f.label :stock_location, t('.stock_location')
|
|
||||||
= f.select :stock_location_id, Spree::StockLocation.all.collect{ |l| [l.name, l.id] }, { style: 'height:100px;', class: 'fullwidth' }
|
|
||||||
= f.error_message_on :reason
|
|
||||||
|
|||||||
@@ -1,83 +1,112 @@
|
|||||||
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products', 'ng-controller' => 'variantUnitsCtrl'}
|
%div{'data-controller': "edit-variant", id: "edit_variant"}
|
||||||
.field
|
.label-block.left.six.columns.alpha
|
||||||
= f.label :display_name, t('.display_name')
|
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||||
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }
|
||||||
.field
|
|
||||||
= f.label :display_as, t('.display_as')
|
|
||||||
= f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder')
|
|
||||||
|
|
||||||
- if @product.variant_unit != 'items'
|
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||||
.field
|
locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) }
|
||||||
= label_tag :unit_value_human, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
|
|
||||||
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
|
|
||||||
= number_field_tag :unit_value_human, nil, {class: "fullwidth", step: 0.01, 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
|
|
||||||
= f.number_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
|
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= f.label :unit_description, t(:spree_admin_unit_description)
|
= f.label :display_name, t('.display_name')
|
||||||
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
|
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
||||||
|
|
||||||
%div
|
.field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||||
.field
|
= f.label :unit_scale do
|
||||||
= f.label :sku, t('.sku')
|
= t('.unit_scale')
|
||||||
= f.text_field :sku, class: 'fullwidth'
|
= content_tag(:span, ' *', class: 'required')
|
||||||
.field
|
= f.hidden_field :variant_unit
|
||||||
= f.label :price, t('.price')
|
= f.hidden_field :variant_unit_scale
|
||||||
= f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'"
|
= f.select :variant_unit_with_scale,
|
||||||
.field
|
options_for_select(WeightsAndMeasures.variant_unit_options, @variant.variant_unit_with_scale),
|
||||||
= hidden_field_tag 'product_variant_unit', @product.variant_unit
|
{ include_blank: true },
|
||||||
= hidden_field_tag 'product_variant_unit_name', @product.variant_unit_name
|
{ class: "fullwidth no-input", 'aria-label': t('.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } }
|
||||||
= f.field_container :unit_price do
|
= error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control'
|
||||||
%div{style: "display: flex"}
|
|
||||||
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
|
|
||||||
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
|
|
||||||
"question-mark-with-tooltip-append-to-body" => "true",
|
|
||||||
"question-mark-with-tooltip-placement" => "top",
|
|
||||||
"question-mark-with-tooltip-animation" => true,
|
|
||||||
key: "'js.admin.unit_price_tooltip'"}
|
|
||||||
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]",
|
|
||||||
"class" => 'fullwidth', "disabled" => true, "ng-model" => "unit_price"}
|
|
||||||
%div{style: "color: black"}
|
|
||||||
= t("spree.admin.products.new.unit_price_legend")
|
|
||||||
%div{ 'set-on-demand' => '' }
|
|
||||||
.field.checkbox
|
|
||||||
%label
|
|
||||||
= f.check_box :on_demand
|
|
||||||
= t(:on_demand)
|
|
||||||
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
|
|
||||||
%a= t('admin.whats_this')
|
|
||||||
.field
|
.field
|
||||||
= f.label :on_hand, t(:on_hand)
|
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none")
|
||||||
.fullwidth
|
= error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control'
|
||||||
= f.text_field :on_hand
|
|
||||||
|
.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
|
||||||
|
= f.label :unit do
|
||||||
|
= t('.unit')
|
||||||
|
= content_tag(:span, ' *', class: 'required')
|
||||||
|
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('.unit'), 'data-popout-target': "button" do
|
||||||
|
= @variant.unit_to_display # Show the generated summary of unit values
|
||||||
|
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
|
||||||
|
.field
|
||||||
|
-# Show a composite field for unit_value and unit_description
|
||||||
|
= f.hidden_field :unit_value
|
||||||
|
= f.hidden_field :unit_description
|
||||||
|
= f.text_field :unit_value_with_description,
|
||||||
|
value: unit_value_with_description(@variant), 'aria-label': t('.unit_value'), required: true
|
||||||
|
.field
|
||||||
|
= f.label :display_as, t('.display_as')
|
||||||
|
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name
|
||||||
|
= error_message_on @variant, :unit_value
|
||||||
|
|
||||||
.right.six.columns.omega.label-block
|
%div
|
||||||
- if @product.variant_unit != 'weight'
|
.field
|
||||||
|
= f.label :sku, t('.sku')
|
||||||
|
= f.text_field :sku, class: 'fullwidth'
|
||||||
|
.field
|
||||||
|
= f.label :price do
|
||||||
|
= t('.price')
|
||||||
|
= content_tag(:span, ' *', class: 'required')
|
||||||
|
= f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip
|
||||||
|
.field
|
||||||
|
= hidden_field_tag 'variant_variant_unit', @variant.variant_unit
|
||||||
|
= hidden_field_tag 'variant_variant_unit_name', @variant.variant_unit_name
|
||||||
|
= f.field_container :unit_price do
|
||||||
|
%div{style: "display: flex"}
|
||||||
|
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
|
||||||
|
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
|
||||||
|
"question-mark-with-tooltip-append-to-body" => "true",
|
||||||
|
"question-mark-with-tooltip-placement" => "top",
|
||||||
|
"question-mark-with-tooltip-animation" => true,
|
||||||
|
key: "'js.admin.unit_price_tooltip'"}
|
||||||
|
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true}
|
||||||
|
%div{style: "color: black"}
|
||||||
|
= t("spree.admin.products.new.unit_price_legend")
|
||||||
|
%div
|
||||||
|
.field.checkbox
|
||||||
|
%label
|
||||||
|
= f.check_box :on_demand, data: { "action": "click->edit-variant#toggleOnHand" }
|
||||||
|
= t(:on_demand)
|
||||||
|
|
||||||
|
= render AdminTooltipComponent.new(text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right")
|
||||||
|
.field
|
||||||
|
= f.label :on_hand, t(:on_hand)
|
||||||
|
.fullwidth
|
||||||
|
= f.text_field :on_hand, data: { "edit-variant-target": "onHand" }
|
||||||
|
|
||||||
|
.right.six.columns.omega.label-block
|
||||||
.field
|
.field
|
||||||
= f.label 'weight', t(:weight)+' (kg)'
|
= f.label 'weight', t(:weight)+' (kg)'
|
||||||
- value = number_with_precision(@variant.weight, precision: 3)
|
- value = number_with_precision(@variant.weight, precision: 3)
|
||||||
= f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001
|
= f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001
|
||||||
|
|
||||||
- [:height, :width, :depth].each do |field|
|
- [:height, :width, :depth].each do |field|
|
||||||
|
.field
|
||||||
|
= f.label field, t(field)
|
||||||
|
- value = number_with_precision(@variant.send(field), precision: 2)
|
||||||
|
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= f.label field, t(field)
|
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
||||||
- value = number_with_precision(@variant.send(field), precision: 2)
|
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
||||||
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
|
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
= f.label :shipping_category_id, t(:shipping_categories)
|
||||||
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= f.label :shipping_category_id, t(:shipping_categories)
|
= f.label :primary_taxon, t('.variant_category')
|
||||||
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
|
= f.label :supplier do
|
||||||
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
= t(:spree_admin_supplier)
|
||||||
|
= content_tag(:span, ' *', class: 'required')
|
||||||
|
|
||||||
.field
|
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||||
= f.label :supplier, t(:spree_admin_supplier)
|
|
||||||
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
|
||||||
|
|
||||||
.clear
|
.clear
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ export default class BulkFormController extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pop out empty variant unit to allow browser side validation to focus the element
|
||||||
|
popoutEmptyVariantUnit() {
|
||||||
|
this.variantUnits = this.element.querySelectorAll("button.popout__button");
|
||||||
|
this.variantUnits.forEach((element) => {
|
||||||
|
if (element.textContent == "") {
|
||||||
|
element.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
|
|
||||||
#registerSubmit() {
|
#registerSubmit() {
|
||||||
@@ -135,7 +145,7 @@ export default class BulkFormController extends Controller {
|
|||||||
|
|
||||||
// Check if changed, and mark with class if it is.
|
// Check if changed, and mark with class if it is.
|
||||||
#checkIsChanged(element) {
|
#checkIsChanged(element) {
|
||||||
if(!element.isConnected) return false;
|
if (!element.isConnected) return false;
|
||||||
|
|
||||||
const changed = this.#isChanged(element);
|
const changed = this.#isChanged(element);
|
||||||
element.classList.toggle("changed", changed);
|
element.classList.toggle("changed", changed);
|
||||||
@@ -143,9 +153,8 @@ export default class BulkFormController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#isChanged(element) {
|
#isChanged(element) {
|
||||||
if (element.type == "checkbox") {
|
if (element.type == "checkbox") {
|
||||||
return element.defaultChecked !== undefined && element.checked != element.defaultChecked;
|
return element.defaultChecked !== undefined && element.checked != element.defaultChecked;
|
||||||
|
|
||||||
} else if (element.type == "select-one") {
|
} else if (element.type == "select-one") {
|
||||||
// (weird) Behavior of select element's include_blank option in Rails:
|
// (weird) Behavior of select element's include_blank option in Rails:
|
||||||
// If a select field has include_blank option selected (its value will be ''),
|
// If a select field has include_blank option selected (its value will be ''),
|
||||||
@@ -155,42 +164,49 @@ export default class BulkFormController extends Controller {
|
|||||||
opt.hasAttribute("selected"),
|
opt.hasAttribute("selected"),
|
||||||
);
|
);
|
||||||
const selectedOption = element.selectedOptions[0];
|
const selectedOption = element.selectedOptions[0];
|
||||||
const areBothBlank = selectedOption.value === '' && defaultSelected === undefined
|
const areBothBlank = selectedOption.value === "" && defaultSelected === undefined;
|
||||||
|
|
||||||
return !areBothBlank && selectedOption !== defaultSelected;
|
return !areBothBlank && selectedOption !== defaultSelected;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return element.defaultValue !== undefined && element.value != element.defaultValue;
|
return element.defaultValue !== undefined && element.value != element.defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#removeAnimationClasses(productRowElement) {
|
#removeAnimationClasses(productRowElement) {
|
||||||
productRowElement.classList.remove('slide-in');
|
productRowElement.classList.remove("slide-in");
|
||||||
productRowElement.removeEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
|
productRowElement.removeEventListener(
|
||||||
|
"animationend",
|
||||||
|
this.#removeAnimationClasses.bind(this, productRowElement),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#observeProductsTableRows() {
|
#observeProductsTableRows() {
|
||||||
this.productsTableObserver = new MutationObserver((mutationList, _observer) => {
|
this.productsTableObserver = new MutationObserver((mutationList, _observer) => {
|
||||||
const mutationRecord = mutationList[0];
|
const mutationRecord = mutationList[0];
|
||||||
|
|
||||||
if(mutationRecord) {
|
if (mutationRecord) {
|
||||||
// Right now we are only using it for product clone, so it's always first
|
// Right now we are only using it for product clone, so it's always first
|
||||||
const productRowElement = mutationRecord.addedNodes[0];
|
const productRowElement = mutationRecord.addedNodes[0];
|
||||||
|
|
||||||
if (productRowElement) {
|
if (productRowElement) {
|
||||||
productRowElement.addEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
|
productRowElement.addEventListener(
|
||||||
|
"animationend",
|
||||||
|
this.#removeAnimationClasses.bind(this, productRowElement),
|
||||||
|
);
|
||||||
// This is equivalent to form.elements.
|
// This is equivalent to form.elements.
|
||||||
const productRowFormElements = productRowElement.querySelectorAll('input, select, textarea, button');
|
const productRowFormElements = productRowElement.querySelectorAll(
|
||||||
|
"input, select, textarea, button",
|
||||||
|
);
|
||||||
this.#registerElements(productRowFormElements);
|
this.#registerElements(productRowFormElements);
|
||||||
this.toggleFormChanged();
|
this.toggleFormChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const productsTable = document.querySelector('.products');
|
const productsTable = document.querySelector(".products");
|
||||||
// Above mutation function will trigger,
|
// Above mutation function will trigger,
|
||||||
// whenever +products+ table rows (first level children) are mutated i.e. added or removed
|
// whenever +products+ table rows (first level children) are mutated i.e. added or removed
|
||||||
// right now we are using this for product clone
|
// right now we are using this for product clone
|
||||||
this.productsTableObserver.observe(productsTable, { childList: true });
|
this.productsTableObserver.observe(productsTable, { childList: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
189
app/webpacker/controllers/edit_variant_controller.js
Normal file
189
app/webpacker/controllers/edit_variant_controller.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { Controller } from "stimulus";
|
||||||
|
import OptionValueNamer from "js/services/option_value_namer";
|
||||||
|
import UnitPrices from "js/services/unit_prices";
|
||||||
|
|
||||||
|
// Dynamically update related variant fields
|
||||||
|
//
|
||||||
|
// TODO refactor so we can extract what's common with Bulk product page
|
||||||
|
export default class EditVariantController extends Controller {
|
||||||
|
static targets = ["onHand"];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.unitPrices = new UnitPrices();
|
||||||
|
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
|
||||||
|
// It could automatically find (and cache a ref to) each dom element and get/set the values.
|
||||||
|
this.variantUnit = this.element.querySelector('[id="variant_variant_unit"]');
|
||||||
|
this.variantUnitScale = this.element.querySelector('[id="variant_variant_unit_scale"]');
|
||||||
|
this.variantUnitName = this.element.querySelector('[id="variant_variant_unit_name"]');
|
||||||
|
this.variantUnitWithScale = this.element.querySelector(
|
||||||
|
'[id="variant_variant_unit_with_scale"]',
|
||||||
|
);
|
||||||
|
this.variantPrice = this.element.querySelector('[id="variant_price"]');
|
||||||
|
|
||||||
|
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
|
||||||
|
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.unitValue = this.element.querySelector('[id="variant_unit_value"]');
|
||||||
|
this.unitDescription = this.element.querySelector('[id="variant_unit_description"]');
|
||||||
|
this.unitValueWithDescription = this.element.querySelector(
|
||||||
|
'[id="variant_unit_value_with_description"]',
|
||||||
|
);
|
||||||
|
this.displayAs = this.element.querySelector('[id="variant_display_as"]');
|
||||||
|
this.unitToDisplay = this.element.querySelector('[id="variant_unit_to_display"]');
|
||||||
|
|
||||||
|
// on unit changed; update display_as:placeholder and unit_to_display
|
||||||
|
[this.variantUnit, this.variantUnitScale, this.variantUnitName].forEach((element) => {
|
||||||
|
element.addEventListener("change", this.#unitChanged.bind(this), { passive: true });
|
||||||
|
});
|
||||||
|
this.variantUnitName.addEventListener("input", this.#unitChanged.bind(this), { passive: true });
|
||||||
|
|
||||||
|
// on unit_value_with_description changed; update unit_value and unit_description
|
||||||
|
// on unit_value and/or unit_description changed; update display_as:placeholder and unit_to_display
|
||||||
|
this.unitValueWithDescription.addEventListener("input", this.#unitChanged.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// on display_as changed; update unit_to_display
|
||||||
|
// TODO: optimise to avoid unnecessary OptionValueNamer calc
|
||||||
|
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
|
||||||
|
|
||||||
|
// update Unit price when variant_unit_with_scale or price changes
|
||||||
|
[this.variantUnitWithScale, this.variantPrice].forEach((element) => {
|
||||||
|
element.addEventListener("change", this.#processUnitPrice.bind(this), { passive: true });
|
||||||
|
});
|
||||||
|
this.unitValueWithDescription.addEventListener("input", this.#processUnitPrice.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// on variantUnit change we need to check if weight needs to be toggled
|
||||||
|
this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true });
|
||||||
|
|
||||||
|
// make sure the unit is correct when page is reload after an error
|
||||||
|
this.#updateUnitDisplay();
|
||||||
|
// update unit price on page load
|
||||||
|
this.#processUnitPrice();
|
||||||
|
|
||||||
|
if (this.variantUnit.value === "weight") {
|
||||||
|
return this.#hideWeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Make sure to clean up anything that happened outside
|
||||||
|
// TODO remove all added event
|
||||||
|
this.variantUnit.removeEventListener("change", this.#toggleWeight.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOnHand(event) {
|
||||||
|
if (event.target.checked === true) {
|
||||||
|
this.onHandTarget.dataStock = this.onHandTarget.value;
|
||||||
|
this.onHandTarget.value = I18n.t("admin.products.variants.infinity");
|
||||||
|
this.onHandTarget.disabled = "disabled";
|
||||||
|
} else {
|
||||||
|
this.onHandTarget.removeAttribute("disabled");
|
||||||
|
this.onHandTarget.value = this.onHandTarget.dataStock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private
|
||||||
|
|
||||||
|
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
|
||||||
|
// and update hidden product fields
|
||||||
|
#unitChanged(event) {
|
||||||
|
//Hmm in hindsight the logic in product_controller should be inn this controller already. then we can do everything in one event, and store the generated name in an instance variable.
|
||||||
|
this.#extractUnitValues();
|
||||||
|
this.#updateUnitDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract unit_value and unit_description
|
||||||
|
#extractUnitValues() {
|
||||||
|
// Extract a number (optional) and text value, separated by a space.
|
||||||
|
const match = this.unitValueWithDescription.value.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
let unit_value = parseFloat(match[1].replace(",", "."));
|
||||||
|
unit_value = isNaN(unit_value) ? null : unit_value;
|
||||||
|
unit_value *= this.variantUnitScale.value ? this.variantUnitScale.value : 1; // Normalise to default scale
|
||||||
|
|
||||||
|
this.unitValue.value = unit_value;
|
||||||
|
this.unitDescription.value = match[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display_as placeholder and unit_to_display
|
||||||
|
#updateUnitDisplay() {
|
||||||
|
const unitDisplay = new OptionValueNamer(this.#variant()).name();
|
||||||
|
this.displayAs.placeholder = unitDisplay;
|
||||||
|
this.unitToDisplay.textContent = this.displayAs.value || unitDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A representation of the variant model to satisfy OptionValueNamer.
|
||||||
|
#variant() {
|
||||||
|
return {
|
||||||
|
unit_value: parseFloat(this.unitValue.value),
|
||||||
|
unit_description: this.unitDescription.value,
|
||||||
|
variant_unit: this.variantUnit.value,
|
||||||
|
variant_unit_scale: parseFloat(this.variantUnitScale.value),
|
||||||
|
variant_unit_name: this.variantUnitName.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
|
||||||
|
// and update hidden product fields
|
||||||
|
#updateUnitAndScale(event) {
|
||||||
|
const variant_unit_with_scale = this.variantUnitWithScale.value;
|
||||||
|
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
this.variantUnit.value = match[1];
|
||||||
|
this.variantUnitScale.value = parseFloat(match[2]);
|
||||||
|
} else {
|
||||||
|
// "items"
|
||||||
|
this.variantUnit.value = variant_unit_with_scale;
|
||||||
|
this.variantUnitScale.value = "";
|
||||||
|
}
|
||||||
|
this.variantUnit.dispatchEvent(new Event("change"));
|
||||||
|
this.variantUnitScale.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#processUnitPrice() {
|
||||||
|
const unit_type = this.variantUnit.value;
|
||||||
|
|
||||||
|
// TODO double check this
|
||||||
|
let unit_value = 1;
|
||||||
|
if (unit_type != "items") {
|
||||||
|
unit_value = this.unitValue.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit_price = this.unitPrices.displayableUnitPrice(
|
||||||
|
this.variantPrice.value,
|
||||||
|
parseFloat(this.variantUnitScale.value),
|
||||||
|
unit_type,
|
||||||
|
unit_value,
|
||||||
|
this.variantUnitName.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.element.querySelector('[id="variant_unit_price"]').value = unit_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideWeight() {
|
||||||
|
this.weight = this.element.querySelector('[id="variant_weight"]');
|
||||||
|
this.weight.parentElement.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleWeight() {
|
||||||
|
if (this.variantUnit.value === "weight") {
|
||||||
|
return this.#hideWeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show weight
|
||||||
|
this.weight = this.element.querySelector('[id="variant_weight"]');
|
||||||
|
this.weight.parentElement.style.display = "block";
|
||||||
|
// Clearing weight value to remove calculated weight for a variant with unit set to "weight"
|
||||||
|
// See Spree::Variant hook update_weight_from_unit_value
|
||||||
|
this.weight.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Controller } from "stimulus";
|
|
||||||
|
|
||||||
// Dynamically update related Product unit fields (expected to move to Variant due to Product Refactor)
|
|
||||||
//
|
|
||||||
export default class ProductController extends Controller {
|
|
||||||
connect() {
|
|
||||||
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
|
|
||||||
// It could automatically find (and cache a ref to) each dom element and get/set the values.
|
|
||||||
this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]');
|
|
||||||
this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]');
|
|
||||||
this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]');
|
|
||||||
|
|
||||||
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
|
|
||||||
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// private
|
|
||||||
|
|
||||||
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
|
|
||||||
// and update hidden product fields
|
|
||||||
#updateUnitAndScale(event) {
|
|
||||||
const variant_unit_with_scale = this.variantUnitWithScale.value;
|
|
||||||
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
this.variantUnit.value = match[1];
|
|
||||||
this.variantUnitScale.value = parseFloat(match[2]);
|
|
||||||
} else {
|
|
||||||
// "items"
|
|
||||||
this.variantUnit.value = variant_unit_with_scale;
|
|
||||||
this.variantUnitScale.value = "";
|
|
||||||
}
|
|
||||||
this.variantUnit.dispatchEvent(new Event("change"));
|
|
||||||
this.variantUnitScale.dispatchEvent(new Event("change"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,8 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changePage(event) {
|
changePage(event) {
|
||||||
|
const productsForm = document.querySelector("#products-form");
|
||||||
|
productsForm.scrollIntoView({ behavior: "smooth" });
|
||||||
this.page.value = event.target.dataset.page;
|
this.page.value = event.target.dataset.page;
|
||||||
this.submitSearch();
|
this.submitSearch();
|
||||||
this.page.value = 1;
|
this.page.value = 1;
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import OptionValueNamer from "js/services/option_value_namer";
|
|||||||
//
|
//
|
||||||
export default class VariantController extends Controller {
|
export default class VariantController extends Controller {
|
||||||
connect() {
|
connect() {
|
||||||
// Assuming these will be available on the variant soon, just a quick hack to find the product fields:
|
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
|
||||||
const product = this.element.closest("[data-record-id]");
|
// It could automatically find (and cache a ref to) each dom element and get/set the values.
|
||||||
this.variantUnit = product.querySelector('[name$="[variant_unit]"]');
|
this.variantUnit = this.element.querySelector('[name$="[variant_unit]"]');
|
||||||
this.variantUnitScale = product.querySelector('[name$="[variant_unit_scale]"]');
|
this.variantUnitScale = this.element.querySelector('[name$="[variant_unit_scale]"]');
|
||||||
this.variantUnitName = product.querySelector('[name$="[variant_unit_name]"]');
|
this.variantUnitName = this.element.querySelector('[name$="[variant_unit_name]"]');
|
||||||
|
this.variantUnitWithScale = this.element.querySelector('[name$="[variant_unit_with_scale]"]');
|
||||||
|
|
||||||
|
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
|
||||||
|
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.unitValue = this.element.querySelector('[name$="[unit_value]"]');
|
this.unitValue = this.element.querySelector('[name$="[unit_value]"]');
|
||||||
this.unitDescription = this.element.querySelector('[name$="[unit_description]"]');
|
this.unitDescription = this.element.querySelector('[name$="[unit_description]"]');
|
||||||
@@ -76,11 +82,27 @@ export default class VariantController extends Controller {
|
|||||||
return {
|
return {
|
||||||
unit_value: parseFloat(this.unitValue.value),
|
unit_value: parseFloat(this.unitValue.value),
|
||||||
unit_description: this.unitDescription.value,
|
unit_description: this.unitDescription.value,
|
||||||
product: {
|
variant_unit: this.variantUnit.value,
|
||||||
variant_unit: this.variantUnit.value,
|
variant_unit_scale: parseFloat(this.variantUnitScale.value),
|
||||||
variant_unit_scale: parseFloat(this.variantUnitScale.value),
|
variant_unit_name: this.variantUnitName.value,
|
||||||
variant_unit_name: this.variantUnitName.value,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
|
||||||
|
// and update hidden product fields
|
||||||
|
#updateUnitAndScale(event) {
|
||||||
|
const variant_unit_with_scale = this.variantUnitWithScale.value;
|
||||||
|
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
this.variantUnit.value = match[1];
|
||||||
|
this.variantUnitScale.value = parseFloat(match[2]);
|
||||||
|
} else {
|
||||||
|
// "items"
|
||||||
|
this.variantUnit.value = variant_unit_with_scale;
|
||||||
|
this.variantUnitScale.value = "";
|
||||||
|
}
|
||||||
|
this.variantUnit.dispatchEvent(new Event("change"));
|
||||||
|
this.variantUnitScale.dispatchEvent(new Event("change"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
border: none;
|
border: none;
|
||||||
border-left: $border-radius solid $color-3;
|
border-left: $border-radius solid $color-3;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05), 0px 2px 2px rgba(0, 0, 0, 0.07);
|
box-shadow:
|
||||||
|
0px 1px 0px rgba(0, 0, 0, 0.05),
|
||||||
|
0px 2px 2px rgba(0, 0, 0, 0.07);
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
|
|
||||||
@@ -47,3 +49,22 @@
|
|||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connected-app__vine {
|
||||||
|
margin: 1em 0;
|
||||||
|
|
||||||
|
.connected-app__vine-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
.vine-api-key {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vine-disable {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Customisations for the new Bulk Edit Products page only
|
// Customisations for the new Bulk Edit Products page only
|
||||||
// Scoped to containing div, because Turbo messes with body classes
|
// Scoped to containing div, because Turbo messes with body classes
|
||||||
|
@import "../admin_v3/pages/unit_popout";
|
||||||
|
|
||||||
#products_v3_page {
|
#products_v3_page {
|
||||||
#content > .row:first-child > .container:first-child {
|
#content > .row:first-child > .container:first-child {
|
||||||
// Allow table to extend to full width of available screen space
|
// Allow table to extend to full width of available screen space
|
||||||
@@ -311,89 +313,7 @@
|
|||||||
|
|
||||||
// Popout widget (todo: move to separate fiel)
|
// Popout widget (todo: move to separate fiel)
|
||||||
.popout {
|
.popout {
|
||||||
position: relative;
|
@include unit_popout;
|
||||||
|
|
||||||
&__button {
|
|
||||||
// override button styles
|
|
||||||
&.popout__button {
|
|
||||||
background: $color-tbl-cell-bg;
|
|
||||||
color: $color-txt-text;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-color: transparent;
|
|
||||||
font-weight: normal;
|
|
||||||
padding-left: $border-radius; // Super compact
|
|
||||||
padding-right: 1rem; // Retain space for arrow
|
|
||||||
height: auto;
|
|
||||||
min-width: 2em;
|
|
||||||
min-height: 1lh; // Line height of parent
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
background: $color-tbl-cell-bg;
|
|
||||||
color: $color-txt-text;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.changed {
|
|
||||||
border-color: $color-txt-changed-brd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(:active):not(:focus):not(.changed) {
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
// for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead
|
|
||||||
&:before {
|
|
||||||
// for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles.
|
|
||||||
// extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes?
|
|
||||||
font-family: FontAwesome;
|
|
||||||
text-decoration: inherit;
|
|
||||||
display: inline-block;
|
|
||||||
speak: none;
|
|
||||||
content: "\f078";
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 0; // Required for empty buttons
|
|
||||||
right: $border-radius;
|
|
||||||
font-size: 0.67em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__container {
|
|
||||||
position: absolute;
|
|
||||||
top: -0.6em;
|
|
||||||
left: -0.2em;
|
|
||||||
z-index: 1; // Cover below row when hover
|
|
||||||
width: 9em;
|
|
||||||
|
|
||||||
padding: $padding-tbl-cell;
|
|
||||||
|
|
||||||
background: $color-tbl-cell-bg;
|
|
||||||
border-radius: $border-radius;
|
|
||||||
box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25);
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin-bottom: 0.75em;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
color: transparent; // hide value completely
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.image-field {
|
a.image-field {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
@import "../admin/dialog";
|
@import "../admin/dialog";
|
||||||
@import "../admin/disabled";
|
@import "../admin/disabled";
|
||||||
@import "components/dropdown"; // admin_v3
|
@import "components/dropdown"; // admin_v3
|
||||||
|
@import "pages/edit_variant"; // admin_v3
|
||||||
@import "pages/enterprise_index_panels"; // admin_v3
|
@import "pages/enterprise_index_panels"; // admin_v3
|
||||||
@import "../admin/enterprises";
|
@import "../admin/enterprises";
|
||||||
@import "../admin/filters_and_controls";
|
@import "../admin/filters_and_controls";
|
||||||
|
|||||||
87
app/webpacker/css/admin_v3/pages/_unit_popout.scss
Normal file
87
app/webpacker/css/admin_v3/pages/_unit_popout.scss
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Popout widget
|
||||||
|
@mixin unit_popout {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
// override button styles
|
||||||
|
&.popout__button {
|
||||||
|
background: $color-tbl-cell-bg;
|
||||||
|
color: $color-txt-text;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-color: transparent;
|
||||||
|
font-weight: normal;
|
||||||
|
padding-left: $border-radius; // Super compact
|
||||||
|
padding-right: 1rem; // Retain space for arrow
|
||||||
|
height: auto;
|
||||||
|
min-width: 2em;
|
||||||
|
min-height: 1lh; // Line height of parent
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
background: $color-tbl-cell-bg;
|
||||||
|
color: $color-txt-text;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.changed {
|
||||||
|
border-color: $color-txt-changed-brd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:active):not(:focus):not(.changed) {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
// for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead
|
||||||
|
&:before {
|
||||||
|
// for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles.
|
||||||
|
// extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes?
|
||||||
|
font-family: FontAwesome;
|
||||||
|
text-decoration: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
speak: none;
|
||||||
|
content: "\f078";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0; // Required for empty buttons
|
||||||
|
right: $border-radius;
|
||||||
|
font-size: 0.67em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.6em;
|
||||||
|
left: -0.2em;
|
||||||
|
z-index: 1; // Cover below row when hover
|
||||||
|
width: 9em;
|
||||||
|
|
||||||
|
padding: $padding-tbl-cell;
|
||||||
|
|
||||||
|
background: $color-tbl-cell-bg;
|
||||||
|
border-radius: $border-radius;
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25);
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 0.75em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: transparent; // hide value completely
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
55
app/webpacker/css/admin_v3/pages/edit_variant.scss
Normal file
55
app/webpacker/css/admin_v3/pages/edit_variant.scss
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
@import "unit_popout";
|
||||||
|
|
||||||
|
#edit_variant {
|
||||||
|
|
||||||
|
.popout {
|
||||||
|
@include unit_popout;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
// override popout button styles
|
||||||
|
&.popout__button {
|
||||||
|
// Reapplying button style from buttons.css
|
||||||
|
background-color: $color-btn-bg;
|
||||||
|
border: 1px solid $color-btn-bg;
|
||||||
|
color: $color-btn-text;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
text-decoration: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
speak: none;
|
||||||
|
content: "\f078";
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0; // Required for empty buttons
|
||||||
|
right: $border-radius;
|
||||||
|
font-size: 0.67em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reapplying button style from buttons.css
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid $color-btn-hover-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-btn-hover-bg;
|
||||||
|
border: 1px solid $color-btn-hover-bg;
|
||||||
|
color: $color-btn-hover-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
width: max-content;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/webpacker/js/services/localize_currency.js
Normal file
24
app/webpacker/js/services/localize_currency.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Convert number to string currency using injected currency configuration.
|
||||||
|
|
||||||
|
// Requires global variable from page: ofn_currency_config
|
||||||
|
export default function (amount) {
|
||||||
|
// Set country code (eg. "US").
|
||||||
|
const currency_code = ofn_currency_config.display_currency
|
||||||
|
? " " + ofn_currency_config.currency
|
||||||
|
: "";
|
||||||
|
// Set decimal points, 2 or 0 if hide_cents.
|
||||||
|
const decimals = ofn_currency_config.hide_cents === "true" ? 0 : 2;
|
||||||
|
// Set format if the currency symbol should come after the number, otherwise (default) use the locale setting.
|
||||||
|
const format = ofn_currency_config.symbol_position === "after" ? "%n %u" : undefined;
|
||||||
|
// We need to use parseFloat as the amount should come in as a string.
|
||||||
|
amount = parseFloat(amount);
|
||||||
|
|
||||||
|
// Build the final price string.
|
||||||
|
return (
|
||||||
|
I18n.toCurrency(amount, {
|
||||||
|
precision: decimals,
|
||||||
|
unit: ofn_currency_config.symbol,
|
||||||
|
format: format,
|
||||||
|
}) + currency_code
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import VariantUnitManager from "../../js/services/variant_unit_manager";
|
import VariantUnitManager from "js/services/variant_unit_manager";
|
||||||
|
|
||||||
// Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
|
// Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
|
||||||
export default class OptionValueNamer {
|
export default class OptionValueNamer {
|
||||||
@@ -9,7 +9,7 @@ export default class OptionValueNamer {
|
|||||||
|
|
||||||
name() {
|
name() {
|
||||||
const [value, unit] = this.option_value_value_unit();
|
const [value, unit] = this.option_value_value_unit();
|
||||||
const separator = this.value_scaled() ? '' : ' ';
|
const separator = this.value_scaled() ? "" : " ";
|
||||||
const name_fields = [];
|
const name_fields = [];
|
||||||
if (value && unit) {
|
if (value && unit) {
|
||||||
name_fields.push(`${value}${separator}${unit}`);
|
name_fields.push(`${value}${separator}${unit}`);
|
||||||
@@ -20,21 +20,21 @@ export default class OptionValueNamer {
|
|||||||
if (this.variant.unit_description) {
|
if (this.variant.unit_description) {
|
||||||
name_fields.push(this.variant.unit_description);
|
name_fields.push(this.variant.unit_description);
|
||||||
}
|
}
|
||||||
return name_fields.join(' ');
|
return name_fields.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
value_scaled() {
|
value_scaled() {
|
||||||
return !!this.variant.product.variant_unit_scale;
|
return !!this.variant.variant_unit_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
option_value_value_unit() {
|
option_value_value_unit() {
|
||||||
let value, unit_name;
|
let value, unit_name;
|
||||||
if (this.variant.unit_value) {
|
if (this.variant.unit_value) {
|
||||||
if (this.variant.product.variant_unit === "weight" || this.variant.product.variant_unit === "volume") {
|
if (this.variant.variant_unit === "weight" || this.variant.variant_unit === "volume") {
|
||||||
[value, unit_name] = this.option_value_value_unit_scaled();
|
[value, unit_name] = this.option_value_value_unit_scaled();
|
||||||
} else {
|
} else {
|
||||||
value = this.variant.unit_value;
|
value = this.variant.unit_value;
|
||||||
unit_name = this.pluralize(this.variant.product.variant_unit_name, value);
|
unit_name = this.pluralize(this.variant.variant_unit_name, value);
|
||||||
}
|
}
|
||||||
if (value == parseInt(value, 10)) {
|
if (value == parseInt(value, 10)) {
|
||||||
value = parseInt(value, 10);
|
value = parseInt(value, 10);
|
||||||
@@ -55,7 +55,7 @@ export default class OptionValueNamer {
|
|||||||
}
|
}
|
||||||
return I18n.t(["inflections", unit_key], {
|
return I18n.t(["inflections", unit_key], {
|
||||||
count: count,
|
count: count,
|
||||||
defaultValue: unit_name
|
defaultValue: unit_name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,17 +83,21 @@ export default class OptionValueNamer {
|
|||||||
// 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.
|
||||||
const product = this.variant.product;
|
const scales = this.variantUnitManager.compatibleUnitScales(
|
||||||
const scales = this.variantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit);
|
this.variant.variant_unit_scale,
|
||||||
|
this.variant.variant_unit,
|
||||||
|
);
|
||||||
const variantUnitValue = this.variant.unit_value;
|
const variantUnitValue = this.variant.unit_value;
|
||||||
|
|
||||||
// sets largestScale = last element in filtered scales array
|
// sets largestScale = last element in filtered scales array
|
||||||
const largestScale = scales.filter(s => variantUnitValue / s >= 1).slice(-1)[0];
|
const largestScale = scales.filter((s) => variantUnitValue / s >= 1).slice(-1)[0];
|
||||||
if (largestScale) {
|
if (largestScale) {
|
||||||
return [largestScale, this.variantUnitManager.getUnitName(largestScale, product.variant_unit)];
|
return [
|
||||||
|
largestScale,
|
||||||
|
this.variantUnitManager.getUnitName(largestScale, this.variant.variant_unit),
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
return [scales[0], this.variantUnitManager.getUnitName(scales[0], product.variant_unit)];
|
return [scales[0], this.variantUnitManager.getUnitName(scales[0], this.variant.variant_unit)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
app/webpacker/js/services/price_parser.js
Normal file
45
app/webpacker/js/services/price_parser.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export default class PriceParser {
|
||||||
|
parse(price) {
|
||||||
|
if (!price) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// used decimal and thousands separators from currency configuration
|
||||||
|
const decimalSeparator = I18n.toCurrency(0.1, { precision: 1, unit: "" }).substring(1, 2);
|
||||||
|
const thousandsSeparator = I18n.toCurrency(1000, { precision: 1, unit: "" }).substring(1, 2);
|
||||||
|
|
||||||
|
// Replace comma used as a decimal separator and remplace by "."
|
||||||
|
price = this.replaceCommaByFinalPoint(price);
|
||||||
|
|
||||||
|
// Remove configured thousands separator if it is actually a thousands separator
|
||||||
|
price = this.removeThousandsSeparator(price, thousandsSeparator);
|
||||||
|
|
||||||
|
if (decimalSeparator === ",") {
|
||||||
|
price = price.replace(",", ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
price = parseFloat(price);
|
||||||
|
|
||||||
|
if (isNaN(price)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceCommaByFinalPoint(price) {
|
||||||
|
if (price.match(/^[0-9]*(,{1})[0-9]{1,2}$/g)) {
|
||||||
|
return price.replace(",", ".");
|
||||||
|
} else {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeThousandsSeparator(price, thousandsSeparator) {
|
||||||
|
if (new RegExp(`^([0-9]*(${thousandsSeparator}{1})[0-9]{3}[0-9\.,]*)*$`, "g").test(price)) {
|
||||||
|
return price.replaceAll(thousandsSeparator, "");
|
||||||
|
} else {
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/webpacker/js/services/unit_prices.js
Normal file
51
app/webpacker/js/services/unit_prices.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import PriceParser from "js/services/price_parser";
|
||||||
|
import VariantUnitManager from "js/services/variant_unit_manager";
|
||||||
|
import localizeCurrency from "js/services/localize_currency";
|
||||||
|
|
||||||
|
export default class UnitPrices {
|
||||||
|
constructor() {
|
||||||
|
this.variantUnitManager = new VariantUnitManager();
|
||||||
|
this.priceParser = new PriceParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) {
|
||||||
|
price = this.priceParser.parse(price);
|
||||||
|
if (price && !isNaN(price) && unit_type && unit_value) {
|
||||||
|
const value = localizeCurrency(
|
||||||
|
this.price(price, scale, unit_type, unit_value, variant_unit_name),
|
||||||
|
);
|
||||||
|
const unit = this.unit(scale, unit_type, variant_unit_name);
|
||||||
|
return `${value} / ${unit}`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
price(price, scale, unit_type, unit_value) {
|
||||||
|
return price / this.denominator(scale, unit_type, unit_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
denominator(scale, unit_type, unit_value) {
|
||||||
|
const unit = this.unit(scale, unit_type);
|
||||||
|
if (unit === "lb") {
|
||||||
|
return unit_value / 453.6;
|
||||||
|
} else if (unit === "kg") {
|
||||||
|
return unit_value / 1000;
|
||||||
|
} else {
|
||||||
|
return unit_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unit(scale, unit_type, variant_unit_name = "") {
|
||||||
|
if (variant_unit_name.length > 0 && unit_type === "items") {
|
||||||
|
return variant_unit_name;
|
||||||
|
} else if (unit_type === "items") {
|
||||||
|
return "item";
|
||||||
|
} else if (this.variantUnitManager.systemOfMeasurement(scale, unit_type) === "imperial") {
|
||||||
|
return "lb";
|
||||||
|
} else if (unit_type === "weight") {
|
||||||
|
return "kg";
|
||||||
|
} else if (unit_type === "volume") {
|
||||||
|
return "L";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,33 +7,41 @@ export default class VariantUnitManager {
|
|||||||
|
|
||||||
getUnitName(scale, unitType) {
|
getUnitName(scale, unitType) {
|
||||||
if (this.units[unitType][scale]) {
|
if (this.units[unitType][scale]) {
|
||||||
return this.units[unitType][scale]['name'];
|
return this.units[unitType][scale]["name"];
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Filter by measurement system
|
// Filter by measurement system
|
||||||
compatibleUnitScales(scale, unitType) {
|
compatibleUnitScales(scale, unitType) {
|
||||||
const scaleSystem = this.units[unitType][scale]['system'];
|
const scaleSystem = this.units[unitType][scale]["system"];
|
||||||
|
|
||||||
return Object.entries(this.units[unitType])
|
return Object.entries(this.units[unitType])
|
||||||
.filter(([scale, scaleInfo]) => {
|
.filter(([scale, scaleInfo]) => {
|
||||||
return scaleInfo['system'] == scaleSystem;
|
return scaleInfo["system"] == scaleSystem;
|
||||||
})
|
})
|
||||||
.map(([scale, _]) => parseFloat(scale))
|
.map(([scale, _]) => parseFloat(scale))
|
||||||
.sort();
|
.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemOfMeasurement(scale, unitType) {
|
||||||
|
if (this.units[unitType][scale]) {
|
||||||
|
return this.units[unitType][scale]["system"];
|
||||||
|
} else {
|
||||||
|
return "custom";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
|
|
||||||
#loadUnits(units) {
|
#loadUnits(units) {
|
||||||
// Transform unit scale to a JS Number for compatibility. This would be way simpler in Ruby or Coffeescript!!
|
// Transform unit scale to a JS Number for compatibility. This would be way simpler in Ruby or Coffeescript!!
|
||||||
const unitsTransformed = Object.entries(units).map(([measurement, measurementInfo]) => {
|
const unitsTransformed = Object.entries(units).map(([measurement, measurementInfo]) => {
|
||||||
const measurementInfoTransformed = Object.fromEntries(Object.entries(measurementInfo).map(([scale, unitInfo]) =>
|
const measurementInfoTransformed = Object.fromEntries(
|
||||||
[ parseFloat(scale), unitInfo ]
|
Object.entries(measurementInfo).map(([scale, unitInfo]) => [parseFloat(scale), unitInfo]),
|
||||||
));
|
);
|
||||||
return [ measurement, measurementInfoTransformed ];
|
return [measurement, measurementInfoTransformed];
|
||||||
});
|
});
|
||||||
return Object.fromEntries(unitsTransformed);
|
return Object.fromEntries(unitsTransformed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,5 +255,16 @@ module Openfoodnetwork
|
|||||||
config.exceptions_app = self.routes
|
config.exceptions_app = self.routes
|
||||||
|
|
||||||
config.view_component.generate.sidecar = true # Always generate components in subfolders
|
config.view_component.generate.sidecar = true # Always generate components in subfolders
|
||||||
|
|
||||||
|
# Database encryption configuration, required for VINE connected app
|
||||||
|
config.active_record.encryption.primary_key = ENV.fetch(
|
||||||
|
"ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY", nil
|
||||||
|
)
|
||||||
|
config.active_record.encryption.deterministic_key = ENV.fetch(
|
||||||
|
"ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY", nil
|
||||||
|
)
|
||||||
|
config.active_record.encryption.key_derivation_salt = ENV.fetch(
|
||||||
|
"ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT", nil
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ if ENV["OPENID_APP_ID"].present? && ENV["OPENID_APP_SECRET"].present?
|
|||||||
config.omniauth :openid_connect, {
|
config.omniauth :openid_connect, {
|
||||||
name: :openid_connect,
|
name: :openid_connect,
|
||||||
issuer: "https://login.lescommuns.org/auth/realms/data-food-consortium",
|
issuer: "https://login.lescommuns.org/auth/realms/data-food-consortium",
|
||||||
scope: [:openid, :profile, :email],
|
scope: [:openid, :profile, :email, :offline_access],
|
||||||
response_type: :code,
|
response_type: :code,
|
||||||
uid_field: "email",
|
uid_field: "email",
|
||||||
discovery: true,
|
discovery: true,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user