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 ed089f5c348..80b5a5e90b3 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" @@ -4639,6 +4639,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using display_name: "Display Name" display_as_placeholder: 'eg. 2 kg' display_name_placeholder: 'eg. Tomatoes' + unit_scale: "Unit scale" + unit: Unit + price: Price + unit_value: Unit value + variant_category: Category autocomplete: out_of_stock: "Out of Stock" producer_name: "Producer" 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 = ` -