mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-06 02:51:34 +00:00
Merge pull request #12787 from rioug/move-variant-unit-attributes-to-variant
[Product Refactor] Move variant unit sizes to variant
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -245,6 +258,7 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def ensure_standard_variant
|
||||
return unless variants.empty?
|
||||
|
||||
@@ -254,31 +268,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
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
# Remove any unsupported HTML.
|
||||
def description
|
||||
@@ -292,27 +291,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?
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,16 @@ 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 = pluralize(@nameable.variant_unit_name, value)
|
||||
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,
|
||||
|
||||
@@ -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'" }
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -4639,6 +4639,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"
|
||||
|
||||
6
db/migrate/20240618013850_add_variant_unit_to_variant.rb
Normal file
6
db/migrate/20240618013850_add_variant_unit_to_variant.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class AddVariantUnitToVariant < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :spree_variants, :variant_unit_scale, :float
|
||||
add_column :spree_variants, :variant_unit_name, :string, limit: 255
|
||||
end
|
||||
end
|
||||
21
db/migrate/20240819045115_migrate_unit_size_to_variants.rb
Normal file
21
db/migrate/20240819045115_migrate_unit_size_to_variants.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class MigrateUnitSizeToVariants < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
# Copy variant_unit only if it's empty in the variant
|
||||
ActiveRecord::Base.connection.execute(<<-SQL
|
||||
UPDATE spree_variants
|
||||
SET variant_unit = spree_products.variant_unit
|
||||
FROM spree_products
|
||||
WHERE spree_variants.product_id = spree_products.id
|
||||
AND spree_variants.variant_unit IS NULL
|
||||
SQL
|
||||
)
|
||||
|
||||
ActiveRecord::Base.connection.execute(<<-SQL
|
||||
UPDATE spree_variants
|
||||
SET variant_unit_scale = spree_products.variant_unit_scale, variant_unit_name = spree_products.variant_unit_name
|
||||
FROM spree_products
|
||||
WHERE spree_variants.product_id = spree_products.id
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -968,6 +968,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_02_014059) do
|
||||
t.bigint "shipping_category_id"
|
||||
t.bigint "primary_taxon_id"
|
||||
t.bigint "supplier_id"
|
||||
t.float "variant_unit_scale"
|
||||
t.string "variant_unit_name", limit: 255
|
||||
t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id"
|
||||
t.index ["product_id"], name: "index_variants_on_product_id"
|
||||
t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class QuantitativeValueBuilder < DfcBuilder
|
||||
def self.quantity(variant)
|
||||
DataFoodConsortium::Connector::QuantitativeValue.new(
|
||||
unit: unit(variant.product.variant_unit),
|
||||
unit: unit(variant.variant_unit),
|
||||
value: variant.unit_value,
|
||||
)
|
||||
end
|
||||
@@ -27,7 +27,7 @@ class QuantitativeValueBuilder < DfcBuilder
|
||||
end
|
||||
end
|
||||
|
||||
def self.apply(quantity, product)
|
||||
def self.apply(quantity, variant)
|
||||
measure, unit_name, unit_scale = map_unit(quantity.unit)
|
||||
value = quantity.value.to_f * unit_scale
|
||||
|
||||
@@ -37,10 +37,10 @@ class QuantitativeValueBuilder < DfcBuilder
|
||||
value = 1
|
||||
end
|
||||
|
||||
product.variant_unit = measure
|
||||
product.variant_unit_name = unit_name if measure == "items"
|
||||
product.variant_unit_scale = unit_scale
|
||||
product.unit_value = value
|
||||
variant.variant_unit = measure
|
||||
variant.variant_unit_name = unit_name if measure == "items"
|
||||
variant.variant_unit_scale = unit_scale
|
||||
variant.unit_value = value
|
||||
end
|
||||
|
||||
# Map DFC units to OFN fields:
|
||||
|
||||
@@ -96,8 +96,7 @@ class SuppliedProductBuilder < DfcBuilder
|
||||
|
||||
variant.display_name = supplied_product.name
|
||||
variant.primary_taxon = taxon(supplied_product)
|
||||
QuantitativeValueBuilder.apply(supplied_product.quantity, variant.product)
|
||||
variant.unit_value = variant.product.unit_value
|
||||
QuantitativeValueBuilder.apply(supplied_product.quantity, variant)
|
||||
|
||||
catalog_item = supplied_product&.catalogItems&.first
|
||||
offer = catalog_item&.offers&.first
|
||||
|
||||
@@ -25,7 +25,8 @@ RSpec.describe AffiliateSalesDataBuilder do
|
||||
:product,
|
||||
name: "Pomme",
|
||||
supplier_id: supplier.id,
|
||||
variant_unit: "item",
|
||||
variant_unit: "items",
|
||||
variant_unit_name: "bag",
|
||||
)
|
||||
variant = product.variants.first
|
||||
distributor = create(
|
||||
|
||||
@@ -4,12 +4,11 @@ require_relative "../spec_helper"
|
||||
|
||||
RSpec.describe QuantitativeValueBuilder do
|
||||
subject(:builder) { described_class }
|
||||
let(:variant) { build(:variant, product:) }
|
||||
let(:product) { build(:product, name: "Apple") }
|
||||
let(:variant) { build(:variant) }
|
||||
|
||||
describe ".quantity" do
|
||||
it "recognises items" do
|
||||
product.variant_unit = "item"
|
||||
variant.variant_unit = "item"
|
||||
variant.unit_value = 1
|
||||
quantity = builder.quantity(variant)
|
||||
|
||||
@@ -18,7 +17,7 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
end
|
||||
|
||||
it "recognises volume" do
|
||||
product.variant_unit = "volume"
|
||||
variant.variant_unit = "volume"
|
||||
variant.unit_value = 2
|
||||
quantity = builder.quantity(variant)
|
||||
|
||||
@@ -27,7 +26,7 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
end
|
||||
|
||||
it "recognises weight" do
|
||||
product.variant_unit = "weight"
|
||||
variant.variant_unit = "weight"
|
||||
variant.unit_value = 1000 # 1kg
|
||||
quantity = builder.quantity(variant)
|
||||
|
||||
@@ -36,7 +35,7 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
end
|
||||
|
||||
it "falls back to items" do
|
||||
product.variant_unit = nil
|
||||
variant.variant_unit = nil
|
||||
quantity = builder.quantity(variant)
|
||||
|
||||
expect(quantity.value).to eq 1.0
|
||||
@@ -46,7 +45,7 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
|
||||
describe ".apply" do
|
||||
let(:quantity_unit) { DfcLoader.connector.MEASURES }
|
||||
let(:product) { Spree::Product.new }
|
||||
let(:variant) { Spree::Variant.new }
|
||||
|
||||
it "uses items for anything unknown" do
|
||||
quantity = DataFoodConsortium::Connector::QuantitativeValue.new(
|
||||
@@ -54,12 +53,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 3,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "items"
|
||||
expect(product.variant_unit_name).to eq "Jar"
|
||||
expect(product.variant_unit_scale).to eq 1
|
||||
expect(product.unit_value).to eq 3
|
||||
expect(variant.variant_unit).to eq "items"
|
||||
expect(variant.variant_unit_name).to eq "Jar"
|
||||
expect(variant.variant_unit_scale).to eq 1
|
||||
expect(variant.unit_value).to eq 3
|
||||
end
|
||||
|
||||
it "knows metric units" do
|
||||
@@ -68,12 +67,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 2,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "volume"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 1
|
||||
expect(product.unit_value).to eq 2
|
||||
expect(variant.variant_unit).to eq "volume"
|
||||
expect(variant.variant_unit_name).to eq nil
|
||||
expect(variant.variant_unit_scale).to eq 1
|
||||
expect(variant.unit_value).to eq 2
|
||||
end
|
||||
|
||||
it "knows metric units with a scale in OFN" do
|
||||
@@ -82,12 +81,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 4,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "weight"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 1_000
|
||||
expect(product.unit_value).to eq 4_000
|
||||
expect(variant.variant_unit).to eq "weight"
|
||||
expect(variant.variant_unit_name).to eq nil
|
||||
expect(variant.variant_unit_scale).to eq 1_000
|
||||
expect(variant.unit_value).to eq 4_000
|
||||
end
|
||||
|
||||
it "knows metric units with a small scale" do
|
||||
@@ -96,12 +95,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 5,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "weight"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 0.001
|
||||
expect(product.unit_value).to eq 0.005
|
||||
expect(variant.variant_unit).to eq "weight"
|
||||
expect(variant.variant_unit_name).to eq nil
|
||||
expect(variant.variant_unit_scale).to eq 0.001
|
||||
expect(variant.unit_value).to eq 0.005
|
||||
end
|
||||
|
||||
it "interpretes values given as a string" do
|
||||
@@ -110,12 +109,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: "0.4",
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "weight"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 1_000
|
||||
expect(product.unit_value).to eq 400
|
||||
expect(variant.variant_unit).to eq "weight"
|
||||
expect(variant.variant_unit_name).to eq nil
|
||||
expect(variant.variant_unit_scale).to eq 1_000
|
||||
expect(variant.unit_value).to eq 400
|
||||
end
|
||||
|
||||
it "knows imperial units" do
|
||||
@@ -124,12 +123,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 10,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "weight"
|
||||
expect(product.variant_unit_name).to eq nil
|
||||
expect(product.variant_unit_scale).to eq 453.59237
|
||||
expect(product.unit_value).to eq 4_535.9237
|
||||
expect(variant.variant_unit).to eq "weight"
|
||||
expect(variant.variant_unit_name).to eq nil
|
||||
expect(variant.variant_unit_scale).to eq 453.59237
|
||||
expect(variant.unit_value).to eq 4_535.9237
|
||||
end
|
||||
|
||||
it "knows customary units" do
|
||||
@@ -138,12 +137,12 @@ RSpec.describe QuantitativeValueBuilder do
|
||||
value: 2,
|
||||
)
|
||||
|
||||
builder.apply(quantity, product)
|
||||
builder.apply(quantity, variant)
|
||||
|
||||
expect(product.variant_unit).to eq "items"
|
||||
expect(product.variant_unit_name).to eq "dozen"
|
||||
expect(product.variant_unit_scale).to eq 12
|
||||
expect(product.unit_value).to eq 24
|
||||
expect(variant.variant_unit).to eq "items"
|
||||
expect(variant.variant_unit_name).to eq "dozen"
|
||||
expect(variant.variant_unit_scale).to eq 12
|
||||
expect(variant.unit_value).to eq 24
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,9 +66,7 @@ module.exports = {
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
moduleDirectories: ["node_modules", "app/webpacker"],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
@@ -157,9 +155,7 @@ module.exports = {
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
"testRegex": [
|
||||
"spec/javascripts/.*_test\\.js"
|
||||
],
|
||||
testRegex: ["spec/javascripts/.*_test\\.js"],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
@@ -177,9 +173,7 @@ module.exports = {
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(stimulus)/)"
|
||||
]
|
||||
transformIgnorePatterns: ["/node_modules/(?!(stimulus)/)"],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
@@ -36,7 +36,7 @@ module OpenFoodNetwork
|
||||
Spree::Variant.
|
||||
ransack(search_params.merge(m: 'or')).
|
||||
result.
|
||||
order("spree_products.name, display_name, display_as, spree_products.variant_unit_name").
|
||||
order("spree_products.name, display_name, display_as, spree_variants.variant_unit_name").
|
||||
includes(:product).
|
||||
joins(:product)
|
||||
end
|
||||
|
||||
@@ -49,7 +49,7 @@ module Reporting
|
||||
|
||||
def group_buy_unit_size(line_items)
|
||||
unit_size = line_items.first.variant.product.group_buy_unit_size || 0.0
|
||||
unit_size / (line_items.first.product.variant_unit_scale || 1)
|
||||
unit_size / (line_items.first.variant.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def max_quantity_excess(line_items)
|
||||
@@ -64,7 +64,7 @@ module Reporting
|
||||
end
|
||||
|
||||
def scaled_unit_value(variant)
|
||||
(variant.unit_value || 0) / (variant.product.variant_unit_scale || 1)
|
||||
(variant.unit_value || 0) / (variant.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def option_value_value(line_items)
|
||||
@@ -98,7 +98,7 @@ module Reporting
|
||||
end
|
||||
|
||||
def scaled_final_weight_volume(line_item)
|
||||
(line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1)
|
||||
(line_item.final_weight_volume || 0) / (line_item.variant.variant_unit_scale || 1)
|
||||
end
|
||||
|
||||
def total_available(line_items)
|
||||
|
||||
@@ -72,8 +72,7 @@ module Reporting
|
||||
return " " if not_all_have_unit?(line_items)
|
||||
|
||||
total_units = line_items.sum do |li|
|
||||
product = li.variant.product
|
||||
li.quantity * li.unit_value / scale_factor(product)
|
||||
li.quantity * li.unit_value / scale_factor(li.variant)
|
||||
end
|
||||
|
||||
total_units.round(3)
|
||||
@@ -92,8 +91,8 @@ module Reporting
|
||||
line_items.map { |li| li.unit_value.nil? }.any?
|
||||
end
|
||||
|
||||
def scale_factor(product)
|
||||
product.variant_unit == 'weight' ? 1000 : 1
|
||||
def scale_factor(variant)
|
||||
variant.variant_unit == 'weight' ? 1000 : 1
|
||||
end
|
||||
|
||||
def report_variant_overrides
|
||||
|
||||
@@ -25,7 +25,6 @@ module Spree
|
||||
new_product.deleted_at = nil
|
||||
new_product.updated_at = nil
|
||||
new_product.price = 0
|
||||
new_product.unit_value = %w(weight volume).include?(product.variant_unit) ? 1.0 : nil
|
||||
new_product.product_properties = reset_properties
|
||||
new_product.image = duplicate_image(product.image) if product.image
|
||||
new_product.variants = duplicate_variants
|
||||
|
||||
@@ -35,8 +35,8 @@ RSpec.describe Api::V0::ProductsController, type: :controller do
|
||||
|
||||
it "gets a single product" do
|
||||
product.create_image!(attachment:)
|
||||
product.variants.create!(unit_value: "1", unit_description: "thing", price: 1,
|
||||
primary_taxon: taxon, supplier:)
|
||||
product.variants.create!(unit_value: "1", variant_unit: "weight", variant_unit_scale: 1,
|
||||
unit_description: "thing", price: 1, primary_taxon: taxon, supplier:)
|
||||
product.variants.first.images.create!(attachment:)
|
||||
product.set_property("spree", "rocks")
|
||||
|
||||
@@ -121,8 +121,8 @@ RSpec.describe Api::V0::ProductsController, type: :controller do
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
errors = json_response["errors"]
|
||||
expect(errors.keys).to match_array([
|
||||
"name", "variant_unit", "price",
|
||||
"primary_taxon_id", "supplier_id"
|
||||
"name", "price", "primary_taxon_id",
|
||||
"supplier_id", "variant_unit"
|
||||
])
|
||||
end
|
||||
|
||||
|
||||
@@ -144,9 +144,9 @@ RSpec.describe Api::V0::VariantsController, type: :controller do
|
||||
|
||||
it "can create a new variant" do
|
||||
original_number_of_variants = variant.product.variants.count
|
||||
api_post :create, variant: { sku: "12345", unit_value: "1", unit_description: "L",
|
||||
price: "1", primary_taxon_id: taxon.id,
|
||||
supplier_id: variant.supplier.id },
|
||||
api_post :create, variant: { sku: "12345", unit_value: "1", variant_unit: "weight",
|
||||
variant_unit_scale: 1, unit_description: "L", price: "1",
|
||||
primary_taxon_id: taxon.id, supplier_id: variant.supplier.id },
|
||||
product_id: variant.product.id
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)
|
||||
|
||||
@@ -111,6 +111,8 @@ RSpec.describe Spree::Admin::ProductsController, type: :controller do
|
||||
"on_hand" => 2,
|
||||
"price" => "5.0",
|
||||
"unit_value" => 4,
|
||||
"variant_unit" => "weight",
|
||||
"variant_unit_scale" => "1",
|
||||
"unit_description" => "",
|
||||
"display_name" => "name",
|
||||
"primary_taxon_id" => taxon.id,
|
||||
|
||||
@@ -12,8 +12,8 @@ module Spree
|
||||
let(:product) { create(:product, name: 'Product A') }
|
||||
let(:deleted_variant) do
|
||||
deleted_variant = product.variants.create(
|
||||
unit_value: "2", price: 1, primary_taxon: create(:taxon),
|
||||
supplier: create(:supplier_enterprise)
|
||||
unit_value: "2", variant_unit: "weight", variant_unit_scale: 1, price: 1,
|
||||
primary_taxon: create(:taxon), supplier: create(:supplier_enterprise)
|
||||
)
|
||||
deleted_variant.delete
|
||||
deleted_variant
|
||||
|
||||
@@ -20,10 +20,8 @@ FactoryBot.define do
|
||||
|
||||
unit_value { 1 }
|
||||
unit_description { '' }
|
||||
|
||||
variant_unit { 'weight' }
|
||||
variant_unit_scale { 1 }
|
||||
variant_unit_name { '' }
|
||||
|
||||
# ensure stock item will be created for this products master
|
||||
before(:create) { DefaultStockLocation.find_or_create }
|
||||
|
||||
@@ -10,6 +10,12 @@ FactoryBot.define do
|
||||
height { generate(:random_float) }
|
||||
width { generate(:random_float) }
|
||||
depth { generate(:random_float) }
|
||||
unit_value { 1 }
|
||||
unit_description { '' }
|
||||
|
||||
variant_unit { 'weight' }
|
||||
variant_unit_scale { 1 }
|
||||
variant_unit_name { '' }
|
||||
|
||||
primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) }
|
||||
supplier { Enterprise.is_primary_producer.first || FactoryBot.create(:supplier_enterprise) }
|
||||
@@ -31,9 +37,6 @@ FactoryBot.define do
|
||||
on_hand { 5 }
|
||||
end
|
||||
|
||||
unit_value { 1 }
|
||||
unit_description { '' }
|
||||
|
||||
after(:create) do |variant, evaluator|
|
||||
variant.on_demand = evaluator.on_demand
|
||||
variant.on_hand = evaluator.on_hand
|
||||
|
||||
@@ -4,8 +4,9 @@ require "spec_helper"
|
||||
|
||||
RSpec.describe Admin::ProductsHelper do
|
||||
describe '#unit_value_with_description' do
|
||||
let(:product) { create(:product, variant_unit_scale: 1000.0) }
|
||||
let(:variant) { create(:variant, product:, unit_value: 2000.0, unit_description: 'kg') }
|
||||
let(:variant) {
|
||||
create(:variant, variant_unit_scale: 1000.0, unit_value: 2000.0, unit_description: 'kg')
|
||||
}
|
||||
|
||||
context 'when unit_value and unit_description are present' do
|
||||
it 'returns the scaled unit value with the description' do
|
||||
@@ -30,7 +31,7 @@ RSpec.describe Admin::ProductsHelper do
|
||||
end
|
||||
|
||||
context 'when variant_unit_scale is nil' do
|
||||
before { product.update_column(:variant_unit_scale, nil) }
|
||||
before { variant.update_column(:variant_unit_scale, nil) }
|
||||
|
||||
it 'uses default scale of 1 and returns the unscaled unit value with the description' do
|
||||
expect(helper.unit_value_with_description(variant)).to eq('2000 kg')
|
||||
|
||||
63
spec/javascripts/services/localize_currency_test.js
Normal file
63
spec/javascripts/services/localize_currency_test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import localizeCurrency from "js/services/localize_currency";
|
||||
|
||||
describe("convert number to localised currency", function () {
|
||||
beforeAll(() => {
|
||||
const mockedToCurrency = jest.fn();
|
||||
mockedToCurrency.mockImplementation((amount, options) => {
|
||||
if (options.format == "%n %u") {
|
||||
return `${amount.toFixed(options.precision)}${options.unit}`;
|
||||
} else {
|
||||
return `${options.unit}${amount.toFixed(options.precision)}`;
|
||||
}
|
||||
});
|
||||
|
||||
global.I18n = { toCurrency: mockedToCurrency };
|
||||
|
||||
// Requires global var from page
|
||||
global.ofn_currency_config = {
|
||||
symbol: "$",
|
||||
symbol_position: "before",
|
||||
currency: "D",
|
||||
hide_cents: "false",
|
||||
};
|
||||
});
|
||||
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
});
|
||||
|
||||
it("adds decimal fraction to an amount", function () {
|
||||
expect(localizeCurrency(10)).toEqual("$10.00");
|
||||
});
|
||||
|
||||
it("handles an existing fraction", function () {
|
||||
expect(localizeCurrency(9.9)).toEqual("$9.90");
|
||||
});
|
||||
|
||||
it("can use any currency symbol", function () {
|
||||
global.ofn_currency_config.symbol = "£";
|
||||
expect(localizeCurrency(404.04)).toEqual("£404.04");
|
||||
});
|
||||
|
||||
it("can place symbols after the amount", function () {
|
||||
global.ofn_currency_config.symbol = "$";
|
||||
global.ofn_currency_config.symbol_position = "after";
|
||||
expect(localizeCurrency(333.3)).toEqual("333.30$");
|
||||
});
|
||||
|
||||
it("can add a currency string", function () {
|
||||
global.ofn_currency_config.display_currency = true;
|
||||
global.ofn_currency_config.symbol_position = "before";
|
||||
expect(localizeCurrency(5)).toEqual("$5.00 D");
|
||||
});
|
||||
|
||||
it("can hide cents", function () {
|
||||
global.ofn_currency_config.display_currency = false;
|
||||
global.ofn_currency_config.hide_cents = "true";
|
||||
expect(localizeCurrency(5)).toEqual("$5");
|
||||
});
|
||||
});
|
||||
@@ -2,141 +2,158 @@
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import OptionValueNamer from "../../../app/webpacker/js/services/option_value_namer";
|
||||
import OptionValueNamer from "js/services/option_value_namer";
|
||||
|
||||
describe("OptionValueNamer", () => {
|
||||
beforeAll(() => {
|
||||
// Requires global var from page
|
||||
global.ofn_available_units_sorted = {"weight":{"1.0":{"name":"g","system":"metric"},"1000.0":{"name":"kg","system":"metric"},"1000000.0":{"name":"T","system":"metric"}},"volume":{"0.001":{"name":"mL","system":"metric"},"1.0":{"name":"L","system":"metric"},"4.54609":{"name":"gal","system":"imperial"},"1000.0":{"name":"kL","system":"metric"}}};
|
||||
})
|
||||
global.ofn_available_units_sorted = {
|
||||
weight: {
|
||||
"1.0": { name: "g", system: "metric" },
|
||||
"1000.0": { name: "kg", system: "metric" },
|
||||
"1000000.0": { name: "T", system: "metric" },
|
||||
},
|
||||
volume: {
|
||||
0.001: { name: "mL", system: "metric" },
|
||||
"1.0": { name: "L", system: "metric" },
|
||||
4.54609: { name: "gal", system: "imperial" },
|
||||
"1000.0": { name: "kL", system: "metric" },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("generating option value name", function() {
|
||||
describe("generating option value name", function () {
|
||||
var v, namer;
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
v = {};
|
||||
var ofn_available_units_sorted = ofn_available_units_sorted;
|
||||
namer = new OptionValueNamer(v);
|
||||
});
|
||||
|
||||
it("when unit is blank (empty items name)", function() {
|
||||
it("when unit is blank (empty items name)", function () {
|
||||
jest.spyOn(namer, "value_scaled").mockImplementation(() => true);
|
||||
jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", ""]);
|
||||
expect(namer.name()).toBe("value");
|
||||
});
|
||||
|
||||
it("when description is blank", function() {
|
||||
it("when description is blank", function () {
|
||||
v.unit_description = null;
|
||||
jest.spyOn(namer, "value_scaled").mockImplementation(() => true);
|
||||
jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]);
|
||||
expect(namer.name()).toBe("valueunit");
|
||||
});
|
||||
|
||||
it("when description is present", function() {
|
||||
v.unit_description = 'desc';
|
||||
it("when description is present", function () {
|
||||
v.unit_description = "desc";
|
||||
jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]);
|
||||
jest.spyOn(namer, "value_scaled").mockImplementation(() => true);
|
||||
expect(namer.name()).toBe("valueunit desc");
|
||||
});
|
||||
|
||||
it("when value is blank and description is present", function() {
|
||||
v.unit_description = 'desc';
|
||||
it("when value is blank and description is present", function () {
|
||||
v.unit_description = "desc";
|
||||
jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => [null, null]);
|
||||
jest.spyOn(namer, "value_scaled").mockImplementation(() => true);
|
||||
expect(namer.name()).toBe("desc");
|
||||
});
|
||||
|
||||
it("spaces value and unit when value is unscaled", function() {
|
||||
it("spaces value and unit when value is unscaled", function () {
|
||||
v.unit_description = null;
|
||||
jest.spyOn(namer, "option_value_value_unit").mockImplementation(() => ["value", "unit"]);
|
||||
jest.spyOn(namer, "value_scaled").mockImplementation(() => false);
|
||||
expect(namer.name()).toBe("value unit");
|
||||
});
|
||||
|
||||
describe("determining if a variant's value is scaled", function() {
|
||||
var p;
|
||||
beforeEach(function() {
|
||||
p = {};
|
||||
v = { product: p };
|
||||
describe("determining if a variant's value is scaled", function () {
|
||||
beforeEach(function () {
|
||||
v = {};
|
||||
namer = new OptionValueNamer(v);
|
||||
});
|
||||
it("returns true when the product has a scale", function() {
|
||||
p.variant_unit_scale = 1000;
|
||||
it("returns true when the product has a scale", function () {
|
||||
v.variant_unit_scale = 1000;
|
||||
expect(namer.value_scaled()).toBe(true);
|
||||
});
|
||||
it("returns false otherwise", function() {
|
||||
it("returns false otherwise", function () {
|
||||
expect(namer.value_scaled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generating option value's value and unit", function() {
|
||||
var v, p, namer;
|
||||
describe("generating option value's value and unit", function () {
|
||||
var v, namer;
|
||||
|
||||
// Mock I18n. TODO: moved to a shared helper
|
||||
beforeAll(() => {
|
||||
const mockedT = jest.fn();
|
||||
mockedT.mockImplementation((string, opts) => (string + ', ' + JSON.stringify(opts)));
|
||||
mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts));
|
||||
|
||||
global.I18n = { t: mockedT };
|
||||
})
|
||||
global.I18n = { t: mockedT };
|
||||
});
|
||||
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
})
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
p = {};
|
||||
v = { product: p };
|
||||
beforeEach(function () {
|
||||
v = {};
|
||||
namer = new OptionValueNamer(v);
|
||||
});
|
||||
it("generates simple values", function() {
|
||||
p.variant_unit = 'weight';
|
||||
p.variant_unit_scale = 1.0;
|
||||
it("generates simple values", function () {
|
||||
v.variant_unit = "weight";
|
||||
v.variant_unit_scale = 1.0;
|
||||
v.unit_value = 100;
|
||||
expect(namer.option_value_value_unit()).toEqual([100, 'g']);
|
||||
expect(namer.option_value_value_unit()).toEqual([100, "g"]);
|
||||
});
|
||||
it("generates values when unit value is non-integer", function() {
|
||||
p.variant_unit = 'weight';
|
||||
p.variant_unit_scale = 1.0;
|
||||
it("generates values when unit value is non-integer", function () {
|
||||
v.variant_unit = "weight";
|
||||
v.variant_unit_scale = 1.0;
|
||||
v.unit_value = 123.45;
|
||||
expect(namer.option_value_value_unit()).toEqual([123.45, 'g']);
|
||||
expect(namer.option_value_value_unit()).toEqual([123.45, "g"]);
|
||||
});
|
||||
it("returns a value of 1 when unit value equals the scale", function() {
|
||||
p.variant_unit = 'weight';
|
||||
p.variant_unit_scale = 1000.0;
|
||||
it("returns a value of 1 when unit value equals the scale", function () {
|
||||
v.variant_unit = "weight";
|
||||
v.variant_unit_scale = 1000.0;
|
||||
v.unit_value = 1000.0;
|
||||
expect(namer.option_value_value_unit()).toEqual([1, 'kg']);
|
||||
expect(namer.option_value_value_unit()).toEqual([1, "kg"]);
|
||||
});
|
||||
it("generates values for all weight scales", function() {
|
||||
[[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']].forEach(([scale, unit]) => {
|
||||
p.variant_unit = 'weight';
|
||||
p.variant_unit_scale = scale;
|
||||
it("generates values for all weight scales", function () {
|
||||
[
|
||||
[1.0, "g"],
|
||||
[1000.0, "kg"],
|
||||
[1000000.0, "T"],
|
||||
].forEach(([scale, unit]) => {
|
||||
v.variant_unit = "weight";
|
||||
v.variant_unit_scale = scale;
|
||||
v.unit_value = 100 * scale;
|
||||
expect(namer.option_value_value_unit()).toEqual([100, unit]);
|
||||
});
|
||||
});
|
||||
it("generates values for all volume scales", function() {
|
||||
[[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].forEach(([scale, unit]) => {
|
||||
p.variant_unit = 'volume';
|
||||
p.variant_unit_scale = scale;
|
||||
it("generates values for all volume scales", function () {
|
||||
[
|
||||
[0.001, "mL"],
|
||||
[1.0, "L"],
|
||||
[1000.0, "kL"],
|
||||
].forEach(([scale, unit]) => {
|
||||
v.variant_unit = "volume";
|
||||
v.variant_unit_scale = scale;
|
||||
v.unit_value = 100 * scale;
|
||||
expect(namer.option_value_value_unit()).toEqual([100, unit]);
|
||||
});
|
||||
});
|
||||
it("generates right values for volume with rounded values", function() {
|
||||
it("generates right values for volume with rounded values", function () {
|
||||
var unit;
|
||||
unit = 'L';
|
||||
p.variant_unit = 'volume';
|
||||
p.variant_unit_scale = 1.0;
|
||||
unit = "L";
|
||||
v.variant_unit = "volume";
|
||||
v.variant_unit_scale = 1.0;
|
||||
v.unit_value = 0.7;
|
||||
expect(namer.option_value_value_unit()).toEqual([700, 'mL']);
|
||||
expect(namer.option_value_value_unit()).toEqual([700, "mL"]);
|
||||
});
|
||||
it("chooses the correct scale when value is very small", function() {
|
||||
p.variant_unit = 'volume';
|
||||
p.variant_unit_scale = 0.001;
|
||||
it("chooses the correct scale when value is very small", function () {
|
||||
v.variant_unit = "volume";
|
||||
v.variant_unit_scale = 0.001;
|
||||
v.unit_value = 0.0001;
|
||||
expect(namer.option_value_value_unit()).toEqual([0.1, 'mL']);
|
||||
expect(namer.option_value_value_unit()).toEqual([0.1, "mL"]);
|
||||
});
|
||||
it("generates values for item units", function() {
|
||||
it("generates values for item units", function () {
|
||||
//TODO
|
||||
// %w(packet box).each do |unit|
|
||||
// p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit)
|
||||
@@ -144,17 +161,17 @@ describe("OptionValueNamer", () => {
|
||||
// v.stub(:unit_value) { 100 }
|
||||
// subject.option_value_value_unit.should == [100, unit.pluralize]
|
||||
});
|
||||
it("generates singular values for item units when value is 1", function() {
|
||||
p.variant_unit = 'items';
|
||||
p.variant_unit_scale = null;
|
||||
p.variant_unit_name = 'packet';
|
||||
it("generates singular values for item units when value is 1", function () {
|
||||
v.variant_unit = "items";
|
||||
v.variant_unit_scale = null;
|
||||
v.variant_unit_name = "packet";
|
||||
v.unit_value = 1;
|
||||
expect(namer.option_value_value_unit()).toEqual([1, 'packet']);
|
||||
expect(namer.option_value_value_unit()).toEqual([1, "packet"]);
|
||||
});
|
||||
it("returns [null, null] when unit value is not set", function() {
|
||||
p.variant_unit = 'items';
|
||||
p.variant_unit_scale = null;
|
||||
p.variant_unit_name = 'foo';
|
||||
it("returns [null, null] when unit value is not set", function () {
|
||||
v.variant_unit = "items";
|
||||
v.variant_unit_scale = null;
|
||||
v.variant_unit_name = "foo";
|
||||
v.unit_value = null;
|
||||
expect(namer.option_value_value_unit()).toEqual([null, null]);
|
||||
});
|
||||
|
||||
150
spec/javascripts/services/price_parser_test.js
Normal file
150
spec/javascripts/services/price_parser_test.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import PriceParse from "js/services/price_parser";
|
||||
|
||||
describe("PriceParser service", function () {
|
||||
let priceParser = null;
|
||||
|
||||
beforeEach(() => {
|
||||
priceParser = new PriceParse();
|
||||
});
|
||||
|
||||
describe("test internal method with Regexp", function () {
|
||||
describe("test replaceCommaByFinalPoint() method", function () {
|
||||
it("handle the default case (with two numbers after comma)", function () {
|
||||
expect(priceParser.replaceCommaByFinalPoint("1,00")).toEqual("1.00");
|
||||
});
|
||||
it("doesn't confuse with thousands separator", function () {
|
||||
expect(priceParser.replaceCommaByFinalPoint("1,000")).toEqual("1,000");
|
||||
});
|
||||
it("handle also when there is only one number after the decimal separator", function () {
|
||||
expect(priceParser.replaceCommaByFinalPoint("1,0")).toEqual("1.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("test removeThousandsSeparator() method", function () {
|
||||
it("handle the default case", function () {
|
||||
expect(priceParser.removeThousandsSeparator("1,000", ",")).toEqual("1000");
|
||||
expect(priceParser.removeThousandsSeparator("1,000,000", ",")).toEqual("1000000");
|
||||
});
|
||||
it("handle the case with decimal separator", function () {
|
||||
expect(priceParser.removeThousandsSeparator("1,000,000.00", ",")).toEqual("1000000.00");
|
||||
});
|
||||
it("handle the case when it is actually a decimal separator (and not a thousands one)", function () {
|
||||
expect(priceParser.removeThousandsSeparator("1,00", ",")).toEqual("1,00");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with point as decimal separator and comma as thousands separator for I18n service", function () {
|
||||
beforeAll(() => {
|
||||
const mockedToCurrency = jest.fn();
|
||||
mockedToCurrency.mockImplementation((arg) => {
|
||||
if (arg == 0.1) {
|
||||
return "0.1";
|
||||
} else if (arg == 1000) {
|
||||
return "1,000";
|
||||
}
|
||||
});
|
||||
|
||||
global.I18n = { toCurrency: mockedToCurrency };
|
||||
});
|
||||
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
});
|
||||
|
||||
it("handle point as decimal separator", function () {
|
||||
expect(priceParser.parse("1.00")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("handle point as decimal separator", function () {
|
||||
expect(priceParser.parse("1.000")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("also handle comma as decimal separator", function () {
|
||||
expect(priceParser.parse("1,0")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("also handle comma as decimal separator", function () {
|
||||
expect(priceParser.parse("1,00")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("also handle comma as decimal separator", function () {
|
||||
expect(priceParser.parse("11,00")).toEqual(11.0);
|
||||
});
|
||||
|
||||
it("handle comma as decimal separator but not confusing with thousands separator", function () {
|
||||
expect(priceParser.parse("11,000")).toEqual(11000);
|
||||
});
|
||||
|
||||
it("handle point as decimal separator and comma as thousands separator", function () {
|
||||
expect(priceParser.parse("1,000,000.00")).toEqual(1000000);
|
||||
});
|
||||
|
||||
it("handle integer number", function () {
|
||||
expect(priceParser.parse("10")).toEqual(10);
|
||||
});
|
||||
|
||||
it("handle integer number with comma as thousands separator", function () {
|
||||
expect(priceParser.parse("1,000")).toEqual(1000);
|
||||
});
|
||||
|
||||
it("handle integer number with no thousands separator", function () {
|
||||
expect(priceParser.parse("1000")).toEqual(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with comma as decimal separator and final point as thousands separator for I18n service", function () {
|
||||
beforeAll(() => {
|
||||
const mockedToCurrency = jest.fn();
|
||||
mockedToCurrency.mockImplementation((arg) => {
|
||||
if (arg == 0.1) {
|
||||
return "0,1";
|
||||
} else if (arg == 1000) {
|
||||
return "1.000";
|
||||
}
|
||||
});
|
||||
|
||||
global.I18n = { toCurrency: mockedToCurrency };
|
||||
});
|
||||
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
});
|
||||
|
||||
it("handle comma as decimal separator", function () {
|
||||
expect(priceParser.parse("1,00")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("handle comma as decimal separator with one digit after the comma", function () {
|
||||
expect(priceParser.parse("11,0")).toEqual(11.0);
|
||||
});
|
||||
|
||||
it("handle comma as decimal separator with two digit after the comma", function () {
|
||||
expect(priceParser.parse("11,00")).toEqual(11.0);
|
||||
});
|
||||
|
||||
it("handle comma as decimal separator with three digit after the comma", function () {
|
||||
expect(priceParser.parse("11,000")).toEqual(11.0);
|
||||
});
|
||||
|
||||
it("also handle point as decimal separator", function () {
|
||||
expect(priceParser.parse("1.00")).toEqual(1.0);
|
||||
});
|
||||
|
||||
it("also handle point as decimal separator with integer part with two digits", function () {
|
||||
expect(priceParser.parse("11.00")).toEqual(11.0);
|
||||
});
|
||||
|
||||
it("handle point as decimal separator and final point as thousands separator", function () {
|
||||
expect(priceParser.parse("1.000.000,00")).toEqual(1000000);
|
||||
});
|
||||
|
||||
it("handle integer number", function () {
|
||||
expect(priceParser.parse("10")).toEqual(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
170
spec/javascripts/services/unit_prices_test.js
Normal file
170
spec/javascripts/services/unit_prices_test.js
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import UnitPrices from "js/services/unit_prices";
|
||||
|
||||
describe("UnitPrices service", function () {
|
||||
let unitPrices = null;
|
||||
|
||||
beforeAll(() => {
|
||||
// Requires global var from page for VariantUnitManager
|
||||
global.ofn_available_units_sorted = {
|
||||
weight: {
|
||||
"1.0": { name: "g", system: "metric" },
|
||||
28.35: { name: "oz", system: "imperial" },
|
||||
453.6: { name: "lb", system: "imperial" },
|
||||
"1000.0": { name: "kg", system: "metric" },
|
||||
"1000000.0": { name: "T", system: "metric" },
|
||||
},
|
||||
volume: {
|
||||
0.001: { name: "mL", system: "metric" },
|
||||
"1.0": { name: "L", system: "metric" },
|
||||
"1000.0": { name: "kL", system: "metric" },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
unitPrices = new UnitPrices();
|
||||
});
|
||||
|
||||
describe("get correct unit price duo unit/value for weight", function () {
|
||||
const unit_type = "weight";
|
||||
|
||||
it("with scale: 1", function () {
|
||||
const price = 1;
|
||||
const scale = 1;
|
||||
const unit_value = 1;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1000);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with scale and unit_value: 1000", function () {
|
||||
const price = 1;
|
||||
const scale = 1000;
|
||||
const unit_value = 1000;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with scale: 1000 and unit_value: 2000", function () {
|
||||
const price = 1;
|
||||
const scale = 1000;
|
||||
const unit_value = 2000;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.5);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with price: 2", function () {
|
||||
const price = 2;
|
||||
const scale = 1;
|
||||
const unit_value = 1;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2000);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with price: 2, scale and unit_value: 1000", function () {
|
||||
const price = 2;
|
||||
const scale = 1000;
|
||||
const unit_value = 1000;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with price: 2, scale: 1000 and unit_value: 2000", function () {
|
||||
const price = 2;
|
||||
const scale = 1000;
|
||||
const unit_value = 2000;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
|
||||
it("with price: 2, scale: 1000 and unit_value: 500", function () {
|
||||
const price = 2;
|
||||
const scale = 1000;
|
||||
const unit_value = 500;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
|
||||
});
|
||||
});
|
||||
|
||||
describe("get correct unit price duo unit/value for volume", function () {
|
||||
const unit_type = "volume";
|
||||
|
||||
it("with scale: 1", function () {
|
||||
const price = 1;
|
||||
const scale = 1;
|
||||
const unit_value = 1;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
|
||||
});
|
||||
|
||||
it("with price: 2 and unit_value: 0.5", function () {
|
||||
const price = 2;
|
||||
const scale = 1;
|
||||
const unit_value = 0.5;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
|
||||
});
|
||||
|
||||
it("with price: 2, scale: 0.001 and unit_value: 0.01", function () {
|
||||
const price = 2;
|
||||
const scale = 0.001;
|
||||
const unit_value = 0.01;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(200);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
|
||||
});
|
||||
|
||||
it("with price: 20000, scale: 1000 and unit_value: 10000", function () {
|
||||
const price = 20000;
|
||||
const scale = 1000;
|
||||
const unit_value = 10000;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
|
||||
});
|
||||
|
||||
it("with price: 2, scale: 1000 and unit_value: 10000 and variant_unit_name: box", function () {
|
||||
const price = 20000;
|
||||
const scale = 1000;
|
||||
const unit_value = 10000;
|
||||
const variant_unit_name = "Box";
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(2);
|
||||
expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("L");
|
||||
});
|
||||
});
|
||||
|
||||
describe("get correct unit price duo unit/value for items", function () {
|
||||
const unit_type = "items";
|
||||
const scale = null;
|
||||
|
||||
it("with price: 1 and unit_value: 1", function () {
|
||||
const price = 1;
|
||||
const unit_value = 1;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
|
||||
});
|
||||
|
||||
it("with price: 1 and unit_value: 10", function () {
|
||||
const price = 1;
|
||||
const unit_value = 10;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.1);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
|
||||
});
|
||||
|
||||
it("with price: 10 and unit_value: 1", function () {
|
||||
const price = 10;
|
||||
const unit_value = 1;
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(10);
|
||||
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
|
||||
});
|
||||
|
||||
it("with price: 10 and unit_value: 1 and variant_unit_name: box", function () {
|
||||
const price = 10;
|
||||
const unit_value = 1;
|
||||
const variant_unit_name = "Box";
|
||||
expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(10);
|
||||
expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("Box");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { Application } from "stimulus";
|
||||
import product_controller from "../../../app/webpacker/controllers/product_controller";
|
||||
|
||||
describe("ProductController", () => {
|
||||
beforeAll(() => {
|
||||
const application = Application.start();
|
||||
application.register("product", product_controller);
|
||||
});
|
||||
|
||||
describe("variant_unit_with_scale", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div data-controller="product">
|
||||
<input id="variant_unit" name="[products][0][variant_unit]" value="weight">
|
||||
<input id="variant_unit_scale" name="[products][0][variant_unit_scale]" value="1.0">
|
||||
<select id="variant_unit_with_scale" name="[products][0][variant_unit_with_scale]">
|
||||
<option selected="selected" value="weight_1">Weight (g)</option>
|
||||
<option value="weight_1000">Weight (kg)</option>
|
||||
<option value="volume_4.54609">Volume (gal)</option>
|
||||
<option value="items">Items</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
describe("change", () => {
|
||||
it("weight_1000", () => {
|
||||
variant_unit_with_scale.selectedIndex = 1;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("weight");
|
||||
expect(variant_unit_scale.value).toBe("1000");
|
||||
});
|
||||
|
||||
it("volume_4.54609", () => {
|
||||
variant_unit_with_scale.selectedIndex = 2;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("volume");
|
||||
expect(variant_unit_scale.value).toBe("4.54609");
|
||||
});
|
||||
|
||||
it("items", () => {
|
||||
variant_unit_with_scale.selectedIndex = 3;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("items");
|
||||
expect(variant_unit_scale.value).toBe("");
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
86
spec/javascripts/stimulus/variant_controller_test.js
Normal file
86
spec/javascripts/stimulus/variant_controller_test.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { Application } from "stimulus";
|
||||
import variant_controller from "controllers/variant_controller";
|
||||
|
||||
describe("VariantController", () => {
|
||||
beforeAll(() => {
|
||||
// Requires global var from page
|
||||
global.ofn_available_units_sorted = {
|
||||
weight: {
|
||||
"1.0": { name: "g", system: "metric" },
|
||||
"1000.0": { name: "kg", system: "metric" },
|
||||
"1000000.0": { name: "T", system: "metric" },
|
||||
},
|
||||
volume: {
|
||||
0.001: { name: "mL", system: "metric" },
|
||||
"1.0": { name: "L", system: "metric" },
|
||||
4.54609: { name: "gal", system: "imperial" },
|
||||
"1000.0": { name: "kL", system: "metric" },
|
||||
},
|
||||
};
|
||||
|
||||
const mockedT = jest.fn();
|
||||
mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts));
|
||||
|
||||
global.I18n = { t: mockedT };
|
||||
|
||||
const application = Application.start();
|
||||
application.register("variant", variant_controller);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete global.I18n;
|
||||
});
|
||||
|
||||
describe("variant_unit_with_scale", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div data-controller="variant">
|
||||
<input id="variant_unit" name="[products][0][variants_attributes][0][variant_unit]" value="weight">
|
||||
<input id="variant_unit_scale" name="[products][0][variants_attributes][0][variant_unit_scale]" value="1.0">
|
||||
<select id="variant_unit_with_scale" name="[products][0][variants_attributes][0][variant_unit_with_scale]">
|
||||
<option selected="selected" value="weight_1">Weight (g)</option>
|
||||
<option value="weight_1000">Weight (kg)</option>
|
||||
<option value="volume_4.54609">Volume (gal)</option>
|
||||
<option value="items">Items</option>
|
||||
</select>
|
||||
<input id="variant_unit_name" name="[products][0][variants_attributes][0][variant_unit_name]" type="text" >
|
||||
<button id="unit_to_display" name="[products][0][variants_attributes][][0][unit_to_display]" type="submit" >2kg</button>
|
||||
<input id="unit_value" name="[products][0][variants_attributes][0][unit_value]" value="2000.0" >
|
||||
<input id="unit_description" name="[products][0][variants_attributes][0][unit_description]" >
|
||||
<input id="unit_value_with_description" name="[products][0][variants_attributes][0][unit_value_with_description]" value="2" type="text" >
|
||||
<input id="display_as" name="[products][0][variants_attributes][0][display_as]" placeholder="2kg" type="text" >
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
describe("change", () => {
|
||||
it("weight_1000", () => {
|
||||
variant_unit_with_scale.selectedIndex = 1;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("weight");
|
||||
expect(variant_unit_scale.value).toBe("1000");
|
||||
});
|
||||
|
||||
it("volume_4.54609", () => {
|
||||
variant_unit_with_scale.selectedIndex = 2;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("volume");
|
||||
expect(variant_unit_scale.value).toBe("4.54609");
|
||||
});
|
||||
|
||||
it("items", () => {
|
||||
variant_unit_with_scale.selectedIndex = 3;
|
||||
variant_unit_with_scale.dispatchEvent(new Event("change"));
|
||||
|
||||
expect(variant_unit.value).toBe("items");
|
||||
expect(variant_unit_scale.value).toBe("");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -156,14 +156,20 @@ describe "filtering products for submission to database", ->
|
||||
it "returns variant_unit_with_scale as variant_unit and variant_unit_scale", ->
|
||||
testProduct =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'weight_1'
|
||||
variants: [
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'weight_1'
|
||||
]
|
||||
|
||||
expect(filterSubmitProducts([testProduct])).toEqual [
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variants_attributes: [
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
]
|
||||
]
|
||||
|
||||
it "returns stock properties of a product if no variant is provided", ->
|
||||
@@ -204,18 +210,15 @@ describe "filtering products for submission to database", ->
|
||||
display_as: "bottle"
|
||||
display_name: "nothing"
|
||||
producer_id: 5
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_name: 'loaf'
|
||||
variant_unit_with_scale: 'volume_1'
|
||||
]
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_name: 'loaf'
|
||||
variant_unit_with_scale: 'volume_1'
|
||||
|
||||
expect(filterSubmitProducts([testProduct])).toEqual [
|
||||
id: 1
|
||||
name: "TestProduct"
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_name: 'loaf'
|
||||
variants_attributes: [
|
||||
id: 1
|
||||
on_hand: 2
|
||||
@@ -226,6 +229,9 @@ describe "filtering products for submission to database", ->
|
||||
display_as: "bottle"
|
||||
display_name: "nothing"
|
||||
supplier_id: 5
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_name: 'loaf'
|
||||
]
|
||||
]
|
||||
|
||||
@@ -281,7 +287,6 @@ describe "AdminProductEditCtrl", ->
|
||||
$scope.initialise()
|
||||
|
||||
expect($scope.q.query).toBe query
|
||||
expect($scope.q.producerFilter).toBe producerFilter
|
||||
expect($scope.q.categoryFilter).toBe categoryFilter
|
||||
expect($scope.q.sorting).toBe sorting
|
||||
expect($scope.q.importDateFilter).toBe importDateFilter
|
||||
@@ -476,13 +481,13 @@ describe "AdminProductEditCtrl", ->
|
||||
|
||||
describe "determining whether a product has a unit", ->
|
||||
it "returns true when it does", ->
|
||||
product =
|
||||
variant_unit_with_scale: 'weight_1000'
|
||||
expect($scope.hasUnit(product)).toBe(true)
|
||||
variant ={variant_unit_with_scale: 'weight_1000'}
|
||||
|
||||
expect($scope.hasUnit(variant)).toBe(true)
|
||||
|
||||
it "returns false when its unit is undefined", ->
|
||||
product = {}
|
||||
expect($scope.hasUnit(product)).toBe(false)
|
||||
variant = {}
|
||||
expect($scope.hasUnit(variant)).toBe(false)
|
||||
|
||||
|
||||
describe "determining whether a variant has been saved", ->
|
||||
@@ -505,51 +510,6 @@ describe "AdminProductEditCtrl", ->
|
||||
window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"]
|
||||
window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c)
|
||||
|
||||
it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", ->
|
||||
testProduct =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'volume_1000'
|
||||
|
||||
$scope.packProduct(testProduct)
|
||||
|
||||
expect(testProduct).toEqual
|
||||
id: 1
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1000
|
||||
variant_unit_with_scale: 'volume_1000'
|
||||
|
||||
it "extracts a null value into null variant_unit and variant_unit_scale", ->
|
||||
testProduct =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: null
|
||||
|
||||
$scope.packProduct(testProduct)
|
||||
|
||||
expect(testProduct).toEqual
|
||||
id: 1
|
||||
variant_unit: null
|
||||
variant_unit_scale: null
|
||||
variant_unit_with_scale: null
|
||||
|
||||
it "extracts when variant_unit_with_scale is 'items'", ->
|
||||
testProduct =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'items'
|
||||
|
||||
$scope.packProduct(testProduct)
|
||||
|
||||
expect(testProduct).toEqual
|
||||
id: 1
|
||||
variant_unit: 'items'
|
||||
variant_unit_scale: null
|
||||
variant_unit_with_scale: 'items'
|
||||
|
||||
it "packs each variant", ->
|
||||
spyOn $scope, "packVariant"
|
||||
testVariant = {id: 1}
|
||||
@@ -559,120 +519,154 @@ describe "AdminProductEditCtrl", ->
|
||||
|
||||
$scope.packProduct(testProduct)
|
||||
|
||||
expect($scope.packVariant).toHaveBeenCalledWith(testProduct, testVariant)
|
||||
expect($scope.packVariant).toHaveBeenCalledWith(testVariant)
|
||||
|
||||
describe "packing variants", ->
|
||||
beforeEach ->
|
||||
window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"]
|
||||
window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c)
|
||||
|
||||
it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", ->
|
||||
testVariant =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'volume_1000'
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
id: 1
|
||||
variant_unit: 'volume'
|
||||
variant_unit_scale: 1000
|
||||
variant_unit_with_scale: 'volume_1000'
|
||||
|
||||
it "extracts when variant_unit_with_scale is 'items'", ->
|
||||
testVariant =
|
||||
id: 1
|
||||
variant_unit: 'weight'
|
||||
variant_unit_scale: 1
|
||||
variant_unit_with_scale: 'items'
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
id: 1
|
||||
variant_unit: 'items'
|
||||
variant_unit_scale: null
|
||||
variant_unit_with_scale: 'items'
|
||||
|
||||
it "extracts unit_value and unit_description from unit_value_with_description", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1.0}
|
||||
testVariant = {unit_value_with_description: "250.5 (bottle)"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 250.5
|
||||
unit_description: "(bottle)"
|
||||
unit_value_with_description: "250.5 (bottle)"
|
||||
|
||||
it "extracts into unit_value when only a number is provided", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1.0}
|
||||
testVariant = {unit_value_with_description: "250.5"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250.5"}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 250.5
|
||||
unit_description: ''
|
||||
unit_value_with_description: "250.5"
|
||||
variant_unit_scale: 1.0
|
||||
|
||||
it "extracts into unit_description when only a string is provided", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {unit_value_with_description: "Medium"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: null
|
||||
unit_description: 'Medium'
|
||||
unit_value_with_description: "Medium"
|
||||
|
||||
it "extracts into unit_description when a string starting with a number is provided", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {unit_value_with_description: "1kg"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: null
|
||||
unit_description: '1kg'
|
||||
unit_value_with_description: "1kg"
|
||||
|
||||
it "sets blank values when no value provided", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {unit_value_with_description: ""}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: null
|
||||
unit_description: ''
|
||||
unit_value_with_description: ""
|
||||
|
||||
it "sets nothing when the field is undefined", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
expect(testVariant).toEqual {}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual({})
|
||||
|
||||
it "sets zero when the field is zero", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1.0}
|
||||
testVariant = {unit_value_with_description: "0"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "0"}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 0
|
||||
unit_description: ''
|
||||
unit_value_with_description: "0"
|
||||
variant_unit_scale: 1.0
|
||||
|
||||
it "converts value from chosen unit to base unit", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1000}
|
||||
testVariant = {unit_value_with_description: "250.5"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
testVariant = {variant_unit_scale: 1000, unit_value_with_description: "250.5"}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 250500
|
||||
unit_description: ''
|
||||
unit_value_with_description: "250.5"
|
||||
variant_unit_scale: 1000
|
||||
|
||||
it "does not convert value when using a non-scaled unit", ->
|
||||
testProduct = {id: 123}
|
||||
testVariant = {unit_value_with_description: "12"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 12
|
||||
unit_description: ''
|
||||
unit_value_with_description: "12"
|
||||
|
||||
it "converts unit_value into a float when a comma separated number is provided", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1.0}
|
||||
testVariant = {unit_value_with_description: "250,5"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250,5"}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 250.5
|
||||
unit_description: ''
|
||||
unit_value_with_description: "250,5"
|
||||
variant_unit_scale: 1.0
|
||||
|
||||
it "rounds off the unit_value upto 2 decimal places", ->
|
||||
testProduct = {id: 123, variant_unit_scale: 1.0}
|
||||
testVariant = {unit_value_with_description: "1234.567"}
|
||||
BulkProducts.products = [testProduct]
|
||||
$scope.packVariant(testProduct, testVariant)
|
||||
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "1234.567"}
|
||||
|
||||
$scope.packVariant(testVariant)
|
||||
|
||||
expect(testVariant).toEqual
|
||||
unit_value: 1234.57
|
||||
unit_description: ''
|
||||
unit_value_with_description: "1234.567"
|
||||
variant_unit_scale: 1.0
|
||||
|
||||
|
||||
describe "filtering products", ->
|
||||
|
||||
@@ -213,35 +213,67 @@ describe "LineItemsCtrl", ->
|
||||
expect(scope.fulfilled()).toEqual ''
|
||||
|
||||
it "returns '' if selectedUnitsVariant has no property 'group_buy_unit_size' or group_buy_unit_size is 0", ->
|
||||
scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 0 }
|
||||
scope.selectedUnitsProduct = { group_buy_unit_size: 0 }
|
||||
scope.selectedUnitsVariant = { variant_unit: "weight" }
|
||||
expect(scope.fulfilled()).toEqual ''
|
||||
scope.selectedUnitsProduct = { variant_unit: "weight" }
|
||||
scope.selectedUnitsProduct = { }
|
||||
scope.selectedUnitsVariant = { variant_unit: "weight" }
|
||||
expect(scope.fulfilled()).toEqual ''
|
||||
|
||||
it "calls Math.round() if variant_unit is 'weight', 'volume', or items", ->
|
||||
spyOn(Math,"round")
|
||||
scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 10 }
|
||||
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
|
||||
scope.selectedUnitsVariant = { variant_unit: "weight" }
|
||||
|
||||
scope.fulfilled()
|
||||
expect(Math.round).toHaveBeenCalled()
|
||||
scope.selectedUnitsProduct = { variant_unit: "volume", group_buy_unit_size: 10 }
|
||||
|
||||
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
|
||||
scope.selectedUnitsVariant = { variant_unit: "volume" }
|
||||
|
||||
scope.fulfilled()
|
||||
expect(Math.round).toHaveBeenCalled()
|
||||
scope.selectedUnitsProduct = { variant_unit: "items", group_buy_unit_size: 10 }
|
||||
|
||||
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
|
||||
scope.selectedUnitsVariant = { variant_unit: "items" }
|
||||
|
||||
scope.fulfilled()
|
||||
expect(Math.round).toHaveBeenCalled()
|
||||
|
||||
|
||||
describe "returns the quantity of fulfilled group buy units", ->
|
||||
describe "returns the quantity of fulfilled group buy units", ->
|
||||
runs = [
|
||||
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 1000, variant_unit_scale: 1 }, arg: 1500, expected: 1.5 },
|
||||
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1000 }, arg: 9, expected: 0.15 },
|
||||
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1 }, arg: 9000, expected: 0.15 }
|
||||
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 5, variant_unit_scale: 28.35 }, arg: 12, expected: 2.4},
|
||||
{ selectedUnitsProduct: { variant_unit: "volume", group_buy_unit_size: 5000, variant_unit_scale: 1 }, arg: 5, expected: 0.001}
|
||||
];
|
||||
runs.forEach ({selectedUnitsProduct, arg, expected}) ->
|
||||
it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsProduct.variant_unit_scale , ->
|
||||
{
|
||||
selectedUnitsProduct: { group_buy_unit_size: 1000 },
|
||||
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 },
|
||||
arg: 1500,
|
||||
expected: 1.5
|
||||
}, {
|
||||
selectedUnitsProduct: { group_buy_unit_size: 60000 } ,
|
||||
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1000 } ,
|
||||
arg: 9,
|
||||
expected: 0.15
|
||||
}, {
|
||||
selectedUnitsProduct: { group_buy_unit_size: 60000 },
|
||||
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 },
|
||||
arg: 9000,
|
||||
expected: 0.15
|
||||
}, {
|
||||
selectedUnitsProduct: { group_buy_unit_size: 5 },
|
||||
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 28.35 },
|
||||
arg: 12,
|
||||
expected: 2.4
|
||||
}, {
|
||||
selectedUnitsProduct: { group_buy_unit_size: 5000 },
|
||||
selectedUnitsVariant: { variant_unit: "volume", variant_unit_scale: 1 },
|
||||
arg: 5,
|
||||
expected: 0.001
|
||||
}
|
||||
]
|
||||
runs.forEach ({selectedUnitsProduct, selectedUnitsVariant, arg, expected}) ->
|
||||
it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsVariant.variant_unit_scale , ->
|
||||
scope.selectedUnitsProduct = selectedUnitsProduct
|
||||
scope.selectedUnitsVariant = selectedUnitsVariant
|
||||
expect(scope.fulfilled(arg)).toEqual expected
|
||||
|
||||
describe "allFinalWeightVolumesPresent()", ->
|
||||
@@ -278,44 +310,44 @@ describe "LineItemsCtrl", ->
|
||||
describe "sumUnitValues()", ->
|
||||
it "returns the sum of the final_weight_volumes line_items if volume", ->
|
||||
scope.filteredLineItems = [
|
||||
{ final_weight_volume: 2, units_product: { variant_unit: "volume" } }
|
||||
{ final_weight_volume: 7, units_product: { variant_unit: "volume" } }
|
||||
{ final_weight_volume: 21, units_product: { variant_unit: "volume" } }
|
||||
{ final_weight_volume: 2, units_variant: { variant_unit: "volume" } }
|
||||
{ final_weight_volume: 7, units_variant: { variant_unit: "volume" } }
|
||||
{ final_weight_volume: 21, units_variant: { variant_unit: "volume" } }
|
||||
]
|
||||
expect(scope.sumUnitValues()).toEqual 30
|
||||
|
||||
it "returns the sum of the quantity line_items if items", ->
|
||||
scope.filteredLineItems = [
|
||||
{ quantity: 2, units_product: { variant_unit: "items" } }
|
||||
{ quantity: 7, units_product: { variant_unit: "items" } }
|
||||
{ quantity: 21, units_product: { variant_unit: "items" } }
|
||||
{ quantity: 2, units_variant: { variant_unit: "items" } }
|
||||
{ quantity: 7, units_variant: { variant_unit: "items" } }
|
||||
{ quantity: 21, units_variant: { variant_unit: "items" } }
|
||||
]
|
||||
expect(scope.sumUnitValues()).toEqual 30
|
||||
|
||||
it "returns the sum of the final_weight_volumes for line_items with both metric and imperial units", ->
|
||||
scope.filteredLineItems = [
|
||||
{ final_weight_volume: 907.2, units_product: { variant_unit: "weight", variant_unit_scale: 453.6 }, units_variant: { unit_value: 453.6 } }
|
||||
{ final_weight_volume: 2000, units_product: { variant_unit: "weight", variant_unit_scale: 1000 }, units_variant: { unit_value: 1000 } }
|
||||
{ final_weight_volume: 56.7, units_product: { variant_unit: "weight", variant_unit_scale: 28.35 }, units_variant: { unit_value: 28.35 } }
|
||||
{ final_weight_volume: 2, units_product: { variant_unit: "volume", variant_unit_scale: 1.0 }, units_variant: { unit_value: 1.0 } }
|
||||
{ final_weight_volume: 907.2, units_variant: { variant_unit: "weight", variant_unit_scale: 453.6, unit_value: 453.6 } }
|
||||
{ final_weight_volume: 2000, units_variant: { variant_unit: "weight", variant_unit_scale: 1000, unit_value: 1000 } }
|
||||
{ final_weight_volume: 56.7, units_variant: { variant_unit: "weight", variant_unit_scale: 28.35, unit_value: 28.35 } }
|
||||
{ final_weight_volume: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1.0, unit_value: 1.0 } }
|
||||
]
|
||||
expect(scope.sumUnitValues()).toEqual 8
|
||||
|
||||
describe "sumMaxUnitValues()", ->
|
||||
it "returns the sum of the product of unit_value and maxOf(max_quantity, pristine quantity) for specified line_items", ->
|
||||
scope.filteredLineItems = [
|
||||
{ id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
|
||||
{ id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
|
||||
{ id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
|
||||
{ id: 1, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 1 }, max_quantity: 5 }
|
||||
{ id: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 2 }, max_quantity: 1 }
|
||||
{ id: 3, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 3 }, max_quantity: 10 }
|
||||
]
|
||||
|
||||
expect(scope.sumMaxUnitValues()).toEqual 37
|
||||
|
||||
it "returns the sum of the product of max_quantity for specified line_items if variant_unit is `items`", ->
|
||||
scope.filteredLineItems = [
|
||||
{ id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "items" } }
|
||||
{ id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "items" } }
|
||||
{ id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "items" } }
|
||||
{ id: 1, units_variant: { variant_unit: "items", unit_value: 1 }, max_quantity: 5 }
|
||||
{ id: 2, units_variant: { variant_unit: "items", unit_value: 2 }, max_quantity: 1 }
|
||||
{ id: 3, units_variant: { variant_unit: "items", unit_value: 3 }, max_quantity: 10 }
|
||||
]
|
||||
|
||||
expect(scope.sumMaxUnitValues()).toEqual 16
|
||||
@@ -331,45 +363,45 @@ describe "LineItemsCtrl", ->
|
||||
expect(scope.formattedValueWithUnitName(1,{})).toEqual ''
|
||||
|
||||
it "returns the value, and does not call Math.round if variant_unit is 'items'", ->
|
||||
unitsProduct = { variant_unit: "items" }
|
||||
expect(scope.formattedValueWithUnitName(1, unitsProduct, unitsVariant)).toEqual "1 items"
|
||||
unitsVariant.variant_unit= "items"
|
||||
expect(scope.formattedValueWithUnitName(1, unitsVariant)).toEqual "1 items"
|
||||
|
||||
it "calls Math.round() if variant_unit is 'weight' or 'volume'", ->
|
||||
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1 }
|
||||
scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant)
|
||||
unitsVariant = { unit_value: "1", variant_unit: "weight", variant_unit_scale: 1 }
|
||||
scope.formattedValueWithUnitName(1, unitsVariant)
|
||||
expect(Math.round).toHaveBeenCalled()
|
||||
scope.selectedUnitsVariant = { variant_unit: "volume" }
|
||||
scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant)
|
||||
|
||||
scope.selectedUnitsVariant = {unit_value: "1", variant_unit: "volume" }
|
||||
scope.formattedValueWithUnitName(1, unitsVariant)
|
||||
expect(Math.round).toHaveBeenCalled()
|
||||
|
||||
it "calls Math.round with the value multiplied by 1000", ->
|
||||
unitsProduct = { variant_unit: "weight", variant_unit_scale: 5 }
|
||||
scope.formattedValueWithUnitName(10, unitsProduct,unitsVariant)
|
||||
unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 5 }
|
||||
scope.formattedValueWithUnitName(10, unitsVariant)
|
||||
expect(Math.round).toHaveBeenCalledWith 10 * 1000
|
||||
|
||||
it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", ->
|
||||
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
|
||||
unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 1000 }
|
||||
spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg"
|
||||
expect(scope.formattedValueWithUnitName(2,unitsProduct,unitsVariant)).toEqual "2 kg"
|
||||
expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 kg"
|
||||
|
||||
it "handle correclty the imperial units", ->
|
||||
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
|
||||
unitsVariant = { unit_value: "453.6" }
|
||||
unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000, unit_value: "453.6" }
|
||||
spyOn(VariantUnitManager, "getUnitName").and.returnValue "lb"
|
||||
expect(scope.formattedValueWithUnitName(2, unitsProduct, unitsVariant)).toEqual "2 lb"
|
||||
expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 lb"
|
||||
|
||||
describe "get group by size formatted value with unit name", ->
|
||||
beforeEach ->
|
||||
spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg"
|
||||
|
||||
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
|
||||
|
||||
|
||||
unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000 }
|
||||
|
||||
it "returns the formatted value with unit name", ->
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsProduct)).toEqual "1 kg"
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsVariant)).toEqual "1 kg"
|
||||
|
||||
it "handle the case when the value is actually null or empty", ->
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsProduct)).toEqual ""
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsProduct)).toEqual ""
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsVariant)).toEqual ""
|
||||
expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsVariant)).toEqual ""
|
||||
|
||||
|
||||
describe "updating the price upon updating the weight of a line item", ->
|
||||
|
||||
@@ -1,29 +1,156 @@
|
||||
describe "OptionValueNamer", ->
|
||||
subject = null
|
||||
OptionValueNamer = null
|
||||
|
||||
beforeEach ->
|
||||
module('admin.products')
|
||||
module "ofn.admin"
|
||||
module "admin.products"
|
||||
module ($provide)->
|
||||
$provide.value "availableUnits", "g,kg,T,mL,L,kL"
|
||||
null
|
||||
inject (_OptionValueNamer_) ->
|
||||
subject = new _OptionValueNamer_
|
||||
|
||||
beforeEach inject (_OptionValueNamer_) ->
|
||||
OptionValueNamer = _OptionValueNamer_
|
||||
|
||||
describe "pluralize a variant unit name", ->
|
||||
namer = null
|
||||
beforeEach ->
|
||||
namer = new OptionValueNamer({})
|
||||
|
||||
it "returns the same word if no plural is known", ->
|
||||
expect(subject.pluralize("foo", 2)).toEqual "foo"
|
||||
expect(namer.pluralize("foo", 2)).toEqual "foo"
|
||||
|
||||
it "returns the same word if we omit the quantity", ->
|
||||
expect(subject.pluralize("loaf")).toEqual "loaf"
|
||||
expect(namer.pluralize("loaf")).toEqual "loaf"
|
||||
|
||||
it "finds the plural of a word", ->
|
||||
expect(subject.pluralize("loaf", 2)).toEqual "loaves"
|
||||
expect(namer.pluralize("loaf", 2)).toEqual "loaves"
|
||||
|
||||
it "finds the singular of a word", ->
|
||||
expect(subject.pluralize("loaves", 1)).toEqual "loaf"
|
||||
expect(namer.pluralize("loaves", 1)).toEqual "loaf"
|
||||
|
||||
it "finds the zero form of a word", ->
|
||||
expect(subject.pluralize("loaf", 0)).toEqual "loaves"
|
||||
expect(namer.pluralize("loaf", 0)).toEqual "loaves"
|
||||
|
||||
it "ignores upper case", ->
|
||||
expect(subject.pluralize("Loaf", 2)).toEqual "loaves"
|
||||
expect(namer.pluralize("Loaf", 2)).toEqual "loaves"
|
||||
|
||||
describe "generating option value name", ->
|
||||
v = namer = null
|
||||
beforeEach ->
|
||||
v = {}
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "when description is blank", ->
|
||||
v.unit_description = null
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
expect(namer.name()).toBe "valueunit"
|
||||
|
||||
it "when description is present", ->
|
||||
v.unit_description = 'desc'
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
expect(namer.name()).toBe "valueunit desc"
|
||||
|
||||
it "when value is blank and description is present", ->
|
||||
v.unit_description = 'desc'
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue [null, null]
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
expect(namer.name()).toBe "desc"
|
||||
|
||||
it "spaces value and unit when value is unscaled", ->
|
||||
v.unit_description = null
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
spyOn(namer, "value_scaled").and.returnValue false
|
||||
expect(namer.name()).toBe "value unit"
|
||||
|
||||
describe "determining if a variant's value is scaled", ->
|
||||
v = namer = null
|
||||
|
||||
beforeEach ->
|
||||
v = {}
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "returns true when the product has a scale", ->
|
||||
v.variant_unit_scale = 1000
|
||||
expect(namer.value_scaled()).toBe true
|
||||
|
||||
it "returns false otherwise", ->
|
||||
expect(namer.value_scaled()).toBe false
|
||||
|
||||
describe "generating option value's value and unit", ->
|
||||
v = namer = null
|
||||
|
||||
beforeEach ->
|
||||
v = {}
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "generates simple values", ->
|
||||
v.variant_unit = 'weight'
|
||||
v.variant_unit_scale = 1.0
|
||||
v.unit_value = 100
|
||||
expect(namer.option_value_value_unit()).toEqual [100, 'g']
|
||||
|
||||
it "generates values when unit value is non-integer", ->
|
||||
v.variant_unit = 'weight'
|
||||
v.variant_unit_scale = 1.0
|
||||
v.unit_value = 123.45
|
||||
expect(namer.option_value_value_unit()).toEqual [123.45, 'g']
|
||||
|
||||
it "returns a value of 1 when unit value equals the scale", ->
|
||||
v.variant_unit = 'weight'
|
||||
v.variant_unit_scale = 1000.0
|
||||
v.unit_value = 1000.0
|
||||
expect(namer.option_value_value_unit()).toEqual [1, 'kg']
|
||||
|
||||
it "generates values for all weight scales", ->
|
||||
for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']]
|
||||
[scale, unit] = units
|
||||
v.variant_unit = 'weight'
|
||||
v.variant_unit_scale = scale
|
||||
v.unit_value = 100 * scale
|
||||
expect(namer.option_value_value_unit()).toEqual [100, unit]
|
||||
|
||||
it "generates values for all volume scales", ->
|
||||
for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']]
|
||||
[scale, unit] = units
|
||||
v.variant_unit = 'volume'
|
||||
v.variant_unit_scale = scale
|
||||
v.unit_value = 100 * scale
|
||||
expect(namer.option_value_value_unit()).toEqual [100, unit]
|
||||
|
||||
it "generates right values for volume with rounded values", ->
|
||||
unit = 'L'
|
||||
v.variant_unit = 'volume'
|
||||
v.variant_unit_scale = 1.0
|
||||
v.unit_value = 0.7
|
||||
expect(namer.option_value_value_unit()).toEqual [700, 'mL']
|
||||
|
||||
it "chooses the correct scale when value is very small", ->
|
||||
v.variant_unit = 'volume'
|
||||
v.variant_unit_scale = 0.001
|
||||
v.unit_value = 0.0001
|
||||
expect(namer.option_value_value_unit()).toEqual [0.1, 'mL']
|
||||
|
||||
it "generates values for item units", ->
|
||||
#TODO
|
||||
# %w(packet box).each do |unit|
|
||||
# p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit)
|
||||
# v.stub(:product) { p }
|
||||
# v.stub(:unit_value) { 100 }
|
||||
# subject.option_value_value_unit.should == [100, unit.pluralize]
|
||||
|
||||
it "generates singular values for item units when value is 1", ->
|
||||
v.variant_unit = 'items'
|
||||
v.variant_unit_scale = null
|
||||
v.variant_unit_name = 'packet'
|
||||
v.unit_value = 1
|
||||
expect(namer.option_value_value_unit()).toEqual [1, 'packet']
|
||||
|
||||
it "returns [nil, nil] when unit value is not set", ->
|
||||
v.variant_unit = 'items'
|
||||
v.variant_unit_scale = null
|
||||
v.variant_unit_name = 'foo'
|
||||
v.unit_value = null
|
||||
expect(namer.option_value_value_unit()).toEqual [null, null]
|
||||
|
||||
|
||||
@@ -41,64 +41,64 @@ describe "unitsCtrl", ->
|
||||
|
||||
describe "interpretting unit_value_with_description", ->
|
||||
beforeEach ->
|
||||
scope.product.master = {}
|
||||
scope.product = {}
|
||||
|
||||
describe "when a variant_unit_scale is present", ->
|
||||
beforeEach ->
|
||||
scope.product.variant_unit_scale = 1
|
||||
|
||||
it "splits by whitespace in to unit_value and unit_description", ->
|
||||
scope.product.master.unit_value_with_description = "12 boxes"
|
||||
scope.product.unit_value_with_description = "12 boxes"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 12
|
||||
expect(scope.product.master.unit_description).toEqual "boxes"
|
||||
expect(scope.product.unit_value).toEqual 12
|
||||
expect(scope.product.unit_description).toEqual "boxes"
|
||||
|
||||
it "uses whole string as unit_value when only numerical characters are present", ->
|
||||
scope.product.master.unit_value_with_description = "12345"
|
||||
scope.product.unit_value_with_description = "12345"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 12345
|
||||
expect(scope.product.master.unit_description).toEqual ''
|
||||
expect(scope.product.unit_value).toEqual 12345
|
||||
expect(scope.product.unit_description).toEqual ''
|
||||
|
||||
it "uses whole string as description when string does not start with a number", ->
|
||||
scope.product.master.unit_value_with_description = "boxes 12"
|
||||
scope.product.unit_value_with_description = "boxes 12"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual null
|
||||
expect(scope.product.master.unit_description).toEqual "boxes 12"
|
||||
expect(scope.product.unit_value).toEqual null
|
||||
expect(scope.product.unit_description).toEqual "boxes 12"
|
||||
|
||||
it "does not require whitespace to split unit value and description", ->
|
||||
scope.product.master.unit_value_with_description = "12boxes"
|
||||
scope.product.unit_value_with_description = "12boxes"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 12
|
||||
expect(scope.product.master.unit_description).toEqual "boxes"
|
||||
expect(scope.product.unit_value).toEqual 12
|
||||
expect(scope.product.unit_description).toEqual "boxes"
|
||||
|
||||
it "once a whitespace occurs, all subsequent numerical characters are counted as description", ->
|
||||
scope.product.master.unit_value_with_description = "123 54 boxes"
|
||||
scope.product.unit_value_with_description = "123 54 boxes"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 123
|
||||
expect(scope.product.master.unit_description).toEqual "54 boxes"
|
||||
expect(scope.product.unit_value).toEqual 123
|
||||
expect(scope.product.unit_description).toEqual "54 boxes"
|
||||
|
||||
it "handle final point as decimal separator", ->
|
||||
scope.product.master.unit_value_with_description = "22.22"
|
||||
scope.product.unit_value_with_description = "22.22"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 22.22
|
||||
expect(scope.product.master.unit_description).toEqual ""
|
||||
expect(scope.product.unit_value).toEqual 22.22
|
||||
expect(scope.product.unit_description).toEqual ""
|
||||
|
||||
it "handle comma as decimal separator", ->
|
||||
scope.product.master.unit_value_with_description = "22,22"
|
||||
scope.product.unit_value_with_description = "22,22"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 22.22
|
||||
expect(scope.product.master.unit_description).toEqual ""
|
||||
|
||||
expect(scope.product.unit_value).toEqual 22.22
|
||||
expect(scope.product.unit_description).toEqual ""
|
||||
|
||||
it "handle comma as decimal separator with description", ->
|
||||
scope.product.master.unit_value_with_description = "22,22 things"
|
||||
scope.product.unit_value_with_description = "22,22 things"
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 22.22
|
||||
expect(scope.product.master.unit_description).toEqual "things"
|
||||
expect(scope.product.unit_value).toEqual 22.22
|
||||
expect(scope.product.unit_description).toEqual "things"
|
||||
|
||||
it "handles nice rounded division", ->
|
||||
# this is a bit absurd, but it assure use that bigDecimal is called
|
||||
window.bigDecimal.multiply.and.returnValue 0.7
|
||||
scope.product.master.unit_value_with_description = "700"
|
||||
scope.product.unit_value_with_description = "700"
|
||||
scope.product.variant_unit_scale = 0.001
|
||||
scope.processUnitValueWithDescription()
|
||||
expect(scope.product.master.unit_value).toEqual 0.7
|
||||
expect(scope.product.unit_value).toEqual 0.7
|
||||
|
||||
@@ -53,126 +53,128 @@ describe "BulkProducts service", ->
|
||||
|
||||
describe "loading variant unit", ->
|
||||
describe "setting product variant_unit_with_scale field", ->
|
||||
it "sets by combining variant_unit and variant_unit_scale", ->
|
||||
it "HERE 2 sets by combining variant_unit and variant_unit_scale", ->
|
||||
product =
|
||||
variant_unit: "volume"
|
||||
variant_unit_scale: .001
|
||||
variants:[
|
||||
id: 10
|
||||
variant_unit: "volume"
|
||||
variant_unit_scale: .001
|
||||
]
|
||||
BulkProducts.loadVariantUnit product
|
||||
expect(product.variant_unit_with_scale).toEqual "volume_0.001"
|
||||
expect(product.variants[0].variant_unit_with_scale).toEqual "volume_0.001"
|
||||
|
||||
it "sets to null when variant_unit is null", ->
|
||||
product = {variant_unit: null, variant_unit_scale: 1000}
|
||||
product =
|
||||
variants: [
|
||||
{variant_unit: null, variant_unit_scale: 1000}
|
||||
]
|
||||
BulkProducts.loadVariantUnit product
|
||||
expect(product.variant_unit_with_scale).toBeNull()
|
||||
|
||||
expect(product.variants[0].variant_unit_with_scale).toBeNull()
|
||||
|
||||
it "sets to variant_unit when variant_unit_scale is null", ->
|
||||
product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
|
||||
product =
|
||||
variants: [
|
||||
{variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
|
||||
]
|
||||
BulkProducts.loadVariantUnit product
|
||||
expect(product.variant_unit_with_scale).toEqual "items"
|
||||
expect(product.variants[0].variant_unit_with_scale).toEqual "items"
|
||||
|
||||
it "sets to variant_unit when variant_unit is 'items'", ->
|
||||
product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
|
||||
product =
|
||||
variants: [
|
||||
{variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
|
||||
]
|
||||
BulkProducts.loadVariantUnit product
|
||||
expect(product.variant_unit_with_scale).toEqual "items"
|
||||
|
||||
it "loads data for variants (incl. master)", ->
|
||||
spyOn BulkProducts, "loadVariantUnitValues"
|
||||
spyOn BulkProducts, "loadVariantUnitValue"
|
||||
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
master: {id: 1, unit_value: 1, unit_description: '(one)'}
|
||||
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
|
||||
BulkProducts.loadVariantUnit product
|
||||
|
||||
expect(BulkProducts.loadVariantUnitValues).toHaveBeenCalledWith product
|
||||
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.master
|
||||
expect(product.variants[0].variant_unit_with_scale).toEqual "items"
|
||||
|
||||
it "loads data for variants (excl. master)", ->
|
||||
spyOn BulkProducts, "loadVariantUnitValue"
|
||||
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
master: {id: 1, unit_value: 1, unit_description: '(one)'}
|
||||
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
|
||||
BulkProducts.loadVariantUnitValues product
|
||||
variants: [
|
||||
{id: 2, variant_unit_scale: 1.0, unit_value: 2, unit_description: '(two)'}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
|
||||
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0]
|
||||
expect(BulkProducts.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master
|
||||
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product.variants[0]
|
||||
|
||||
describe "setting variant unit_value_with_description", ->
|
||||
it "sets by combining unit_value and unit_description", ->
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
{id: 1, variant_unit_scale: 1.0, unit_value: 1, unit_description: '(bottle)'}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0]).toEqual
|
||||
id: 1
|
||||
variant_unit_scale: 1.0,
|
||||
variant_unit_with_scale: null,
|
||||
unit_value: 1
|
||||
unit_description: '(bottle)'
|
||||
unit_value_with_description: '1 (bottle)'
|
||||
|
||||
it "uses unit_value when description is missing", ->
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
variants: [{id: 1, unit_value: 1}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
{id: 1, variant_unit_scale: 1.0, unit_value: 1}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0].unit_value_with_description).toEqual '1'
|
||||
|
||||
it "uses unit_description when value is missing", ->
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
variants: [{id: 1, unit_description: 'Small'}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
{id: 1, variant_unit_scale: 1.0, unit_description: 'Small'}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
|
||||
|
||||
it "converts values from base value to chosen unit", ->
|
||||
product =
|
||||
variant_unit_scale: 1000.0
|
||||
variants: [{id: 1, unit_value: 2500}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
id: 1, variant_unit_scale: 1000.0, unit_value: 2500
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
|
||||
|
||||
it "converts values from base value to chosen unit without breaking precision", ->
|
||||
product =
|
||||
variant_unit_scale: 0.001
|
||||
variants: [{id: 1, unit_value: 0.35}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
{id: 1,variant_unit_scale: 0.001, unit_value: 0.35}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0].unit_value_with_description).toEqual '350'
|
||||
|
||||
it "displays a unit_value of zero", ->
|
||||
product =
|
||||
variant_unit_scale: 1.0
|
||||
variants: [{id: 1, unit_value: 0}]
|
||||
BulkProducts.loadVariantUnitValues product, product.variants[0]
|
||||
variants: [
|
||||
{id: 1, variant_unit_scale: 1.0, unit_value: 0}
|
||||
]
|
||||
BulkProducts.loadVariantUnitValues product.variants
|
||||
expect(product.variants[0].unit_value_with_description).toEqual '0'
|
||||
|
||||
|
||||
describe "calculating the scaled unit value for a variant", ->
|
||||
it "returns the scaled value when variant has a unit_value", ->
|
||||
product = {variant_unit_scale: 0.001}
|
||||
variant = {unit_value: 5}
|
||||
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5000
|
||||
variant = {variant_unit_scale: 0.001, unit_value: 5}
|
||||
expect(BulkProducts.variantUnitValue(variant)).toEqual 5000
|
||||
|
||||
it "returns the scaled value rounded off upto 2 decimal points", ->
|
||||
product = {variant_unit_scale: 28.35}
|
||||
variant = {unit_value: 1234.5}
|
||||
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 43.54
|
||||
variant = {variant_unit_scale: 28.35, unit_value: 1234.5}
|
||||
expect(BulkProducts.variantUnitValue(variant)).toEqual 43.54
|
||||
|
||||
it "returns the unscaled value when the product has no scale", ->
|
||||
product = {}
|
||||
variant = {unit_value: 5}
|
||||
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5
|
||||
expect(BulkProducts.variantUnitValue(variant)).toEqual 5
|
||||
|
||||
it "returns zero when the value is zero", ->
|
||||
product = {}
|
||||
variant = {unit_value: 0}
|
||||
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 0
|
||||
expect(BulkProducts.variantUnitValue(variant)).toEqual 0
|
||||
|
||||
it "returns null when the variant has no unit_value", ->
|
||||
product = {}
|
||||
variant = {}
|
||||
expect(BulkProducts.variantUnitValue(product, variant)).toEqual null
|
||||
expect(BulkProducts.variantUnitValue(variant)).toEqual null
|
||||
|
||||
|
||||
describe "fetching a product by id", ->
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
describe "Option Value Namer", ->
|
||||
OptionValueNamer = null
|
||||
|
||||
beforeEach ->
|
||||
module "ofn.admin"
|
||||
module "admin.products"
|
||||
module ($provide)->
|
||||
$provide.value "availableUnits", "g,kg,T,mL,L,kL"
|
||||
null
|
||||
|
||||
beforeEach inject (_OptionValueNamer_) ->
|
||||
OptionValueNamer = _OptionValueNamer_
|
||||
|
||||
describe "generating option value name", ->
|
||||
v = namer = null
|
||||
beforeEach ->
|
||||
v = {}
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "when description is blank", ->
|
||||
v.unit_description = null
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
expect(namer.name()).toBe "valueunit"
|
||||
|
||||
it "when description is present", ->
|
||||
v.unit_description = 'desc'
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
expect(namer.name()).toBe "valueunit desc"
|
||||
|
||||
it "when value is blank and description is present", ->
|
||||
v.unit_description = 'desc'
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue [null, null]
|
||||
spyOn(namer, "value_scaled").and.returnValue true
|
||||
expect(namer.name()).toBe "desc"
|
||||
|
||||
it "spaces value and unit when value is unscaled", ->
|
||||
v.unit_description = null
|
||||
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
|
||||
spyOn(namer, "value_scaled").and.returnValue false
|
||||
expect(namer.name()).toBe "value unit"
|
||||
|
||||
describe "determining if a variant's value is scaled", ->
|
||||
v = p = namer = null
|
||||
|
||||
beforeEach ->
|
||||
p = {}
|
||||
v = { product: p }
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "returns true when the product has a scale", ->
|
||||
p.variant_unit_scale = 1000
|
||||
expect(namer.value_scaled()).toBe true
|
||||
|
||||
it "returns false otherwise", ->
|
||||
expect(namer.value_scaled()).toBe false
|
||||
|
||||
describe "generating option value's value and unit", ->
|
||||
v = p = namer = null
|
||||
|
||||
beforeEach ->
|
||||
p = {}
|
||||
v = { product: p }
|
||||
namer = new OptionValueNamer(v)
|
||||
|
||||
it "generates simple values", ->
|
||||
p.variant_unit = 'weight'
|
||||
p.variant_unit_scale = 1.0
|
||||
v.unit_value = 100
|
||||
expect(namer.option_value_value_unit()).toEqual [100, 'g']
|
||||
|
||||
it "generates values when unit value is non-integer", ->
|
||||
p.variant_unit = 'weight'
|
||||
p.variant_unit_scale = 1.0
|
||||
v.unit_value = 123.45
|
||||
expect(namer.option_value_value_unit()).toEqual [123.45, 'g']
|
||||
|
||||
it "returns a value of 1 when unit value equals the scale", ->
|
||||
p.variant_unit = 'weight'
|
||||
p.variant_unit_scale = 1000.0
|
||||
v.unit_value = 1000.0
|
||||
expect(namer.option_value_value_unit()).toEqual [1, 'kg']
|
||||
|
||||
it "generates values for all weight scales", ->
|
||||
for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']]
|
||||
[scale, unit] = units
|
||||
p.variant_unit = 'weight'
|
||||
p.variant_unit_scale = scale
|
||||
v.unit_value = 100 * scale
|
||||
expect(namer.option_value_value_unit()).toEqual [100, unit]
|
||||
|
||||
it "generates values for all volume scales", ->
|
||||
for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']]
|
||||
[scale, unit] = units
|
||||
p.variant_unit = 'volume'
|
||||
p.variant_unit_scale = scale
|
||||
v.unit_value = 100 * scale
|
||||
expect(namer.option_value_value_unit()).toEqual [100, unit]
|
||||
|
||||
it "generates right values for volume with rounded values", ->
|
||||
unit = 'L'
|
||||
p.variant_unit = 'volume'
|
||||
p.variant_unit_scale = 1.0
|
||||
v.unit_value = 0.7
|
||||
expect(namer.option_value_value_unit()).toEqual [700, 'mL']
|
||||
|
||||
it "chooses the correct scale when value is very small", ->
|
||||
p.variant_unit = 'volume'
|
||||
p.variant_unit_scale = 0.001
|
||||
v.unit_value = 0.0001
|
||||
expect(namer.option_value_value_unit()).toEqual [0.1, 'mL']
|
||||
|
||||
it "generates values for item units", ->
|
||||
#TODO
|
||||
# %w(packet box).each do |unit|
|
||||
# p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit)
|
||||
# v.stub(:product) { p }
|
||||
# v.stub(:unit_value) { 100 }
|
||||
# subject.option_value_value_unit.should == [100, unit.pluralize]
|
||||
|
||||
it "generates singular values for item units when value is 1", ->
|
||||
p.variant_unit = 'items'
|
||||
p.variant_unit_scale = null
|
||||
p.variant_unit_name = 'packet'
|
||||
v.unit_value = 1
|
||||
expect(namer.option_value_value_unit()).toEqual [1, 'packet']
|
||||
|
||||
it "returns [nil, nil] when unit value is not set", ->
|
||||
p.variant_unit = 'items'
|
||||
p.variant_unit_scale = null
|
||||
p.variant_unit_name = 'foo'
|
||||
v.unit_value = null
|
||||
expect(namer.option_value_value_unit()).toEqual [null, null]
|
||||
@@ -34,8 +34,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
|
||||
let(:variant) { item.variant }
|
||||
|
||||
it "contains a sum of total items" do
|
||||
variant.product.update!(variant_unit: "items", variant_unit_name: "bottle")
|
||||
variant.update!(unit_value: 6) # six-pack
|
||||
variant.update!(variant_unit: "items", variant_unit_name: "bottle", unit_value: 6) # six-pack
|
||||
item.update!(final_weight_volume: nil) # reset unit information
|
||||
item.update!(quantity: 3)
|
||||
|
||||
@@ -44,8 +43,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
|
||||
end
|
||||
|
||||
it "contains a sum of total weight" do
|
||||
variant.product.update!(variant_unit: "weight")
|
||||
variant.update!(unit_value: 200) # grams
|
||||
variant.update!(variant_unit: "weight", unit_value: 200) # grams
|
||||
item.update!(final_weight_volume: nil) # reset unit information
|
||||
item.update!(quantity: 3)
|
||||
|
||||
@@ -57,8 +55,8 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
|
||||
# This is not possible with the current code but was possible years ago.
|
||||
# So I'm using `update_columns` to save invalid data.
|
||||
# We still have lots of that data in our databases though.
|
||||
variant.product.update(variant_unit: "items", variant_unit_name: "container")
|
||||
variant.update_columns(unit_value: nil, unit_description: "vacuum")
|
||||
variant.update_columns(variant_unit: "items", variant_unit_name: "container",
|
||||
unit_value: nil, unit_description: "vacuum")
|
||||
item.update!(final_weight_volume: nil) # reset unit information
|
||||
|
||||
expect(table_headers[4]).to eq "Total Units"
|
||||
@@ -69,8 +67,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
|
||||
expect(report).to receive(:display_summary_row?).and_return(true)
|
||||
# assures product appears first on report table
|
||||
variant.product.update!(name: "Alpha-Product #000")
|
||||
variant.product.update!(variant_unit: "weight")
|
||||
variant.update!(unit_value: 200) # grams
|
||||
variant.update!(variant_unit: "weight", unit_value: 200) # grams
|
||||
item.update!(final_weight_volume: nil) # reset unit information
|
||||
item.update!(quantity: 3)
|
||||
|
||||
@@ -89,8 +86,8 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
|
||||
# This is not possible with the current code but was possible years ago.
|
||||
# So I'm using `update_columns` to save invalid data.
|
||||
# We still have lots of that data in our databases though.
|
||||
variant.product.update(variant_unit: "items", variant_unit_name: "container")
|
||||
variant.update_columns(unit_value: nil, unit_description: "vacuum")
|
||||
variant.update_columns(variant_unit: "items", variant_unit_name: "container",
|
||||
unit_value: nil, unit_description: "vacuum")
|
||||
item.update!(final_weight_volume: nil) # reset unit information
|
||||
|
||||
# This second line item will have a default a bigint value.
|
||||
|
||||
@@ -5,58 +5,28 @@ require 'spec_helper'
|
||||
RSpec.describe Spree::Core::ProductDuplicator do
|
||||
describe "unit" do
|
||||
let(:product) do
|
||||
double 'Product',
|
||||
name: "foo",
|
||||
product_properties: [property],
|
||||
variants: [variant],
|
||||
image:,
|
||||
variant_unit: 'item'
|
||||
instance_double(
|
||||
Spree::Product,
|
||||
name: "foo",
|
||||
product_properties: [property],
|
||||
variants: [variant],
|
||||
image:,
|
||||
variant_unit: 'item'
|
||||
)
|
||||
end
|
||||
|
||||
let(:new_product) do
|
||||
double 'New Product',
|
||||
save!: true
|
||||
end
|
||||
|
||||
let(:property) do
|
||||
double 'Property'
|
||||
end
|
||||
|
||||
let(:new_property) do
|
||||
double 'New Property'
|
||||
end
|
||||
|
||||
let(:new_product) { instance_double(Spree::Product, save!: true) }
|
||||
let(:property) { instance_double(Spree::ProductProperty) }
|
||||
let(:new_property) { instance_double(Spree::ProductProperty) }
|
||||
let(:variant) do
|
||||
double 'Variant 1',
|
||||
sku: "67890",
|
||||
price: 19.50,
|
||||
currency: "AUD",
|
||||
images: [image_variant]
|
||||
end
|
||||
|
||||
let(:new_variant) do
|
||||
double 'New Variant 1',
|
||||
sku: "67890"
|
||||
end
|
||||
|
||||
let(:image) do
|
||||
double 'Image',
|
||||
attachment: double('Attachment')
|
||||
end
|
||||
|
||||
let(:new_image) do
|
||||
double 'New Image'
|
||||
end
|
||||
|
||||
let(:image_variant) do
|
||||
double 'Image Variant',
|
||||
attachment: double('Attachment')
|
||||
end
|
||||
|
||||
let(:new_image_variant) do
|
||||
double 'New Image Variant',
|
||||
attachment: double('Attachment')
|
||||
instance_double(
|
||||
Spree::Variant, sku: "67890", price: 19.50, currency: "AUD", images: [image_variant]
|
||||
)
|
||||
end
|
||||
let(:new_variant) { instance_double(Spree::Variant, sku: "67890") }
|
||||
let(:image) { instance_double(Spree::Image, attachment: double('Attachment')) }
|
||||
let(:new_image) { instance_double(Spree::Image) }
|
||||
let(:image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) }
|
||||
let(:new_image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) }
|
||||
|
||||
before do
|
||||
expect(product).to receive(:dup).and_return(new_product)
|
||||
@@ -73,7 +43,6 @@ RSpec.describe Spree::Core::ProductDuplicator do
|
||||
expect(new_product).to receive(:product_properties=).with([new_property])
|
||||
expect(new_product).to receive(:created_at=).with(nil)
|
||||
expect(new_product).to receive(:price=).with(0)
|
||||
expect(new_product).to receive(:unit_value=).with(nil)
|
||||
expect(new_product).to receive(:updated_at=).with(nil)
|
||||
expect(new_product).to receive(:deleted_at=).with(nil)
|
||||
expect(new_product).to receive(:variants=).with([new_variant])
|
||||
@@ -100,14 +69,17 @@ RSpec.describe Spree::Core::ProductDuplicator do
|
||||
|
||||
describe "errors" do
|
||||
context "with invalid product" do
|
||||
# Name has a max length of 255 char, when cloning a product the cloned product has a name
|
||||
# starting with "COPY OF <product.name>". So we set a name with 254 char to make sure the
|
||||
# cloned product will be invalid
|
||||
let(:product) {
|
||||
# name is a required field
|
||||
create(:product).tap{ |p| p.update_columns(variant_unit: nil) }
|
||||
create(:product).tap{ |v| v.update_columns(name: "l" * 254) }
|
||||
}
|
||||
|
||||
subject { Spree::Core::ProductDuplicator.new(product).duplicate }
|
||||
|
||||
it "raises RecordInvalid error" do
|
||||
expect{ subject }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
expect{ subject }.to raise_error(ActiveRecord::ActiveRecordError)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ RSpec.describe ProductImport::EntryValidator do
|
||||
unit_value: 500,
|
||||
variant_unit_scale: 1,
|
||||
variant_unit: 'weight',
|
||||
variants: [create(:variant, supplier: enterprise, unit_value: 500)]
|
||||
supplier_id: enterprise.id
|
||||
)
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ RSpec.describe ProductImport::EntryValidator do
|
||||
unit_value: 1000,
|
||||
variant_unit_scale: 1000,
|
||||
variant_unit: 'weight',
|
||||
variants: [create(:variant, supplier: enterprise, unit_value: 1000)]
|
||||
supplier_id: enterprise.id
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
91
spec/models/product_import/spreadsheet_entry_spec.rb
Normal file
91
spec/models/product_import/spreadsheet_entry_spec.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
require 'spec_helper'
|
||||
RSpec.describe ProductImport::SpreadsheetEntry do
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
let(:entry) {
|
||||
ProductImport::SpreadsheetEntry.new(
|
||||
"units" => "500",
|
||||
"unit_type" => "kg",
|
||||
"name" => "Tomato",
|
||||
"enterprise" => enterprise,
|
||||
"enterprise_id" => enterprise.id,
|
||||
"producer" => enterprise,
|
||||
"producer_id" => enterprise.id,
|
||||
"distributor" => enterprise,
|
||||
"price" => "1.0",
|
||||
"on_hand" => "1",
|
||||
"display_name" => display_name,
|
||||
)
|
||||
}
|
||||
let(:display_name) { "" }
|
||||
|
||||
# TODO test match on display_name
|
||||
describe "#match_variant?" do
|
||||
it "returns true if matching" do
|
||||
variant = create(:variant, unit_value: 500)
|
||||
|
||||
expect(entry.match_variant?(variant)).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if not machting" do
|
||||
variant = create(:variant, unit_value: 250)
|
||||
|
||||
expect(entry.match_variant?(variant)).to be(false)
|
||||
end
|
||||
|
||||
context "with same display_name" do
|
||||
let(:display_name) { "Good" }
|
||||
|
||||
it "returns true" do
|
||||
variant = create(:variant, unit_value: 500, display_name: "Good")
|
||||
|
||||
expect(entry.match_variant?(variant)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "with different display_name" do
|
||||
let(:display_name) { "Bad" }
|
||||
|
||||
it "returns false" do
|
||||
variant = create(:variant, unit_value: 500, display_name: "Good")
|
||||
|
||||
expect(entry.match_variant?(variant)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#match_inventory_variant?" do
|
||||
it "returns true if matching" do
|
||||
variant = create(:variant, unit_value: 500_000)
|
||||
|
||||
expect(entry.match_inventory_variant?(variant)).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if not machting" do
|
||||
variant = create(:variant, unit_value: 500)
|
||||
|
||||
expect(entry.match_inventory_variant?(variant)).to be(false)
|
||||
end
|
||||
|
||||
context "with same display_name" do
|
||||
let(:display_name) { "Good" }
|
||||
|
||||
it "returns true" do
|
||||
variant = create(:variant, unit_value: 500_000, display_name: "Good")
|
||||
|
||||
expect(entry.match_inventory_variant?(variant)).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "with different display_name" do
|
||||
let(:display_name) { "Bad" }
|
||||
|
||||
it "returns false" do
|
||||
variant = create(:variant, unit_value: 500_000, display_name: "Good")
|
||||
|
||||
expect(entry.match_inventory_variant?(variant)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -160,60 +160,60 @@ RSpec.describe ProductImport::ProductImporter do
|
||||
carrots = Spree::Product.find_by(name: 'Carrots')
|
||||
carrots_variant = carrots.variants.first
|
||||
expect(carrots.on_hand).to eq 5
|
||||
expect(carrots.variant_unit).to eq 'weight'
|
||||
expect(carrots.variant_unit_scale).to eq 1
|
||||
|
||||
expect(carrots_variant.supplier).to eq enterprise
|
||||
expect(carrots_variant.price).to eq 3.20
|
||||
expect(carrots_variant.unit_value).to eq 500
|
||||
expect(carrots_variant.variant_unit).to eq 'weight'
|
||||
expect(carrots_variant.variant_unit_scale).to eq 1
|
||||
expect(carrots_variant.on_demand).not_to eq true
|
||||
expect(carrots_variant.import_date).to be_within(1.minute).of Time.zone.now
|
||||
|
||||
potatoes = Spree::Product.find_by(name: 'Potatoes')
|
||||
potatoes_variant = potatoes.variants.first
|
||||
expect(potatoes.on_hand).to eq 6
|
||||
expect(potatoes.variant_unit).to eq 'weight'
|
||||
expect(potatoes.variant_unit_scale).to eq 1000
|
||||
|
||||
expect(potatoes_variant.supplier).to eq enterprise
|
||||
expect(potatoes_variant.price).to eq 6.50
|
||||
expect(potatoes_variant.unit_value).to eq 2000
|
||||
expect(potatoes_variant.variant_unit).to eq 'weight'
|
||||
expect(potatoes_variant.variant_unit_scale).to eq 1000
|
||||
expect(potatoes_variant.on_demand).not_to eq true
|
||||
expect(potatoes_variant.import_date).to be_within(1.minute).of Time.zone.now
|
||||
|
||||
pea_soup = Spree::Product.find_by(name: 'Pea Soup')
|
||||
pea_soup_variant = pea_soup.variants.first
|
||||
expect(pea_soup.on_hand).to eq 8
|
||||
expect(pea_soup.variant_unit).to eq 'volume'
|
||||
expect(pea_soup.variant_unit_scale).to eq 0.001
|
||||
|
||||
expect(pea_soup_variant.supplier).to eq enterprise
|
||||
expect(pea_soup_variant.price).to eq 5.50
|
||||
expect(pea_soup_variant.unit_value).to eq 0.75
|
||||
expect(pea_soup_variant.variant_unit).to eq 'volume'
|
||||
expect(pea_soup_variant.variant_unit_scale).to eq 0.001
|
||||
expect(pea_soup_variant.on_demand).not_to eq true
|
||||
expect(pea_soup_variant.import_date).to be_within(1.minute).of Time.zone.now
|
||||
|
||||
salad = Spree::Product.find_by(name: 'Salad')
|
||||
salad_variant = salad.variants.first
|
||||
expect(salad.on_hand).to eq 7
|
||||
expect(salad.variant_unit).to eq 'items'
|
||||
expect(salad.variant_unit_scale).to eq nil
|
||||
|
||||
expect(salad_variant.supplier).to eq enterprise
|
||||
expect(salad_variant.price).to eq 4.50
|
||||
expect(salad_variant.unit_value).to eq 1
|
||||
expect(salad_variant.variant_unit).to eq 'items'
|
||||
expect(salad_variant.variant_unit_scale).to eq nil
|
||||
expect(salad_variant.on_demand).not_to eq true
|
||||
expect(salad_variant.import_date).to be_within(1.minute).of Time.zone.now
|
||||
|
||||
buns = Spree::Product.find_by(name: 'Hot Cross Buns')
|
||||
buns_variant = buns.variants.first
|
||||
expect(buns.on_hand).to eq 7
|
||||
expect(buns.variant_unit).to eq 'items'
|
||||
expect(buns.variant_unit_scale).to eq nil
|
||||
|
||||
expect(buns_variant.supplier).to eq enterprise
|
||||
expect(buns_variant.price).to eq 3.50
|
||||
expect(buns_variant.unit_value).to eq 1
|
||||
expect(buns_variant.variant_unit).to eq 'items'
|
||||
expect(buns_variant.variant_unit_scale).to eq nil
|
||||
expect(buns_variant.on_demand).to eq true
|
||||
expect(buns_variant.import_date).to be_within(1.minute).of Time.zone.now
|
||||
end
|
||||
@@ -575,12 +575,15 @@ RSpec.describe ProductImport::ProductImporter do
|
||||
end
|
||||
end
|
||||
|
||||
describe "updating non-updatable fields on existing products" do
|
||||
describe "updating non-updatable fields on existing variants" do
|
||||
let(:csv_data) {
|
||||
CSV.generate do |csv|
|
||||
csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type"]
|
||||
csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg"]
|
||||
csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg"]
|
||||
csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type",
|
||||
"shipping_category"]
|
||||
csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg",
|
||||
shipping_category.name]
|
||||
csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg",
|
||||
shipping_category.name]
|
||||
end
|
||||
}
|
||||
let(:importer) { import_data csv_data }
|
||||
|
||||
@@ -699,11 +699,11 @@ module Spree
|
||||
|
||||
describe "getting unit for display" do
|
||||
let(:o) { create(:order) }
|
||||
let(:p1) { create(:product, name: 'Clear Honey', variant_unit_scale: 1) }
|
||||
let(:v1) { create(:variant, product: p1, unit_value: 500) }
|
||||
let(:p1) { create(:product, name: 'Clear Honey') }
|
||||
let(:v1) { create(:variant, product: p1, variant_unit_scale: 1, unit_value: 500) }
|
||||
let(:li1) { create(:line_item, order: o, product: p1, variant: v1) }
|
||||
let(:p2) { create(:product, name: 'Clear United States Honey', variant_unit_scale: 453.6) }
|
||||
let(:v2) { create(:variant, product: p2, unit_value: 453.6) }
|
||||
let(:p2) { create(:product, name: 'Clear United States Honey') }
|
||||
let(:v2) { create(:variant, product: p2, variant_unit_scale: 453.6, unit_value: 453.6) }
|
||||
let(:li2) { create(:line_item, order: o, product: p2, variant: v2) }
|
||||
|
||||
before do
|
||||
@@ -723,8 +723,11 @@ module Spree
|
||||
end
|
||||
|
||||
context "when the line_item has a final_weight_volume set" do
|
||||
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') }
|
||||
let!(:p0) { create(:simple_product) }
|
||||
let!(:v) {
|
||||
create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1,
|
||||
unit_value: 10, unit_description: 'bar')
|
||||
}
|
||||
|
||||
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:li) { create(:line_item, product: p, final_weight_volume: 5) }
|
||||
@@ -742,8 +745,11 @@ module Spree
|
||||
end
|
||||
|
||||
context "when the variant already has a value set" do
|
||||
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') }
|
||||
let!(:p0) { create(:simple_product) }
|
||||
let!(:v) {
|
||||
create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1,
|
||||
unit_value: 10, unit_description: 'bar')
|
||||
}
|
||||
|
||||
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:li) { create(:line_item, product: p, final_weight_volume: 5) }
|
||||
|
||||
@@ -34,5 +34,25 @@ module Spree
|
||||
expect(variant.reload.price).to eq 10.25
|
||||
end
|
||||
end
|
||||
|
||||
describe "#price=" do
|
||||
subject { Spree::Price.new }
|
||||
|
||||
context "with a number" do
|
||||
it "returns the same number" do
|
||||
subject.price = 12.5
|
||||
|
||||
expect(subject.price).to eq(12.5)
|
||||
end
|
||||
end
|
||||
|
||||
context "with empty string" do
|
||||
it "sets the price to nil" do
|
||||
subject.price = ""
|
||||
|
||||
expect(subject.price).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,8 +19,8 @@ module Spree
|
||||
end
|
||||
|
||||
it 'fails to duplicate invalid product' do
|
||||
# Existing product is invalid:
|
||||
product.update_columns(variant_unit: nil)
|
||||
# cloned product will be invalid
|
||||
product.update_columns(name: "l" * 254)
|
||||
|
||||
expect{ product.duplicate }.to raise_error(ActiveRecord::ActiveRecordError)
|
||||
end
|
||||
@@ -123,27 +123,6 @@ module Spree
|
||||
it { is_expected.to validate_length_of(:name).is_at_most(255) }
|
||||
it { is_expected.to validate_length_of(:sku).is_at_most(255) }
|
||||
|
||||
context "unit value" do
|
||||
it "requires a unit value when variant unit is weight" do
|
||||
expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name',
|
||||
unit_value: nil)).not_to be_valid
|
||||
expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name',
|
||||
unit_value: 0)).not_to be_valid
|
||||
end
|
||||
|
||||
it "requires a unit value when variant unit is volume" do
|
||||
expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name',
|
||||
unit_value: nil)).not_to be_valid
|
||||
expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name',
|
||||
unit_value: 0)).not_to be_valid
|
||||
end
|
||||
|
||||
it "does not require a unit value when variant unit is items" do
|
||||
expect(build(:simple_product, variant_unit: 'items', variant_unit_name: 'name',
|
||||
unit_value: nil)).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context "when the product has variants" do
|
||||
let(:product) do
|
||||
product = create(:simple_product)
|
||||
@@ -153,29 +132,6 @@ module Spree
|
||||
|
||||
it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) }
|
||||
|
||||
it "requires a unit" do
|
||||
product.variant_unit = nil
|
||||
expect(product).not_to be_valid
|
||||
end
|
||||
|
||||
%w(weight volume).each do |unit|
|
||||
context "when unit is #{unit}" do
|
||||
it "is valid when unit scale is set and unit name is not" do
|
||||
product.variant_unit = unit
|
||||
product.variant_unit_scale = 1
|
||||
product.variant_unit_name = nil
|
||||
expect(product).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid when unit scale is not set" do
|
||||
product.variant_unit = unit
|
||||
product.variant_unit_scale = nil
|
||||
product.variant_unit_name = nil
|
||||
expect(product).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "saving a new product" do
|
||||
let!(:product){ Spree::Product.new }
|
||||
let!(:shipping_category){ create(:shipping_category) }
|
||||
@@ -184,62 +140,127 @@ module Spree
|
||||
|
||||
before do
|
||||
create(:stock_location)
|
||||
end
|
||||
|
||||
it "copies properties to the first standard variant" do
|
||||
product.primary_taxon_id = taxon.id
|
||||
product.name = "Product1"
|
||||
product.variant_unit = "weight"
|
||||
product.variant_unit_scale = 1000
|
||||
product.unit_value = 1
|
||||
product.unit_description = "some product"
|
||||
product.price = 4.27
|
||||
product.shipping_category_id = shipping_category.id
|
||||
product.supplier_id = supplier.id
|
||||
product.save!
|
||||
end
|
||||
product.save(context: :create_and_create_standard_variant)
|
||||
|
||||
it "copies properties to the first standard variant" do
|
||||
expect(product.variants.reload.length).to eq 1
|
||||
standard_variant = product.variants.reload.first
|
||||
|
||||
expect(standard_variant).to be_valid
|
||||
expect(standard_variant.variant_unit).to eq("weight")
|
||||
expect(standard_variant.variant_unit_scale).to eq(1000)
|
||||
expect(standard_variant.unit_value).to eq(1)
|
||||
expect(standard_variant.unit_description).to eq("some product")
|
||||
expect(standard_variant.price).to eq 4.27
|
||||
expect(standard_variant.shipping_category).to eq shipping_category
|
||||
expect(standard_variant.primary_taxon).to eq taxon
|
||||
expect(standard_variant.supplier).to eq supplier
|
||||
end
|
||||
end
|
||||
|
||||
context "when the unit is items" do
|
||||
it "is valid when unit name is set and unit scale is not" do
|
||||
product.variant_unit = 'items'
|
||||
product.variant_unit_name = 'loaf'
|
||||
product.variant_unit_scale = nil
|
||||
expect(product).to be_valid
|
||||
context "with variant attributes" do
|
||||
it {
|
||||
is_expected.to validate_presence_of(:variant_unit)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
it {
|
||||
is_expected.to validate_presence_of(:supplier_id)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
it {
|
||||
is_expected.to validate_presence_of(:primary_taxon_id)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
|
||||
describe "unit_value" do
|
||||
subject { build(:simple_product, variant_unit: "items") }
|
||||
|
||||
it {
|
||||
is_expected.to validate_numericality_of(:unit_value).is_greater_than(0)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
it {
|
||||
is_expected.not_to validate_presence_of(:unit_value)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
|
||||
["weight", "volume"].each do |variant_unit|
|
||||
context "when variant_unit is #{variant_unit}" do
|
||||
subject { build(:simple_product, variant_unit:) }
|
||||
|
||||
it {
|
||||
is_expected.to validate_presence_of(:unit_value)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "unit_description" do
|
||||
it {
|
||||
is_expected.not_to validate_presence_of(:unit_description)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
|
||||
context "when variant_unit is et and unit_value is nil" do
|
||||
subject {
|
||||
build(:simple_product, variant_unit: "items", unit_value: nil,
|
||||
unit_description: "box")
|
||||
}
|
||||
|
||||
it {
|
||||
is_expected.to validate_presence_of(:unit_description)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_scale" do
|
||||
it {
|
||||
is_expected.not_to validate_presence_of(:variant_unit_scale)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
|
||||
["weight", "volume"].each do |variant_unit|
|
||||
context "when variant_unit is #{variant_unit}" do
|
||||
subject { build(:simple_product, variant_unit:) }
|
||||
|
||||
it {
|
||||
is_expected.to validate_presence_of(:variant_unit_scale)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_name" do
|
||||
subject { build(:simple_product, variant_unit: "volume") }
|
||||
|
||||
it {
|
||||
is_expected.not_to validate_presence_of(:variant_unit_name)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
|
||||
context "when variant_unit is items" do
|
||||
subject { build(:simple_product, variant_unit: "items") }
|
||||
|
||||
it {
|
||||
is_expected.to validate_presence_of(:variant_unit_name)
|
||||
.on(:create_and_create_standard_variant)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is invalid when unit name is not set" do
|
||||
product.variant_unit = 'items'
|
||||
product.variant_unit_name = nil
|
||||
product.variant_unit_scale = nil
|
||||
expect(product).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "a basic product" do
|
||||
let(:product) { build_stubbed(:simple_product) }
|
||||
|
||||
it "requires variant unit fields" do
|
||||
product.variant_unit = nil
|
||||
product.variant_unit_name = nil
|
||||
product.variant_unit_scale = nil
|
||||
|
||||
expect(product).not_to be_valid
|
||||
end
|
||||
|
||||
it "requires a unit scale when variant unit is weight" do
|
||||
product.variant_unit = 'weight'
|
||||
product.variant_unit_scale = nil
|
||||
product.variant_unit_name = nil
|
||||
|
||||
expect(product).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
@@ -328,30 +349,6 @@ module Spree
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "updates units when saved change to variant unit" do
|
||||
product.variant_unit = 'items'
|
||||
product.variant_unit_scale = nil
|
||||
product.variant_unit_name = 'loaf'
|
||||
product.save!
|
||||
|
||||
expect(product.variant_unit_name).to eq 'loaf'
|
||||
|
||||
product.update(variant_unit_name: 'bag')
|
||||
|
||||
expect(product.variant_unit_name).to eq 'bag'
|
||||
|
||||
product.variant_unit = 'weight'
|
||||
product.variant_unit_scale = 1
|
||||
product.variant_unit_name = 'g'
|
||||
product.save!
|
||||
|
||||
expect(product.variant_unit).to eq 'weight'
|
||||
|
||||
product.update(variant_unit: 'volume')
|
||||
|
||||
expect(product.variant_unit).to eq 'volume'
|
||||
end
|
||||
end
|
||||
|
||||
describe "scopes" do
|
||||
@@ -682,28 +679,6 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant units" do
|
||||
context "when the product already has a variant unit set" do
|
||||
let!(:p) {
|
||||
create(:simple_product,
|
||||
variant_unit: 'weight',
|
||||
variant_unit_scale: 1,
|
||||
variant_unit_name: nil)
|
||||
}
|
||||
|
||||
it "updates its variants unit values" do
|
||||
v = create(:variant, unit_value: 1, product: p)
|
||||
p.reload
|
||||
|
||||
expect(v.unit_presentation).to eq "1g"
|
||||
|
||||
p.update!(variant_unit: 'volume', variant_unit_scale: 0.001)
|
||||
|
||||
expect(v.reload.unit_presentation).to eq "1L"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "deletion" do
|
||||
let(:product) { create(:simple_product) }
|
||||
let(:variant) { create(:variant, product:) }
|
||||
|
||||
@@ -44,21 +44,164 @@ RSpec.describe Spree::Variant do
|
||||
end
|
||||
end
|
||||
|
||||
# add test for the other validation
|
||||
context "validations" do
|
||||
it "should validate price is greater than 0" do
|
||||
variant.price = -1
|
||||
expect(variant).not_to be_valid
|
||||
describe "validations" do
|
||||
describe "variant_unit" do
|
||||
subject(:variant) { build(:variant) }
|
||||
|
||||
it { is_expected.to validate_presence_of :variant_unit }
|
||||
|
||||
context "when the unit is items" do
|
||||
subject(:variant) { build(:variant, variant_unit: "items", variant_unit_name: "box") }
|
||||
|
||||
it "is valid with only unit value set" do
|
||||
variant.unit_value = 1
|
||||
variant.unit_description = nil
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with only unit description set" do
|
||||
variant.unit_value = nil
|
||||
variant.unit_description = 'Medium'
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "sets unit_value to 1.0 before validation if it's nil" do
|
||||
variant.unit_value = nil
|
||||
variant.unit_description = nil
|
||||
expect(variant).to be_valid
|
||||
expect(variant.unit_value).to eq 1.0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the product's unit is non-weight" do
|
||||
subject(:variant) { build(:variant, variant_unit: "volume") }
|
||||
|
||||
it "sets weight to decimal before save if it's integer" do
|
||||
variant.weight = 1
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 1.0
|
||||
end
|
||||
|
||||
it "sets weight to 0.0 before save if it's nil" do
|
||||
variant.weight = nil
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 0.0
|
||||
end
|
||||
|
||||
it "sets weight to 0.0 if input is a non numerical string" do
|
||||
variant.weight = "BANANAS!"
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 0.0
|
||||
end
|
||||
|
||||
it "sets weight to correct decimal value if input is numerical string" do
|
||||
variant.weight = "2"
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 2.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "should validate price is 0" do
|
||||
variant.price = 0
|
||||
expect(variant).to be_valid
|
||||
describe "price" do
|
||||
it { is_expected.to validate_presence_of :price }
|
||||
|
||||
it "should validate price is greater than 0" do
|
||||
variant.price = -1
|
||||
expect(variant).not_to be_valid
|
||||
end
|
||||
|
||||
it "should validate price is 0" do
|
||||
variant.price = 0
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "should validate unit_value is greater than 0" do
|
||||
variant.unit_value = 0
|
||||
|
||||
expect(variant).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it "should validate unit_value is greater than 0" do
|
||||
variant.unit_value = 0
|
||||
expect(variant).not_to be_valid
|
||||
describe "unit_value" do
|
||||
subject(:variant) { build(:variant, variant_unit: "item", unit_value: "") }
|
||||
|
||||
it { is_expected.not_to validate_presence_of(:unit_value) }
|
||||
|
||||
%w(weight volume).each do |unit|
|
||||
context "when variant_unit is #{unit}" do
|
||||
subject(:variant) { build(:variant, variant_unit: unit) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:unit_value) }
|
||||
it { is_expected.to validate_numericality_of(:unit_value).is_greater_than(0) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "unit_description" do
|
||||
subject(:variant) { build(:variant) }
|
||||
|
||||
it { expect(variant).to be_valid }
|
||||
it { is_expected.not_to validate_presence_of(:unit_description) }
|
||||
|
||||
context "when variant_unit is set and unit_value is nil" do
|
||||
subject(:variant) {
|
||||
build(:variant, variant_unit: "item", unit_value: nil, unit_description: "box")
|
||||
}
|
||||
|
||||
it { is_expected.to validate_presence_of(:unit_description) }
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_scale" do
|
||||
subject(:variant) { build(:variant, variant_unit: "box") }
|
||||
|
||||
it { is_expected.not_to validate_presence_of :variant_unit_scale }
|
||||
|
||||
%w(weight volume).each do |unit|
|
||||
context "when variant_unit is #{unit}" do
|
||||
subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) }
|
||||
|
||||
it { is_expected.to validate_presence_of :variant_unit_scale }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_name" do
|
||||
subject(:variant) { build(:variant) }
|
||||
|
||||
it { is_expected.not_to validate_presence_of :variant_unit_name }
|
||||
|
||||
context "when variant_unit is items" do
|
||||
subject(:variant) { build(:variant, variant_unit: "items") }
|
||||
|
||||
it { is_expected.to validate_presence_of :variant_unit_name }
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_scale" do
|
||||
subject(:variant) { build(:variant, variant_unit: "box") }
|
||||
|
||||
it { is_expected.not_to validate_presence_of :variant_unit_scale }
|
||||
|
||||
%w(weight volume).each do |unit|
|
||||
context "when variant_unit is #{unit}" do
|
||||
subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) }
|
||||
|
||||
it { is_expected.to validate_presence_of :variant_unit_scale }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant_unit_name" do
|
||||
subject(:variant) { build(:variant) }
|
||||
|
||||
it { is_expected.not_to validate_presence_of :variant_unit_name }
|
||||
|
||||
context "when variant_unit is items" do
|
||||
subject(:variant) { build(:variant, variant_unit: "items") }
|
||||
|
||||
it { is_expected.to validate_presence_of :variant_unit_name }
|
||||
end
|
||||
end
|
||||
|
||||
describe "tax category" do
|
||||
@@ -529,8 +672,8 @@ RSpec.describe Spree::Variant do
|
||||
context "handling nil values for related naming attributes" do
|
||||
it "returns empty string or product name" do
|
||||
product.name = "Apple"
|
||||
product.variant_unit = "items"
|
||||
product.display_as = nil
|
||||
variant.variant_unit = "items"
|
||||
variant.display_as = nil
|
||||
variant.display_name = nil
|
||||
|
||||
@@ -540,8 +683,8 @@ RSpec.describe Spree::Variant do
|
||||
|
||||
it "uses the display name correctly" do
|
||||
product.name = "Apple"
|
||||
product.variant_unit = "items"
|
||||
product.display_as = nil
|
||||
variant.variant_unit = "items"
|
||||
variant.display_as = nil
|
||||
variant.unit_presentation = nil
|
||||
variant.display_name = "Green"
|
||||
@@ -554,8 +697,8 @@ RSpec.describe Spree::Variant do
|
||||
|
||||
describe "calculating the price with enterprise fees" do
|
||||
it "returns the price plus the fees" do
|
||||
distributor = double(:distributor)
|
||||
order_cycle = double(:order_cycle)
|
||||
distributor = instance_double(Enterprise)
|
||||
order_cycle = instance_double(OrderCycle)
|
||||
|
||||
variant = Spree::Variant.new price: 100
|
||||
expect(variant).to receive(:fees_for).with(distributor, order_cycle) { 23 }
|
||||
@@ -565,8 +708,8 @@ RSpec.describe Spree::Variant do
|
||||
|
||||
describe "calculating the fees" do
|
||||
it "delegates to EnterpriseFeeCalculator" do
|
||||
distributor = double(:distributor)
|
||||
order_cycle = double(:order_cycle)
|
||||
distributor = instance_double(Enterprise)
|
||||
order_cycle = instance_double(OrderCycle)
|
||||
variant = Spree::Variant.new
|
||||
|
||||
expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator)
|
||||
@@ -578,10 +721,10 @@ RSpec.describe Spree::Variant do
|
||||
|
||||
describe "calculating fees broken down by fee type" do
|
||||
it "delegates to EnterpriseFeeCalculator" do
|
||||
distributor = double(:distributor)
|
||||
order_cycle = double(:order_cycle)
|
||||
distributor = instance_double(Enterprise)
|
||||
order_cycle = instance_double(OrderCycle)
|
||||
variant = Spree::Variant.new
|
||||
fees = double(:fees)
|
||||
fees = instance_double(EnterpriseFee)
|
||||
|
||||
expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator)
|
||||
.to receive(:fees_by_type_for).with(variant) { fees }
|
||||
@@ -590,90 +733,6 @@ RSpec.describe Spree::Variant do
|
||||
end
|
||||
end
|
||||
|
||||
context "when the product has variants" do
|
||||
let!(:product) { create(:simple_product) }
|
||||
let!(:variant) { create(:variant, product:) }
|
||||
|
||||
%w(weight volume).each do |unit|
|
||||
context "when the product's unit is #{unit}" do
|
||||
before do
|
||||
product.update_attribute :variant_unit, unit
|
||||
product.reload
|
||||
end
|
||||
|
||||
it "is valid when unit value is set and unit description is not" do
|
||||
variant.unit_value = 1
|
||||
variant.unit_description = nil
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid when unit value is not set" do
|
||||
variant.unit_value = nil
|
||||
expect(variant).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the product's unit is items" do
|
||||
before do
|
||||
product.update_attribute :variant_unit, 'items'
|
||||
product.reload
|
||||
variant.reload
|
||||
end
|
||||
|
||||
it "is valid with only unit value set" do
|
||||
variant.unit_value = 1
|
||||
variant.unit_description = nil
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with only unit description set" do
|
||||
variant.unit_value = nil
|
||||
variant.unit_description = 'Medium'
|
||||
expect(variant).to be_valid
|
||||
end
|
||||
|
||||
it "sets unit_value to 1.0 before validation if it's nil" do
|
||||
variant.unit_value = nil
|
||||
variant.unit_description = nil
|
||||
expect(variant).to be_valid
|
||||
expect(variant.unit_value).to eq 1.0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the product's unit is non-weight" do
|
||||
before do
|
||||
product.update_attribute :variant_unit, 'volume'
|
||||
product.reload
|
||||
variant.reload
|
||||
end
|
||||
|
||||
it "sets weight to decimal before save if it's integer" do
|
||||
variant.weight = 1
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 1.0
|
||||
end
|
||||
|
||||
it "sets weight to 0.0 before save if it's nil" do
|
||||
variant.weight = nil
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 0.0
|
||||
end
|
||||
|
||||
it "sets weight to 0.0 if input is a non numerical string" do
|
||||
variant.weight = "BANANAS!"
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 0.0
|
||||
end
|
||||
|
||||
it "sets weight to correct decimal value if input is numerical string" do
|
||||
variant.weight = "2"
|
||||
variant.save!
|
||||
expect(variant.weight).to eq 2.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "unit value/description" do
|
||||
let(:v) { Spree::Variant.new(unit_presentation: "small" ) }
|
||||
|
||||
@@ -739,42 +798,41 @@ RSpec.describe Spree::Variant do
|
||||
|
||||
describe "setting the variant's weight from the unit value" do
|
||||
it "sets the variant's weight when unit is weight" do
|
||||
p = create(:simple_product, variant_unit: 'volume')
|
||||
v = create(:variant, product: p, weight: 0)
|
||||
|
||||
p.update! variant_unit: 'weight', variant_unit_scale: 1
|
||||
v.update! unit_value: 10, unit_description: 'foo'
|
||||
v = create(:variant, weight: 0)
|
||||
v.update!(
|
||||
variant_unit: 'weight', variant_unit_scale: 1, unit_value: 10, unit_description: 'foo'
|
||||
)
|
||||
|
||||
expect(v.reload.weight).to eq(0.01)
|
||||
end
|
||||
|
||||
it "does nothing when unit is not weight" do
|
||||
p = create(:simple_product, variant_unit: 'volume')
|
||||
v = create(:variant, product: p, weight: 123)
|
||||
|
||||
p.update! variant_unit: 'volume', variant_unit_scale: 1
|
||||
v.update! unit_value: 10, unit_description: 'foo'
|
||||
v = create(:variant, weight: 123, variant_unit: 'volume')
|
||||
v.update! variant_unit: 'volume', variant_unit_scale: 1, unit_value: 10,
|
||||
unit_description: 'foo'
|
||||
|
||||
expect(v.reload.weight).to eq(123)
|
||||
end
|
||||
|
||||
it "does nothing when unit_value is not set" do
|
||||
p = create(:simple_product, variant_unit: 'volume')
|
||||
v = create(:variant, product: p, weight: 123)
|
||||
|
||||
p.update! variant_unit: 'weight', variant_unit_scale: 1
|
||||
v = create(:variant, weight: 123, variant_unit: 'volume')
|
||||
|
||||
# Although invalid, this calls the before_validation callback, which would
|
||||
# error if not handling unit_value == nil case
|
||||
expect(v.update(unit_value: nil, unit_description: 'foo')).to be false
|
||||
expect(
|
||||
v.update(variant_unit: "weight", variant_unit_scale: 1, unit_value: nil,
|
||||
unit_description: "foo")
|
||||
).to be false
|
||||
|
||||
expect(v.reload.weight).to eq(123)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the variant already has a value set" do
|
||||
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
|
||||
let!(:v) {
|
||||
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
|
||||
unit_description: 'bar')
|
||||
}
|
||||
|
||||
it "assigns the new option value" do
|
||||
expect(v.unit_presentation).to eq "5g bar"
|
||||
@@ -786,28 +844,30 @@ RSpec.describe Spree::Variant do
|
||||
end
|
||||
|
||||
context "when the variant does not have a display_as value set" do
|
||||
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:v) {
|
||||
create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: '')
|
||||
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
|
||||
unit_description: 'bar', display_as: '')
|
||||
}
|
||||
|
||||
it "requests the new value from OptionValueName" do
|
||||
expect_any_instance_of(VariantUnits::OptionValueNamer)
|
||||
.to receive(:name).exactly(1).times.and_call_original
|
||||
v.update(unit_value: 10, unit_description: 'foo')
|
||||
|
||||
expect(v.unit_presentation).to eq "10g foo"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the variant has a display_as value set" do
|
||||
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
|
||||
let!(:v) {
|
||||
create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: 'FOOS!')
|
||||
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
|
||||
unit_description: 'bar', display_as: 'FOOS!')
|
||||
}
|
||||
|
||||
it "does not request the new value from OptionValueName" do
|
||||
expect_any_instance_of(VariantUnits::OptionValueNamer).not_to receive(:name)
|
||||
v.update!(unit_value: 10, unit_description: 'foo')
|
||||
|
||||
expect(v.unit_presentation).to eq("FOOS!")
|
||||
end
|
||||
end
|
||||
@@ -873,12 +933,11 @@ RSpec.describe Spree::Variant do
|
||||
end
|
||||
|
||||
describe "#ensure_unit_value" do
|
||||
let(:product) { create(:product, variant_unit: "weight") }
|
||||
let(:variant) { create(:variant, product_id: product.id) }
|
||||
let(:variant) { create(:variant, variant_unit: "weight") }
|
||||
|
||||
context "when a product's variant_unit value is changed from weight to items" do
|
||||
context "when variant_unit value is changed from weight to items" do
|
||||
it "sets the variant's unit_value to 1" do
|
||||
product.update(variant_unit: "items")
|
||||
variant.update(variant_unit: "items")
|
||||
|
||||
expect(variant.unit_value).to eq 1
|
||||
end
|
||||
@@ -908,4 +967,40 @@ RSpec.describe Spree::Variant do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "after save callback" do
|
||||
let(:variant) { create(:variant) }
|
||||
|
||||
it "updates units and unit_presenation when saved change to variant unit" do
|
||||
variant.variant_unit = 'items'
|
||||
variant.variant_unit_scale = nil
|
||||
variant.variant_unit_name = 'loaf'
|
||||
variant.save!
|
||||
|
||||
expect(variant.variant_unit_name).to eq 'loaf'
|
||||
expect(variant.unit_presentation).to eq "1 loaf"
|
||||
|
||||
variant.update(variant_unit_name: 'bag')
|
||||
|
||||
expect(variant.variant_unit_name).to eq 'bag'
|
||||
expect(variant.unit_presentation).to eq "1 bag"
|
||||
|
||||
variant.variant_unit = 'weight'
|
||||
variant.variant_unit_scale = 1
|
||||
variant.variant_unit_name = 'g'
|
||||
variant.save!
|
||||
|
||||
expect(variant.variant_unit).to eq 'weight'
|
||||
expect(variant.unit_presentation).to eq "1g"
|
||||
|
||||
variant.update(variant_unit: 'volume')
|
||||
|
||||
expect(variant.variant_unit).to eq 'volume'
|
||||
expect(variant.unit_presentation).to eq "1L"
|
||||
|
||||
variant.update(display_as: 'My display')
|
||||
|
||||
expect(variant.unit_presentation).to eq "My display"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -69,22 +69,27 @@ RSpec.describe Sets::ProductSet do
|
||||
unit_description: 'some description'
|
||||
)
|
||||
end
|
||||
let(:variant) { product.variants.first }
|
||||
|
||||
let(:collection_hash) do
|
||||
{
|
||||
0 => {
|
||||
id: product.id,
|
||||
variant_unit: 'weight',
|
||||
variant_unit_scale: 1
|
||||
variants_attributes: [{
|
||||
id: variant.id.to_s,
|
||||
variant_unit: 'weight',
|
||||
variant_unit_scale: 1
|
||||
}]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the product without error' do
|
||||
expect(product_set.save).to eq true
|
||||
expect(product_set.saved_count).to eq 1
|
||||
# updating variant doesn't increment saved_count
|
||||
# expect(product_set.saved_count).to eq 1
|
||||
|
||||
expect(product.reload.attributes).to include(
|
||||
expect(variant.reload.attributes).to include(
|
||||
'variant_unit' => 'weight'
|
||||
)
|
||||
|
||||
@@ -305,8 +310,8 @@ RSpec.describe Sets::ProductSet do
|
||||
{ id: product.variants.first.id.to_s }, # default variant unchanged
|
||||
# omit ID for new variant
|
||||
{
|
||||
sku: "new sku", price: "5.00", unit_value: "5",
|
||||
supplier_id: supplier.id, primary_taxon_id: create(:taxon).id
|
||||
sku: "new sku", price: "5.00", unit_value: "5", variant_unit: "weight",
|
||||
variant_unit_scale: 1, supplier_id: supplier.id, primary_taxon_id: create(:taxon).id
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -318,9 +323,12 @@ RSpec.describe Sets::ProductSet do
|
||||
expect(product_set.errors).to be_empty
|
||||
}.to change { product.variants.count }.by(1)
|
||||
|
||||
expect(product.variants.last.sku).to eq "new sku"
|
||||
expect(product.variants.last.price).to eq 5.00
|
||||
expect(product.variants.last.unit_value).to eq 5
|
||||
variant = product.variants.last
|
||||
expect(variant.sku).to eq "new sku"
|
||||
expect(variant.price).to eq 5.00
|
||||
expect(variant.unit_value).to eq 5
|
||||
expect(variant.variant_unit).to eq "weight"
|
||||
expect(variant.variant_unit_scale).to eq 1
|
||||
end
|
||||
|
||||
context "variant has error" do
|
||||
|
||||
@@ -3,53 +3,46 @@
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe UnitPrice do
|
||||
subject { UnitPrice.new(variant) }
|
||||
let(:variant) { Spree::Variant.new }
|
||||
let(:product) { instance_double(Spree::Product) }
|
||||
|
||||
before do
|
||||
allow(variant).to receive(:product) { product }
|
||||
allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL")
|
||||
end
|
||||
|
||||
describe "#unit" do
|
||||
context "metric" do
|
||||
before do
|
||||
allow(product).to receive(:variant_unit_scale) { 1.0 }
|
||||
end
|
||||
|
||||
it "returns kg for weight" do
|
||||
allow(product).to receive(:variant_unit) { "weight" }
|
||||
expect(subject.unit).to eq("kg")
|
||||
variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "weight")
|
||||
|
||||
expect(UnitPrice.new(variant).unit).to eq("kg")
|
||||
end
|
||||
|
||||
it "returns L for volume" do
|
||||
allow(product).to receive(:variant_unit) { "volume" }
|
||||
expect(subject.unit).to eq("L")
|
||||
variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "volume")
|
||||
|
||||
expect(UnitPrice.new(variant).unit).to eq("L")
|
||||
end
|
||||
end
|
||||
|
||||
context "imperial" do
|
||||
it "returns lbs" do
|
||||
allow(product).to receive(:variant_unit_scale) { 453.6 }
|
||||
allow(product).to receive(:variant_unit) { "weight" }
|
||||
expect(subject.unit).to eq("lb")
|
||||
variant = Spree::Variant.new(variant_unit_scale: 453.6, variant_unit: "weight")
|
||||
|
||||
expect(UnitPrice.new(variant).unit).to eq("lb")
|
||||
end
|
||||
end
|
||||
|
||||
context "items" do
|
||||
it "returns items if no unit is specified" do
|
||||
allow(product).to receive(:variant_unit_name) { nil }
|
||||
allow(product).to receive(:variant_unit_scale) { nil }
|
||||
allow(product).to receive(:variant_unit) { "items" }
|
||||
expect(subject.unit).to eq("Item")
|
||||
variant = Spree::Variant.new(variant_unit_name: nil, variant_unit_scale: nil,
|
||||
variant_unit: "items")
|
||||
|
||||
expect(UnitPrice.new(variant).unit).to eq("Item")
|
||||
end
|
||||
|
||||
it "returns the unit if a unit is specified" do
|
||||
allow(product).to receive(:variant_unit_name) { "bunch" }
|
||||
allow(product).to receive(:variant_unit_scale) { nil }
|
||||
allow(product).to receive(:variant_unit) { "items" }
|
||||
expect(subject.unit).to eq("bunch")
|
||||
variant = Spree::Variant.new(variant_unit_name: "bunch", variant_unit_scale: nil,
|
||||
variant_unit: "items")
|
||||
|
||||
expect(UnitPrice.new(variant).unit).to eq("bunch")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -57,49 +50,47 @@ RSpec.describe UnitPrice do
|
||||
describe "#denominator" do
|
||||
context "metric" do
|
||||
it "returns 0.5 for a 500g variant" do
|
||||
allow(product).to receive(:variant_unit_scale) { 1.0 }
|
||||
allow(product).to receive(:variant_unit) { "weight" }
|
||||
variant.unit_value = 500
|
||||
expect(subject.denominator).to eq(0.5)
|
||||
variant = Spree::Variant.new(variant_unit_scale: 1.0, unit_value: 500,
|
||||
variant_unit: "weight")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(0.5)
|
||||
end
|
||||
|
||||
it "returns 2 for a 2kg variant" do
|
||||
allow(product).to receive(:variant_unit_scale) { 1000 }
|
||||
allow(product).to receive(:variant_unit) { "weight" }
|
||||
variant.unit_value = 2000
|
||||
expect(subject.denominator).to eq(2)
|
||||
variant = Spree::Variant.new(variant_unit_scale: 1000, unit_value: 2000,
|
||||
variant_unit: "weight")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(2)
|
||||
end
|
||||
|
||||
it "returns 0.5 for a 500mL variant" do
|
||||
allow(product).to receive(:variant_unit_scale) { 0.001 }
|
||||
allow(product).to receive(:variant_unit) { "volume" }
|
||||
variant.unit_value = 0.5
|
||||
expect(subject.denominator).to eq(0.5)
|
||||
variant = Spree::Variant.new(variant_unit_scale: 0.001, unit_value: 0.5,
|
||||
variant_unit: "volume")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(0.5)
|
||||
end
|
||||
end
|
||||
|
||||
context "imperial" do
|
||||
it "returns 2 for a 2 pound variant" do
|
||||
allow(product).to receive(:variant_unit_scale) { 453.6 }
|
||||
allow(product).to receive(:variant_unit) { "weight" }
|
||||
variant.unit_value = 2 * 453.6
|
||||
expect(subject.denominator).to eq(2)
|
||||
variant = Spree::Variant.new(variant_unit_scale: 453.6, unit_value: 2 * 453.6,
|
||||
variant_unit: "weight")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "items" do
|
||||
it "returns 1 if no unit is specified" do
|
||||
allow(product).to receive(:variant_unit_scale) { nil }
|
||||
allow(product).to receive(:variant_unit) { "items" }
|
||||
variant.unit_value = 1
|
||||
expect(subject.denominator).to eq(1)
|
||||
variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 1, variant_unit: "items")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(1)
|
||||
end
|
||||
|
||||
it "returns 2 for multi-item units" do
|
||||
allow(product).to receive(:variant_unit_scale) { nil }
|
||||
allow(product).to receive(:variant_unit) { "items" }
|
||||
variant.unit_value = 2
|
||||
expect(subject.denominator).to eq(2)
|
||||
variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 2, variant_unit: "items")
|
||||
|
||||
expect(UnitPrice.new(variant).denominator).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,9 +5,8 @@ require 'spec_helper'
|
||||
module VariantUnits
|
||||
RSpec.describe OptionValueNamer do
|
||||
describe "generating option value name" do
|
||||
subject { OptionValueNamer.new(v) }
|
||||
let(:v) { Spree::Variant.new }
|
||||
let(:p) { Spree::Product.new }
|
||||
let(:subject) { OptionValueNamer.new(v) }
|
||||
|
||||
it "when description is blank" do
|
||||
allow(v).to receive(:unit_description) { nil }
|
||||
@@ -40,18 +39,14 @@ module VariantUnits
|
||||
|
||||
describe "determining if a variant's value is scaled" do
|
||||
it "returns true when the product has a scale" do
|
||||
p = Spree::Product.new variant_unit_scale: 1000
|
||||
v = Spree::Variant.new
|
||||
allow(v).to receive(:product) { p }
|
||||
v = Spree::Variant.new variant_unit_scale: 1000
|
||||
subject = OptionValueNamer.new v
|
||||
|
||||
expect(subject.__send__(:value_scaled?)).to be true
|
||||
end
|
||||
|
||||
it "returns false otherwise" do
|
||||
p = Spree::Product.new
|
||||
v = Spree::Variant.new
|
||||
allow(v).to receive(:product) { p }
|
||||
subject = OptionValueNamer.new v
|
||||
|
||||
expect(subject.__send__(:value_scaled?)).to be false
|
||||
@@ -59,115 +54,109 @@ module VariantUnits
|
||||
end
|
||||
|
||||
describe "generating option value's value and unit" do
|
||||
let(:v) { Spree::Variant.new }
|
||||
let(:subject) { OptionValueNamer.new v }
|
||||
|
||||
before do
|
||||
allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL")
|
||||
end
|
||||
|
||||
it "generates simple values" do
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 100 }
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
|
||||
unit_value: 100)
|
||||
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [100, 'g']
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, 'g']
|
||||
end
|
||||
|
||||
it "generates values when unit value is non-integer" do
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 123.45 }
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
|
||||
unit_value: 123.45)
|
||||
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [123.45, 'g']
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [123.45, 'g']
|
||||
end
|
||||
|
||||
it "returns a value of 1 when unit value equals the scale" do
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1000.0)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 1000.0 }
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1000.0,
|
||||
unit_value: 1000.0)
|
||||
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [1, 'kg']
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'kg']
|
||||
end
|
||||
|
||||
it "returns only values that are in the same measurement systems" do
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 500 }
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
|
||||
unit_value: 500)
|
||||
|
||||
# 500g would convert to > 1 pound, but we don't want the namer to use
|
||||
# pounds since it's in a different measurement system.
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [500, 'g']
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [500, 'g']
|
||||
end
|
||||
|
||||
it "generates values for all weight scales" do
|
||||
[[1.0, 'g'], [28.35, 'oz'], [453.6, 'lb'], [1000.0, 'kg'],
|
||||
[1_000_000.0, 'T']].each do |scale, unit|
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: scale)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 10.0 * scale }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [10, unit]
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: scale,
|
||||
unit_value: 10.0 * scale)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [10, unit]
|
||||
end
|
||||
end
|
||||
|
||||
it "generates values for all volume scales" do
|
||||
[[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].each do |scale, unit|
|
||||
p = double(:product, variant_unit: 'volume', variant_unit_scale: scale)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 3 * scale }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [3, unit]
|
||||
v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: scale,
|
||||
unit_value: 3 * scale)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [3, unit]
|
||||
end
|
||||
end
|
||||
|
||||
it "chooses the correct scale when value is very small" do
|
||||
p = double(:product, variant_unit: 'volume', variant_unit_scale: 0.001)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 0.0001 }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [0.1, 'mL']
|
||||
v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: 0.001,
|
||||
unit_value: 0.0001)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [0.1, 'mL']
|
||||
end
|
||||
|
||||
it "generates values for item units" do
|
||||
%w(packet box).each do |unit|
|
||||
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: unit)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 100 }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [100, unit.pluralize]
|
||||
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: unit, unit_value: 100)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, unit.pluralize]
|
||||
end
|
||||
end
|
||||
|
||||
it "generates singular values for item units when value is 1" do
|
||||
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: 'packet')
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
allow(v).to receive(:unit_value) { 1 }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [1, 'packet']
|
||||
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: 'packet', unit_value: 1)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'packet']
|
||||
end
|
||||
|
||||
it "returns [nil, nil] when unit value is not set" do
|
||||
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: 'foo')
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(v).to receive(:unit_value) { nil }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [nil, nil]
|
||||
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
|
||||
variant_unit_name: 'foo', unit_value: nil)
|
||||
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [nil, nil]
|
||||
end
|
||||
|
||||
it "truncates value to 2 decimals maximum" do
|
||||
oz_scale = 28.35
|
||||
p = double(:product, variant_unit: 'weight', variant_unit_scale: oz_scale)
|
||||
allow(v).to receive(:product) { p }
|
||||
allow(p).to receive(:persisted?) { true }
|
||||
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: oz_scale,
|
||||
unit_value: (12.5 * oz_scale).round(2))
|
||||
|
||||
# The unit_value is stored rounded to 2 decimals
|
||||
allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) }
|
||||
expect(subject.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6), 'oz']
|
||||
# allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) }
|
||||
option_value_namer = OptionValueNamer.new v
|
||||
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6),
|
||||
'oz']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user