mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-12 18:36:49 +00:00
Compare commits
209 Commits
v4.6.12
...
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 | ||
|
|
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 | ||
|
|
641b7beee3 | ||
|
|
60e8db9adc | ||
|
|
b7285e48b3 | ||
|
|
bc87c98e92 | ||
|
|
216883101e | ||
|
|
aa5feb6605 | ||
|
|
b2b6847882 | ||
|
|
d01d312b4f | ||
|
|
a74cf97083 | ||
|
|
03dbd54b25 | ||
|
|
fafd86a2db | ||
|
|
eb8050d61d | ||
|
|
ef6e37e7ca | ||
|
|
ffc2fed9b5 |
@@ -16,7 +16,7 @@ STRIPE_PUBLIC_TEST_API_KEY="bogus_stripe_publishable_key"
|
||||
SITE_URL="test.host"
|
||||
|
||||
OPENID_APP_ID="test-provider"
|
||||
OPENID_APP_SECRET="12345"
|
||||
OPENID_APP_SECRET="dummy-openid-app-secret-token"
|
||||
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
||||
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="test_primary_key"
|
||||
|
||||
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
|
||||
- [ ] Include translations: `script/release/update_locales`
|
||||
@@ -26,8 +26,9 @@ assignees: ''
|
||||
- [ ] Move this issue to Test Ready.
|
||||
- [ ] Notify `@testers` in [#testing].
|
||||
- [ ] 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)
|
||||
- [ ] Deploy the new release to all managed instances.
|
||||
@@ -40,7 +41,7 @@ assignees: ''
|
||||
</details>
|
||||
- [ ] Notify [#instance-managers]:
|
||||
> @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.
|
||||
|
||||
@@ -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
|
||||
[#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
|
||||
[#core-devs]: https://openfoodnetwork.slack.com/archives/GK2T38QPJ
|
||||
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
|
||||
[Transifex Client]: https://developers.transifex.com/docs/cli
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*.yaml
|
||||
*.json
|
||||
*.html
|
||||
**/*.rb
|
||||
|
||||
# JS
|
||||
# Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js
|
||||
@@ -27,6 +28,5 @@ postcss.config.js
|
||||
/coverage/
|
||||
/engines/
|
||||
/public/
|
||||
/spec/
|
||||
/tmp/
|
||||
/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.
|
||||
- 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.
|
||||
- [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
|
||||
* Redis (for background jobs)
|
||||
* Chrome (for testing)
|
||||
|
||||
@@ -187,9 +187,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
product.variants.length > 0
|
||||
|
||||
|
||||
$scope.hasUnit = (product) ->
|
||||
product.variant_unit_with_scale?
|
||||
|
||||
$scope.hasUnit = (variant) ->
|
||||
variant.variant_unit_with_scale?
|
||||
|
||||
$scope.variantSaved = (variant) ->
|
||||
variant.hasOwnProperty('id') && variant.id > 0
|
||||
@@ -242,32 +241,28 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
|
||||
$window.location = destination
|
||||
|
||||
$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
|
||||
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")
|
||||
match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/)
|
||||
if match
|
||||
product = BulkProducts.find product.id
|
||||
variant.unit_value = parseFloat(match[1].replace(",", "."))
|
||||
variant.unit_value = null if isNaN(variant.unit_value)
|
||||
if variant.unit_value && product.variant_unit_scale
|
||||
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2))
|
||||
if variant.unit_value && variant.variant_unit_scale
|
||||
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2))
|
||||
variant.unit_description = match[3]
|
||||
|
||||
$scope.incrementLimit = ->
|
||||
@@ -321,13 +316,6 @@ filterSubmitProducts = (productsToFilter) ->
|
||||
if product.hasOwnProperty("price")
|
||||
filteredProduct.price = product.price
|
||||
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
|
||||
filteredProduct.on_hand = product.on_hand
|
||||
hasUpdatableProperty = true
|
||||
@@ -383,6 +371,14 @@ filterSubmitVariant = (variant) ->
|
||||
if variant.hasOwnProperty("producer_id")
|
||||
filteredVariant.supplier_id = variant.producer_id
|
||||
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}
|
||||
|
||||
|
||||
|
||||
@@ -4,31 +4,30 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
|
||||
scope.$watchCollection ->
|
||||
return [
|
||||
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
|
||||
scope.product.variant_unit_name
|
||||
scope.product.variant_unit_with_scale
|
||||
scope.variant.variant_unit_name
|
||||
scope.variant.variant_unit_with_scale
|
||||
]
|
||||
, ->
|
||||
[variant_unit, variant_unit_scale] = productUnitProperties()
|
||||
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
|
||||
variant_object =
|
||||
variant_object =
|
||||
unit_value: unit_value
|
||||
unit_description: unit_description
|
||||
product:
|
||||
variant_unit_scale: variant_unit_scale
|
||||
variant_unit: variant_unit
|
||||
variant_unit_name: scope.product.variant_unit_name
|
||||
variant_unit_scale: variant_unit_scale
|
||||
variant_unit: variant_unit
|
||||
variant_unit_name: scope.variant.variant_unit_name
|
||||
|
||||
scope.placeholder_text = new OptionValueNamer(variant_object).name()
|
||||
|
||||
productUnitProperties = ->
|
||||
# get relevant product properties
|
||||
if scope.product.variant_unit_with_scale?
|
||||
match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if scope.variant.variant_unit_with_scale?
|
||||
match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
variant_unit = match[1]
|
||||
variant_unit_scale = parseFloat(match[2])
|
||||
else
|
||||
variant_unit = scope.product.variant_unit_with_scale
|
||||
variant_unit = scope.variant.variant_unit_with_scale
|
||||
variant_unit_scale = null
|
||||
else
|
||||
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 *= variant_unit_scale if unit_value && variant_unit_scale
|
||||
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.getLineItemScale = (lineItem) ->
|
||||
if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume")
|
||||
lineItem.units_product.variant_unit_scale
|
||||
if lineItem.units_variant && lineItem.units_variant.variant_unit_scale && (lineItem.units_variant.variant_unit == "weight" || lineItem.units_variant.variant_unit == "volume")
|
||||
lineItem.units_variant.variant_unit_scale
|
||||
else
|
||||
1
|
||||
|
||||
$scope.sumUnitValues = ->
|
||||
sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
|
||||
if lineItem.units_product.variant_unit == "items"
|
||||
if lineItem.units_variant.variant_unit == "items"
|
||||
sum + lineItem.quantity
|
||||
else
|
||||
sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
|
||||
@@ -214,7 +214,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
|
||||
|
||||
$scope.sumMaxUnitValues = ->
|
||||
sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
|
||||
if lineItem.units_product.variant_unit == "items"
|
||||
if lineItem.units_variant.variant_unit == "items"
|
||||
sum + lineItem.max_quantity
|
||||
else
|
||||
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)
|
||||
true
|
||||
|
||||
$scope.getScale = (unitsProduct, unitsVariant) ->
|
||||
if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume")
|
||||
unitsProduct.variant_unit_scale
|
||||
else if unitsProduct.hasOwnProperty("variant_unit") && unitsProduct.variant_unit == "items"
|
||||
$scope.getScale = (unitsVariant) ->
|
||||
if unitsVariant.hasOwnProperty("variant_unit") && (unitsVariant.variant_unit == "weight" || unitsVariant.variant_unit == "volume")
|
||||
unitsVariant.variant_unit_scale
|
||||
else if unitsVariant.hasOwnProperty("variant_unit") && unitsVariant.variant_unit == "items"
|
||||
1
|
||||
else
|
||||
null
|
||||
|
||||
$scope.getFormattedValueWithUnitName = (value, unitsProduct, unitsVariant, scale) ->
|
||||
unit_name = VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit)
|
||||
$scope.getFormattedValueWithUnitName = (value, unitsVariant, scale) ->
|
||||
unit_name = VariantUnitManager.getUnitName(scale, unitsVariant.variant_unit)
|
||||
$scope.roundToThreeDecimals(value) + " " + unit_name
|
||||
|
||||
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
|
||||
scale = $scope.getScale(unitsProduct, unitsVariant)
|
||||
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsVariant) ->
|
||||
scale = $scope.getScale(unitsVariant)
|
||||
if scale && value
|
||||
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
|
||||
''
|
||||
|
||||
$scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
|
||||
scale = $scope.getScale(unitsProduct, unitsVariant)
|
||||
$scope.formattedValueWithUnitName = (value, unitsVariant) ->
|
||||
scale = $scope.getScale(unitsVariant)
|
||||
if scale
|
||||
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
|
||||
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
|
||||
else
|
||||
''
|
||||
|
||||
$scope.fulfilled = (sumOfUnitValues) ->
|
||||
# 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 &&
|
||||
$scope.selectedUnitsProduct.hasOwnProperty("variant_unit")
|
||||
if $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume"
|
||||
scale = $scope.selectedUnitsProduct.variant_unit_scale
|
||||
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
|
||||
$scope.selectedUnitsVariant.hasOwnProperty("variant_unit")
|
||||
|
||||
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
|
||||
$scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size)
|
||||
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)
|
||||
angular.module("admin.products")
|
||||
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
|
||||
$scope.product = { master: {} }
|
||||
$scope.product.master.product = $scope.product
|
||||
$scope.product = {}
|
||||
$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.processUnitValueWithDescription()
|
||||
$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()
|
||||
|
||||
@@ -38,24 +37,24 @@ angular.module("admin.products")
|
||||
# Extract unit_value and unit_description from text field unit_value_with_description,
|
||||
# and update hidden variant fields
|
||||
$scope.processUnitValueWithDescription = ->
|
||||
if $scope.product.master.hasOwnProperty("unit_value_with_description")
|
||||
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
|
||||
if $scope.product.hasOwnProperty("unit_value_with_description")
|
||||
match = $scope.product.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
|
||||
if match
|
||||
$scope.product.master.unit_value = PriceParser.parse(match[1])
|
||||
$scope.product.master.unit_value = null if isNaN($scope.product.master.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.master.unit_description = match[3]
|
||||
$scope.product.unit_value = PriceParser.parse(match[1])
|
||||
$scope.product.unit_value = null if isNaN($scope.product.unit_value)
|
||||
$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.unit_description = match[3]
|
||||
else
|
||||
value = $scope.product.master.unit_value
|
||||
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
|
||||
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description
|
||||
value = $scope.product.unit_value
|
||||
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
|
||||
$scope.product.unit_value_with_description = value + " " + $scope.product.unit_description
|
||||
|
||||
# Calculate unit price based on product price and variant_unit_scale
|
||||
$scope.processUnitPrice = ->
|
||||
price = $scope.product.price
|
||||
scale = $scope.product.variant_unit_scale
|
||||
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
|
||||
$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 ' '
|
||||
|
||||
value_scaled: ->
|
||||
@variant.product.variant_unit_scale?
|
||||
@variant.variant_unit_scale?
|
||||
|
||||
option_value_value_unit: ->
|
||||
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()
|
||||
|
||||
else
|
||||
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)
|
||||
|
||||
@@ -58,14 +58,13 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
|
||||
# to >= 1 when expressed in it.
|
||||
# If there is none available where this is true, use the smallest
|
||||
# available unit.
|
||||
product = @variant.product
|
||||
scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit)
|
||||
scales = VariantUnitManager.compatibleUnitScales(@variant.variant_unit_scale, @variant.variant_unit)
|
||||
variantUnitValue = @variant.unit_value
|
||||
|
||||
# sets largestScale = last element in filtered scales array
|
||||
[_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1)
|
||||
|
||||
if (largestScale)
|
||||
[largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)]
|
||||
[largestScale, VariantUnitManager.getUnitName(largestScale, @variant.variant_unit)]
|
||||
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
|
||||
product = @findProductInList(server_product.id, @products)
|
||||
product.variants = server_product.variants
|
||||
@loadVariantUnitValues product
|
||||
@loadVariantUnitValues product.variants
|
||||
|
||||
find: (id) ->
|
||||
@findProductInList id, @products
|
||||
@@ -38,34 +38,32 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
|
||||
@products.splice(index + 1, 0, newProduct)
|
||||
|
||||
unpackProduct: (product) ->
|
||||
#$scope.matchProducer product
|
||||
@loadVariantUnit product
|
||||
|
||||
loadVariantUnit: (product) ->
|
||||
product.variant_unit_with_scale =
|
||||
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
|
||||
"#{product.variant_unit}_#{product.variant_unit_scale}"
|
||||
else if product.variant_unit
|
||||
product.variant_unit
|
||||
@loadVariantUnitValues product.variants if product.variants
|
||||
|
||||
loadVariantUnitValues: (variants) ->
|
||||
for variant in variants
|
||||
@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
|
||||
null
|
||||
|
||||
@loadVariantUnitValues product if product.variants
|
||||
@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 = @variantUnitValue variant
|
||||
unit_value = if unit_value? then unit_value else ''
|
||||
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
|
||||
|
||||
variantUnitValue: (product, variant) ->
|
||||
variantUnitValue: (variant) ->
|
||||
if variant.unit_value?
|
||||
if product.variant_unit_scale
|
||||
variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale
|
||||
if variant.variant_unit_scale
|
||||
variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale
|
||||
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
|
||||
else
|
||||
variant.unit_value
|
||||
|
||||
@@ -21,8 +21,7 @@ module Admin
|
||||
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user,
|
||||
params[:settings])
|
||||
@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
|
||||
|
||||
@ams_data = ams_data
|
||||
|
||||
@@ -21,7 +21,7 @@ module Api
|
||||
authorize! :create, Spree::Product
|
||||
@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
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
|
||||
@@ -51,7 +51,7 @@ module OrderCompletion
|
||||
|
||||
def order_invalid!
|
||||
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
|
||||
payload.add_metadata :order, @order
|
||||
payload.add_metadata :order, :order, @order
|
||||
end
|
||||
|
||||
flash[:error] = t('checkout.order_not_loaded')
|
||||
|
||||
@@ -21,7 +21,7 @@ module OrderStockCheck
|
||||
return unless current_order_cycle&.closed?
|
||||
|
||||
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
|
||||
current_order.empty!
|
||||
current_order.set_order_cycle! nil
|
||||
|
||||
@@ -7,7 +7,7 @@ class ErrorsController < ApplicationController
|
||||
Bugsnag.notify("404") do |event|
|
||||
event.severity = "info"
|
||||
|
||||
event.add_metadata(:request, request.env)
|
||||
event.add_metadata(:request, :env, request.env)
|
||||
end
|
||||
render status: :not_found, formats: :html
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ module Spree
|
||||
def create
|
||||
delete_stock_params_and_set_after do
|
||||
@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)
|
||||
redirect_after_save
|
||||
else
|
||||
@@ -214,10 +214,10 @@ module Spree
|
||||
|
||||
def notify_bugsnag(error, product, variant)
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_metadata(:product, product.attributes)
|
||||
report.add_metadata(:product_error, product.errors.first) unless product.valid?
|
||||
report.add_metadata(:variant, variant.attributes)
|
||||
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
|
||||
report.add_metadata(:product,
|
||||
{ product: product.attributes, variant: variant.attributes })
|
||||
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
|
||||
report.add_metadata(:product, :variant_error, variant.errors.first) unless variant.valid?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ module Spree
|
||||
|
||||
def permitted_resource_params
|
||||
params.require(:return_authorization).
|
||||
permit(:amount, :reason, :stock_location_id)
|
||||
permit(:amount, :reason)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ require 'open_food_network/scope_variants_for_search'
|
||||
module Spree
|
||||
module Admin
|
||||
class VariantsController < ::Admin::ResourceController
|
||||
helper ::Admin::ProductsHelper
|
||||
|
||||
belongs_to 'spree/product'
|
||||
|
||||
before_action :load_data, only: [:new, :edit]
|
||||
|
||||
@@ -18,17 +18,15 @@ module Admin
|
||||
end
|
||||
|
||||
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.product.variant_unit_scale || 1)
|
||||
precised_unit_value = number_with_precision(
|
||||
scaled_unit_value,
|
||||
precision: nil,
|
||||
strip_insignificant_zeros: true,
|
||||
significant: false,
|
||||
)
|
||||
end
|
||||
scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1)
|
||||
precised_unit_value = number_with_precision(
|
||||
scaled_unit_value,
|
||||
precision: nil,
|
||||
strip_insignificant_zeros: true,
|
||||
significant: false,
|
||||
)
|
||||
|
||||
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
|
||||
end
|
||||
|
||||
@@ -58,4 +58,9 @@ module ReportsHelper
|
||||
.where(order_id: orders.map(&:id))
|
||||
.pluck(:originator_id)
|
||||
end
|
||||
|
||||
def datepicker_time(datetime)
|
||||
datetime = Time.zone.parse(datetime) if datetime.is_a? String
|
||||
datetime.strftime('%Y-%m-%d %H:%M')
|
||||
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
|
||||
|
||||
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?
|
||||
rescue StandardError => e
|
||||
# Errors here shouldn't affect the checkout. So let's report them
|
||||
# separately:
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, order)
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,8 @@ class BackorderJob < ApplicationJob
|
||||
user = order.distributor.owner
|
||||
items = backorderable_items(order)
|
||||
|
||||
return if items.empty?
|
||||
|
||||
# We are assuming that all variants are linked to the same wholesale
|
||||
# shop and its catalog:
|
||||
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||
@@ -131,5 +133,7 @@ class BackorderJob < ApplicationJob
|
||||
.perform_later(
|
||||
user, order.distributor, order.order_cycle, placed_order.semanticId
|
||||
)
|
||||
|
||||
order.exchange.semantic_links.create!(semantic_id: placed_order.semanticId)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,12 +19,18 @@ class CompleteBackorderJob < ApplicationJob
|
||||
# someone else's order.
|
||||
def perform(user, distributor, order_cycle, order_id)
|
||||
order = FdcBackorderer.new(user, nil).find_order(order_id)
|
||||
|
||||
return if order&.lines.blank?
|
||||
|
||||
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
||||
|
||||
variants = order_cycle.variants_distributed_by(distributor)
|
||||
adjust_quantities(order_cycle, user, order, urls, variants)
|
||||
|
||||
FdcBackorderer.new(user, urls).complete_order(order)
|
||||
|
||||
exchange = order_cycle.exchanges.outgoing.find_by(receiver: distributor)
|
||||
exchange.semantic_links.find_by(semantic_id: order_id)&.destroy!
|
||||
rescue StandardError
|
||||
BackorderMailer.backorder_incomplete(user, distributor, order_cycle, order_id).deliver_later
|
||||
|
||||
@@ -45,6 +51,11 @@ class CompleteBackorderJob < ApplicationJob
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@ class StockSyncJob < ApplicationJob
|
||||
# Errors here shouldn't affect the shopping. So let's report them
|
||||
# separately:
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, order)
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,13 +30,13 @@ class StockSyncJob < ApplicationJob
|
||||
# Errors here shouldn't affect the shopping. So let's report them
|
||||
# separately:
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata(:order, order)
|
||||
payload.add_metadata(:order, :order, order)
|
||||
end
|
||||
end
|
||||
|
||||
def self.catalog_ids(order)
|
||||
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.map do |product_id|
|
||||
FdcUrlBuilder.new(product_id).catalog_url
|
||||
|
||||
@@ -56,7 +56,7 @@ class SubscriptionConfirmJob < ApplicationJob
|
||||
send_failed_payment_email(order)
|
||||
else
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :order, order
|
||||
payload.add_metadata :order, :order, order
|
||||
end
|
||||
send_failed_payment_email(order, e.message)
|
||||
end
|
||||
@@ -109,8 +109,7 @@ class SubscriptionConfirmJob < ApplicationJob
|
||||
SubscriptionMailer.failed_payment_email(order).deliver_now
|
||||
rescue StandardError => e
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :order, order
|
||||
payload.add_metadata :error_message, error_message
|
||||
payload.add_metadata :subscription_data, { order:, error_message: }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,6 @@ class CustomTab < ApplicationRecord
|
||||
|
||||
validates :title, presence: true, length: { maximum: 20 }
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def content
|
||||
HtmlSanitizer.sanitize(super)
|
||||
end
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def content=(html)
|
||||
super(HtmlSanitizer.sanitize(html))
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
class Customer < ApplicationRecord
|
||||
include SetUnusedAddressFields
|
||||
|
||||
self.ignored_columns += ['name']
|
||||
|
||||
acts_as_taggable
|
||||
|
||||
searchable_attributes :first_name, :last_name, :email, :code
|
||||
|
||||
@@ -74,11 +74,6 @@ class EnterpriseGroup < ApplicationRecord
|
||||
permalink
|
||||
end
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def long_description
|
||||
HtmlSanitizer.sanitize_and_enforce_link_target_blank(super)
|
||||
end
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def long_description=(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 :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] }
|
||||
|
||||
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
|
||||
variant = product.variants.first
|
||||
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.supplier_id = entry.producer_id
|
||||
variant.save
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
module ProductImport
|
||||
class EntryValidator
|
||||
SKIP_VALIDATE_ON_UPDATE = [:description].freeze
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises,
|
||||
inventory_permissions, reset_counts, import_settings, all_entries)
|
||||
@@ -22,9 +20,8 @@ module ProductImport
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def self.non_updatable_fields
|
||||
def self.non_updatable_variant_fields
|
||||
{
|
||||
description: :description,
|
||||
unit_type: :variant_unit_scale,
|
||||
variant_unit_name: :variant_unit_name,
|
||||
}
|
||||
@@ -67,8 +64,7 @@ module ProductImport
|
||||
|
||||
def mark_as_new_variant(entry, product_id)
|
||||
variant_attributes = entry.assignable_attributes.except(
|
||||
'id', 'product_id', 'on_hand', 'on_demand', 'variant_unit', 'variant_unit_name',
|
||||
'variant_unit_scale'
|
||||
'id', 'product_id', 'on_hand', 'on_demand'
|
||||
)
|
||||
# Variant needs a product. Product needs to be assigned first in order for
|
||||
# delegate to work. name= will fail otherwise.
|
||||
@@ -297,11 +293,11 @@ module ProductImport
|
||||
end
|
||||
|
||||
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
|
||||
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)
|
||||
return validate_inventory_item(entry, variant_override)
|
||||
end
|
||||
@@ -311,17 +307,6 @@ module ProductImport
|
||||
error: I18n.t('admin.product_import.model.not_found'))
|
||||
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)
|
||||
category_name = entry.category
|
||||
|
||||
@@ -364,13 +349,13 @@ module ProductImport
|
||||
return
|
||||
end
|
||||
|
||||
products.each { |product| product_field_errors(entry, product) }
|
||||
|
||||
products.flat_map(&:variants).each do |existing_variant|
|
||||
if entry_matches_existing_variant?(entry, existing_variant) &&
|
||||
existing_variant.deleted_at.nil?
|
||||
return mark_as_existing_variant(entry, existing_variant)
|
||||
end
|
||||
next unless entry.match_variant?(existing_variant) &&
|
||||
existing_variant.deleted_at.nil?
|
||||
|
||||
variant_field_errors(entry, existing_variant)
|
||||
|
||||
return mark_as_existing_variant(entry, existing_variant)
|
||||
end
|
||||
|
||||
mark_as_new_variant(entry, products.first.id)
|
||||
@@ -392,8 +377,7 @@ module ProductImport
|
||||
|
||||
def mark_as_existing_variant(entry, existing_variant)
|
||||
existing_variant.assign_attributes(
|
||||
entry.assignable_attributes.except('id', 'product_id', 'variant_unit', 'variant_unit_name',
|
||||
'variant_unit_scale')
|
||||
entry.assignable_attributes.except('id', 'product_id')
|
||||
)
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
|
||||
@@ -406,11 +390,10 @@ module ProductImport
|
||||
end
|
||||
end
|
||||
|
||||
def product_field_errors(entry, existing_product)
|
||||
EntryValidator.non_updatable_fields.each do |display_name, attribute|
|
||||
next if attributes_match?(attribute, existing_product, entry) ||
|
||||
attributes_blank?(attribute, existing_product, entry)
|
||||
next if ignore_when_updating_product?(attribute)
|
||||
def variant_field_errors(entry, existing_variant)
|
||||
EntryValidator.non_updatable_variant_fields.each do |display_name, attribute|
|
||||
next if attributes_match?(attribute, existing_variant, entry) ||
|
||||
attributes_blank?(attribute, existing_variant, entry)
|
||||
|
||||
mark_as_invalid(entry, attribute: display_name,
|
||||
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)
|
||||
end
|
||||
|
||||
def ignore_when_updating_product?(attribute)
|
||||
SKIP_VALIDATE_ON_UPDATE.include? attribute
|
||||
end
|
||||
|
||||
def convert_to_trusted_type(untrusted_attribute, trusted_attribute)
|
||||
case trusted_attribute
|
||||
when Integer
|
||||
|
||||
@@ -84,6 +84,14 @@ module ProductImport
|
||||
invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES)
|
||||
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
|
||||
|
||||
def remove_empty_skus(attrs)
|
||||
@@ -99,5 +107,11 @@ module ProductImport
|
||||
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
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
# Link a Spree::Variant to an external DFC SuppliedProduct.
|
||||
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
|
||||
end
|
||||
|
||||
@@ -244,7 +244,7 @@ module Spree
|
||||
can [:admin, :index, :show, :create], ::Admin::ReportsController
|
||||
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
|
||||
: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
|
||||
|
||||
def add_order_cycle_management_abilities(user)
|
||||
|
||||
@@ -45,7 +45,8 @@ module Spree
|
||||
after_destroy :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
|
||||
attr_accessor :skip_stock_check, :target_shipment
|
||||
|
||||
@@ -67,8 +67,12 @@ module Spree
|
||||
class_name: 'Spree::Adjustment',
|
||||
dependent: :destroy
|
||||
has_many :invoices, dependent: :restrict_with_exception
|
||||
|
||||
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 :customer, optional: true
|
||||
has_one :proxy_order, dependent: :destroy
|
||||
|
||||
@@ -142,6 +142,8 @@ module Spree
|
||||
|
||||
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
||||
update(payment_state: updater.update_payment_state)
|
||||
|
||||
AmendBackorderJob.perform_later(self)
|
||||
end
|
||||
|
||||
def after_resume
|
||||
|
||||
@@ -155,7 +155,6 @@ module Spree
|
||||
if adjustment
|
||||
adjustment.originator = payment_method
|
||||
adjustment.label = adjustment_label
|
||||
adjustment.amount = payment_method.compute_amount(self)
|
||||
adjustment.save
|
||||
elsif !processing_refund? && payment_method.present?
|
||||
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
|
||||
def parse_price(price)
|
||||
return nil if price.blank?
|
||||
return price unless price.is_a?(String)
|
||||
|
||||
separator, _delimiter = I18n.t([:'number.currency.format.separator',
|
||||
|
||||
@@ -22,7 +22,12 @@ module Spree
|
||||
include LogDestroyPerformer
|
||||
|
||||
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
|
||||
|
||||
@@ -45,20 +50,30 @@ module Spree
|
||||
|
||||
validates_lengths_from_database
|
||||
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
|
||||
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 :product_properties,
|
||||
allow_destroy: true,
|
||||
@@ -66,14 +81,12 @@ module Spree
|
||||
|
||||
# Transient attributes used temporarily when creating a new product,
|
||||
# these values are persisted on the product's variant
|
||||
attr_accessor :price, :display_as, :unit_value, :unit_description, :tax_category_id,
|
||||
:shipping_category_id, :primary_taxon_id, :supplier_id
|
||||
attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit,
|
||||
: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_update :touch_supplier, if: :saved_change_to_primary_taxon_id?
|
||||
around_destroy :destruction
|
||||
after_save :update_units
|
||||
after_touch :touch_supplier
|
||||
|
||||
# -- Scopes
|
||||
@@ -198,10 +211,6 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
def total_on_hand
|
||||
stock_items.sum(&:count_on_hand)
|
||||
end
|
||||
|
||||
def properties_including_inherited
|
||||
# Product properties override producer properties
|
||||
ps = product_properties.all
|
||||
@@ -245,6 +254,7 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def ensure_standard_variant
|
||||
return unless variants.empty?
|
||||
|
||||
@@ -254,36 +264,16 @@ module Spree
|
||||
variant.display_as = display_as
|
||||
variant.unit_value = unit_value
|
||||
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.shipping_category_id = shipping_category_id
|
||||
variant.primary_taxon_id = primary_taxon_id
|
||||
variant.supplier_id = supplier_id
|
||||
variants << variant
|
||||
end
|
||||
|
||||
# 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
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def description=(html)
|
||||
@@ -292,27 +282,6 @@ module Spree
|
||||
|
||||
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
|
||||
return if variants.empty?
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
module Spree
|
||||
class ReturnAuthorization < ApplicationRecord
|
||||
self.ignored_columns += [:stock_location_id]
|
||||
acts_as_paranoid
|
||||
|
||||
belongs_to :order, class_name: 'Spree::Order', inverse_of: :return_authorizations
|
||||
|
||||
has_many :inventory_units, inverse_of: :return_authorization, dependent: :nullify
|
||||
has_one :stock_location, dependent: nil
|
||||
before_save :force_positive_amount
|
||||
before_create :generate_number
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ module Spree
|
||||
if default_zone_or_zone_match?(item.order)
|
||||
calculator.compute(item)
|
||||
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.
|
||||
calculator.compute(item) * - 1
|
||||
end
|
||||
|
||||
@@ -60,7 +60,7 @@ module Spree
|
||||
has_many :exchanges, through: :exchange_variants
|
||||
has_many :variant_overrides, 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
|
||||
|
||||
localize_number :price, :weight
|
||||
@@ -71,21 +71,25 @@ module Spree
|
||||
validates :tax_category, presence: true,
|
||||
if: proc { Spree::Config.products_require_tax_category }
|
||||
|
||||
validates :variant_unit, presence: true
|
||||
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 :price, numericality: { greater_than_or_equal_to: 0 }
|
||||
|
||||
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 :ensure_shipping_category
|
||||
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_save :assign_units, if: ->(variant) {
|
||||
@@ -95,6 +99,9 @@ module Spree
|
||||
after_create :create_stock_items
|
||||
around_destroy :destruction
|
||||
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
|
||||
scope :deleted, -> { where.not(deleted_at: nil) }
|
||||
@@ -219,6 +226,25 @@ module Spree
|
||||
Spree::Stock::Quantifier.new(self).total_on_hand
|
||||
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
|
||||
|
||||
def check_currency
|
||||
@@ -248,7 +274,7 @@ module Spree
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
@@ -268,7 +294,7 @@ module Spree
|
||||
|
||||
def ensure_unit_value
|
||||
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
|
||||
end
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
module Api
|
||||
module Admin
|
||||
class ProductSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name,
|
||||
:inherits_properties, :on_hand, :price, :import_date, :image_url,
|
||||
attributes :id, :name, :sku, :inherits_properties, :on_hand, :price, :import_date, :image_url,
|
||||
:thumb_url, :variants
|
||||
|
||||
def variants
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
module Admin
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
module Admin
|
||||
class UnitsVariantSerializer < ActiveModel::Serializer
|
||||
attributes :id, :full_name, :unit_value
|
||||
attributes :id, :full_name, :unit_value, :variant_unit, :variant_unit_scale
|
||||
|
||||
def 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,
|
||||
:options_text, :unit_value, :unit_description, :unit_to_display,
|
||||
: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 :supplier, key: :producer_id, embed: :id
|
||||
|
||||
@@ -10,7 +10,7 @@ class FdcBackorderer
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def build_new_order(ofn_order)
|
||||
@@ -19,7 +19,37 @@ class FdcBackorderer
|
||||
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)
|
||||
open_orders = graph&.select do |o|
|
||||
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
|
||||
|
||||
@@ -4,11 +4,10 @@ module PermittedAttributes
|
||||
class Variant
|
||||
def self.attributes
|
||||
[
|
||||
:id, :sku, :on_hand, :on_demand, :shipping_category_id,
|
||||
:price, :unit_value, :unit_description,
|
||||
:display_name, :display_as, :tax_category_id,
|
||||
:weight, :height, :width, :depth, :taxon_ids, :primary_taxon_id,
|
||||
:supplier_id
|
||||
:id, :sku, :on_hand, :on_demand, :shipping_category_id, :price, :unit_value,
|
||||
:unit_description, :variant_unit, :variant_unit_name, :variant_unit_scale, :display_name,
|
||||
:display_as, :tax_category_id, :weight, :height, :width, :depth, :taxon_ids,
|
||||
:primary_taxon_id, :supplier_id
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ class PlaceProxyOrder
|
||||
rescue StandardError => e
|
||||
summarizer.record_and_log_error(:processing, order, e.message)
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :order, order
|
||||
payload.add_metadata :order, :order, order
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,8 +57,7 @@ class PlaceProxyOrder
|
||||
true
|
||||
rescue StandardError => e
|
||||
Bugsnag.notify(e) do |payload|
|
||||
payload.add_metadata :subscription, subscription
|
||||
payload.add_metadata :proxy_order, proxy_order
|
||||
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
@@ -146,11 +146,11 @@ module Sets
|
||||
|
||||
def notify_bugsnag(error, product, variant, variant_attributes)
|
||||
Bugsnag.notify(error) do |report|
|
||||
report.add_metadata(:product, product.attributes)
|
||||
report.add_metadata(:product_error, product.errors.first) unless product.valid?
|
||||
report.add_metadata(:variant_attributes, variant_attributes)
|
||||
report.add_metadata(:variant, variant.attributes)
|
||||
report.add_metadata(:variant_error, variant.errors.first) unless variant.valid?
|
||||
report.add_metadata( :product_set,
|
||||
{ product: product.attributes, variant_attributes:,
|
||||
variant: variant.attributes } )
|
||||
report.add_metadata(:product_set, :product_error, product.errors.first) if !product.valid?
|
||||
report.add_metadata(:product_set, :variant_error, variant.errors.first) if !variant.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
class UnitPrice
|
||||
def initialize(variant)
|
||||
@variant = variant
|
||||
@product = variant.product
|
||||
end
|
||||
|
||||
def denominator
|
||||
# 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
|
||||
when "lb"
|
||||
@@ -23,13 +22,13 @@ class UnitPrice
|
||||
def unit
|
||||
return "lb" if WeightsAndMeasures.new(@variant).system == "imperial"
|
||||
|
||||
case @product&.variant_unit
|
||||
case @variant.variant_unit
|
||||
when "weight"
|
||||
"kg"
|
||||
when "volume"
|
||||
"L"
|
||||
else
|
||||
@product.variant_unit_name.presence || I18n.t("item")
|
||||
@variant.variant_unit_name.presence || I18n.t("item")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,16 +32,18 @@ module VariantUnits
|
||||
private
|
||||
|
||||
def value_scaled?
|
||||
@nameable.product.variant_unit_scale.present?
|
||||
@nameable.variant_unit_scale.present?
|
||||
end
|
||||
|
||||
def option_value_value_unit
|
||||
if @nameable.unit_value.present? && @nameable.product&.persisted?
|
||||
if %w(weight volume).include? @nameable.product.variant_unit
|
||||
if @nameable.unit_value.present?
|
||||
if %w(weight volume).include? @nameable.variant_unit
|
||||
value, unit_name = option_value_value_unit_scaled
|
||||
else
|
||||
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
|
||||
|
||||
value = value.to_i if value == value.to_i
|
||||
|
||||
@@ -64,12 +64,12 @@ module VariantUnits
|
||||
|
||||
def unit_value_attributes
|
||||
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
|
||||
end
|
||||
|
||||
def weight_from_unit_value
|
||||
(unit_value || 0) / 1000 if product.variant_unit == 'weight'
|
||||
(unit_value || 0) / 1000 if variant_unit == 'weight'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -16,10 +16,10 @@ class WeightsAndMeasures
|
||||
def system
|
||||
return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true)
|
||||
|
||||
product_scale = @variant.product.variant_unit_scale&.to_f
|
||||
return "custom" unless product_scale.present? && product_scale.positive?
|
||||
variant_scale = @variant.variant_unit_scale&.to_f
|
||||
return "custom" unless variant_scale.present? && variant_scale.positive?
|
||||
|
||||
scales[product_scale]['system']
|
||||
scales[variant_scale]['system']
|
||||
end
|
||||
|
||||
# @returns enumerable with label and value for select
|
||||
@@ -92,9 +92,9 @@ class WeightsAndMeasures
|
||||
}.freeze
|
||||
|
||||
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'])
|
||||
}
|
||||
end
|
||||
|
||||
@@ -7,18 +7,8 @@
|
||||
%td.col-sku.field.naked_inputs
|
||||
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
||||
= error_message_on product, :sku
|
||||
%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||
= f.hidden_field :variant_unit
|
||||
= 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_scale.align-right
|
||||
-# empty
|
||||
%td.col-unit.align-right
|
||||
-# empty
|
||||
%td.col-price.align-right
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
.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" }
|
||||
= t('.reset')
|
||||
= form.submit t('.save'), class: "medium"
|
||||
= form.submit t('.save'), { class: "medium", data: { action: "click->bulk-form#popoutEmptyVariantUnit" }}
|
||||
%tr
|
||||
%th.col-image.align-left= # image
|
||||
= render partial: 'spree/admin/shared/stimulus_sortable_header',
|
||||
|
||||
@@ -7,8 +7,17 @@
|
||||
%td.col-sku.field.naked_inputs
|
||||
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
|
||||
= error_message_on variant, :sku
|
||||
%td.col-unit_scale
|
||||
-# empty
|
||||
%td.col-unir_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||
= 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"}
|
||||
= 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
|
||||
@@ -18,7 +27,7 @@
|
||||
= 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('admin.products_page.columns.unit_value')
|
||||
value: unit_value_with_description(variant), 'aria-label': t('admin.products_page.columns.unit_value'), required: true
|
||||
.field
|
||||
= f.label :display_as, t('admin.products_page.columns.display_as')
|
||||
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(variant).name
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
= 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,
|
||||
producer_options: producers, producer_id: @producer_id,
|
||||
category_options: categories, category_id: @category_id,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
-# Field used for ransack search. This date range is mostly used for Spree::Order
|
||||
-# so default field is 'completed_at'
|
||||
- field ||= 'completed_at'
|
||||
- start_date ||= params[:q].try(:[], :completed_at_gt)
|
||||
- end_date ||= params[:q].try(:[], :completed_at_lt)
|
||||
- start_field = "#{field}_gt"
|
||||
- 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
|
||||
.alpha.two.columns= label_tag nil, t(:date_range)
|
||||
.omega.fourteen.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 "q[#{field}_gt]", 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[#{start_field}]", nil, data: { "flatpickr-target": "start" }, style: "display: none", value: start_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',
|
||||
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
|
||||
.alpha.two.columns= label_tag nil, t(:report_hubs)
|
||||
@@ -14,4 +14,4 @@
|
||||
.row
|
||||
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
|
||||
.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
|
||||
.text-center
|
||||
= 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
|
||||
.text-center
|
||||
= t("admin.orders.bulk_management.total_qtt_ordered")
|
||||
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
|
||||
.text-center {{ formattedValueWithUnitName( sumUnitValues(), selectedUnitsVariant ) }}
|
||||
.three.columns
|
||||
.text-center
|
||||
= t("admin.orders.bulk_management.max_qtt_ordered")
|
||||
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsProduct, selectedUnitsVariant ) }}
|
||||
.text-center {{ formattedValueWithUnitName( sumMaxUnitValues(), selectedUnitsVariant ) }}
|
||||
.three.columns
|
||||
.text-center
|
||||
= t("admin.orders.bulk_management.current_fulfilled_units")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
%div.admin-product-form-fields
|
||||
.left.twelve.columns.alpha
|
||||
.left.sixteen.columns.alpha
|
||||
= f.field_container :name do
|
||||
= f.label :name, raw(t(:name) + content_tag(:span, ' *', :class => 'required'))
|
||||
= f.text_field :name, :class => 'fullwidth title'
|
||||
@@ -10,25 +10,6 @@
|
||||
= f.hidden_field :description, id: "product_description", value: @product.description
|
||||
%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
|
||||
|
||||
%div
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
%td.name{ 'ng-show' => 'columns.name.visible' }
|
||||
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ofn-track-product' => 'name', :type => 'text' }
|
||||
%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.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)' }
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
%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 }}" }
|
||||
%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' }
|
||||
%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' }
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
= f.field_container :unit_value do
|
||||
= f.label :unit_value, t(".value"), 'ng-disabled' => "!hasUnit(product)"
|
||||
%span.required *
|
||||
= f.text_field :unit_value, placeholder: "eg. 2", 'ng-model' => 'product.master.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.master.unit_description', "ng-init": "product.master.unit_description='#{@product.unit_description}'", name: 'product[unit_description]' }
|
||||
= 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.unit_value', "ng-init": "product.unit_value='#{@product.unit_value}'", name: 'product[unit_value]' }
|
||||
%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
|
||||
= render 'display_as', f: f
|
||||
.six.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }
|
||||
|
||||
@@ -41,8 +41,3 @@
|
||||
= f.label :reason, t('.reason')
|
||||
= f.text_area :reason, { style: 'height:100px;', class: 'fullwidth' }
|
||||
= 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'}
|
||||
.field
|
||||
= f.label :display_name, t('.display_name')
|
||||
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
||||
.field
|
||||
= f.label :display_as, t('.display_as')
|
||||
= f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder')
|
||||
%div{'data-controller': "edit-variant", id: "edit_variant"}
|
||||
.label-block.left.six.columns.alpha
|
||||
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }
|
||||
|
||||
- if @product.variant_unit != 'items'
|
||||
.field
|
||||
= 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'}
|
||||
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
|
||||
locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) }
|
||||
|
||||
.field
|
||||
= f.label :unit_description, t(:spree_admin_unit_description)
|
||||
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
|
||||
.field
|
||||
= f.label :display_name, t('.display_name')
|
||||
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
|
||||
|
||||
%div
|
||||
.field
|
||||
= f.label :sku, t('.sku')
|
||||
= f.text_field :sku, class: 'fullwidth'
|
||||
.field
|
||||
= f.label :price, t('.price')
|
||||
= f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'"
|
||||
.field
|
||||
= hidden_field_tag 'product_variant_unit', @product.variant_unit
|
||||
= hidden_field_tag 'product_variant_unit_name', @product.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, "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{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
|
||||
= f.label :unit_scale do
|
||||
= t('.unit_scale')
|
||||
= content_tag(:span, ' *', class: 'required')
|
||||
= 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('.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } }
|
||||
= error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control'
|
||||
.field
|
||||
= f.label :on_hand, t(:on_hand)
|
||||
.fullwidth
|
||||
= f.text_field :on_hand
|
||||
= 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'
|
||||
|
||||
.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
|
||||
- if @product.variant_unit != 'weight'
|
||||
%div
|
||||
.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
|
||||
= f.label 'weight', t(:weight)+' (kg)'
|
||||
- value = number_with_precision(@variant.weight, precision: 3)
|
||||
= 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
|
||||
= 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
|
||||
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
||||
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
||||
|
||||
.field
|
||||
= f.label :tax_category, t(:tax_category), for: :tax_category_id
|
||||
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
|
||||
.field
|
||||
= f.label :shipping_category_id, t(:shipping_categories)
|
||||
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
||||
|
||||
.field
|
||||
= f.label :shipping_category_id, t(:shipping_categories)
|
||||
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
|
||||
.field
|
||||
= f.label :primary_taxon, t('.variant_category')
|
||||
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
||||
|
||||
.field
|
||||
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
|
||||
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
|
||||
.field
|
||||
= f.label :supplier do
|
||||
= t(:spree_admin_supplier)
|
||||
= content_tag(:span, ' *', class: 'required')
|
||||
|
||||
.field
|
||||
= f.label :supplier, t(:spree_admin_supplier)
|
||||
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
|
||||
= 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
|
||||
|
||||
#registerSubmit() {
|
||||
@@ -135,7 +145,7 @@ export default class BulkFormController extends Controller {
|
||||
|
||||
// Check if changed, and mark with class if it is.
|
||||
#checkIsChanged(element) {
|
||||
if(!element.isConnected) return false;
|
||||
if (!element.isConnected) return false;
|
||||
|
||||
const changed = this.#isChanged(element);
|
||||
element.classList.toggle("changed", changed);
|
||||
@@ -143,9 +153,8 @@ export default class BulkFormController extends Controller {
|
||||
}
|
||||
|
||||
#isChanged(element) {
|
||||
if (element.type == "checkbox") {
|
||||
if (element.type == "checkbox") {
|
||||
return element.defaultChecked !== undefined && element.checked != element.defaultChecked;
|
||||
|
||||
} else if (element.type == "select-one") {
|
||||
// (weird) Behavior of select element's include_blank option in Rails:
|
||||
// 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"),
|
||||
);
|
||||
const selectedOption = element.selectedOptions[0];
|
||||
const areBothBlank = selectedOption.value === '' && defaultSelected === undefined
|
||||
const areBothBlank = selectedOption.value === "" && defaultSelected === undefined;
|
||||
|
||||
return !areBothBlank && selectedOption !== defaultSelected;
|
||||
|
||||
} else {
|
||||
return element.defaultValue !== undefined && element.value != element.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
#removeAnimationClasses(productRowElement) {
|
||||
productRowElement.classList.remove('slide-in');
|
||||
productRowElement.removeEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
|
||||
productRowElement.classList.remove("slide-in");
|
||||
productRowElement.removeEventListener(
|
||||
"animationend",
|
||||
this.#removeAnimationClasses.bind(this, productRowElement),
|
||||
);
|
||||
}
|
||||
|
||||
#observeProductsTableRows() {
|
||||
this.productsTableObserver = new MutationObserver((mutationList, _observer) => {
|
||||
const mutationRecord = mutationList[0];
|
||||
|
||||
if(mutationRecord) {
|
||||
if (mutationRecord) {
|
||||
// Right now we are only using it for product clone, so it's always first
|
||||
const productRowElement = mutationRecord.addedNodes[0];
|
||||
|
||||
if (productRowElement) {
|
||||
productRowElement.addEventListener('animationend', this.#removeAnimationClasses.bind(this, productRowElement));
|
||||
productRowElement.addEventListener(
|
||||
"animationend",
|
||||
this.#removeAnimationClasses.bind(this, productRowElement),
|
||||
);
|
||||
// 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.toggleFormChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const productsTable = document.querySelector('.products');
|
||||
const productsTable = document.querySelector(".products");
|
||||
// Above mutation function will trigger,
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const productsForm = document.querySelector("#products-form");
|
||||
productsForm.scrollIntoView({ behavior: "smooth" });
|
||||
this.page.value = event.target.dataset.page;
|
||||
this.submitSearch();
|
||||
this.page.value = 1;
|
||||
|
||||
@@ -5,11 +5,17 @@ import OptionValueNamer from "js/services/option_value_namer";
|
||||
//
|
||||
export default class VariantController extends Controller {
|
||||
connect() {
|
||||
// Assuming these will be available on the variant soon, just a quick hack to find the product fields:
|
||||
const product = this.element.closest("[data-record-id]");
|
||||
this.variantUnit = product.querySelector('[name$="[variant_unit]"]');
|
||||
this.variantUnitScale = product.querySelector('[name$="[variant_unit_scale]"]');
|
||||
this.variantUnitName = product.querySelector('[name$="[variant_unit_name]"]');
|
||||
// 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.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.unitDescription = this.element.querySelector('[name$="[unit_description]"]');
|
||||
@@ -76,11 +82,27 @@ export default class VariantController extends Controller {
|
||||
return {
|
||||
unit_value: parseFloat(this.unitValue.value),
|
||||
unit_description: this.unitDescription.value,
|
||||
product: {
|
||||
variant_unit: this.variantUnit.value,
|
||||
variant_unit_scale: parseFloat(this.variantUnitScale.value),
|
||||
variant_unit_name: this.variantUnitName.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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Customisations for the new Bulk Edit Products page only
|
||||
// Scoped to containing div, because Turbo messes with body classes
|
||||
@import "../admin_v3/pages/unit_popout";
|
||||
|
||||
#products_v3_page {
|
||||
#content > .row:first-child > .container:first-child {
|
||||
// Allow table to extend to full width of available screen space
|
||||
@@ -311,89 +313,7 @@
|
||||
|
||||
// Popout widget (todo: move to separate fiel)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@include unit_popout;
|
||||
}
|
||||
|
||||
a.image-field {
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
@import "../admin/dialog";
|
||||
@import "../admin/disabled";
|
||||
@import "components/dropdown"; // admin_v3
|
||||
@import "pages/edit_variant"; // admin_v3
|
||||
@import "pages/enterprise_index_panels"; // admin_v3
|
||||
@import "../admin/enterprises";
|
||||
@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.
|
||||
export default class OptionValueNamer {
|
||||
@@ -9,7 +9,7 @@ export default class OptionValueNamer {
|
||||
|
||||
name() {
|
||||
const [value, unit] = this.option_value_value_unit();
|
||||
const separator = this.value_scaled() ? '' : ' ';
|
||||
const separator = this.value_scaled() ? "" : " ";
|
||||
const name_fields = [];
|
||||
if (value && unit) {
|
||||
name_fields.push(`${value}${separator}${unit}`);
|
||||
@@ -20,21 +20,21 @@ export default class OptionValueNamer {
|
||||
if (this.variant.unit_description) {
|
||||
name_fields.push(this.variant.unit_description);
|
||||
}
|
||||
return name_fields.join(' ');
|
||||
return name_fields.join(" ");
|
||||
}
|
||||
|
||||
value_scaled() {
|
||||
return !!this.variant.product.variant_unit_scale;
|
||||
return !!this.variant.variant_unit_scale;
|
||||
}
|
||||
|
||||
option_value_value_unit() {
|
||||
let value, unit_name;
|
||||
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();
|
||||
} else {
|
||||
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)) {
|
||||
value = parseInt(value, 10);
|
||||
@@ -55,7 +55,7 @@ export default class OptionValueNamer {
|
||||
}
|
||||
return I18n.t(["inflections", unit_key], {
|
||||
count: count,
|
||||
defaultValue: unit_name
|
||||
defaultValue: unit_name,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,17 +83,21 @@ export default class OptionValueNamer {
|
||||
// to >= 1 when expressed in it.
|
||||
// If there is none available where this is true, use the smallest
|
||||
// available unit.
|
||||
const product = this.variant.product;
|
||||
const scales = this.variantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit);
|
||||
const scales = this.variantUnitManager.compatibleUnitScales(
|
||||
this.variant.variant_unit_scale,
|
||||
this.variant.variant_unit,
|
||||
);
|
||||
const variantUnitValue = this.variant.unit_value;
|
||||
|
||||
// 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) {
|
||||
return [largestScale, this.variantUnitManager.getUnitName(largestScale, product.variant_unit)];
|
||||
return [
|
||||
largestScale,
|
||||
this.variantUnitManager.getUnitName(largestScale, this.variant.variant_unit),
|
||||
];
|
||||
} 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) {
|
||||
if (this.units[unitType][scale]) {
|
||||
return this.units[unitType][scale]['name'];
|
||||
return this.units[unitType][scale]["name"];
|
||||
} else {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Filter by measurement system
|
||||
// Filter by measurement system
|
||||
compatibleUnitScales(scale, unitType) {
|
||||
const scaleSystem = this.units[unitType][scale]['system'];
|
||||
const scaleSystem = this.units[unitType][scale]["system"];
|
||||
|
||||
return Object.entries(this.units[unitType])
|
||||
.filter(([scale, scaleInfo]) => {
|
||||
return scaleInfo['system'] == scaleSystem;
|
||||
return scaleInfo["system"] == scaleSystem;
|
||||
})
|
||||
.map(([scale, _]) => parseFloat(scale))
|
||||
.sort();
|
||||
}
|
||||
|
||||
systemOfMeasurement(scale, unitType) {
|
||||
if (this.units[unitType][scale]) {
|
||||
return this.units[unitType][scale]["system"];
|
||||
} else {
|
||||
return "custom";
|
||||
}
|
||||
}
|
||||
|
||||
// private
|
||||
|
||||
#loadUnits(units) {
|
||||
// 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 measurementInfoTransformed = Object.fromEntries(Object.entries(measurementInfo).map(([scale, unitInfo]) =>
|
||||
[ parseFloat(scale), unitInfo ]
|
||||
));
|
||||
return [ measurement, measurementInfoTransformed ];
|
||||
const measurementInfoTransformed = Object.fromEntries(
|
||||
Object.entries(measurementInfo).map(([scale, unitInfo]) => [parseFloat(scale), unitInfo]),
|
||||
);
|
||||
return [measurement, measurementInfoTransformed];
|
||||
});
|
||||
return Object.fromEntries(unitsTransformed);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ if ENV["OPENID_APP_ID"].present? && ENV["OPENID_APP_SECRET"].present?
|
||||
config.omniauth :openid_connect, {
|
||||
name: :openid_connect,
|
||||
issuer: "https://login.lescommuns.org/auth/realms/data-food-consortium",
|
||||
scope: [:openid, :profile, :email],
|
||||
scope: [:openid, :profile, :email, :offline_access],
|
||||
response_type: :code,
|
||||
uid_field: "email",
|
||||
discovery: true,
|
||||
|
||||
@@ -46,12 +46,12 @@ ar:
|
||||
price: "السعر"
|
||||
primary_taxon_id: "نوع المنتج "
|
||||
shipping_category_id: "نوع الشحن"
|
||||
variant_unit_name: "اسم وحدة النوع"
|
||||
unit_value: "قيمةالوحدة"
|
||||
spree/variant:
|
||||
primary_taxon: "نوع المنتج "
|
||||
shipping_category_id: "نوع الشحن"
|
||||
supplier: "المورد"
|
||||
variant_unit_name: "اسم وحدة النوع"
|
||||
unit_value: "قيمةالوحدة"
|
||||
spree/credit_card:
|
||||
base: "بطاقة ائتمان"
|
||||
number: "رقم "
|
||||
@@ -1146,6 +1146,8 @@ ar:
|
||||
<a href="https://regenerative.org.au/" target="_blank"><b>Visit Discover Regenerative</b>
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
vine:
|
||||
enable: "الاتصال"
|
||||
actions:
|
||||
edit_profile: الإعدادات
|
||||
properties: الخصائص
|
||||
@@ -2802,6 +2804,7 @@ ar:
|
||||
report_header_quantity: الكمية
|
||||
report_header_max_quantity: اعلى كمية
|
||||
report_header_variant: النوع
|
||||
report_header_variant_unit_name: اسم وحدة النوع
|
||||
report_header_variant_value: قيمة النوع
|
||||
report_header_variant_unit: وحدة النوع
|
||||
report_header_total_available: القيمة الكلية متاحة
|
||||
@@ -4190,12 +4193,15 @@ ar:
|
||||
new_variant: "نوع جديد"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "السعر"
|
||||
unit_price: "سعر الوحدة"
|
||||
display_as: "عرض ب"
|
||||
display_name: "اسم العرض"
|
||||
display_as_placeholder: 'على سبيل المثال 2 كجم'
|
||||
display_name_placeholder: 'على سبيل المثال طماطم'
|
||||
unit: وحدة
|
||||
price: السعر
|
||||
unit_value: قيمةالوحدة
|
||||
variant_category: الفئة
|
||||
autocomplete:
|
||||
out_of_stock: "غير متوفر"
|
||||
producer_name: "المنتج"
|
||||
|
||||
@@ -46,12 +46,12 @@ ca:
|
||||
price: "Preu"
|
||||
primary_taxon_id: "Categoria del producte"
|
||||
shipping_category_id: "Categoria d'enviament"
|
||||
variant_unit_name: "Nom de la unitat de la variant"
|
||||
unit_value: "Valor de la unitat"
|
||||
spree/variant:
|
||||
primary_taxon: "Categoria del producte"
|
||||
shipping_category_id: "Categoria d'enviament"
|
||||
supplier: "Proveïdora"
|
||||
variant_unit_name: "Nom de la unitat de la variant"
|
||||
unit_value: "Valor de la unitat"
|
||||
spree/credit_card:
|
||||
base: "Targeta de crèdit"
|
||||
number: "Número"
|
||||
@@ -1200,6 +1200,8 @@ ca:
|
||||
loading: "S'està carregant"
|
||||
discover_regen:
|
||||
loading: "S'està carregant"
|
||||
vine:
|
||||
enable: "Connecta"
|
||||
actions:
|
||||
edit_profile: Configuració
|
||||
properties: Propietats
|
||||
@@ -2789,6 +2791,7 @@ ca:
|
||||
report_header_quantity: Quantitat
|
||||
report_header_max_quantity: Quantitat màxima
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Nom de la unitat de la variant
|
||||
report_header_variant_value: Valor de la variant
|
||||
report_header_variant_unit: Unitat de la variant
|
||||
report_header_total_available: Total disponible
|
||||
@@ -4068,12 +4071,15 @@ ca:
|
||||
new_variant: "Nova variant"
|
||||
form:
|
||||
sku: "Número de referència (SKU)"
|
||||
price: "Preu"
|
||||
unit_price: "Preu unitari"
|
||||
display_as: "Mostra com"
|
||||
display_name: "Nom de visualització"
|
||||
display_as_placeholder: 'per exemple. 2 kg'
|
||||
display_name_placeholder: 'per exemple. Tomàquets'
|
||||
unit: Unitat
|
||||
price: Preu
|
||||
unit_value: Valor de la unitat
|
||||
variant_category: Categoria
|
||||
autocomplete:
|
||||
out_of_stock: "Fora d'existència"
|
||||
producer_name: "Productor"
|
||||
|
||||
@@ -49,12 +49,12 @@ cy:
|
||||
price: "Pris"
|
||||
primary_taxon_id: "Categori Cynnyrch"
|
||||
shipping_category_id: "Categori dosbarthu."
|
||||
variant_unit_name: "Enw Uned Amrywiol"
|
||||
unit_value: "Gwerth yr uned"
|
||||
spree/variant:
|
||||
primary_taxon: "Categori Cynnyrch"
|
||||
shipping_category_id: "Categori Anfon"
|
||||
supplier: "Cyflenwr"
|
||||
variant_unit_name: "Enw Uned Amrywiolyn"
|
||||
unit_value: "Gwerth yr uned"
|
||||
spree/credit_card:
|
||||
base: "Cerdyn credyd"
|
||||
number: "Rhif"
|
||||
@@ -1260,6 +1260,8 @@ cy:
|
||||
disable: "Rhoi’r gorau i rannu"
|
||||
loading: "Yn llwytho"
|
||||
link_label: "Rheoli’r rhestru"
|
||||
vine:
|
||||
enable: "Adnoddau"
|
||||
actions:
|
||||
edit_profile: Gosodiadau
|
||||
properties: Manylion
|
||||
@@ -2947,6 +2949,7 @@ cy:
|
||||
report_header_quantity: Nifer
|
||||
report_header_max_quantity: Uchafswm nifer
|
||||
report_header_variant: Amrywiolyn
|
||||
report_header_variant_unit_name: Enw Uned Amrywiolyn
|
||||
report_header_variant_value: Gwerth Amrywiolyn
|
||||
report_header_variant_unit: Uned Amrywiolyn
|
||||
report_header_total_available: Cyfanswm ar gael
|
||||
@@ -4320,12 +4323,15 @@ cy:
|
||||
new_variant: "Amrywiolyn newydd"
|
||||
form:
|
||||
sku: "Cod y Cynnyrch"
|
||||
price: "Pris"
|
||||
unit_price: "Pris Uned"
|
||||
display_as: "Arddangos fel"
|
||||
display_name: "Enw Arddangos"
|
||||
display_as_placeholder: 'e.e. 2 kg'
|
||||
display_name_placeholder: 'e.e. Tomatos'
|
||||
unit: Uned
|
||||
price: Pris
|
||||
unit_value: Gwerth yr uned
|
||||
variant_category: Categori
|
||||
autocomplete:
|
||||
out_of_stock: "Allan o stoc"
|
||||
producer_name: "Cynhyrchydd"
|
||||
|
||||
@@ -35,11 +35,11 @@ de_CH:
|
||||
price: "Preis"
|
||||
primary_taxon_id: "Produktkategorie"
|
||||
shipping_category_id: "Lieferkategorie"
|
||||
variant_unit_name: "Name der Varianteneinheit"
|
||||
spree/variant:
|
||||
primary_taxon: "Produktkategorie"
|
||||
shipping_category_id: "Lieferkategorie"
|
||||
supplier: "Lieferant"
|
||||
variant_unit_name: "Name der Varianteneinheit"
|
||||
spree/credit_card:
|
||||
base: "Kreditkarte"
|
||||
number: "Kreditkartennummer"
|
||||
@@ -1112,6 +1112,8 @@ de_CH:
|
||||
loading: "Wird geladen ..."
|
||||
discover_regen:
|
||||
loading: "Wird geladen ..."
|
||||
vine:
|
||||
enable: "Über OFN"
|
||||
actions:
|
||||
edit_profile: Einstellungen
|
||||
properties: Eigenschaften
|
||||
@@ -2710,6 +2712,7 @@ de_CH:
|
||||
report_header_quantity: Menge
|
||||
report_header_max_quantity: Max Menge
|
||||
report_header_variant: Produktvariante
|
||||
report_header_variant_unit_name: Name der Varianteneinheit
|
||||
report_header_variant_value: Wert der Produktvarianten
|
||||
report_header_variant_unit: Varianteneinheit
|
||||
report_header_total_available: Insgesamt verfügbar
|
||||
@@ -3974,12 +3977,14 @@ de_CH:
|
||||
new_variant: "Neue Produktvariante"
|
||||
form:
|
||||
sku: "Artikelnummer"
|
||||
price: "Preis"
|
||||
unit_price: "Grundpreis"
|
||||
display_as: "Anzeigen als"
|
||||
display_name: "Variantenname"
|
||||
display_as_placeholder: 'z. B. 2 kg'
|
||||
display_name_placeholder: 'z. B. Tomaten'
|
||||
unit: Einheit
|
||||
price: Preis
|
||||
variant_category: Kategorie
|
||||
autocomplete:
|
||||
out_of_stock: "nicht vorrätig"
|
||||
producer_name: "Produzent"
|
||||
|
||||
@@ -49,12 +49,12 @@ de_DE:
|
||||
price: "Preis"
|
||||
primary_taxon_id: "Produktkategorie"
|
||||
shipping_category_id: "Lieferkategorie"
|
||||
variant_unit_name: "Name der Varianteneinheit"
|
||||
unit_value: "Menge"
|
||||
spree/variant:
|
||||
primary_taxon: "Produktkategorie"
|
||||
shipping_category_id: "Lieferkategorie"
|
||||
supplier: "Lieferant"
|
||||
variant_unit_name: "Name der Varianteneinheit"
|
||||
unit_value: "Menge"
|
||||
spree/credit_card:
|
||||
base: "Kreditkarte"
|
||||
number: "Kreditkartennummer"
|
||||
@@ -1240,6 +1240,8 @@ de_DE:
|
||||
loading: "Wird geladen ..."
|
||||
discover_regen:
|
||||
loading: "Wird geladen ..."
|
||||
vine:
|
||||
enable: "Über OFN"
|
||||
actions:
|
||||
edit_profile: Einstellungen
|
||||
properties: Eigenschaften
|
||||
@@ -2927,6 +2929,7 @@ de_DE:
|
||||
report_header_quantity: Menge
|
||||
report_header_max_quantity: Max Menge
|
||||
report_header_variant: Produktvariante
|
||||
report_header_variant_unit_name: Name der Varianteneinheit
|
||||
report_header_variant_value: Wert der Produktvarianten
|
||||
report_header_variant_unit: Varianteneinheit
|
||||
report_header_total_available: Insgesamt verfügbar
|
||||
@@ -4250,12 +4253,15 @@ de_DE:
|
||||
new_variant: "Neue Produktvariante"
|
||||
form:
|
||||
sku: "Artikelnummer"
|
||||
price: "Preis"
|
||||
unit_price: "Grundpreis"
|
||||
display_as: "Anzeigen als"
|
||||
display_name: "Variantenname"
|
||||
display_as_placeholder: 'z. B. 2 kg'
|
||||
display_name_placeholder: 'z. B. Tomaten'
|
||||
unit: Einheit
|
||||
price: Preis
|
||||
unit_value: Menge
|
||||
variant_category: Kategorie
|
||||
autocomplete:
|
||||
out_of_stock: "nicht vorrätig"
|
||||
producer_name: "Produzent"
|
||||
|
||||
@@ -46,12 +46,12 @@ el:
|
||||
price: "Τιμή"
|
||||
primary_taxon_id: "Κατηγορία προϊόντος"
|
||||
shipping_category_id: "Κατηγορία μεταφορικών"
|
||||
variant_unit_name: "Όνομα μεταβλητής"
|
||||
unit_value: "Τιμή μεταβλητής"
|
||||
spree/variant:
|
||||
primary_taxon: "Κατηγορία προϊόντος"
|
||||
shipping_category_id: "Κατηγορία μεταφοράς"
|
||||
supplier: "Προμηθευτής"
|
||||
variant_unit_name: "Όνομα μεταβλητής"
|
||||
unit_value: "Τιμή μεταβλητής"
|
||||
spree/credit_card:
|
||||
base: "Κάρτα πιστωτική/χρεωστική"
|
||||
number: "Αριθμός"
|
||||
@@ -1314,6 +1314,10 @@ el:
|
||||
target="_blank"><b>Μάθετε περισσότερα για το Discover Regenerative</b>
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
vine:
|
||||
enable: "Σύνδεση"
|
||||
disable: "Αποσύνδεση."
|
||||
need_to_be_manager: "Μόνο οι διαχειριστές μπορούν να συνδέουν εφαρμογές."
|
||||
actions:
|
||||
edit_profile: Ρυθμήσεις
|
||||
properties: Ιδιότητες
|
||||
@@ -2993,6 +2997,7 @@ el:
|
||||
report_header_quantity: Ποσότητα
|
||||
report_header_max_quantity: Μέγιστη Ποσότητα
|
||||
report_header_variant: Παραλαγή
|
||||
report_header_variant_unit_name: Όνομα μεταβλητής
|
||||
report_header_variant_value: Παραλλαγή Αξίας
|
||||
report_header_variant_unit: Μονάδα μέτρησης μεταβλητής
|
||||
report_header_total_available: Σύνολο διαθέσιμο
|
||||
@@ -4326,12 +4331,16 @@ el:
|
||||
new_variant: "Νέα παραλλαγή"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "Τιμή"
|
||||
unit_price: "Τιμή Μονάδας"
|
||||
display_as: "Εμφάνιση Ως"
|
||||
display_name: "Εμφανιζόμενο όνομα"
|
||||
display_as_placeholder: 'π.χ. 2 κιλά'
|
||||
display_name_placeholder: 'π.χ. Ντομάτες'
|
||||
unit_scale: "Κλίμακα μονάδας"
|
||||
unit: Μονάδα
|
||||
price: Τιμή
|
||||
unit_value: Τιμή μεταβλητής
|
||||
variant_category: Κατηγορία
|
||||
autocomplete:
|
||||
out_of_stock: "Εκτός αποθέματος"
|
||||
producer_name: "Παραγωγός"
|
||||
|
||||
@@ -70,13 +70,13 @@ en:
|
||||
price: "Price"
|
||||
primary_taxon_id: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit: "Unit Scale"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
unit_value: "Unit value"
|
||||
spree/variant:
|
||||
primary_taxon: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
supplier: "Supplier"
|
||||
variant_unit: "Unit Scale"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
unit_value: "Unit value"
|
||||
spree/credit_card:
|
||||
base: "Credit Card"
|
||||
number: "Number"
|
||||
@@ -1412,7 +1412,7 @@ en:
|
||||
connected_apps:
|
||||
legend: "Connected apps"
|
||||
affiliate_sales_data:
|
||||
title: "INRAE / UFC QUE CHOISIR Research"
|
||||
title: "INRAE Research"
|
||||
tagline: "Allow this research project to access your orders data anonymously"
|
||||
enable: "Allow data sharing"
|
||||
disable: "Stop sharing"
|
||||
@@ -1420,10 +1420,10 @@ en:
|
||||
need_to_be_manager: "Only managers can connect apps."
|
||||
description_html: |
|
||||
<p>
|
||||
INRAE and UFC QUE CHOISIR are teaming up to study food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
|
||||
INRAE are studiying food prices in short food systems and compare them with prices in the supermarket, for a given set of products. The data that is used by INRAE is mixed with data coming from other short food chain platforms in France. No individual product prices will be publicly disclosed through this project.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://apropos.coopcircuits.fr/"
|
||||
<a href="https://pepr-sams.fr/2024/03/12/plat4terfood/"
|
||||
target="_blank"><b>Learn more about this research project</b>
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
@@ -1768,6 +1768,7 @@ en:
|
||||
pack_by_customer: Pack By Customer
|
||||
pack_by_supplier: Pack By Supplier
|
||||
pack_by_product: Pack By Product
|
||||
pay_your_suppliers: Pay your suppliers
|
||||
display:
|
||||
report_is_big: "This report is big and may slow down your device."
|
||||
display_anyway: "Display anyway"
|
||||
@@ -1814,6 +1815,8 @@ en:
|
||||
enterprise_fee_summary:
|
||||
name: "Enterprise Fee Summary"
|
||||
description: "Summary of Enterprise Fees collected"
|
||||
suppliers:
|
||||
name: Suppliers
|
||||
enterprise_fees_with_tax_report_by_order: "Enterprise Fees With Tax Report By Order"
|
||||
enterprise_fees_with_tax_report_by_producer: "Enterprise Fees With Tax Report By Producer"
|
||||
errors:
|
||||
@@ -3172,6 +3175,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
report_render_options: Rendering Options
|
||||
report_header_ofn_uid: OFN UID
|
||||
report_header_order_cycle: Order Cycle
|
||||
report_header_order_cycle_start_date: OC Start Date
|
||||
report_header_order_cycle_end_date: OC End Date
|
||||
report_header_user: User
|
||||
report_header_email: Email
|
||||
report_header_status: Status
|
||||
@@ -3192,6 +3197,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
report_header_hub_legal_name: "Hub Legal Name"
|
||||
report_header_hub_contact_name: "Hub Contact Name"
|
||||
report_header_hub_email: "Hub Public Email"
|
||||
report_header_hub_contact_email: Hub Contact Email
|
||||
report_header_hub_owner_email: Hub Owner Email
|
||||
report_header_hub_phone: "Hub Phone Number"
|
||||
report_header_hub_address_line1: "Hub Address Line 1"
|
||||
@@ -3252,6 +3258,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
report_header_quantity: Quantity
|
||||
report_header_max_quantity: Max Quantity
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Variant Unit Name
|
||||
report_header_variant_value: Variant Value
|
||||
report_header_variant_unit: Variant Unit
|
||||
report_header_total_available: Total available
|
||||
@@ -3263,6 +3270,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
report_header_producer_suburb: Producer Suburb
|
||||
report_header_producer_tax_status: Producer Tax Status
|
||||
report_header_producer_charges_sales_tax?: GST/VAT Registered
|
||||
report_header_producer_abn_acn: Producer ABN/ACN
|
||||
report_header_producer_address: Producer Address
|
||||
report_header_unit: Unit
|
||||
report_header_group_buy_unit_quantity: Group Buy Unit Quantity
|
||||
report_header_cost: Cost
|
||||
@@ -3323,7 +3332,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
report_header_total_units: Total Units
|
||||
report_header_sum_max_total: "Sum Max Total"
|
||||
report_header_total_excl_vat: "Total excl. tax (%{currency_symbol})"
|
||||
report_header_total_fees_excl_tax: "Total fees excl. tax (%{currency_symbol})"
|
||||
report_header_total_tax_on_fees: "Total tax on fees (%{currency_symbol})"
|
||||
report_header_total: "Total (%{currency_symbol})"
|
||||
report_header_total_incl_vat: "Total incl. tax (%{currency_symbol})"
|
||||
report_header_total_excl_fees_and_tax: "Total excl. fees and tax (%{currency_symbol})"
|
||||
report_header_temp_controlled: TempControlled?
|
||||
report_header_is_producer: Producer?
|
||||
report_header_not_confirmed: Not Confirmed
|
||||
@@ -4639,6 +4652,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
display_name: "Display Name"
|
||||
display_as_placeholder: 'eg. 2 kg'
|
||||
display_name_placeholder: 'eg. Tomatoes'
|
||||
unit_scale: "Unit scale"
|
||||
unit: Unit
|
||||
price: Price
|
||||
unit_value: Unit value
|
||||
variant_category: Category
|
||||
autocomplete:
|
||||
out_of_stock: "Out of Stock"
|
||||
producer_name: "Producer"
|
||||
|
||||
@@ -30,11 +30,11 @@ en_AU:
|
||||
price: "Price"
|
||||
primary_taxon_id: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/variant:
|
||||
primary_taxon: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
supplier: "Supplier"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/credit_card:
|
||||
base: "Credit Card"
|
||||
number: "Number"
|
||||
@@ -955,6 +955,8 @@ en_AU:
|
||||
<a href="https://regenerative.org.au/" target="_blank"><b>Visit Discover Regenerative</b>
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
vine:
|
||||
enable: "Open Road"
|
||||
actions:
|
||||
edit_profile: Settings
|
||||
properties: Properties
|
||||
@@ -2461,6 +2463,7 @@ en_AU:
|
||||
report_header_quantity: Quantity
|
||||
report_header_max_quantity: Max Quantity
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Variant Unit Name
|
||||
report_header_variant_value: Variant Value
|
||||
report_header_variant_unit: Variant Unit
|
||||
report_header_total_available: Total available
|
||||
@@ -3630,12 +3633,14 @@ en_AU:
|
||||
new_variant: "New Variant"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "Price"
|
||||
unit_price: "Unit Price"
|
||||
display_as: "Display As"
|
||||
display_name: "Display Name"
|
||||
display_as_placeholder: 'eg. 2 kg'
|
||||
display_name_placeholder: 'eg. Tomatoes'
|
||||
unit: Unit
|
||||
price: Price
|
||||
variant_category: Category
|
||||
autocomplete:
|
||||
out_of_stock: "Out of Stock"
|
||||
producer_name: "Producer"
|
||||
|
||||
@@ -29,11 +29,11 @@ en_BE:
|
||||
price: "Price"
|
||||
primary_taxon_id: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/variant:
|
||||
primary_taxon: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
supplier: "Supplier"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/credit_card:
|
||||
base: "Credit Card"
|
||||
number: "Number"
|
||||
@@ -885,6 +885,8 @@ en_BE:
|
||||
loading: "Loading"
|
||||
discover_regen:
|
||||
loading: "Loading"
|
||||
vine:
|
||||
enable: "Connect"
|
||||
actions:
|
||||
edit_profile: Settings
|
||||
properties: Properties
|
||||
@@ -2319,6 +2321,7 @@ en_BE:
|
||||
report_header_quantity: Quantity
|
||||
report_header_max_quantity: Max Quantity
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Variant Unit Name
|
||||
report_header_variant_value: Variant Value
|
||||
report_header_variant_unit: Variant Unit
|
||||
report_header_total_available: Total available
|
||||
@@ -3288,9 +3291,11 @@ en_BE:
|
||||
option_types: "Option Types"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "Price"
|
||||
unit_price: "Unit Price"
|
||||
display_as: "Display As"
|
||||
unit: Unit
|
||||
price: Price
|
||||
variant_category: Category
|
||||
autocomplete:
|
||||
out_of_stock: "Out of Stock"
|
||||
producer_name: "Producer"
|
||||
|
||||
@@ -49,13 +49,13 @@ en_CA:
|
||||
price: "Price"
|
||||
primary_taxon_id: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit: "Unit Scale"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
unit_value: "Unit value"
|
||||
spree/variant:
|
||||
primary_taxon: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
supplier: "Supplier"
|
||||
variant_unit: "Unit Scale"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
unit_value: "Unit value"
|
||||
spree/credit_card:
|
||||
base: "Credit Card"
|
||||
number: "Number"
|
||||
@@ -1372,6 +1372,10 @@ en_CA:
|
||||
<a href="https://waterlooregionfood.ca/" target="_blank">Learn more about the Waterloo Food Directory.
|
||||
<i class="icon-external-link"></i></a>
|
||||
</p>
|
||||
vine:
|
||||
enable: "Donate"
|
||||
disable: "Disconnect"
|
||||
need_to_be_manager: "Only managers can connect apps."
|
||||
actions:
|
||||
edit_profile: Settings
|
||||
properties: Properties
|
||||
@@ -3079,6 +3083,7 @@ en_CA:
|
||||
report_header_quantity: Quantity
|
||||
report_header_max_quantity: Max Quantity
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Variant Unit Name
|
||||
report_header_variant_value: Variant Value
|
||||
report_header_variant_unit: Variant Unit
|
||||
report_header_total_available: Total available
|
||||
@@ -4429,12 +4434,16 @@ en_CA:
|
||||
new_variant: "New Variant"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "Price"
|
||||
unit_price: "Unit Price"
|
||||
display_as: "Display As"
|
||||
display_name: "Display Name"
|
||||
display_as_placeholder: 'eg. 2 kg'
|
||||
display_name_placeholder: 'eg. Tomatoes'
|
||||
unit_scale: "Unit scale"
|
||||
unit: Unit
|
||||
price: Price
|
||||
unit_value: Unit value
|
||||
variant_category: Category
|
||||
autocomplete:
|
||||
out_of_stock: "Out of Stock"
|
||||
producer_name: "Producer"
|
||||
|
||||
@@ -29,11 +29,11 @@ en_DE:
|
||||
price: "Price"
|
||||
primary_taxon_id: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/variant:
|
||||
primary_taxon: "Product Category"
|
||||
shipping_category_id: "Shipping Category"
|
||||
supplier: "Supplier"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
spree/credit_card:
|
||||
base: "Credit Card"
|
||||
number: "Number"
|
||||
@@ -893,6 +893,8 @@ en_DE:
|
||||
loading: "Loading"
|
||||
discover_regen:
|
||||
loading: "Loading"
|
||||
vine:
|
||||
enable: "Connect"
|
||||
actions:
|
||||
edit_profile: Settings
|
||||
properties: Properties
|
||||
@@ -2327,6 +2329,7 @@ en_DE:
|
||||
report_header_quantity: Quantity
|
||||
report_header_max_quantity: Max Quantity
|
||||
report_header_variant: Variant
|
||||
report_header_variant_unit_name: Variant Unit Name
|
||||
report_header_variant_value: Variant Value
|
||||
report_header_variant_unit: Variant Unit
|
||||
report_header_total_available: Total available
|
||||
@@ -3303,9 +3306,11 @@ en_DE:
|
||||
option_types: "Option Types"
|
||||
form:
|
||||
sku: "SKU"
|
||||
price: "Price"
|
||||
unit_price: "Unit Price"
|
||||
display_as: "Display As"
|
||||
unit: Unit
|
||||
price: Price
|
||||
variant_category: Category
|
||||
autocomplete:
|
||||
out_of_stock: "Out of Stock"
|
||||
producer_name: "Producer"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user