diff --git a/.prettierignore b/.prettierignore index e6c0ce1c7f5..ed5fb01b740 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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/ diff --git a/app/assets/javascripts/admin/bulk_product_update.js.coffee b/app/assets/javascripts/admin/bulk_product_update.js.coffee index d32977b8c05..eae103015a2 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js.coffee +++ b/app/assets/javascripts/admin/bulk_product_update.js.coffee @@ -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 = (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 - $scope.packVariant = (product, variant) -> 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} diff --git a/app/assets/javascripts/admin/directives/display_as.js.coffee b/app/assets/javascripts/admin/directives/display_as.js.coffee index 375f1795eef..bb624113ba8 100644 --- a/app/assets/javascripts/admin/directives/display_as.js.coffee +++ b/app/assets/javascripts/admin/directives/display_as.js.coffee @@ -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] \ No newline at end of file + [unit_value, unit_description] diff --git a/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee b/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee deleted file mode 100644 index 55306f6d778..00000000000 --- a/app/assets/javascripts/admin/directives/maintain_unit_scale.js.coffee +++ /dev/null @@ -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 - \ No newline at end of file diff --git a/app/assets/javascripts/admin/directives/track_master.js.coffee b/app/assets/javascripts/admin/directives/track_master.js.coffee deleted file mode 100644 index be84e0b2046..00000000000 --- a/app/assets/javascripts/admin/directives/track_master.js.coffee +++ /dev/null @@ -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 diff --git a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee index 88479620faa..d10de21f262 100644 --- a/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee +++ b/app/assets/javascripts/admin/line_items/controllers/line_items_controller.js.coffee @@ -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 diff --git a/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee deleted file mode 100644 index 196ca473193..00000000000 --- a/app/assets/javascripts/admin/products/controllers/edit_units_controller.js.coffee +++ /dev/null @@ -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 diff --git a/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee index a92f4aeadae..446dc1794bd 100644 --- a/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee +++ b/app/assets/javascripts/admin/products/controllers/units_controller.js.coffee @@ -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) diff --git a/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee deleted file mode 100644 index 91549bfc607..00000000000 --- a/app/assets/javascripts/admin/products/controllers/variant_units_controller.js.coffee +++ /dev/null @@ -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() diff --git a/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee b/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee deleted file mode 100644 index d554a194a43..00000000000 --- a/app/assets/javascripts/admin/products/directives/set_variant_on_demand.js.coffee +++ /dev/null @@ -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 diff --git a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee index 0e7c94331d2..2a61b3efc19 100644 --- a/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee +++ b/app/assets/javascripts/admin/products/services/option_value_namer.js.coffee @@ -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)] diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index e8b8bb6c3d8..a2823bf9970 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -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 - else - null + @loadVariantUnitValues product.variants if product.variants - @loadVariantUnitValues product if product.variants - @loadVariantUnitValue product, product.master if product.master + loadVariantUnitValues: (variants) -> + for variant in variants + @loadVariantUnitValue variant - loadVariantUnitValues: (product) -> - for variant in product.variants - @loadVariantUnitValue product, 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 - 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 diff --git a/app/controllers/admin/product_import_controller.rb b/app/controllers/admin/product_import_controller.rb index a2dc50ce433..9331023aad6 100644 --- a/app/controllers/admin/product_import_controller.rb +++ b/app/controllers/admin/product_import_controller.rb @@ -21,8 +21,7 @@ def import @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 diff --git a/app/controllers/api/v0/products_controller.rb b/app/controllers/api/v0/products_controller.rb index 6df0356cf53..801b6a343a1 100644 --- a/app/controllers/api/v0/products_controller.rb +++ b/app/controllers/api/v0/products_controller.rb @@ -21,7 +21,7 @@ def create 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) diff --git a/app/controllers/spree/admin/products_controller.rb b/app/controllers/spree/admin/products_controller.rb index 4580a751f01..d383bee4e8e 100644 --- a/app/controllers/spree/admin/products_controller.rb +++ b/app/controllers/spree/admin/products_controller.rb @@ -39,7 +39,7 @@ def edit 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 diff --git a/app/controllers/spree/admin/variants_controller.rb b/app/controllers/spree/admin/variants_controller.rb index 33f29e1b45b..765a011dff3 100644 --- a/app/controllers/spree/admin/variants_controller.rb +++ b/app/controllers/spree/admin/variants_controller.rb @@ -5,6 +5,8 @@ module Spree module Admin class VariantsController < ::Admin::ResourceController + helper ::Admin::ProductsHelper + belongs_to 'spree/product' before_action :load_data, only: [:new, :edit] diff --git a/app/helpers/admin/products_helper.rb b/app/helpers/admin/products_helper.rb index fe00c220022..ea18887e04d 100644 --- a/app/helpers/admin/products_helper.rb +++ b/app/helpers/admin/products_helper.rb @@ -18,17 +18,15 @@ def prepare_new_variant(product, producer_options) end def unit_value_with_description(variant) - precised_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 + return variant.unit_description.to_s if variant.unit_value.nil? + + scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1) + precised_unit_value = number_with_precision( + scaled_unit_value, + precision: nil, + strip_insignificant_zeros: true, + significant: false, + ) [precised_unit_value, variant.unit_description].compact_blank.join(" ") end diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb index c7596cc1ae3..6393f14578c 100644 --- a/app/models/product_import/entry_processor.rb +++ b/app/models/product_import/entry_processor.rb @@ -224,6 +224,9 @@ def ensure_variant_updated(product, entry) # 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 diff --git a/app/models/product_import/entry_validator.rb b/app/models/product_import/entry_validator.rb index 4db5a60d2c1..a6a2277afda 100644 --- a/app/models/product_import/entry_validator.rb +++ b/app/models/product_import/entry_validator.rb @@ -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 @@ def initialize(current_user, import_time, spreadsheet_data, editable_enterprises 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 @@ def enterprise_field 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 @@ def inventory_validation(entry) 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 @@ def inventory_validation(entry) 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 @@ def product_validation(entry) 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 @@ def mark_as_new_product(entry) 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 @@ def mark_as_existing_variant(entry, existing_variant) 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 @@ def attributes_match?(attribute, existing_product, entry) 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 diff --git a/app/models/product_import/spreadsheet_entry.rb b/app/models/product_import/spreadsheet_entry.rb index a46d061ee76..23743e6f90f 100644 --- a/app/models/product_import/spreadsheet_entry.rb +++ b/app/models/product_import/spreadsheet_entry.rb @@ -84,6 +84,14 @@ def invalid_attributes 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 @@ def assign_units(attrs) 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 diff --git a/app/models/spree/line_item.rb b/app/models/spree/line_item.rb index 23fed5fbc60..e70834839bd 100644 --- a/app/models/spree/line_item.rb +++ b/app/models/spree/line_item.rb @@ -45,7 +45,8 @@ class LineItem < ApplicationRecord 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 diff --git a/app/models/spree/price.rb b/app/models/spree/price.rb index 0c2d12c285f..9fcdac64597 100644 --- a/app/models/spree/price.rb +++ b/app/models/spree/price.rb @@ -38,6 +38,7 @@ def check_price # 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', diff --git a/app/models/spree/product.rb b/app/models/spree/product.rb index d4872b3e848..57f765413ce 100755 --- a/app/models/spree/product.rb +++ b/app/models/spree/product.rb @@ -22,7 +22,12 @@ class Product < ApplicationRecord 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 @@ class Product < ApplicationRecord 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 @@ class Product < ApplicationRecord # 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 @@ def destruction end end + # rubocop:disable Metrics/AbcSize def ensure_standard_variant return unless variants.empty? @@ -254,31 +268,16 @@ def ensure_standard_variant 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 @@ def description=(html) 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? diff --git a/app/models/spree/variant.rb b/app/models/spree/variant.rb index 4425c24523a..fefd41ac0d8 100644 --- a/app/models/spree/variant.rb +++ b/app/models/spree/variant.rb @@ -71,21 +71,25 @@ class Variant < ApplicationRecord 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 @@ class Variant < ApplicationRecord 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 @@ def total_on_hand 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 @@ def create_stock_items 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 @@ def destruction 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 diff --git a/app/serializers/api/admin/product_serializer.rb b/app/serializers/api/admin/product_serializer.rb index 836b78bdaa3..7056c5745cd 100644 --- a/app/serializers/api/admin/product_serializer.rb +++ b/app/serializers/api/admin/product_serializer.rb @@ -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 diff --git a/app/serializers/api/admin/units_product_serializer.rb b/app/serializers/api/admin/units_product_serializer.rb index 60c5f70b970..4d6e83a8fd5 100644 --- a/app/serializers/api/admin/units_product_serializer.rb +++ b/app/serializers/api/admin/units_product_serializer.rb @@ -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 diff --git a/app/serializers/api/admin/units_variant_serializer.rb b/app/serializers/api/admin/units_variant_serializer.rb index 99423957ffe..6557767afff 100644 --- a/app/serializers/api/admin/units_variant_serializer.rb +++ b/app/serializers/api/admin/units_variant_serializer.rb @@ -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 diff --git a/app/serializers/api/admin/variant_serializer.rb b/app/serializers/api/admin/variant_serializer.rb index 5ccb8347c73..516351a3e40 100644 --- a/app/serializers/api/admin/variant_serializer.rb +++ b/app/serializers/api/admin/variant_serializer.rb @@ -6,7 +6,8 @@ class VariantSerializer < ActiveModel::Serializer 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 diff --git a/app/services/permitted_attributes/variant.rb b/app/services/permitted_attributes/variant.rb index 4d4fdcea38e..9d20975cb0e 100644 --- a/app/services/permitted_attributes/variant.rb +++ b/app/services/permitted_attributes/variant.rb @@ -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 diff --git a/app/services/unit_price.rb b/app/services/unit_price.rb index 0b28c973d55..004ec68332a 100644 --- a/app/services/unit_price.rb +++ b/app/services/unit_price.rb @@ -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 @@ def denominator 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 diff --git a/app/services/variant_units/option_value_namer.rb b/app/services/variant_units/option_value_namer.rb index 6a36684fb37..40b1bd04416 100644 --- a/app/services/variant_units/option_value_namer.rb +++ b/app/services/variant_units/option_value_namer.rb @@ -32,16 +32,16 @@ def unit 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 diff --git a/app/services/variant_units/variant_and_line_item_naming.rb b/app/services/variant_units/variant_and_line_item_naming.rb index 9b074131554..b9d726b6691 100644 --- a/app/services/variant_units/variant_and_line_item_naming.rb +++ b/app/services/variant_units/variant_and_line_item_naming.rb @@ -64,12 +64,12 @@ def update_units 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 diff --git a/app/services/weights_and_measures.rb b/app/services/weights_and_measures.rb index e285a0baaca..ab436a712e2 100644 --- a/app/services/weights_and_measures.rb +++ b/app/services/weights_and_measures.rb @@ -16,10 +16,10 @@ def scale_for_unit_value 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 @@ def self.available_units_sorted }.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 diff --git a/app/views/admin/products_v3/_product_row.html.haml b/app/views/admin/products_v3/_product_row.html.haml index edd9d406b67..510ad506055 100644 --- a/app/views/admin/products_v3/_product_row.html.haml +++ b/app/views/admin/products_v3/_product_row.html.haml @@ -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 diff --git a/app/views/admin/products_v3/_table.html.haml b/app/views/admin/products_v3/_table.html.haml index 71e777ba13a..5f302914085 100644 --- a/app/views/admin/products_v3/_table.html.haml +++ b/app/views/admin/products_v3/_table.html.haml @@ -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', diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 0db0b5873c2..fa423bbce75 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -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 diff --git a/app/views/admin/products_v3/index.html.haml b/app/views/admin/products_v3/index.html.haml index fed068a609f..dfa010fdc85 100644 --- a/app/views/admin/products_v3/index.html.haml +++ b/app/views/admin/products_v3/index.html.haml @@ -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, diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 6f27875d8df..f4b57f832fa 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -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") diff --git a/app/views/spree/admin/products/_form.html.haml b/app/views/spree/admin/products/_form.html.haml index 546efeace4f..ab9843b7e22 100644 --- a/app/views/spree/admin/products/_form.html.haml +++ b/app/views/spree/admin/products/_form.html.haml @@ -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 diff --git a/app/views/spree/admin/products/index/_products_product.html.haml b/app/views/spree/admin/products/index/_products_product.html.haml index b5a778a043b..aa10e5e29ad 100644 --- a/app/views/spree/admin/products/index/_products_product.html.haml +++ b/app/views/spree/admin/products/index/_products_product.html.haml @@ -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)' } diff --git a/app/views/spree/admin/products/index/_products_variant.html.haml b/app/views/spree/admin/products/index/_products_variant.html.haml index 8a5c2c03115..4aa5e13f78d 100644 --- a/app/views/spree/admin/products/index/_products_variant.html.haml +++ b/app/views/spree/admin/products/index/_products_variant.html.haml @@ -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' } diff --git a/app/views/spree/admin/products/new.html.haml b/app/views/spree/admin/products/new.html.haml index a7162157380..d495fc170d9 100644 --- a/app/views/spree/admin/products/new.html.haml +++ b/app/views/spree/admin/products/new.html.haml @@ -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'" } diff --git a/app/views/spree/admin/variants/_form.html.haml b/app/views/spree/admin/variants/_form.html.haml index dcb4d68b6e4..25adcabac37 100644 --- a/app/views/spree/admin/variants/_form.html.haml +++ b/app/views/spree/admin/variants/_form.html.haml @@ -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 } + + %script= render partial: "admin/shared/global_var_ofn", formats: :js, + locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) } - - 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'} + = f.label :display_name, t('.display_name') + = f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder') - .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{ '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.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 - %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') + %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 - = f.label :on_hand, t(:on_hand) - .fullwidth - = f.text_field :on_hand + = 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) -.right.six.columns.omega.label-block - - if @product.variant_unit != 'weight' + = 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 diff --git a/app/webpacker/controllers/bulk_form_controller.js b/app/webpacker/controllers/bulk_form_controller.js index 5558b6d52ad..476bf003918 100644 --- a/app/webpacker/controllers/bulk_form_controller.js +++ b/app/webpacker/controllers/bulk_form_controller.js @@ -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 }); } } diff --git a/app/webpacker/controllers/edit_variant_controller.js b/app/webpacker/controllers/edit_variant_controller.js new file mode 100644 index 00000000000..10e5b742f34 --- /dev/null +++ b/app/webpacker/controllers/edit_variant_controller.js @@ -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 = ""; + } +} diff --git a/app/webpacker/controllers/product_controller.js b/app/webpacker/controllers/product_controller.js deleted file mode 100644 index 942f3b02e1d..00000000000 --- a/app/webpacker/controllers/product_controller.js +++ /dev/null @@ -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")); - } -} diff --git a/app/webpacker/controllers/variant_controller.js b/app/webpacker/controllers/variant_controller.js index 07780e25319..5a529a338f7 100644 --- a/app/webpacker/controllers/variant_controller.js +++ b/app/webpacker/controllers/variant_controller.js @@ -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")); + } } diff --git a/app/webpacker/css/admin/products_v3.scss b/app/webpacker/css/admin/products_v3.scss index 912e9406086..bb144cd6e87 100644 --- a/app/webpacker/css/admin/products_v3.scss +++ b/app/webpacker/css/admin/products_v3.scss @@ -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 { diff --git a/app/webpacker/css/admin_v3/all.scss b/app/webpacker/css/admin_v3/all.scss index a5c673b9096..32eba7ddde6 100644 --- a/app/webpacker/css/admin_v3/all.scss +++ b/app/webpacker/css/admin_v3/all.scss @@ -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"; diff --git a/app/webpacker/css/admin_v3/pages/_unit_popout.scss b/app/webpacker/css/admin_v3/pages/_unit_popout.scss new file mode 100644 index 00000000000..475bc9bcd25 --- /dev/null +++ b/app/webpacker/css/admin_v3/pages/_unit_popout.scss @@ -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 + } + } + } +} + diff --git a/app/webpacker/css/admin_v3/pages/edit_variant.scss b/app/webpacker/css/admin_v3/pages/edit_variant.scss new file mode 100644 index 00000000000..694c72bd932 --- /dev/null +++ b/app/webpacker/css/admin_v3/pages/edit_variant.scss @@ -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; + } + } +} diff --git a/app/webpacker/js/services/localize_currency.js b/app/webpacker/js/services/localize_currency.js new file mode 100644 index 00000000000..c542d195921 --- /dev/null +++ b/app/webpacker/js/services/localize_currency.js @@ -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 + ); +} diff --git a/app/webpacker/js/services/option_value_namer.js b/app/webpacker/js/services/option_value_namer.js index ba4b12fd961..f0eaf8ea519 100644 --- a/app/webpacker/js/services/option_value_namer.js +++ b/app/webpacker/js/services/option_value_namer.js @@ -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)]; } } } - diff --git a/app/webpacker/js/services/price_parser.js b/app/webpacker/js/services/price_parser.js new file mode 100644 index 00000000000..873f665c9a6 --- /dev/null +++ b/app/webpacker/js/services/price_parser.js @@ -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; + } + } +} diff --git a/app/webpacker/js/services/unit_prices.js b/app/webpacker/js/services/unit_prices.js new file mode 100644 index 00000000000..6344a2e073d --- /dev/null +++ b/app/webpacker/js/services/unit_prices.js @@ -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"; + } + } +} diff --git a/app/webpacker/js/services/variant_unit_manager.js b/app/webpacker/js/services/variant_unit_manager.js index 1711025fcd0..f13af68264c 100644 --- a/app/webpacker/js/services/variant_unit_manager.js +++ b/app/webpacker/js/services/variant_unit_manager.js @@ -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); } diff --git a/config/locales/en.yml b/config/locales/en.yml index cba29dc7a3c..5b820de04a5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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" @@ -4615,6 +4615,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" diff --git a/db/migrate/20240618013850_add_variant_unit_to_variant.rb b/db/migrate/20240618013850_add_variant_unit_to_variant.rb new file mode 100644 index 00000000000..ec7d7f775ed --- /dev/null +++ b/db/migrate/20240618013850_add_variant_unit_to_variant.rb @@ -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 diff --git a/db/migrate/20240819045115_migrate_unit_size_to_variants.rb b/db/migrate/20240819045115_migrate_unit_size_to_variants.rb new file mode 100644 index 00000000000..4747d849397 --- /dev/null +++ b/db/migrate/20240819045115_migrate_unit_size_to_variants.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 9ca97c3eef7..ced400f493c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -968,6 +968,8 @@ 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" diff --git a/engines/dfc_provider/app/services/quantitative_value_builder.rb b/engines/dfc_provider/app/services/quantitative_value_builder.rb index 31401a33695..10896578b50 100644 --- a/engines/dfc_provider/app/services/quantitative_value_builder.rb +++ b/engines/dfc_provider/app/services/quantitative_value_builder.rb @@ -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 @@ def self.unit(unit_name) 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 @@ def self.apply(quantity, product) 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: diff --git a/engines/dfc_provider/app/services/supplied_product_builder.rb b/engines/dfc_provider/app/services/supplied_product_builder.rb index 4eb488c4f23..8707ce04b0f 100644 --- a/engines/dfc_provider/app/services/supplied_product_builder.rb +++ b/engines/dfc_provider/app/services/supplied_product_builder.rb @@ -96,8 +96,7 @@ def self.apply(supplied_product, variant) 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 diff --git a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb index 9673b70045f..6d11af2b7cc 100644 --- a/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb +++ b/engines/dfc_provider/spec/services/affiliate_sales_data_builder_spec.rb @@ -25,7 +25,8 @@ :product, name: "Pomme", supplier_id: supplier.id, - variant_unit: "item", + variant_unit: "items", + variant_unit_name: "bag", ) variant = product.variants.first distributor = create( diff --git a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb index f18a58b5611..04783becfad 100644 --- a/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb +++ b/engines/dfc_provider/spec/services/quantitative_value_builder_spec.rb @@ -4,12 +4,11 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 diff --git a/jest.config.js b/jest.config.js index 4c914cf1f8d..18863711393 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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, diff --git a/lib/open_food_network/scope_variants_for_search.rb b/lib/open_food_network/scope_variants_for_search.rb index e1540965249..7c51cd8b613 100644 --- a/lib/open_food_network/scope_variants_for_search.rb +++ b/lib/open_food_network/scope_variants_for_search.rb @@ -36,7 +36,7 @@ def query_scope 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 diff --git a/lib/reporting/reports/bulk_coop/base.rb b/lib/reporting/reports/bulk_coop/base.rb index ce4c154e353..bd6b3d03e7b 100644 --- a/lib/reporting/reports/bulk_coop/base.rb +++ b/lib/reporting/reports/bulk_coop/base.rb @@ -49,7 +49,7 @@ def full_name(line_items) 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 @@ def max_quantity_amount(line_items) 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 @@ def total_amount(line_items) 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) diff --git a/lib/reporting/reports/orders_and_fulfillment/base.rb b/lib/reporting/reports/orders_and_fulfillment/base.rb index 36b6ecf325c..0aa6ff3128b 100644 --- a/lib/reporting/reports/orders_and_fulfillment/base.rb +++ b/lib/reporting/reports/orders_and_fulfillment/base.rb @@ -72,8 +72,7 @@ def total_units(line_items) 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 @@ def not_all_have_unit?(line_items) 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 diff --git a/lib/spree/core/product_duplicator.rb b/lib/spree/core/product_duplicator.rb index 82837bf3a61..5000caf7cd4 100644 --- a/lib/spree/core/product_duplicator.rb +++ b/lib/spree/core/product_duplicator.rb @@ -25,7 +25,6 @@ def duplicate_product 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 diff --git a/spec/controllers/api/v0/products_controller_spec.rb b/spec/controllers/api/v0/products_controller_spec.rb index 3633dab6ea7..fd3a0ade81a 100644 --- a/spec/controllers/api/v0/products_controller_spec.rb +++ b/spec/controllers/api/v0/products_controller_spec.rb @@ -35,8 +35,8 @@ 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 @@ 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 diff --git a/spec/controllers/api/v0/variants_controller_spec.rb b/spec/controllers/api/v0/variants_controller_spec.rb index ce9f4865126..ec200f13892 100644 --- a/spec/controllers/api/v0/variants_controller_spec.rb +++ b/spec/controllers/api/v0/variants_controller_spec.rb @@ -144,9 +144,9 @@ 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) diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index f7f81f2cdc0..deae56bd80d 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -111,6 +111,8 @@ "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, diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb index ba6e1542e5b..a443e4d4491 100644 --- a/spec/controllers/spree/admin/variants_controller_spec.rb +++ b/spec/controllers/spree/admin/variants_controller_spec.rb @@ -12,8 +12,8 @@ module Admin 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 diff --git a/spec/factories/product_factory.rb b/spec/factories/product_factory.rb index f2fab0e39f6..453a442be45 100644 --- a/spec/factories/product_factory.rb +++ b/spec/factories/product_factory.rb @@ -20,10 +20,8 @@ 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 } diff --git a/spec/factories/variant_factory.rb b/spec/factories/variant_factory.rb index 4382af7e0af..1aed03f1833 100644 --- a/spec/factories/variant_factory.rb +++ b/spec/factories/variant_factory.rb @@ -10,6 +10,12 @@ 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 @@ 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 diff --git a/spec/helpers/admin/products_helper_spec.rb b/spec/helpers/admin/products_helper_spec.rb index cdd4df9e2a1..cfd04180d99 100644 --- a/spec/helpers/admin/products_helper_spec.rb +++ b/spec/helpers/admin/products_helper_spec.rb @@ -4,8 +4,9 @@ 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 @@ 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') diff --git a/spec/javascripts/services/localize_currency_test.js b/spec/javascripts/services/localize_currency_test.js new file mode 100644 index 00000000000..fd8a2da3599 --- /dev/null +++ b/spec/javascripts/services/localize_currency_test.js @@ -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"); + }); +}); diff --git a/spec/javascripts/services/option_value_namer_test.js b/spec/javascripts/services/option_value_namer_test.js index e7878adae10..61f13ebd970 100644 --- a/spec/javascripts/services/option_value_namer_test.js +++ b/spec/javascripts/services/option_value_namer_test.js @@ -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]); }); diff --git a/spec/javascripts/services/price_parser_test.js b/spec/javascripts/services/price_parser_test.js new file mode 100644 index 00000000000..b8d77948cac --- /dev/null +++ b/spec/javascripts/services/price_parser_test.js @@ -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); + }); + }); +}); diff --git a/spec/javascripts/services/unit_prices_test.js b/spec/javascripts/services/unit_prices_test.js new file mode 100644 index 00000000000..6df9eb791f5 --- /dev/null +++ b/spec/javascripts/services/unit_prices_test.js @@ -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"); + }); + }); +}); diff --git a/spec/javascripts/stimulus/product_controller_test.js b/spec/javascripts/stimulus/product_controller_test.js deleted file mode 100644 index bcd4f8bdad8..00000000000 --- a/spec/javascripts/stimulus/product_controller_test.js +++ /dev/null @@ -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 = ` -
- - - -
- `; - }); - - 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(""); - }); - }) - }); -}); diff --git a/spec/javascripts/stimulus/variant_controller_test.js b/spec/javascripts/stimulus/variant_controller_test.js new file mode 100644 index 00000000000..032bcc7d091 --- /dev/null +++ b/spec/javascripts/stimulus/variant_controller_test.js @@ -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 = ` +
+ + + + + + + + + +
+ `; + }); + + 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(""); + }); + }); + }); +}); diff --git a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee b/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee index 70fe6dac589..532f569d69e 100644 --- a/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee +++ b/spec/javascripts/unit/admin/bulk_product_update_spec.js.coffee @@ -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,174 +510,163 @@ 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", -> + it "packs each variant", -> + spyOn $scope, "packVariant" + testVariant = {id: 1} testProduct = id: 1 - variant_unit: 'weight' - variant_unit_scale: 1 - variant_unit_with_scale: 'volume_1000' + variants: {1: testVariant} $scope.packProduct(testProduct) - expect(testProduct).toEqual - id: 1 - variant_unit: 'volume' - variant_unit_scale: 1000 - variant_unit_with_scale: 'volume_1000' + expect($scope.packVariant).toHaveBeenCalledWith(testVariant) - it "extracts a null value into null variant_unit and variant_unit_scale", -> - testProduct = + 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: null + variant_unit_with_scale: 'volume_1000' - $scope.packProduct(testProduct) + $scope.packVariant(testVariant) - expect(testProduct).toEqual + expect(testVariant).toEqual id: 1 - variant_unit: null - variant_unit_scale: null - variant_unit_with_scale: null + variant_unit: 'volume' + variant_unit_scale: 1000 + variant_unit_with_scale: 'volume_1000' it "extracts when variant_unit_with_scale is 'items'", -> - testProduct = + testVariant = id: 1 variant_unit: 'weight' variant_unit_scale: 1 variant_unit_with_scale: 'items' - $scope.packProduct(testProduct) + $scope.packVariant(testVariant) - expect(testProduct).toEqual + expect(testVariant).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} - testProduct = - id: 1 - variants: {1: testVariant} - - $scope.packProduct(testProduct) - - expect($scope.packVariant).toHaveBeenCalledWith(testProduct, 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 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", -> diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index e77ee3780ea..39797ccc6b6 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -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", -> diff --git a/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee index ba03fe30cbd..bfc04b7a759 100644 --- a/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee +++ b/spec/javascripts/unit/admin/products/services/option_value_namer_spec.js.coffee @@ -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] + diff --git a/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee b/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee index b0e7908b025..4b65a40a241 100644 --- a/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/products/units_controller_spec.js.coffee @@ -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 diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 466c09109f9..ea741adc35b 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -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", -> diff --git a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee b/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee deleted file mode 100644 index 8eaae5bbeed..00000000000 --- a/spec/javascripts/unit/admin/services/option_value_namer_spec.js.coffee +++ /dev/null @@ -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] diff --git a/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb index e81596cbbd4..38cedaa0153 100644 --- a/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb +++ b/spec/lib/reports/orders_and_fulfillment/orders_cycle_supplier_totals_report_spec.rb @@ -34,8 +34,7 @@ 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 @@ 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 @@ # 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 @@ 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 @@ # 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. diff --git a/spec/lib/spree/core/product_duplicator_spec.rb b/spec/lib/spree/core/product_duplicator_spec.rb index 141030048a8..e8163e961d7 100644 --- a/spec/lib/spree/core/product_duplicator_spec.rb +++ b/spec/lib/spree/core/product_duplicator_spec.rb @@ -5,58 +5,28 @@ 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 @@ 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 @@ 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 ". 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 diff --git a/spec/models/product_import/entry_validator_spec.rb b/spec/models/product_import/entry_validator_spec.rb index 47512e5a786..db240b94986 100644 --- a/spec/models/product_import/entry_validator_spec.rb +++ b/spec/models/product_import/entry_validator_spec.rb @@ -124,7 +124,7 @@ 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 @@ unit_value: 1000, variant_unit_scale: 1000, variant_unit: 'weight', - variants: [create(:variant, supplier: enterprise, unit_value: 1000)] + supplier_id: enterprise.id ) } diff --git a/spec/models/product_import/spreadsheet_entry_spec.rb b/spec/models/product_import/spreadsheet_entry_spec.rb new file mode 100644 index 00000000000..f2e98c86c0e --- /dev/null +++ b/spec/models/product_import/spreadsheet_entry_spec.rb @@ -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 diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index 4dc3df2199e..60d42c6154b 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -160,60 +160,60 @@ 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 @@ 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 } diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index b6fecf1f33a..37aeaa6fc1b 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -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) } diff --git a/spec/models/spree/price_spec.rb b/spec/models/spree/price_spec.rb index 441a7a185b5..047d5da2683 100644 --- a/spec/models/spree/price_spec.rb +++ b/spec/models/spree/price_spec.rb @@ -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 diff --git a/spec/models/spree/product_spec.rb b/spec/models/spree/product_spec.rb index 385588538d3..df80c8f0521 100644 --- a/spec/models/spree/product_spec.rb +++ b/spec/models/spree/product_spec.rb @@ -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,65 +140,130 @@ 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 - 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 + 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 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 - describe "#validate_image" do subject(:product) { create(:product_with_image) } @@ -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:) } diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 36860d1872a..c0974a3baa8 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -44,21 +44,164 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 diff --git a/spec/services/sets/product_set_spec.rb b/spec/services/sets/product_set_spec.rb index 35b2d340677..d415f7096de 100644 --- a/spec/services/sets/product_set_spec.rb +++ b/spec/services/sets/product_set_spec.rb @@ -69,22 +69,27 @@ 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 @@ { 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 @@ 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 diff --git a/spec/services/unit_prices_spec.rb b/spec/services/unit_prices_spec.rb index 2219eab6c8e..5166346aa12 100644 --- a/spec/services/unit_prices_spec.rb +++ b/spec/services/unit_prices_spec.rb @@ -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 @@ 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 diff --git a/spec/services/variant_units/option_value_namer_spec.rb b/spec/services/variant_units/option_value_namer_spec.rb index 58ce97d2461..c3f06746f5f 100644 --- a/spec/services/variant_units/option_value_namer_spec.rb +++ b/spec/services/variant_units/option_value_namer_spec.rb @@ -5,9 +5,8 @@ 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 diff --git a/spec/services/weights_and_measures_spec.rb b/spec/services/weights_and_measures_spec.rb index 0992316886c..5b359469e24 100644 --- a/spec/services/weights_and_measures_spec.rb +++ b/spec/services/weights_and_measures_spec.rb @@ -4,57 +4,55 @@ RSpec.describe WeightsAndMeasures do subject { WeightsAndMeasures.new(variant) } - let(:variant) { Spree::Variant.new } - let(:product) { instance_double(Spree::Product) } + let(:variant) { instance_double(Spree::Variant) } let(:available_units) { ["mg", "g", "kg", "T", "oz", "lb", "mL", "cL", "dL", "L", "kL", "gal"].join(",") } before do - allow(variant).to receive(:product) { product } allow(Spree::Config).to receive(:available_units).and_return(available_units) end describe "#system" do context "weight" do before do - allow(product).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit) { "weight" } end it "when scale is for a metric unit" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("metric") end it "when scale is for an imperial unit" do - allow(product).to receive(:variant_unit_scale) { 28.35 } + allow(variant).to receive(:variant_unit_scale) { 28.35 } expect(subject.system).to eq("imperial") end it "when precise scale is for an imperial unit" do - allow(product).to receive(:variant_unit_scale) { 28.349523125 } + allow(variant).to receive(:variant_unit_scale) { 28.349523125 } expect(subject.system).to eq("imperial") end end context "volume" do it "when scale is for a metric unit" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("metric") end end context "items" do it "when variant unit is items" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when variant unit is items, even if the scale is present" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end end @@ -62,38 +60,38 @@ # In the event of corrupt data, we don't want an exception context "corrupt data" do it "when unit is invalid, scale is valid" do - allow(product).to receive(:variant_unit) { "blah" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "blah" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end it "when unit is invalid, scale is nil" do - allow(product).to receive(:variant_unit) { "blah" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "blah" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is nil, scale is valid" do - allow(product).to receive(:variant_unit) { nil } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { nil } + allow(variant).to receive(:variant_unit_scale) { 1.0 } expect(subject.system).to eq("custom") end it "when unit is nil, scale is nil" do - allow(product).to receive(:variant_unit) { nil } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { nil } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is valid, but scale is nil" do - allow(product).to receive(:variant_unit) { "weight" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit_scale) { nil } expect(subject.system).to eq("custom") end it "when unit is valid, but scale is 0" do - allow(product).to receive(:variant_unit) { "weight" } - allow(product).to receive(:variant_unit_scale) { 0.0 } + allow(variant).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit_scale) { 0.0 } expect(subject.system).to eq("custom") end end @@ -149,18 +147,18 @@ describe "#scales_for_unit_value" do context "weight" do before do - allow(product).to receive(:variant_unit) { "weight" } + allow(variant).to receive(:variant_unit) { "weight" } end context "metric" do it "for a unit value that should display in grams" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 500 } expect(subject.scale_for_unit_value).to eq([1.0, "g"]) end it "for a unit value that should display in kg" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1000.0, "kg"]) end @@ -169,7 +167,7 @@ let(:available_units) { ["mg", "g", "T"].join(",") } it "should display in g" do - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1.0, "g"]) end @@ -179,15 +177,15 @@ context "volume" do it "for a unit value that should display in kL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 1500 } expect(subject.scale_for_unit_value).to eq([1000, "kL"]) end it "for a unit value that should display in dL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 0.5 } expect(subject.scale_for_unit_value).to eq([0.1, "dL"]) end @@ -195,8 +193,8 @@ context "should not display in dL/cL if those units are not selected" do let(:available_units){ ["mL", "L", "kL", "gal"].join(",") } it "for a unit value that should display in mL" do - allow(product).to receive(:variant_unit) { "volume" } - allow(product).to receive(:variant_unit_scale) { 1.0 } + allow(variant).to receive(:variant_unit) { "volume" } + allow(variant).to receive(:variant_unit_scale) { 1.0 } allow(variant).to receive(:unit_value) { 0.5 } expect(subject.scale_for_unit_value).to eq([0.001, "mL"]) end @@ -205,8 +203,8 @@ context "items" do it "when scale is for items" do - allow(product).to receive(:variant_unit) { "items" } - allow(product).to receive(:variant_unit_scale) { nil } + allow(variant).to receive(:variant_unit) { "items" } + allow(variant).to receive(:variant_unit_scale) { nil } allow(variant).to receive(:unit_value) { 4 } expect(subject.scale_for_unit_value).to eq([nil, nil]) end diff --git a/spec/system/admin/bulk_product_update_spec.rb b/spec/system/admin/bulk_product_update_spec.rb index e78b5e35050..704cc30407f 100644 --- a/spec/system/admin/bulk_product_update_spec.rb +++ b/spec/system/admin/bulk_product_update_spec.rb @@ -72,19 +72,21 @@ end it "displays a select box for the unit of measure for the product's variants" do - p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1, - variant_unit_name: '') + create(:product, variant_unit: 'weight', variant_unit_scale: 1, + variant_unit_name: '') visit spree.admin_products_path + click_expand_all expect(page).to have_select "variant_unit_with_scale", selected: "Weight (g)" end it "displays a text field for the item name when unit is set to 'Items'" do - p = FactoryBot.create(:product, variant_unit: 'items', variant_unit_scale: nil, - variant_unit_name: 'packet') + create(:product, variant_unit: 'items', variant_unit_scale: nil, + variant_unit_name: 'packet') visit spree.admin_products_path + click_expand_all expect(page).to have_select "variant_unit_with_scale", selected: "Items" expect(page).to have_field "variant_unit_name", with: "packet" @@ -145,17 +147,15 @@ end it "displays a unit value field (for each variant) for each product" do - p1 = FactoryBot.create(:product, price: 2.0, variant_unit: "weight", - variant_unit_scale: "1000") - v1 = FactoryBot.create(:variant, product: p1, price: 12.75, - unit_value: 1200, unit_description: "(small bag)", - display_as: "bag") - v2 = FactoryBot.create(:variant, product: p1, price: 2.50, - unit_value: 4800, unit_description: "(large bag)", - display_as: "bin") + p1 = create(:product, price: 2.0, variant_unit: "weight", variant_unit_scale: "1000") + v1 = create(:variant, product: p1, price: 12.75, unit_value: 1200, variant_unit_scale: "1000", + unit_description: "(small bag)", display_as: "bag") + v2 = create(:variant, product: p1, price: 2.50, unit_value: 4800, variant_unit_scale: "1000", + unit_description: "(large bag)", display_as: "bin") visit spree.admin_products_path expect(page).to have_selector "a.view-variants", count: 1 + all("a.view-variants").each(&:click) expect(page).to have_field "variant_unit_value_with_description", with: "1.2 (small bag)" @@ -220,7 +220,7 @@ let!(:new_supplier) { create(:supplier_enterprise) } let!(:product) { create(:product, variant_unit: 'weight', variant_unit_scale: 1000, supplier_id: supplier.id) - } + } # Weight (kg) before do login_as_admin @@ -249,6 +249,7 @@ # When I fill out variant details and hit update select new_supplier.name, from: 'producer_id' + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_display_name", with: "Case of 12 Bottles" fill_in "variant_unit_value_with_description", with: "3 (12x250 mL bottles)" fill_in "variant_display_as", with: "Case" @@ -290,6 +291,7 @@ within "tr#v_-1" do select supplier.name, from: 'producer_id' + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120" fill_in "variant_price", with: "6.66" end @@ -304,6 +306,7 @@ end within "tr#v_-1" do + tomselect_select "Weight (g)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120g" fill_in "variant_price", with: "6.66" fill_in "variant_on_hand", with: "222" @@ -322,6 +325,7 @@ end within "tr#v_-1" do + tomselect_select "Weight (g)", from: "variant_unit_with_scale" fill_in "variant_unit_value_with_description", with: "120g" fill_in "variant_price", with: "6.66" check "variant_on_demand" @@ -335,12 +339,13 @@ end it "updating product attributes" do - s1 = FactoryBot.create(:supplier_enterprise) - s2 = FactoryBot.create(:supplier_enterprise) - t1 = FactoryBot.create(:taxon) - t2 = FactoryBot.create(:taxon) - p = FactoryBot.create(:product, supplier_id: s1.id, variant_unit: 'volume', - variant_unit_scale: 1, primary_taxon: t2, sku: "OLD SKU") + s1 = create(:supplier_enterprise) + create(:supplier_enterprise) + create(:taxon) + t2 = create(:taxon) + p = create(:product, supplier_id: s1.id, variant_unit: 'volume', variant_unit_scale: 1, + primary_taxon: t2, sku: "OLD SKU") + variant = p.variants.first login_as_admin visit spree.admin_products_path @@ -348,34 +353,43 @@ toggle_columns /^Category?/i, "Inherits Properties?", "SKU" within "tr#p_#{p.id}" do + page.find('a.view-variants').click + expect(page).to have_field "product_name", with: p.name - expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" expect(page).to have_checked_field "inherits_properties" expect(page).to have_field "product_sku", with: p.sku fill_in "product_name", with: "Big Bag Of Potatoes" - select "Weight (kg)", from: "variant_unit_with_scale" uncheck "inherits_properties" fill_in "product_sku", with: "NEW SKU" end + within "tr#v_#{variant.id}" do + expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)" + + tomselect_select "Weight (kg)", from: "variant_unit_with_scale" + end click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." p.reload expect(p.name).to eq "Big Bag Of Potatoes" - expect(p.variant_unit).to eq "weight" - expect(p.variant_unit_scale).to eq 1000 # Kg expect(p.inherits_properties).to be false expect(p.sku).to eq "NEW SKU" + + variant.reload + expect(variant.variant_unit).to eq "weight" + expect(variant.variant_unit_scale).to eq 1000 # Kg end it "updating a product with a variant unit of 'items'" do - p = FactoryBot.create(:product, variant_unit: 'weight', variant_unit_scale: 1000) + p = create(:product, variant_unit: 'weight', variant_unit_scale: 1000) login_as_admin visit spree.admin_products_path + page.find('a.view-variants').click + expect(page).to have_select "variant_unit_with_scale", selected: "Weight (kg)" select "Items", from: "variant_unit_with_scale" @@ -384,10 +398,10 @@ click_button 'Save Changes', match: :first expect(page.find("#status-message")).to have_content "Changes saved." - p.reload - expect(p.variant_unit).to eq "items" - expect(p.variant_unit_scale).to be_nil - expect(p.variant_unit_name).to eq "loaf" + variant = p.variants.first + expect(variant.variant_unit).to eq "items" + expect(variant.variant_unit_scale).to be_nil + expect(variant.variant_unit_name).to eq "loaf" end it "updating a product with variants" do @@ -853,14 +867,15 @@ expect(page).to have_field "product_name", with: p.name fill_in "product_name", with: "Big Bag Of Potatoes" - select "Weight (kg)", from: "variant_unit_with_scale" find("a.view-variants").click end within "#v_#{v.id}" do expect(page).to have_select "producer_id", selected: supplier_permitted.name + select supplier_managed2.name, from: 'producer_id' + select "Weight (kg)", from: "variant_unit_with_scale" fill_in "variant_price", with: "20" fill_in "variant_on_hand", with: "18" fill_in "variant_display_as", with: "Big Bag" @@ -872,8 +887,8 @@ p.reload v.reload expect(p.name).to eq "Big Bag Of Potatoes" - expect(p.variant_unit).to eq "weight" - expect(p.variant_unit_scale).to eq 1000 # Kg + expect(v.variant_unit).to eq "weight" + expect(v.variant_unit_scale).to eq 1000 # Kg expect(v.supplier).to eq supplier_managed2 expect(v.display_as).to eq "Big Bag" expect(v.price).to eq 20.0 diff --git a/spec/system/admin/invoice_print_spec.rb b/spec/system/admin/invoice_print_spec.rb index 3c9a6adb398..9b5375b4c63 100644 --- a/spec/system/admin/invoice_print_spec.rb +++ b/spec/system/admin/invoice_print_spec.rb @@ -303,7 +303,8 @@ context "Line item with variant having variant_unit as 'items'" do before do - line_item1.variant.update!(variant_unit: "items", display_as: "1 bucket") + line_item1.variant.update!(variant_unit: "items", display_as: "1 bucket", + variant_unit_name: "bucket") login_as_admin visit spree.print_admin_order_path(order1, params: url_params) convert_pdf_to_page diff --git a/spec/system/admin/product_import_spec.rb b/spec/system/admin/product_import_spec.rb index 49c90699180..a8095dc3383 100644 --- a/spec/system/admin/product_import_spec.rb +++ b/spec/system/admin/product_import_spec.rb @@ -704,8 +704,9 @@ expect(page).to have_input("[products][2][variants_attributes][0][display_name]", text: "Cupcake") - expect(page).to have_select "_products_2_variant_unit_with_scale", selected: "Items" - expect(page).to have_input("[products][2][variant_unit_name]", + expect(page).to have_select("_products_2_variants_attributes_0_variant_unit_with_scale", + selected: "Items") + expect(page).to have_input("[products][2][variants_attributes][0][variant_unit_name]", text: "Bunch") within(:xpath, '//*[@id="products-form"]/table/tbody[3]/tr[2]/td[7]') do expect(page).to have_content("5") diff --git a/spec/system/admin/products_spec.rb b/spec/system/admin/products_spec.rb index 37ad9c967cc..f2a1c322f37 100644 --- a/spec/system/admin/products_spec.rb +++ b/spec/system/admin/products_spec.rb @@ -50,8 +50,6 @@ expect(page.find("#product_description", visible: false).value).to eq('
A description...
') expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)' - - expect(page).to have_content "Name can't be blank" end it "display all attributes when submitting with error: Unit Value must be grater than 0" do @@ -111,21 +109,23 @@ expect(current_path).to eq spree.admin_products_path expect(flash_message).to eq('Product "A new product !!!" has been successfully created!') + product = Spree::Product.find_by(name: 'A new product !!!') - expect(product.variant_unit).to eq('weight') - expect(product.variant_unit_scale).to eq(1000) - expect(product.variants.first.unit_value).to eq(5000) - expect(product.variants.first.unit_description).to eq("") - expect(product.variant_unit_name).to eq("") - expect(product.variants.first.primary_taxon_id).to eq(taxon.id) - expect(product.variants.first.price.to_s).to eq('19.99') - expect(product.on_hand).to eq(5) - expect(product.variants.first.tax_category_id).to eq(tax_category.id) - expect(product.variants.first.shipping_category).to eq(shipping_category) + variant = product.variants.first + expect(product.description).to eq("
A description...
") expect(product.group_buy).to be_falsey - variant = product.variants.first + expect(variant.variant_unit).to eq('weight') + expect(variant.variant_unit_scale).to eq(1000) + expect(variant.unit_value).to eq(5000) + expect(variant.unit_description).to eq("") + expect(variant.variant_unit_name).to eq("") + expect(variant.primary_taxon_id).to eq(taxon.id) + expect(variant.price.to_s).to eq('19.99') + expect(variant.on_hand).to eq(5) + expect(variant.tax_category_id).to eq(tax_category.id) + expect(variant.shipping_category).to eq(shipping_category) expect(variant.unit_presentation).to eq("5kg") expect(variant.supplier).to eq(supplier) end @@ -167,7 +167,7 @@ click_button 'Create' expect(current_path).to eq spree.admin_products_path - expect(page).to have_content "Unit value is not a number" + expect(page).to have_content "Unit value can't be blank" end it "creating product with empty product category fails" do @@ -640,46 +640,5 @@ expect("#{uri.path}?#{uri.query}").to eq spree.admin_product_images_path(product, filter) end end - - context "editing a product's variant unit scale" do - let(:product) { create(:simple_product, name: 'a product', supplier_id: supplier2.id) } - - before do - allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL") - visit spree.edit_admin_product_path product - end - - shared_examples 'selecting a unit from dropdown' do |dropdown_option, - var_unit:, var_unit_scale:| - it 'checks if the dropdown selection is persistent' do - select dropdown_option, from: 'product_variant_unit_with_scale' - click_button 'Update' - expect(flash_message).to eq('Product "a product" has been successfully updated!') - product.reload - expect(product.variant_unit).to eq(var_unit) - expect(page).to have_select('product_variant_unit_with_scale', selected: dropdown_option) - expect(product.variant_unit_scale).to eq(var_unit_scale) - end - end - - describe 'a shared example' do - it_behaves_like 'selecting a unit from dropdown', 'Weight (g)', var_unit: 'weight', - var_unit_scale: 1 - it_behaves_like 'selecting a unit from dropdown', 'Weight (kg)', var_unit: 'weight', - var_unit_scale: 1000 - it_behaves_like 'selecting a unit from dropdown', 'Weight (T)', var_unit: 'weight', - var_unit_scale: 1_000_000 - it_behaves_like 'selecting a unit from dropdown', 'Weight (oz)', var_unit: 'weight', - var_unit_scale: 28.35 - it_behaves_like 'selecting a unit from dropdown', 'Weight (lb)', var_unit: 'weight', - var_unit_scale: 453.6 - it_behaves_like 'selecting a unit from dropdown', 'Volume (mL)', var_unit: 'volume', - var_unit_scale: 0.001 - it_behaves_like 'selecting a unit from dropdown', 'Volume (L)', var_unit: 'volume', - var_unit_scale: 1 - it_behaves_like 'selecting a unit from dropdown', 'Volume (kL)', var_unit: 'volume', - var_unit_scale: 1000 - end - end end end diff --git a/spec/system/admin/products_v3/actions_spec.rb b/spec/system/admin/products_v3/actions_spec.rb index 368f6d85950..9435ca128ab 100644 --- a/spec/system/admin/products_v3/actions_spec.rb +++ b/spec/system/admin/products_v3/actions_spec.rb @@ -111,13 +111,10 @@ def save_preferences let!(:variant_a1) { product_a.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5, - on_demand: false - } - } - let!(:product_a) { - create(:simple_product, name: "Apples", sku: "APL-00", - variant_unit: "weight", variant_unit_scale: 1) # Grams + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } + let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") } context "when they are under 11" do before do @@ -296,19 +293,20 @@ def save_preferences expect(input_content).to match /COPY OF Apples/ end end + end - it "shows error message when cloning invalid record" do - # Existing product is invalid: - product_a.update_columns(variant_unit: nil) + it "shows error message when cloning invalid record" do + # The cloned product will be invalid + product_a.update_columns(name: "L" * 254) - click_product_clone "Apples" + # The page has not been reloaded so the product's name is still "Apples" + click_product_clone "Apples" - expect(page).to have_content "Unit Scale can't be blank" + expect(page).to have_content "Product Name is too long (maximum is 255 characters)" - within "table.products" do - # Products does not include the cloned product. - expect(all_input_values).not_to match /COPY OF Apples/ - end + within "table.products" do + # Products does not include the cloned product. + expect(all_input_values).not_to match /COPY OF #{('L' * 254)}/ end end end diff --git a/spec/system/admin/products_v3/create_spec.rb b/spec/system/admin/products_v3/create_spec.rb index 9683ebe2a31..e767ab38911 100644 --- a/spec/system/admin/products_v3/create_spec.rb +++ b/spec/system/admin/products_v3/create_spec.rb @@ -38,7 +38,7 @@ end describe "creating new variants" do - let!(:product) { create(:product, variant_unit: 'weight', variant_unit_scale: 1000) } + let!(:product) { create(:product) } before { visit_products_page_as_admin } @@ -80,6 +80,7 @@ within page.all("tr.condensed")[1] do # selects second variant row find('input[id$="_sku"]').fill_in with: "345" find('input[id$="_display_name"]').fill_in with: "Small bag" + tomselect_select "Weight (g)", from: "Unit scale" find('button[id$="unit_to_display"]').click # opens the unit value pop out find('input[id$="_unit_value_with_description"]').fill_in with: "0.002" find('input[id$="_display_as"]').fill_in with: "2 grams" @@ -111,7 +112,9 @@ new_variant = Spree::Variant.where(deleted_at: nil).last expect(new_variant.sku).to eq "345" expect(new_variant.display_name).to eq "Small bag" - expect(new_variant.unit_value).to eq 2.0 + expect(new_variant.variant_unit).to eq "weight" + expect(new_variant.variant_unit_scale).to eq 1 # g + expect(new_variant.unit_value).to eq 0.002 expect(new_variant.display_as).to eq "2 grams" expect(new_variant.unit_presentation).to eq "2 grams" expect(new_variant.price).to eq 11.1 diff --git a/spec/system/admin/products_v3/update_spec.rb b/spec/system/admin/products_v3/update_spec.rb index 57245ecb6b4..df298457884 100644 --- a/spec/system/admin/products_v3/update_spec.rb +++ b/spec/system/admin/products_v3/update_spec.rb @@ -25,22 +25,20 @@ let!(:variant_a1) { product_a.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "APL-01", price: 5.25, on_hand: 5, - on_demand: false - } + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } let!(:product_a) { - create(:simple_product, name: "Apples", sku: "APL-00", - variant_unit: "weight", variant_unit_scale: 1) # Grams + create(:simple_product, name: "Apples", sku: "APL-00" ) } let(:variant_b1) { product_b.variants.first.tap{ |v| v.update! display_name: "Medium box", sku: "TMT-01", price: 5, on_hand: 5, - on_demand: false - } + on_demand: false, variant_unit: "weight", variant_unit_scale: 1 + } # Grams } let(:product_b) { - create(:simple_product, name: "Tomatoes", sku: "TMT-01", - variant_unit: "weight", variant_unit_scale: 1) # Grams + create(:simple_product, name: "Tomatoes", sku: "TMT-01") } before do visit admin_products_url @@ -50,31 +48,32 @@ within row_containing_name("Apples") do fill_in "Name", with: "Pommes" fill_in "SKU", with: "POM-00" - tomselect_select "Volume (mL)", from: "Unit scale" end - # Unit popout - click_on "Unit" # activate popout - # have to use below method to trigger the +change+ event, - # +fill_in "Unit value", with: ""+ does not trigger +change+ event - find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field - click_button "Save changes" # attempt to save and should fail with below error - expect(page).to have_content "must be greater than 0" - click_on "Unit" # activate popout - fill_in "Unit value", with: "500.1" - within row_containing_name("Medium box") do fill_in "Name", with: "Large box" fill_in "SKU", with: "POM-01" + + tomselect_select "Volume (mL)", from: "Unit scale" + + # Unit popout + click_on "Unit" # activate popout + # have to use below method to trigger the +change+ event, + # +fill_in "Unit value", with: ""+ does not trigger +change+ event + find_field('Unit value').send_keys(:control, 'a', :backspace) # empty the field + expect_browser_validation('input[aria-label="Unit value"]') + + fill_in "Unit value", with: "500.1" fill_in "Price", with: "10.25" + # Stock popout click_on "On Hand" # activate popout + fill_in "On Hand", with: "-1" end - # Stock popout - fill_in "On Hand", with: "-1" click_button "Save changes" # attempt to save or close the popout expect(page).to have_field "On Hand", with: "-1" # popout is still open + fill_in "On Hand", with: "6" expect { @@ -85,13 +84,13 @@ variant_a1.reload }.to change { product_a.name }.to("Pommes") .and change{ product_a.sku }.to("POM-00") - .and change{ product_a.variant_unit }.to("volume") - .and change{ product_a.variant_unit_scale }.to(0.001) .and change{ variant_a1.display_name }.to("Large box") .and change{ variant_a1.sku }.to("POM-01") .and change{ variant_a1.unit_value }.to(0.5001) # volumes are stored in litres .and change{ variant_a1.price }.to(10.25) .and change{ variant_a1.on_hand }.to(6) + .and change{ variant_a1.variant_unit }.to("volume") + .and change{ variant_a1.variant_unit_scale }.to(0.001) within row_containing_name("Pommes") do expect(page).to have_field "Name", with: "Pommes" @@ -130,11 +129,7 @@ it "saves unit values using the new scale" do within row_containing_name("Medium box") do expect(page).to have_button "Unit", text: "1g" - end - within row_containing_name("Apples") do tomselect_select "Weight (kg)", from: "Unit scale" - end - within row_containing_name("Medium box") do # New scale is visible immediately expect(page).to have_button "Unit", text: "1kg" end @@ -142,10 +137,10 @@ click_button "Save changes" expect(page).to have_content "Changes saved" - product_a.reload - expect(product_a.variant_unit).to eq "weight" - expect(product_a.variant_unit_scale).to eq 1000 # kg - expect(variant_a1.reload.unit_value).to eq 1000 # 1kg + variant_a1.reload + expect(variant_a1.variant_unit).to eq "weight" + expect(variant_a1.variant_unit_scale).to eq 1000 # kg + expect(variant_a1.unit_value).to eq 1000 # 1kg within row_containing_name("Medium box") do expect(page).to have_button "Unit", text: "1kg" @@ -153,7 +148,7 @@ end it "saves a custom item unit name" do - within row_containing_name("Apples") do + within row_containing_name("Medium box") do tomselect_select "Items", from: "Unit scale" fill_in "Items", with: "box" end @@ -162,9 +157,9 @@ click_button "Save changes" expect(page).to have_content "Changes saved" - product_a.reload - }.to change{ product_a.variant_unit }.to("items") - .and change{ product_a.variant_unit_name }.to("box") + variant_a1.reload + }.to change{ variant_a1.variant_unit }.to("items") + .and change{ variant_a1.variant_unit_name }.to("box") within row_containing_name("Apples") do pending "#12005" @@ -344,8 +339,9 @@ fill_in "Name", with: "Large box" fill_in "SKU", with: "APL-02" + tomselect_select("Weight (kg)", from: "Unit scale") click_on "Unit" # activate popout - fill_in "Unit value", with: "1000" + fill_in "Unit value", with: "1" fill_in "Price", with: 10.25 @@ -367,7 +363,9 @@ expect(new_variant.display_name).to eq "Large box" expect(new_variant.sku).to eq "APL-02" expect(new_variant.price).to eq 10.25 - expect(new_variant.unit_value).to eq 1000 + expect(new_variant.variant_unit).to eq "weight" + expect(new_variant.unit_value).to eq 1 * 1000 + expect(new_variant.variant_unit_scale).to eq 1000 expect(new_variant.on_hand).to eq 3 expect(new_variant.tax_category_id).to be_nil @@ -486,11 +484,12 @@ end context "with invalid data" do + let(:new_variant_row) { find_field("Name", placeholder: "Apples", with: "").ancestor("tr") } + before do click_on "New variant" # find empty row for Apples - new_variant_row = find_field("Name", placeholder: "Apples", with: "").ancestor("tr") expect(new_variant_row).to be_present within new_variant_row do @@ -509,6 +508,30 @@ fill_in "Price", with: "10.25" end + # Client side validation + click_button "Save changes" + within new_variant_row do + expect_browser_validation('select[aria-label="Unit scale"]') + end + + # Fix error + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + end + + # Client side validation + click_button "Save changes" + within new_variant_row do + # In CI we get "Please fill out this field." and locally we get + # "Please fill in this field." + expect_browser_validation('input[aria-label="Unit value"]') + end + + # Fix error + within new_variant_row do + fill_in "Unit value", with: "200" + end + expect { click_button "Save changes" @@ -524,8 +547,6 @@ expect(page).to have_content "is too long" expect(page.find('.col-producer')).to have_content('must exist') expect(page.find('.col-category')).to have_content('must exist') - expect(page.find_button("Unit")).to have_text "" # have_button selector don't work here - expect(page).to have_content "can't be blank" expect(page).to have_field "Price", with: "10.25" # other updated value is retained end @@ -538,6 +559,13 @@ end it "saves changes after fixing errors" do + # Fill value to satisfy client side validation + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + click_on "Unit" # activate popout + fill_in "Unit value", with: "200" + end + expect { click_button "Save changes" @@ -548,9 +576,6 @@ fill_in "Name", with: "Nice box" fill_in "SKU", with: "APL-02" - click_on "Unit" # activate popout - fill_in "Unit value", with: "200" - select producer.name, from: 'Producer' select taxon.name, from: 'Category' end @@ -566,10 +591,18 @@ expect(new_variant.display_name).to eq "Nice box" expect(new_variant.sku).to eq "APL-02" expect(new_variant.price).to eq 10.25 - expect(new_variant.unit_value).to eq 200 + expect(new_variant.variant_unit_scale).to eq 1000 + expect(new_variant.unit_value).to eq 200 * 1000 end it "removes unsaved record" do + # Fill value to satisfy client side validation + within new_variant_row do + tomselect_select("Weight (kg)", from: "Unit scale") + click_on "Unit" # activate popout + fill_in "Unit value", with: "200" + end + click_button "Save changes" expect(page).to have_text("1 product could not be saved.") @@ -707,4 +740,11 @@ include_examples "updating image" end end + + # Check a validation message is set, we don't check the message itself because the value is based + # on the browser's locale. + def expect_browser_validation(selector) + browser_message = page.find(selector)["validationMessage"] + expect(browser_message).to be_present + end end diff --git a/spec/system/admin/unit_price_spec.rb b/spec/system/admin/unit_price_spec.rb index f8682f12b67..6375af57e87 100644 --- a/spec/system/admin/unit_price_spec.rb +++ b/spec/system/admin/unit_price_spec.rb @@ -30,7 +30,10 @@ login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'Weight (g)', with: '1' + + tomselect_select "Weight (g)", from: "Unit scale" + click_on "Unit" # activate popout + fill_in "Unit value", with: "1" fill_in 'Price', with: '1' expect(find_field("Unit Price", disabled: true).value).to eq '$1,000.00 / kg' @@ -66,7 +69,10 @@ visit spree.admin_dashboard_path(locale: 'es') visit spree.admin_product_variants_path product click_link 'Nueva Variante' - fill_in 'Peso (g)', with: '1' + + tomselect_select "Peso (g)", from: "Unit scale" + click_on "Unit" # activate popout + fill_in "Unit value", with: "1" fill_in 'Precio', with: '1,5' expect(find_field("Precio por unidad", disabled: true).value).to eq '1.500,00 $ / kg' diff --git a/spec/system/admin/variants_spec.rb b/spec/system/admin/variants_spec.rb index 7de52b82721..2f6280f7783 100644 --- a/spec/system/admin/variants_spec.rb +++ b/spec/system/admin/variants_spec.rb @@ -13,27 +13,39 @@ describe "new variant" do it "creating a new variant" do - # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + # Given a product + product = create(:simple_product) # When I create a variant on the product login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'unit_value_human', with: '1' - fill_in 'variant_unit_description', with: 'foo' + tomselect_select("Volume (L)", from: "Unit scale") + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "1" + fill_in 'Price', with: 2.5 select taxon.name, from: "variant_primary_taxon_id" select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + click_button 'Create' # Then the variant should have been created expect(page).to have_content "Variant \"#{product.name}\" has been successfully created!" + + new_variant = Spree::Variant.last + expect(new_variant.unit_value).to eq(1) + expect(new_variant.variant_unit).to eq("volume") + expect(new_variant.variant_unit_scale).to eq(1) # Liter + expect(new_variant.price).to eq(2.5) + expect(new_variant.primary_taxon).to eq(taxon) + expect(new_variant.supplier).to eq(product.variants.first.supplier) end it "creating a new variant from product variant page with filter" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -54,7 +66,7 @@ it "creating a new variant with non-weight unit type" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "volume", variant_unit_scale: "1") + product = create(:simple_product) # When I create a variant on the product login_as_admin @@ -62,11 +74,16 @@ click_link 'New Variant' - # Expect variant_weight to accept 3 decimal places - fill_in 'variant_weight', with: '1.234' - fill_in 'unit_value_human', with: 1 + tomselect_select("Volume (L)", from: "Unit scale") + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "1" + fill_in 'Price', with: 2.5 select taxon.name, from: "variant_primary_taxon_id" select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + + # Expect variant_weight to accept 3 decimal places + fill_in 'variant_weight', with: '1.234' click_button 'Create' # Then the variant should have been created @@ -74,12 +91,20 @@ end it "show validation errors if present" do - product = create(:simple_product, variant_unit: "volume", variant_unit_scale: "1") + product = create(:simple_product) login_as_admin visit spree.admin_product_variants_path product click_link 'New Variant' - fill_in 'unit_value_human', with: 0 + tomselect_select("Volume (L)", from: "Unit scale") + fill_in 'Price', with: 2.5 + select taxon.name, from: "variant_primary_taxon_id" + select2_select product.variants.first.supplier.name, from: "variant_supplier_id" + + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "0" + # fill_in 'unit_value_human', with: 0 click_button 'Create' expect(page).to have_content "Unit value must be greater than 0" @@ -89,7 +114,7 @@ describe "viewing product variant" do it "when the product page has a product filter" do # Given a product with a unit-related option type - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -123,7 +148,7 @@ describe "editing unit value and description for a variant" do it "when the product variant page has product filter" do - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) filter = { producerFilter: 2 } # When I create a variant on the product @@ -140,9 +165,10 @@ it "when variant_unit is weight" do # Given a product with unit-related option types, with a variant - product = create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") + product = create(:simple_product) variant = product.variants.first - variant.update( unit_value: 1, unit_description: 'foo' ) + variant.update( unit_value: 1, unit_description: 'foo', variant_unit: "weight", + variant_unit_scale: "1") # When I view the variant login_as_admin @@ -151,41 +177,47 @@ page.find('table.index .icon-edit').click # And I should see unit value and description fields for the unit-related option value - expect(page).to have_field "unit_value_human", with: "1" - expect(page).to have_field "variant_unit_description", with: "foo" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value", with: "1 foo" # When I update the fields and save the variant - fill_in "unit_value_human", with: "123" - fill_in "variant_unit_description", with: "bar" + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "123 bar" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the unit value and description should have been saved - expect(variant.reload.unit_value).to eq(123) + variant.reload + expect(variant.unit_value).to eq(123) expect(variant.unit_description).to eq('bar') end it "can update unit_description when variant_unit is items" do - product = create(:simple_product, variant_unit: "items", variant_unit_name: "bunches") + product = create(:simple_product) variant = product.variants.first - variant.update(unit_description: 'foo') + variant.update(unit_description: 'foo', variant_unit: "items", variant_unit_name: "bunches") login_as_admin visit spree.edit_admin_product_variant_path(product, variant) - expect(page).not_to have_field "unit_value_human" expect(page).to have_field "variant_weight" - expect(page).to have_field "variant_unit_description", with: "foo" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value", with: "1 foo" - fill_in "variant_unit_description", with: "bar" + click_on "Unit" # activate popout + # Unit popout + fill_in "Unit value", with: "123 bar" fill_in "variant_weight", with: "1.234" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) expect(variant.reload.unit_description).to eq('bar') end context "with ES as a locale" do - let(:product) { create(:simple_product, variant_unit: "weight", variant_unit_scale: "1") } + let(:product) { create(:simple_product) } let(:variant) { product.variants.first } around do |example| @@ -318,7 +350,7 @@ end describe "editing variant attributes" do - let!(:variant) { create(:variant) } + let!(:variant) { create(:variant, variant_unit: "weight", variant_unit_scale: "1") } let(:product) { variant.product } let!(:tax_category) { create(:tax_category) } @@ -330,34 +362,34 @@ it "editing display name for a variant" do # It should allow the display name to be changed expect(page).to have_field "variant_display_name" + click_on "Unit" # activate popout expect(page).to have_field "variant_display_as" # When I update the fields and save the variant fill_in "variant_display_name", with: "Display Name" + click_on "Unit" # activate popout fill_in "variant_display_as", with: "Display As This" + click_button 'Update' expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the displayed values should have been saved - expect(variant.reload.display_name).to eq("Display Name") + variant.reload + expect(variant.display_name).to eq("Display Name") expect(variant.display_as).to eq("Display As This") end it "editing weight for a variant" do # It should allow the weight to be changed - expect(page).to have_field "unit_value_human" + click_on "Unit" # activate popout + expect(page).to have_field "Unit value" # When I update the fields and save the variant with invalid value - fill_in "unit_value_human", with: "1.234" - click_button 'Update' - expect(page).not_to have_content %(Variant "#{product.name}" has been successfully updated!) - - fill_in "unit_value_human", with: "1.23" + fill_in "Unit value", with: "1.234" click_button 'Update' - expect(page).to have_content %(Variant "#{product.name}" has been successfully updated!) # Then the displayed values should have been saved - expect(variant.reload.unit_value).to eq(1.23) + expect(variant.reload.unit_value).to eq(1.234) end context "editing variant tax category" do