Merge pull request #12787 from rioug/move-variant-unit-attributes-to-variant

[Product Refactor] Move variant unit sizes to variant
This commit is contained in:
Filipe
2024-10-15 19:58:45 +01:00
committed by GitHub
110 changed files with 2875 additions and 1902 deletions

View File

@@ -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/

View File

@@ -187,9 +187,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
product.variants.length > 0
$scope.hasUnit = (product) ->
product.variant_unit_with_scale?
$scope.hasUnit = (variant) ->
variant.variant_unit_with_scale?
$scope.variantSaved = (variant) ->
variant.hasOwnProperty('id') && variant.id > 0
@@ -242,32 +241,28 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$window.location = destination
$scope.packProduct = (product) ->
if product.variant_unit_with_scale
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
product.variant_unit = match[1]
product.variant_unit_scale = parseFloat(match[2])
else
product.variant_unit = product.variant_unit_with_scale
product.variant_unit_scale = null
else
product.variant_unit = product.variant_unit_scale = null
if product.variants
for id, variant of product.variants
$scope.packVariant product, variant
$scope.packVariant variant
$scope.packVariant = (product, variant) ->
$scope.packVariant = (variant) ->
if variant.variant_unit_with_scale
match = variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
variant.variant_unit = match[1]
variant.variant_unit_scale = parseFloat(match[2])
else
variant.variant_unit = variant.variant_unit_with_scale
variant.variant_unit_scale = null
if variant.hasOwnProperty("unit_value_with_description")
match = variant.unit_value_with_description.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/)
if match
product = BulkProducts.find product.id
variant.unit_value = parseFloat(match[1].replace(",", "."))
variant.unit_value = null if isNaN(variant.unit_value)
if variant.unit_value && product.variant_unit_scale
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2))
if variant.unit_value && variant.variant_unit_scale
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, variant.variant_unit_scale, 2))
variant.unit_description = match[3]
$scope.incrementLimit = ->
@@ -321,13 +316,6 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("price")
filteredProduct.price = product.price
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_with_scale")
filteredProduct.variant_unit = product.variant_unit
filteredProduct.variant_unit_scale = product.variant_unit_scale
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_name")
filteredProduct.variant_unit_name = product.variant_unit_name
hasUpdatableProperty = true
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
@@ -383,6 +371,14 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("producer_id")
filteredVariant.supplier_id = variant.producer_id
hasUpdatableProperty = true
if variant.hasOwnProperty("variant_unit_with_scale")
filteredVariant.variant_unit = variant.variant_unit
filteredVariant.variant_unit_scale = variant.variant_unit_scale
hasUpdatableProperty = true
if variant.hasOwnProperty("variant_unit_name")
filteredVariant.variant_unit_name = variant.variant_unit_name
hasUpdatableProperty = true
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}

View File

@@ -4,31 +4,30 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
scope.$watchCollection ->
return [
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
scope.product.variant_unit_name
scope.product.variant_unit_with_scale
scope.variant.variant_unit_name
scope.variant.variant_unit_with_scale
]
, ->
[variant_unit, variant_unit_scale] = productUnitProperties()
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
variant_object =
variant_object =
unit_value: unit_value
unit_description: unit_description
product:
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.product.variant_unit_name
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.variant.variant_unit_name
scope.placeholder_text = new OptionValueNamer(variant_object).name()
productUnitProperties = ->
# get relevant product properties
if scope.product.variant_unit_with_scale?
match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if scope.variant.variant_unit_with_scale?
match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
variant_unit = match[1]
variant_unit_scale = parseFloat(match[2])
else
variant_unit = scope.product.variant_unit_with_scale
variant_unit = scope.variant.variant_unit_with_scale
variant_unit_scale = null
else
variant_unit = variant_unit_scale = null
@@ -45,4 +44,4 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
unit_value = null if isNaN(unit_value)
unit_value *= variant_unit_scale if unit_value && variant_unit_scale
unit_description = match[3]
[unit_value, unit_description]
[unit_value, unit_description]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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)]

View File

@@ -19,7 +19,7 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
for server_product in serverProducts
product = @findProductInList(server_product.id, @products)
product.variants = server_product.variants
@loadVariantUnitValues product
@loadVariantUnitValues product.variants
find: (id) ->
@findProductInList id, @products
@@ -38,34 +38,32 @@ angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetche
@products.splice(index + 1, 0, newProduct)
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product
loadVariantUnit: (product) ->
product.variant_unit_with_scale =
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
"#{product.variant_unit}_#{product.variant_unit_scale}"
else if product.variant_unit
product.variant_unit
@loadVariantUnitValues product.variants if product.variants
loadVariantUnitValues: (variants) ->
for variant in variants
@loadVariantUnitValue variant
loadVariantUnitValue: (variant) ->
variant.variant_unit_with_scale =
if variant.variant_unit && variant.variant_unit_scale && variant.variant_unit != 'items'
"#{variant.variant_unit}_#{variant.variant_unit_scale}"
else if variant.variant_unit
variant.variant_unit
else
null
@loadVariantUnitValues product if product.variants
@loadVariantUnitValue product, product.master if product.master
loadVariantUnitValues: (product) ->
for variant in product.variants
@loadVariantUnitValue product, variant
loadVariantUnitValue: (product, variant) ->
unit_value = @variantUnitValue product, variant
unit_value = @variantUnitValue variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
variantUnitValue: (product, variant) ->
variantUnitValue: (variant) ->
if variant.unit_value?
if product.variant_unit_scale
variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale
if variant.variant_unit_scale
variant_unit_value = @divideAsInteger variant.unit_value, variant.variant_unit_scale
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
else
variant.unit_value

View File

@@ -21,8 +21,7 @@ module Admin
@importer = ProductImport::ProductImporter.new(File.new(@filepath), spree_current_user,
params[:settings])
@original_filename = params[:file].try(:original_filename)
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_fields
@non_updatable_fields = ProductImport::EntryValidator.non_updatable_variant_fields
return if contains_errors? @importer
@ams_data = ams_data

View File

@@ -21,7 +21,7 @@ module Api
authorize! :create, Spree::Product
@product = Spree::Product.new(product_params)
if @product.save
if @product.save(context: :create_and_create_standard_variant)
render json: @product, serializer: Api::Admin::ProductSerializer, status: :created
else
invalid_resource!(@product)

View File

@@ -39,7 +39,7 @@ module Spree
def create
delete_stock_params_and_set_after do
@object.attributes = permitted_resource_params
if @object.save
if @object.save(context: :create_and_create_standard_variant)
flash[:success] = flash_message_for(@object, :successfully_created)
redirect_after_save
else

View File

@@ -5,6 +5,8 @@ require 'open_food_network/scope_variants_for_search'
module Spree
module Admin
class VariantsController < ::Admin::ResourceController
helper ::Admin::ProductsHelper
belongs_to 'spree/product'
before_action :load_data, only: [:new, :edit]

View File

@@ -18,17 +18,15 @@ module Admin
end
def unit_value_with_description(variant)
precised_unit_value = nil
return variant.unit_description.to_s if variant.unit_value.nil?
if variant.unit_value
scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1)
precised_unit_value = number_with_precision(
scaled_unit_value,
precision: nil,
strip_insignificant_zeros: true,
significant: false,
)
end
scaled_unit_value = variant.unit_value / (variant.variant_unit_scale || 1)
precised_unit_value = number_with_precision(
scaled_unit_value,
precision: nil,
strip_insignificant_zeros: true,
significant: false,
)
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
end

View File

@@ -224,6 +224,9 @@ module ProductImport
# Ensure attributes are correctly copied to a new product's variant
variant = product.variants.first
variant.display_name = entry.display_name if entry.display_name
variant.variant_unit = entry.variant_unit if entry.variant_unit
variant.variant_unit_name = entry.variant_unit_name if entry.variant_unit_name
variant.variant_unit_scale = entry.variant_unit_scale if entry.variant_unit_scale
variant.import_date = @import_time
variant.supplier_id = entry.producer_id
variant.save

View File

@@ -6,8 +6,6 @@
module ProductImport
class EntryValidator
SKIP_VALIDATE_ON_UPDATE = [:description].freeze
# rubocop:disable Metrics/ParameterLists
def initialize(current_user, import_time, spreadsheet_data, editable_enterprises,
inventory_permissions, reset_counts, import_settings, all_entries)
@@ -22,9 +20,8 @@ module ProductImport
end
# rubocop:enable Metrics/ParameterLists
def self.non_updatable_fields
def self.non_updatable_variant_fields
{
description: :description,
unit_type: :variant_unit_scale,
variant_unit_name: :variant_unit_name,
}
@@ -67,8 +64,7 @@ module ProductImport
def mark_as_new_variant(entry, product_id)
variant_attributes = entry.assignable_attributes.except(
'id', 'product_id', 'on_hand', 'on_demand', 'variant_unit', 'variant_unit_name',
'variant_unit_scale'
'id', 'product_id', 'on_hand', 'on_demand'
)
# Variant needs a product. Product needs to be assigned first in order for
# delegate to work. name= will fail otherwise.
@@ -297,11 +293,11 @@ module ProductImport
end
products.flat_map(&:variants).each do |existing_variant|
unit_scale = existing_variant.product.variant_unit_scale
unit_scale = existing_variant.variant_unit_scale
unscaled_units = entry.unscaled_units.to_f || 0
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
if entry_matches_existing_variant?(entry, existing_variant)
if entry.match_inventory_variant?(existing_variant)
variant_override = create_inventory_item(entry, existing_variant)
return validate_inventory_item(entry, variant_override)
end
@@ -311,17 +307,6 @@ module ProductImport
error: I18n.t('admin.product_import.model.not_found'))
end
def entry_matches_existing_variant?(entry, existing_variant)
display_name_are_the_same?(entry, existing_variant) &&
existing_variant.unit_value == entry.unit_value.to_f
end
def display_name_are_the_same?(entry, existing_variant)
return true if entry.display_name.blank? && existing_variant.display_name.blank?
existing_variant.display_name == entry.display_name
end
def category_validation(entry)
category_name = entry.category
@@ -364,13 +349,13 @@ module ProductImport
return
end
products.each { |product| product_field_errors(entry, product) }
products.flat_map(&:variants).each do |existing_variant|
if entry_matches_existing_variant?(entry, existing_variant) &&
existing_variant.deleted_at.nil?
return mark_as_existing_variant(entry, existing_variant)
end
next unless entry.match_variant?(existing_variant) &&
existing_variant.deleted_at.nil?
variant_field_errors(entry, existing_variant)
return mark_as_existing_variant(entry, existing_variant)
end
mark_as_new_variant(entry, products.first.id)
@@ -392,8 +377,7 @@ module ProductImport
def mark_as_existing_variant(entry, existing_variant)
existing_variant.assign_attributes(
entry.assignable_attributes.except('id', 'product_id', 'variant_unit', 'variant_unit_name',
'variant_unit_scale')
entry.assignable_attributes.except('id', 'product_id')
)
check_on_hand_nil(entry, existing_variant)
@@ -406,11 +390,10 @@ module ProductImport
end
end
def product_field_errors(entry, existing_product)
EntryValidator.non_updatable_fields.each do |display_name, attribute|
next if attributes_match?(attribute, existing_product, entry) ||
attributes_blank?(attribute, existing_product, entry)
next if ignore_when_updating_product?(attribute)
def variant_field_errors(entry, existing_variant)
EntryValidator.non_updatable_variant_fields.each do |display_name, attribute|
next if attributes_match?(attribute, existing_variant, entry) ||
attributes_blank?(attribute, existing_variant, entry)
mark_as_invalid(entry, attribute: display_name,
error: I18n.t('admin.product_import.model.not_updatable'))
@@ -423,10 +406,6 @@ module ProductImport
existing_product_value == convert_to_trusted_type(entry_value, existing_product_value)
end
def ignore_when_updating_product?(attribute)
SKIP_VALIDATE_ON_UPDATE.include? attribute
end
def convert_to_trusted_type(untrusted_attribute, trusted_attribute)
case trusted_attribute
when Integer

View File

@@ -84,6 +84,14 @@ module ProductImport
invalid_attrs.except(* NON_PRODUCT_ATTRIBUTES, *NON_DISPLAY_ATTRIBUTES)
end
def match_variant?(variant)
match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d
end
def match_inventory_variant?(variant)
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
end
private
def remove_empty_skus(attrs)
@@ -99,5 +107,11 @@ module ProductImport
end
end
end
def match_display_name?(variant)
return true if display_name.blank? && variant.display_name.blank?
variant.display_name == display_name
end
end
end

View File

@@ -45,7 +45,8 @@ module Spree
after_destroy :update_order
after_save :update_order
delegate :product, :variant_unit, :unit_description, :display_name, :display_as, to: :variant
delegate :product, :variant_unit, :unit_description, :display_name, :display_as,
:variant_unit_scale, :variant_unit_name, to: :variant
# Allows manual skipping of Stock::AvailabilityValidator
attr_accessor :skip_stock_check, :target_shipment

View File

@@ -38,6 +38,7 @@ module Spree
# strips all non-price-like characters from the price, taking into account locale settings
def parse_price(price)
return nil if price.blank?
return price unless price.is_a?(String)
separator, _delimiter = I18n.t([:'number.currency.format.separator',

View File

@@ -22,7 +22,12 @@ module Spree
include LogDestroyPerformer
self.belongs_to_required_by_default = false
self.ignored_columns += [:supplier_id]
# These columns have been moved to variant. Currently this is only for documentation purposes,
# because they are declared as attr_accessor below, declaring them as ignored columns has no
# effect
self.ignored_columns += [
:supplier_id, :primary_taxon_id, :variant_unit, :variant_unit_scale, :variant_unit_name
]
acts_as_paranoid
@@ -45,20 +50,30 @@ module Spree
validates_lengths_from_database
validates :name, presence: true
validates :variant_unit, presence: true
validates :unit_value, numericality: {
greater_than: 0,
if: ->(p) { p.variant_unit.in?(%w(weight volume)) && new_record? }
}
validates :variant_unit_scale,
presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } }
validates :variant_unit_name,
presence: { if: ->(p) { p.variant_unit == 'items' } }
validate :validate_image
validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } }
accepts_nested_attributes_for :variants, allow_destroy: true
# These validators are used to make sure the standard variant created via
# `ensure_standard_variant` will be valid. The are only used when creating a new product
with_options on: :create_and_create_standard_variant do
validates :supplier_id, presence: true
validates :primary_taxon_id, presence: true
validates :variant_unit, presence: true
validates :unit_value, presence: true, if: ->(product) {
%w(weight volume).include?(product.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
validates :unit_description, presence: true, if: ->(product) {
product.variant_unit.present? && product.unit_value.nil?
}
validates :variant_unit_scale, presence: true, if: ->(product) {
%w(weight volume).include?(product.variant_unit)
}
validates :variant_unit_name, presence: true, if: ->(product) {
product.variant_unit == 'items'
}
end
accepts_nested_attributes_for :image
accepts_nested_attributes_for :product_properties,
allow_destroy: true,
@@ -66,14 +81,12 @@ module Spree
# Transient attributes used temporarily when creating a new product,
# these values are persisted on the product's variant
attr_accessor :price, :display_as, :unit_value, :unit_description, :tax_category_id,
:shipping_category_id, :primary_taxon_id, :supplier_id
attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit,
:variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id,
:primary_taxon_id, :supplier_id
after_validation :validate_variant_attrs, on: :create
after_create :ensure_standard_variant
after_update :touch_supplier, if: :saved_change_to_primary_taxon_id?
around_destroy :destruction
after_save :update_units
after_touch :touch_supplier
# -- Scopes
@@ -245,6 +258,7 @@ module Spree
end
end
# rubocop:disable Metrics/AbcSize
def ensure_standard_variant
return unless variants.empty?
@@ -254,31 +268,16 @@ module Spree
variant.display_as = display_as
variant.unit_value = unit_value
variant.unit_description = unit_description
variant.variant_unit = variant_unit
variant.variant_unit_name = variant_unit_name
variant.variant_unit_scale = variant_unit_scale
variant.tax_category_id = tax_category_id
variant.shipping_category_id = shipping_category_id
variant.primary_taxon_id = primary_taxon_id
variant.supplier_id = supplier_id
variants << variant
end
# Format as per WeightsAndMeasures (todo: re-orgnaise maybe after product/variant refactor)
def variant_unit_with_scale
# Our code is based upon English based number formatting with a period `.`
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true,
locale: :en)
[variant_unit, scale_clean].compact_blank.join("_")
end
def variant_unit_with_scale=(variant_unit_with_scale)
values = variant_unit_with_scale.split("_")
assign_attributes(
variant_unit: values[0],
variant_unit_scale: values[1] || nil
)
end
# rubocop:enable Metrics/AbcSize
# Remove any unsupported HTML.
def description
@@ -292,27 +291,6 @@ module Spree
private
def validate_variant_attrs
# Avoid running validation when we can't set variant attrs
# eg clone product. Will raise error if clonning a product with no variant
return if variants.first&.valid?
errors.add(:primary_taxon_id, :blank) unless Spree::Taxon.find_by(id: primary_taxon_id)
errors.add(:supplier_id, :blank) unless Enterprise.find_by(id: supplier_id)
end
def update_units
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
variants.each do |v|
if v.persisted?
v.update_units
else
v.assign_units
end
end
end
def touch_supplier
return if variants.empty?

View File

@@ -71,21 +71,25 @@ module Spree
validates :tax_category, presence: true,
if: proc { Spree::Config.products_require_tax_category }
validates :variant_unit, presence: true
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product&.variant_unit)
%w(weight volume).include?(variant.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
validates :price, numericality: { greater_than_or_equal_to: 0 }
validates :unit_description, presence: true, if: ->(variant) {
variant.product&.variant_unit.present? && variant.unit_value.nil?
variant.variant_unit.present? && variant.unit_value.nil?
}
validates :variant_unit_scale, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.variant_unit)
}
validates :variant_unit_name, presence: true, if: ->(variant) {
variant.variant_unit == 'items'
}
before_validation :set_cost_currency
before_validation :ensure_shipping_category
before_validation :ensure_unit_value
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
before_validation :update_weight_from_unit_value
before_validation :convert_variant_weight_to_decimal
before_save :assign_units, if: ->(variant) {
@@ -95,6 +99,9 @@ module Spree
after_create :create_stock_items
around_destroy :destruction
after_save :save_default_price
after_save :update_units, if: -> {
saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
}
# default variant scope only lists non-deleted variants
scope :deleted, -> { where.not(deleted_at: nil) }
@@ -219,6 +226,25 @@ module Spree
Spree::Stock::Quantifier.new(self).total_on_hand
end
# Format as per WeightsAndMeasures
def variant_unit_with_scale
# Our code is based upon English based number formatting with a period `.`
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true,
locale: :en)
[variant_unit, scale_clean].compact_blank.join("_")
end
def variant_unit_with_scale=(variant_unit_with_scale)
values = variant_unit_with_scale.split("_")
assign_attributes(
variant_unit: values[0],
variant_unit_scale: values[1] || nil
)
end
private
def check_currency
@@ -248,7 +274,7 @@ module Spree
end
def update_weight_from_unit_value
return unless product.variant_unit == 'weight' && unit_value.present?
return unless variant_unit == 'weight' && unit_value.present?
self.weight = weight_from_unit_value
end
@@ -268,7 +294,7 @@ module Spree
def ensure_unit_value
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
return unless (product&.variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
self.unit_value = 1.0
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -6,7 +6,8 @@ module Api
attributes :id, :name, :producer_name, :image, :sku, :import_date, :tax_category_id,
:options_text, :unit_value, :unit_description, :unit_to_display,
:display_as, :display_name, :name_to_display, :variant_overrides_count,
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name,
:variant_unit, :variant_unit_scale, :variant_unit_name, :variant_unit_with_scale
has_one :primary_taxon, key: :category_id, embed: :id
has_one :supplier, key: :producer_id, embed: :id

View File

@@ -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

View File

@@ -3,12 +3,11 @@
class UnitPrice
def initialize(variant)
@variant = variant
@product = variant.product
end
def denominator
# catches any case where unit is not kg, lb, or L.
return @variant.unit_value if @product&.variant_unit == "items"
return @variant.unit_value if @variant.variant_unit == "items"
case unit
when "lb"
@@ -23,13 +22,13 @@ class UnitPrice
def unit
return "lb" if WeightsAndMeasures.new(@variant).system == "imperial"
case @product&.variant_unit
case @variant.variant_unit
when "weight"
"kg"
when "volume"
"L"
else
@product.variant_unit_name.presence || I18n.t("item")
@variant.variant_unit_name.presence || I18n.t("item")
end
end
end

View File

@@ -32,16 +32,16 @@ module VariantUnits
private
def value_scaled?
@nameable.product.variant_unit_scale.present?
@nameable.variant_unit_scale.present?
end
def option_value_value_unit
if @nameable.unit_value.present? && @nameable.product&.persisted?
if %w(weight volume).include? @nameable.product.variant_unit
if @nameable.unit_value.present?
if %w(weight volume).include? @nameable.variant_unit
value, unit_name = option_value_value_unit_scaled
else
value = @nameable.unit_value
unit_name = pluralize(@nameable.product.variant_unit_name, value)
unit_name = pluralize(@nameable.variant_unit_name, value)
end
value = value.to_i if value == value.to_i

View File

@@ -64,12 +64,12 @@ module VariantUnits
def unit_value_attributes
units = { unit_presentation: option_value_name }
units.merge!(variant_unit: product.variant_unit) if has_attribute?(:variant_unit)
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
units
end
def weight_from_unit_value
(unit_value || 0) / 1000 if product.variant_unit == 'weight'
(unit_value || 0) / 1000 if variant_unit == 'weight'
end
private

View File

@@ -16,10 +16,10 @@ class WeightsAndMeasures
def system
return "custom" unless scales = scales_for_variant_unit(ignore_available_units: true)
product_scale = @variant.product.variant_unit_scale&.to_f
return "custom" unless product_scale.present? && product_scale.positive?
variant_scale = @variant.variant_unit_scale&.to_f
return "custom" unless variant_scale.present? && variant_scale.positive?
scales[product_scale]['system']
scales[variant_scale]['system']
end
# @returns enumerable with label and value for select
@@ -92,9 +92,9 @@ class WeightsAndMeasures
}.freeze
def scales_for_variant_unit(ignore_available_units: false)
return @units[@variant.product.variant_unit] if ignore_available_units
return @units[@variant.variant_unit] if ignore_available_units
@units[@variant.product.variant_unit]&.reject { |_scale, unit_info|
@units[@variant.variant_unit]&.reject { |_scale, unit_info|
self.class.available_units.exclude?(unit_info['name'])
}
end

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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,

View File

@@ -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")

View File

@@ -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

View File

@@ -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)' }

View File

@@ -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' }

View File

@@ -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'" }

View File

@@ -1,83 +1,112 @@
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products', 'ng-controller' => 'variantUnitsCtrl'}
.field
= f.label :display_name, t('.display_name')
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
.field
= f.label :display_as, t('.display_as')
= f.text_field :display_as, class: "fullwidth", placeholder: t('.display_as_placeholder')
%div{'data-controller': "edit-variant", id: "edit_variant"}
.label-block.left.six.columns.alpha
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
locals: { name: :available_units_sorted, value: WeightsAndMeasures.available_units_sorted }
- if @product.variant_unit != 'items'
.field
= label_tag :unit_value_human, "#{t('admin.'+@product.variant_unit)} ({{unitName(#{@product.variant_unit_scale}, '#{@product.variant_unit}')}})"
= hidden_field_tag 'product_variant_unit_scale', @product.variant_unit_scale
= number_field_tag :unit_value_human, nil, {class: "fullwidth", step: 0.01, 'ng-model' => 'unit_value_human', 'ng-change' => 'updateValue()'}
= f.number_field :unit_value, {hidden: true, 'ng-value' => 'unit_value'}
%script= render partial: "admin/shared/global_var_ofn", formats: :js,
locals: { name: :currency_config, value: Api::CurrencyConfigSerializer.new({}) }
.field
= f.label :unit_description, t(:spree_admin_unit_description)
= f.text_field :unit_description, class: "fullwidth", placeholder: t('admin.products.unit_name_placeholder')
.field
= f.label :display_name, t('.display_name')
= f.text_field :display_name, class: "fullwidth", placeholder: t('.display_name_placeholder')
%div
.field
= f.label :sku, t('.sku')
= f.text_field :sku, class: 'fullwidth'
.field
= f.label :price, t('.price')
= f.text_field :price, class: 'fullwidth', "ng-model" => "variant.price", "ng-init" => "variant.price = '#{number_to_currency(@variant.price, unit: '')&.strip}'"
.field
= hidden_field_tag 'product_variant_unit', @product.variant_unit
= hidden_field_tag 'product_variant_unit_name', @product.variant_unit_name
= f.field_container :unit_price do
%div{style: "display: flex"}
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.admin.unit_price_tooltip'"}
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]",
"class" => 'fullwidth', "disabled" => true, "ng-model" => "unit_price"}
%div{style: "color: black"}
= t("spree.admin.products.new.unit_price_legend")
%div{ 'set-on-demand' => '' }
.field.checkbox
%label
= f.check_box :on_demand
= t(:on_demand)
%div{'ofn-with-tip' => t('admin.products.variants.to_order_tip')}
%a= t('admin.whats_this')
.field{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.label :unit_scale do
= t('.unit_scale')
= content_tag(:span, ' *', class: 'required')
= f.hidden_field :variant_unit
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,
options_for_select(WeightsAndMeasures.variant_unit_options, @variant.variant_unit_with_scale),
{ include_blank: true },
{ class: "fullwidth no-input", 'aria-label': t('.unit_scale'), data: { "controller": "tom-select", "tom-select-options-value": '{ "plugins": [] }', action: "change->toggle-control#displayIfMatch" } }
= error_message_on @variant, :variant_unit, 'data-toggle-control-target': 'control'
.field
= f.label :on_hand, t(:on_hand)
.fullwidth
= f.text_field :on_hand
= f.text_field :variant_unit_name, 'aria-label': t('items'), 'data-toggle-control-target': 'control', style: (@variant.variant_unit == "items" ? "" : "display: none")
= error_message_on @variant, :variant_unit_name, 'data-toggle-control-target': 'control'
.field.popout{'data-controller': "popout", 'data-popout-update-display-value': "false"}
= f.label :unit do
= t('.unit')
= content_tag(:span, ' *', class: 'required')
= f.button :unit_to_display, class: "popout__button", 'aria-label': t('.unit'), 'data-popout-target': "button" do
= @variant.unit_to_display # Show the generated summary of unit values
%div.popout__container{ style: 'display: none;', 'data-controller': 'toggle-control', 'data-popout-target': "dialog" }
.field
-# Show a composite field for unit_value and unit_description
= f.hidden_field :unit_value
= f.hidden_field :unit_description
= f.text_field :unit_value_with_description,
value: unit_value_with_description(@variant), 'aria-label': t('.unit_value'), required: true
.field
= f.label :display_as, t('.display_as')
= f.text_field :display_as, placeholder: VariantUnits::OptionValueNamer.new(@variant).name
= error_message_on @variant, :unit_value
.right.six.columns.omega.label-block
- if @product.variant_unit != 'weight'
%div
.field
= f.label :sku, t('.sku')
= f.text_field :sku, class: 'fullwidth'
.field
= f.label :price do
= t('.price')
= content_tag(:span, ' *', class: 'required')
= f.text_field :price, class: 'fullwidth', value: number_to_currency(@variant.price, unit: '')&.strip
.field
= hidden_field_tag 'variant_variant_unit', @variant.variant_unit
= hidden_field_tag 'variant_variant_unit_name', @variant.variant_unit_name
= f.field_container :unit_price do
%div{style: "display: flex"}
= f.label :unit_price, t(".unit_price"), {style: "display: inline-block"}
%question-mark-with-tooltip{"question-mark-with-tooltip" => "_",
"question-mark-with-tooltip-append-to-body" => "true",
"question-mark-with-tooltip-placement" => "top",
"question-mark-with-tooltip-animation" => true,
key: "'js.admin.unit_price_tooltip'"}
%input{ "type" => "text", "id" => "variant_unit_price", "name" => "variant[unit_price]", "class" => 'fullwidth', "disabled" => true}
%div{style: "color: black"}
= t("spree.admin.products.new.unit_price_legend")
%div
.field.checkbox
%label
= f.check_box :on_demand, data: { "action": "click->edit-variant#toggleOnHand" }
= t(:on_demand)
= render AdminTooltipComponent.new(text: t('admin.products.variants.to_order_tip'), link_text: t('admin.whats_this'), placement: "right")
.field
= f.label :on_hand, t(:on_hand)
.fullwidth
= f.text_field :on_hand, data: { "edit-variant-target": "onHand" }
.right.six.columns.omega.label-block
.field
= f.label 'weight', t(:weight)+' (kg)'
- value = number_with_precision(@variant.weight, precision: 3)
= f.number_field 'weight', value: value, class: 'fullwidth', step: 0.001
- [:height, :width, :depth].each do |field|
- [:height, :width, :depth].each do |field|
.field
= f.label field, t(field)
- value = number_with_precision(@variant.send(field), precision: 2)
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
.field
= f.label field, t(field)
- value = number_with_precision(@variant.send(field), precision: 2)
= f.number_field field, value: value, class: 'fullwidth', step: 0.01
= f.label :tax_category, t(:tax_category), for: :tax_category_id
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
.field
= f.label :tax_category, t(:tax_category), for: :tax_category_id
= f.collection_select(:tax_category_id, @tax_categories, :id, :name, { include_blank: t(:none) }, { class: 'select2 fullwidth' })
.field
= f.label :shipping_category_id, t(:shipping_categories)
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
.field
= f.label :shipping_category_id, t(:shipping_categories)
= f.collection_select(:shipping_category_id, @shipping_categories, :id, :name, {}, { class: 'select2 fullwidth' })
.field
= f.label :primary_taxon, t('.variant_category')
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
.field
= f.label :primary_taxon, t('spree.admin.products.primary_taxon_form.product_category')
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, { include_blank: true }, { class: "select2 fullwidth" })
.field
= f.label :supplier do
= t(:spree_admin_supplier)
= content_tag(:span, ' *', class: 'required')
.field
= f.label :supplier, t(:spree_admin_supplier)
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
.clear
.clear

View File

@@ -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 });
}
}

View File

@@ -0,0 +1,189 @@
import { Controller } from "stimulus";
import OptionValueNamer from "js/services/option_value_namer";
import UnitPrices from "js/services/unit_prices";
// Dynamically update related variant fields
//
// TODO refactor so we can extract what's common with Bulk product page
export default class EditVariantController extends Controller {
static targets = ["onHand"];
connect() {
this.unitPrices = new UnitPrices();
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
this.variantUnit = this.element.querySelector('[id="variant_variant_unit"]');
this.variantUnitScale = this.element.querySelector('[id="variant_variant_unit_scale"]');
this.variantUnitName = this.element.querySelector('[id="variant_variant_unit_name"]');
this.variantUnitWithScale = this.element.querySelector(
'[id="variant_variant_unit_with_scale"]',
);
this.variantPrice = this.element.querySelector('[id="variant_price"]');
// on variant_unit_with_scale changed; update variant_unit and variant_unit_scale
this.variantUnitWithScale.addEventListener("change", this.#updateUnitAndScale.bind(this), {
passive: true,
});
this.unitValue = this.element.querySelector('[id="variant_unit_value"]');
this.unitDescription = this.element.querySelector('[id="variant_unit_description"]');
this.unitValueWithDescription = this.element.querySelector(
'[id="variant_unit_value_with_description"]',
);
this.displayAs = this.element.querySelector('[id="variant_display_as"]');
this.unitToDisplay = this.element.querySelector('[id="variant_unit_to_display"]');
// on unit changed; update display_as:placeholder and unit_to_display
[this.variantUnit, this.variantUnitScale, this.variantUnitName].forEach((element) => {
element.addEventListener("change", this.#unitChanged.bind(this), { passive: true });
});
this.variantUnitName.addEventListener("input", this.#unitChanged.bind(this), { passive: true });
// on unit_value_with_description changed; update unit_value and unit_description
// on unit_value and/or unit_description changed; update display_as:placeholder and unit_to_display
this.unitValueWithDescription.addEventListener("input", this.#unitChanged.bind(this), {
passive: true,
});
// on display_as changed; update unit_to_display
// TODO: optimise to avoid unnecessary OptionValueNamer calc
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
// update Unit price when variant_unit_with_scale or price changes
[this.variantUnitWithScale, this.variantPrice].forEach((element) => {
element.addEventListener("change", this.#processUnitPrice.bind(this), { passive: true });
});
this.unitValueWithDescription.addEventListener("input", this.#processUnitPrice.bind(this), {
passive: true,
});
// on variantUnit change we need to check if weight needs to be toggled
this.variantUnit.addEventListener("change", this.#toggleWeight.bind(this), { passive: true });
// make sure the unit is correct when page is reload after an error
this.#updateUnitDisplay();
// update unit price on page load
this.#processUnitPrice();
if (this.variantUnit.value === "weight") {
return this.#hideWeight();
}
}
disconnect() {
// Make sure to clean up anything that happened outside
// TODO remove all added event
this.variantUnit.removeEventListener("change", this.#toggleWeight.bind(this), {
passive: true,
});
}
toggleOnHand(event) {
if (event.target.checked === true) {
this.onHandTarget.dataStock = this.onHandTarget.value;
this.onHandTarget.value = I18n.t("admin.products.variants.infinity");
this.onHandTarget.disabled = "disabled";
} else {
this.onHandTarget.removeAttribute("disabled");
this.onHandTarget.value = this.onHandTarget.dataStock;
}
}
// private
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#unitChanged(event) {
//Hmm in hindsight the logic in product_controller should be inn this controller already. then we can do everything in one event, and store the generated name in an instance variable.
this.#extractUnitValues();
this.#updateUnitDisplay();
}
// Extract unit_value and unit_description
#extractUnitValues() {
// Extract a number (optional) and text value, separated by a space.
const match = this.unitValueWithDescription.value.match(/^([\d\.\,]+(?= |$)|)( |)(.*)$/);
if (match) {
let unit_value = parseFloat(match[1].replace(",", "."));
unit_value = isNaN(unit_value) ? null : unit_value;
unit_value *= this.variantUnitScale.value ? this.variantUnitScale.value : 1; // Normalise to default scale
this.unitValue.value = unit_value;
this.unitDescription.value = match[3];
}
}
// Update display_as placeholder and unit_to_display
#updateUnitDisplay() {
const unitDisplay = new OptionValueNamer(this.#variant()).name();
this.displayAs.placeholder = unitDisplay;
this.unitToDisplay.textContent = this.displayAs.value || unitDisplay;
}
// A representation of the variant model to satisfy OptionValueNamer.
#variant() {
return {
unit_value: parseFloat(this.unitValue.value),
unit_description: this.unitDescription.value,
variant_unit: this.variantUnit.value,
variant_unit_scale: parseFloat(this.variantUnitScale.value),
variant_unit_name: this.variantUnitName.value,
};
}
// Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
// and update hidden product fields
#updateUnitAndScale(event) {
const variant_unit_with_scale = this.variantUnitWithScale.value;
const match = variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/); // eg "weight_1000"
if (match) {
this.variantUnit.value = match[1];
this.variantUnitScale.value = parseFloat(match[2]);
} else {
// "items"
this.variantUnit.value = variant_unit_with_scale;
this.variantUnitScale.value = "";
}
this.variantUnit.dispatchEvent(new Event("change"));
this.variantUnitScale.dispatchEvent(new Event("change"));
}
#processUnitPrice() {
const unit_type = this.variantUnit.value;
// TODO double check this
let unit_value = 1;
if (unit_type != "items") {
unit_value = this.unitValue.value;
}
const unit_price = this.unitPrices.displayableUnitPrice(
this.variantPrice.value,
parseFloat(this.variantUnitScale.value),
unit_type,
unit_value,
this.variantUnitName.value,
);
this.element.querySelector('[id="variant_unit_price"]').value = unit_price;
}
#hideWeight() {
this.weight = this.element.querySelector('[id="variant_weight"]');
this.weight.parentElement.style.display = "none";
}
#toggleWeight() {
if (this.variantUnit.value === "weight") {
return this.#hideWeight();
}
// Show weight
this.weight = this.element.querySelector('[id="variant_weight"]');
this.weight.parentElement.style.display = "block";
// Clearing weight value to remove calculated weight for a variant with unit set to "weight"
// See Spree::Variant hook update_weight_from_unit_value
this.weight.value = "";
}
}

View File

@@ -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"));
}
}

View File

@@ -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"));
}
}

View File

@@ -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 {

View File

@@ -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";

View File

@@ -0,0 +1,87 @@
// Popout widget
@mixin unit_popout {
position: relative;
&__button {
// override button styles
&.popout__button {
background: $color-tbl-cell-bg;
color: $color-txt-text;
white-space: nowrap;
border-color: transparent;
font-weight: normal;
padding-left: $border-radius; // Super compact
padding-right: 1rem; // Retain space for arrow
height: auto;
min-width: 2em;
min-height: 1lh; // Line height of parent
&:hover,
&:active,
&:focus {
background: $color-tbl-cell-bg;
color: $color-txt-text;
position: relative;
}
&.changed {
border-color: $color-txt-changed-brd;
}
}
&:hover:not(:active):not(:focus):not(.changed) {
border-color: transparent;
}
&:hover,
&:active,
&:focus {
// for some reason, sass ignores &:active, &:focus here. we could make this a mixin and include it in multiple rules instead
&:before {
// for some reason, sass seems to extends the selector to include every other :before selector in the app! probably causing the above, and potentially breaking other styles.
// extending .icon-chevron-down causes infinite loop in compilation. does @include work for classes?
font-family: FontAwesome;
text-decoration: inherit;
display: inline-block;
speak: none;
content: "\f078";
position: absolute;
top: 0; // Required for empty buttons
right: $border-radius;
font-size: 0.67em;
}
}
}
&__container {
position: absolute;
top: -0.6em;
left: -0.2em;
z-index: 1; // Cover below row when hover
width: 9em;
padding: $padding-tbl-cell;
background: $color-tbl-cell-bg;
border-radius: $border-radius;
box-shadow: 0px 0px 8px 0px rgba($near-black, 0.25);
.field {
margin-bottom: 0.75em;
&:last-child {
margin-bottom: 0;
}
}
input {
height: auto;
&[disabled] {
color: transparent; // hide value completely
}
}
}
}

View File

@@ -0,0 +1,55 @@
@import "unit_popout";
#edit_variant {
.popout {
@include unit_popout;
&__button {
// override popout button styles
&.popout__button {
// Reapplying button style from buttons.css
background-color: $color-btn-bg;
border: 1px solid $color-btn-bg;
color: $color-btn-text;
font-weight: bold;
&:before {
font-family: FontAwesome;
text-decoration: inherit;
display: inline-block;
speak: none;
content: "\f078";
position: absolute;
top: 0; // Required for empty buttons
right: $border-radius;
font-size: 0.67em;
}
// Reapplying button style from buttons.css
&:active,
&:focus {
outline: none;
border: 1px solid $color-btn-hover-border;
}
&:active:focus {
box-shadow: none;
}
&:hover {
background-color: $color-btn-hover-bg;
border: 1px solid $color-btn-hover-bg;
color: $color-btn-hover-text;
}
}
}
&__container {
width: max-content;
top: auto;
left: auto;
}
}
}

View File

@@ -0,0 +1,24 @@
// Convert number to string currency using injected currency configuration.
// Requires global variable from page: ofn_currency_config
export default function (amount) {
// Set country code (eg. "US").
const currency_code = ofn_currency_config.display_currency
? " " + ofn_currency_config.currency
: "";
// Set decimal points, 2 or 0 if hide_cents.
const decimals = ofn_currency_config.hide_cents === "true" ? 0 : 2;
// Set format if the currency symbol should come after the number, otherwise (default) use the locale setting.
const format = ofn_currency_config.symbol_position === "after" ? "%n %u" : undefined;
// We need to use parseFloat as the amount should come in as a string.
amount = parseFloat(amount);
// Build the final price string.
return (
I18n.toCurrency(amount, {
precision: decimals,
unit: ofn_currency_config.symbol,
format: format,
}) + currency_code
);
}

View File

@@ -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)];
}
}
}

View File

@@ -0,0 +1,45 @@
export default class PriceParser {
parse(price) {
if (!price) {
return null;
}
// used decimal and thousands separators from currency configuration
const decimalSeparator = I18n.toCurrency(0.1, { precision: 1, unit: "" }).substring(1, 2);
const thousandsSeparator = I18n.toCurrency(1000, { precision: 1, unit: "" }).substring(1, 2);
// Replace comma used as a decimal separator and remplace by "."
price = this.replaceCommaByFinalPoint(price);
// Remove configured thousands separator if it is actually a thousands separator
price = this.removeThousandsSeparator(price, thousandsSeparator);
if (decimalSeparator === ",") {
price = price.replace(",", ".");
}
price = parseFloat(price);
if (isNaN(price)) {
return null;
}
return price;
}
replaceCommaByFinalPoint(price) {
if (price.match(/^[0-9]*(,{1})[0-9]{1,2}$/g)) {
return price.replace(",", ".");
} else {
return price;
}
}
removeThousandsSeparator(price, thousandsSeparator) {
if (new RegExp(`^([0-9]*(${thousandsSeparator}{1})[0-9]{3}[0-9\.,]*)*$`, "g").test(price)) {
return price.replaceAll(thousandsSeparator, "");
} else {
return price;
}
}
}

View File

@@ -0,0 +1,51 @@
import PriceParser from "js/services/price_parser";
import VariantUnitManager from "js/services/variant_unit_manager";
import localizeCurrency from "js/services/localize_currency";
export default class UnitPrices {
constructor() {
this.variantUnitManager = new VariantUnitManager();
this.priceParser = new PriceParser();
}
displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name) {
price = this.priceParser.parse(price);
if (price && !isNaN(price) && unit_type && unit_value) {
const value = localizeCurrency(
this.price(price, scale, unit_type, unit_value, variant_unit_name),
);
const unit = this.unit(scale, unit_type, variant_unit_name);
return `${value} / ${unit}`;
}
return null;
}
price(price, scale, unit_type, unit_value) {
return price / this.denominator(scale, unit_type, unit_value);
}
denominator(scale, unit_type, unit_value) {
const unit = this.unit(scale, unit_type);
if (unit === "lb") {
return unit_value / 453.6;
} else if (unit === "kg") {
return unit_value / 1000;
} else {
return unit_value;
}
}
unit(scale, unit_type, variant_unit_name = "") {
if (variant_unit_name.length > 0 && unit_type === "items") {
return variant_unit_name;
} else if (unit_type === "items") {
return "item";
} else if (this.variantUnitManager.systemOfMeasurement(scale, unit_type) === "imperial") {
return "lb";
} else if (unit_type === "weight") {
return "kg";
} else if (unit_type === "volume") {
return "L";
}
}
}

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -0,0 +1,6 @@
class AddVariantUnitToVariant < ActiveRecord::Migration[7.0]
def change
add_column :spree_variants, :variant_unit_scale, :float
add_column :spree_variants, :variant_unit_name, :string, limit: 255
end
end

View File

@@ -0,0 +1,21 @@
class MigrateUnitSizeToVariants < ActiveRecord::Migration[7.0]
def up
# Copy variant_unit only if it's empty in the variant
ActiveRecord::Base.connection.execute(<<-SQL
UPDATE spree_variants
SET variant_unit = spree_products.variant_unit
FROM spree_products
WHERE spree_variants.product_id = spree_products.id
AND spree_variants.variant_unit IS NULL
SQL
)
ActiveRecord::Base.connection.execute(<<-SQL
UPDATE spree_variants
SET variant_unit_scale = spree_products.variant_unit_scale, variant_unit_name = spree_products.variant_unit_name
FROM spree_products
WHERE spree_variants.product_id = spree_products.id
SQL
)
end
end

View File

@@ -968,6 +968,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_10_02_014059) do
t.bigint "shipping_category_id"
t.bigint "primary_taxon_id"
t.bigint "supplier_id"
t.float "variant_unit_scale"
t.string "variant_unit_name", limit: 255
t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id"
t.index ["product_id"], name: "index_variants_on_product_id"
t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id"

View File

@@ -11,7 +11,7 @@
class QuantitativeValueBuilder < DfcBuilder
def self.quantity(variant)
DataFoodConsortium::Connector::QuantitativeValue.new(
unit: unit(variant.product.variant_unit),
unit: unit(variant.variant_unit),
value: variant.unit_value,
)
end
@@ -27,7 +27,7 @@ class QuantitativeValueBuilder < DfcBuilder
end
end
def self.apply(quantity, product)
def self.apply(quantity, variant)
measure, unit_name, unit_scale = map_unit(quantity.unit)
value = quantity.value.to_f * unit_scale
@@ -37,10 +37,10 @@ class QuantitativeValueBuilder < DfcBuilder
value = 1
end
product.variant_unit = measure
product.variant_unit_name = unit_name if measure == "items"
product.variant_unit_scale = unit_scale
product.unit_value = value
variant.variant_unit = measure
variant.variant_unit_name = unit_name if measure == "items"
variant.variant_unit_scale = unit_scale
variant.unit_value = value
end
# Map DFC units to OFN fields:

View File

@@ -96,8 +96,7 @@ class SuppliedProductBuilder < DfcBuilder
variant.display_name = supplied_product.name
variant.primary_taxon = taxon(supplied_product)
QuantitativeValueBuilder.apply(supplied_product.quantity, variant.product)
variant.unit_value = variant.product.unit_value
QuantitativeValueBuilder.apply(supplied_product.quantity, variant)
catalog_item = supplied_product&.catalogItems&.first
offer = catalog_item&.offers&.first

View File

@@ -25,7 +25,8 @@ RSpec.describe AffiliateSalesDataBuilder do
:product,
name: "Pomme",
supplier_id: supplier.id,
variant_unit: "item",
variant_unit: "items",
variant_unit_name: "bag",
)
variant = product.variants.first
distributor = create(

View File

@@ -4,12 +4,11 @@ require_relative "../spec_helper"
RSpec.describe QuantitativeValueBuilder do
subject(:builder) { described_class }
let(:variant) { build(:variant, product:) }
let(:product) { build(:product, name: "Apple") }
let(:variant) { build(:variant) }
describe ".quantity" do
it "recognises items" do
product.variant_unit = "item"
variant.variant_unit = "item"
variant.unit_value = 1
quantity = builder.quantity(variant)
@@ -18,7 +17,7 @@ RSpec.describe QuantitativeValueBuilder do
end
it "recognises volume" do
product.variant_unit = "volume"
variant.variant_unit = "volume"
variant.unit_value = 2
quantity = builder.quantity(variant)
@@ -27,7 +26,7 @@ RSpec.describe QuantitativeValueBuilder do
end
it "recognises weight" do
product.variant_unit = "weight"
variant.variant_unit = "weight"
variant.unit_value = 1000 # 1kg
quantity = builder.quantity(variant)
@@ -36,7 +35,7 @@ RSpec.describe QuantitativeValueBuilder do
end
it "falls back to items" do
product.variant_unit = nil
variant.variant_unit = nil
quantity = builder.quantity(variant)
expect(quantity.value).to eq 1.0
@@ -46,7 +45,7 @@ RSpec.describe QuantitativeValueBuilder do
describe ".apply" do
let(:quantity_unit) { DfcLoader.connector.MEASURES }
let(:product) { Spree::Product.new }
let(:variant) { Spree::Variant.new }
it "uses items for anything unknown" do
quantity = DataFoodConsortium::Connector::QuantitativeValue.new(
@@ -54,12 +53,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 3,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "items"
expect(product.variant_unit_name).to eq "Jar"
expect(product.variant_unit_scale).to eq 1
expect(product.unit_value).to eq 3
expect(variant.variant_unit).to eq "items"
expect(variant.variant_unit_name).to eq "Jar"
expect(variant.variant_unit_scale).to eq 1
expect(variant.unit_value).to eq 3
end
it "knows metric units" do
@@ -68,12 +67,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 2,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "volume"
expect(product.variant_unit_name).to eq nil
expect(product.variant_unit_scale).to eq 1
expect(product.unit_value).to eq 2
expect(variant.variant_unit).to eq "volume"
expect(variant.variant_unit_name).to eq nil
expect(variant.variant_unit_scale).to eq 1
expect(variant.unit_value).to eq 2
end
it "knows metric units with a scale in OFN" do
@@ -82,12 +81,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 4,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "weight"
expect(product.variant_unit_name).to eq nil
expect(product.variant_unit_scale).to eq 1_000
expect(product.unit_value).to eq 4_000
expect(variant.variant_unit).to eq "weight"
expect(variant.variant_unit_name).to eq nil
expect(variant.variant_unit_scale).to eq 1_000
expect(variant.unit_value).to eq 4_000
end
it "knows metric units with a small scale" do
@@ -96,12 +95,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 5,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "weight"
expect(product.variant_unit_name).to eq nil
expect(product.variant_unit_scale).to eq 0.001
expect(product.unit_value).to eq 0.005
expect(variant.variant_unit).to eq "weight"
expect(variant.variant_unit_name).to eq nil
expect(variant.variant_unit_scale).to eq 0.001
expect(variant.unit_value).to eq 0.005
end
it "interpretes values given as a string" do
@@ -110,12 +109,12 @@ RSpec.describe QuantitativeValueBuilder do
value: "0.4",
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "weight"
expect(product.variant_unit_name).to eq nil
expect(product.variant_unit_scale).to eq 1_000
expect(product.unit_value).to eq 400
expect(variant.variant_unit).to eq "weight"
expect(variant.variant_unit_name).to eq nil
expect(variant.variant_unit_scale).to eq 1_000
expect(variant.unit_value).to eq 400
end
it "knows imperial units" do
@@ -124,12 +123,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 10,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "weight"
expect(product.variant_unit_name).to eq nil
expect(product.variant_unit_scale).to eq 453.59237
expect(product.unit_value).to eq 4_535.9237
expect(variant.variant_unit).to eq "weight"
expect(variant.variant_unit_name).to eq nil
expect(variant.variant_unit_scale).to eq 453.59237
expect(variant.unit_value).to eq 4_535.9237
end
it "knows customary units" do
@@ -138,12 +137,12 @@ RSpec.describe QuantitativeValueBuilder do
value: 2,
)
builder.apply(quantity, product)
builder.apply(quantity, variant)
expect(product.variant_unit).to eq "items"
expect(product.variant_unit_name).to eq "dozen"
expect(product.variant_unit_scale).to eq 12
expect(product.unit_value).to eq 24
expect(variant.variant_unit).to eq "items"
expect(variant.variant_unit_name).to eq "dozen"
expect(variant.variant_unit_scale).to eq 12
expect(variant.unit_value).to eq 24
end
end
end

View File

@@ -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,

View File

@@ -36,7 +36,7 @@ module OpenFoodNetwork
Spree::Variant.
ransack(search_params.merge(m: 'or')).
result.
order("spree_products.name, display_name, display_as, spree_products.variant_unit_name").
order("spree_products.name, display_name, display_as, spree_variants.variant_unit_name").
includes(:product).
joins(:product)
end

View File

@@ -49,7 +49,7 @@ module Reporting
def group_buy_unit_size(line_items)
unit_size = line_items.first.variant.product.group_buy_unit_size || 0.0
unit_size / (line_items.first.product.variant_unit_scale || 1)
unit_size / (line_items.first.variant.variant_unit_scale || 1)
end
def max_quantity_excess(line_items)
@@ -64,7 +64,7 @@ module Reporting
end
def scaled_unit_value(variant)
(variant.unit_value || 0) / (variant.product.variant_unit_scale || 1)
(variant.unit_value || 0) / (variant.variant_unit_scale || 1)
end
def option_value_value(line_items)
@@ -98,7 +98,7 @@ module Reporting
end
def scaled_final_weight_volume(line_item)
(line_item.final_weight_volume || 0) / (line_item.product.variant_unit_scale || 1)
(line_item.final_weight_volume || 0) / (line_item.variant.variant_unit_scale || 1)
end
def total_available(line_items)

View File

@@ -72,8 +72,7 @@ module Reporting
return " " if not_all_have_unit?(line_items)
total_units = line_items.sum do |li|
product = li.variant.product
li.quantity * li.unit_value / scale_factor(product)
li.quantity * li.unit_value / scale_factor(li.variant)
end
total_units.round(3)
@@ -92,8 +91,8 @@ module Reporting
line_items.map { |li| li.unit_value.nil? }.any?
end
def scale_factor(product)
product.variant_unit == 'weight' ? 1000 : 1
def scale_factor(variant)
variant.variant_unit == 'weight' ? 1000 : 1
end
def report_variant_overrides

View File

@@ -25,7 +25,6 @@ module Spree
new_product.deleted_at = nil
new_product.updated_at = nil
new_product.price = 0
new_product.unit_value = %w(weight volume).include?(product.variant_unit) ? 1.0 : nil
new_product.product_properties = reset_properties
new_product.image = duplicate_image(product.image) if product.image
new_product.variants = duplicate_variants

View File

@@ -35,8 +35,8 @@ RSpec.describe Api::V0::ProductsController, type: :controller do
it "gets a single product" do
product.create_image!(attachment:)
product.variants.create!(unit_value: "1", unit_description: "thing", price: 1,
primary_taxon: taxon, supplier:)
product.variants.create!(unit_value: "1", variant_unit: "weight", variant_unit_scale: 1,
unit_description: "thing", price: 1, primary_taxon: taxon, supplier:)
product.variants.first.images.create!(attachment:)
product.set_property("spree", "rocks")
@@ -121,8 +121,8 @@ RSpec.describe Api::V0::ProductsController, type: :controller do
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
errors = json_response["errors"]
expect(errors.keys).to match_array([
"name", "variant_unit", "price",
"primary_taxon_id", "supplier_id"
"name", "price", "primary_taxon_id",
"supplier_id", "variant_unit"
])
end

View File

@@ -144,9 +144,9 @@ RSpec.describe Api::V0::VariantsController, type: :controller do
it "can create a new variant" do
original_number_of_variants = variant.product.variants.count
api_post :create, variant: { sku: "12345", unit_value: "1", unit_description: "L",
price: "1", primary_taxon_id: taxon.id,
supplier_id: variant.supplier.id },
api_post :create, variant: { sku: "12345", unit_value: "1", variant_unit: "weight",
variant_unit_scale: 1, unit_description: "L", price: "1",
primary_taxon_id: taxon.id, supplier_id: variant.supplier.id },
product_id: variant.product.id
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)

View File

@@ -111,6 +111,8 @@ RSpec.describe Spree::Admin::ProductsController, type: :controller do
"on_hand" => 2,
"price" => "5.0",
"unit_value" => 4,
"variant_unit" => "weight",
"variant_unit_scale" => "1",
"unit_description" => "",
"display_name" => "name",
"primary_taxon_id" => taxon.id,

View File

@@ -12,8 +12,8 @@ module Spree
let(:product) { create(:product, name: 'Product A') }
let(:deleted_variant) do
deleted_variant = product.variants.create(
unit_value: "2", price: 1, primary_taxon: create(:taxon),
supplier: create(:supplier_enterprise)
unit_value: "2", variant_unit: "weight", variant_unit_scale: 1, price: 1,
primary_taxon: create(:taxon), supplier: create(:supplier_enterprise)
)
deleted_variant.delete
deleted_variant

View File

@@ -20,10 +20,8 @@ FactoryBot.define do
unit_value { 1 }
unit_description { '' }
variant_unit { 'weight' }
variant_unit_scale { 1 }
variant_unit_name { '' }
# ensure stock item will be created for this products master
before(:create) { DefaultStockLocation.find_or_create }

View File

@@ -10,6 +10,12 @@ FactoryBot.define do
height { generate(:random_float) }
width { generate(:random_float) }
depth { generate(:random_float) }
unit_value { 1 }
unit_description { '' }
variant_unit { 'weight' }
variant_unit_scale { 1 }
variant_unit_name { '' }
primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) }
supplier { Enterprise.is_primary_producer.first || FactoryBot.create(:supplier_enterprise) }
@@ -31,9 +37,6 @@ FactoryBot.define do
on_hand { 5 }
end
unit_value { 1 }
unit_description { '' }
after(:create) do |variant, evaluator|
variant.on_demand = evaluator.on_demand
variant.on_hand = evaluator.on_hand

View File

@@ -4,8 +4,9 @@ require "spec_helper"
RSpec.describe Admin::ProductsHelper do
describe '#unit_value_with_description' do
let(:product) { create(:product, variant_unit_scale: 1000.0) }
let(:variant) { create(:variant, product:, unit_value: 2000.0, unit_description: 'kg') }
let(:variant) {
create(:variant, variant_unit_scale: 1000.0, unit_value: 2000.0, unit_description: 'kg')
}
context 'when unit_value and unit_description are present' do
it 'returns the scaled unit value with the description' do
@@ -30,7 +31,7 @@ RSpec.describe Admin::ProductsHelper do
end
context 'when variant_unit_scale is nil' do
before { product.update_column(:variant_unit_scale, nil) }
before { variant.update_column(:variant_unit_scale, nil) }
it 'uses default scale of 1 and returns the unscaled unit value with the description' do
expect(helper.unit_value_with_description(variant)).to eq('2000 kg')

View File

@@ -0,0 +1,63 @@
/**
* @jest-environment jsdom
*/
import localizeCurrency from "js/services/localize_currency";
describe("convert number to localised currency", function () {
beforeAll(() => {
const mockedToCurrency = jest.fn();
mockedToCurrency.mockImplementation((amount, options) => {
if (options.format == "%n %u") {
return `${amount.toFixed(options.precision)}${options.unit}`;
} else {
return `${options.unit}${amount.toFixed(options.precision)}`;
}
});
global.I18n = { toCurrency: mockedToCurrency };
// Requires global var from page
global.ofn_currency_config = {
symbol: "$",
symbol_position: "before",
currency: "D",
hide_cents: "false",
};
});
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
afterAll(() => {
delete global.I18n;
});
it("adds decimal fraction to an amount", function () {
expect(localizeCurrency(10)).toEqual("$10.00");
});
it("handles an existing fraction", function () {
expect(localizeCurrency(9.9)).toEqual("$9.90");
});
it("can use any currency symbol", function () {
global.ofn_currency_config.symbol = "£";
expect(localizeCurrency(404.04)).toEqual("£404.04");
});
it("can place symbols after the amount", function () {
global.ofn_currency_config.symbol = "$";
global.ofn_currency_config.symbol_position = "after";
expect(localizeCurrency(333.3)).toEqual("333.30$");
});
it("can add a currency string", function () {
global.ofn_currency_config.display_currency = true;
global.ofn_currency_config.symbol_position = "before";
expect(localizeCurrency(5)).toEqual("$5.00 D");
});
it("can hide cents", function () {
global.ofn_currency_config.display_currency = false;
global.ofn_currency_config.hide_cents = "true";
expect(localizeCurrency(5)).toEqual("$5");
});
});

View File

@@ -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]);
});

View File

@@ -0,0 +1,150 @@
/**
* @jest-environment jsdom
*/
import PriceParse from "js/services/price_parser";
describe("PriceParser service", function () {
let priceParser = null;
beforeEach(() => {
priceParser = new PriceParse();
});
describe("test internal method with Regexp", function () {
describe("test replaceCommaByFinalPoint() method", function () {
it("handle the default case (with two numbers after comma)", function () {
expect(priceParser.replaceCommaByFinalPoint("1,00")).toEqual("1.00");
});
it("doesn't confuse with thousands separator", function () {
expect(priceParser.replaceCommaByFinalPoint("1,000")).toEqual("1,000");
});
it("handle also when there is only one number after the decimal separator", function () {
expect(priceParser.replaceCommaByFinalPoint("1,0")).toEqual("1.0");
});
});
describe("test removeThousandsSeparator() method", function () {
it("handle the default case", function () {
expect(priceParser.removeThousandsSeparator("1,000", ",")).toEqual("1000");
expect(priceParser.removeThousandsSeparator("1,000,000", ",")).toEqual("1000000");
});
it("handle the case with decimal separator", function () {
expect(priceParser.removeThousandsSeparator("1,000,000.00", ",")).toEqual("1000000.00");
});
it("handle the case when it is actually a decimal separator (and not a thousands one)", function () {
expect(priceParser.removeThousandsSeparator("1,00", ",")).toEqual("1,00");
});
});
});
describe("with point as decimal separator and comma as thousands separator for I18n service", function () {
beforeAll(() => {
const mockedToCurrency = jest.fn();
mockedToCurrency.mockImplementation((arg) => {
if (arg == 0.1) {
return "0.1";
} else if (arg == 1000) {
return "1,000";
}
});
global.I18n = { toCurrency: mockedToCurrency };
});
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
afterAll(() => {
delete global.I18n;
});
it("handle point as decimal separator", function () {
expect(priceParser.parse("1.00")).toEqual(1.0);
});
it("handle point as decimal separator", function () {
expect(priceParser.parse("1.000")).toEqual(1.0);
});
it("also handle comma as decimal separator", function () {
expect(priceParser.parse("1,0")).toEqual(1.0);
});
it("also handle comma as decimal separator", function () {
expect(priceParser.parse("1,00")).toEqual(1.0);
});
it("also handle comma as decimal separator", function () {
expect(priceParser.parse("11,00")).toEqual(11.0);
});
it("handle comma as decimal separator but not confusing with thousands separator", function () {
expect(priceParser.parse("11,000")).toEqual(11000);
});
it("handle point as decimal separator and comma as thousands separator", function () {
expect(priceParser.parse("1,000,000.00")).toEqual(1000000);
});
it("handle integer number", function () {
expect(priceParser.parse("10")).toEqual(10);
});
it("handle integer number with comma as thousands separator", function () {
expect(priceParser.parse("1,000")).toEqual(1000);
});
it("handle integer number with no thousands separator", function () {
expect(priceParser.parse("1000")).toEqual(1000);
});
});
describe("with comma as decimal separator and final point as thousands separator for I18n service", function () {
beforeAll(() => {
const mockedToCurrency = jest.fn();
mockedToCurrency.mockImplementation((arg) => {
if (arg == 0.1) {
return "0,1";
} else if (arg == 1000) {
return "1.000";
}
});
global.I18n = { toCurrency: mockedToCurrency };
});
// (jest still doesn't have aroundEach https://github.com/jestjs/jest/issues/4543 )
afterAll(() => {
delete global.I18n;
});
it("handle comma as decimal separator", function () {
expect(priceParser.parse("1,00")).toEqual(1.0);
});
it("handle comma as decimal separator with one digit after the comma", function () {
expect(priceParser.parse("11,0")).toEqual(11.0);
});
it("handle comma as decimal separator with two digit after the comma", function () {
expect(priceParser.parse("11,00")).toEqual(11.0);
});
it("handle comma as decimal separator with three digit after the comma", function () {
expect(priceParser.parse("11,000")).toEqual(11.0);
});
it("also handle point as decimal separator", function () {
expect(priceParser.parse("1.00")).toEqual(1.0);
});
it("also handle point as decimal separator with integer part with two digits", function () {
expect(priceParser.parse("11.00")).toEqual(11.0);
});
it("handle point as decimal separator and final point as thousands separator", function () {
expect(priceParser.parse("1.000.000,00")).toEqual(1000000);
});
it("handle integer number", function () {
expect(priceParser.parse("10")).toEqual(10);
});
});
});

View File

@@ -0,0 +1,170 @@
/**
* @jest-environment jsdom
*/
import UnitPrices from "js/services/unit_prices";
describe("UnitPrices service", function () {
let unitPrices = null;
beforeAll(() => {
// Requires global var from page for VariantUnitManager
global.ofn_available_units_sorted = {
weight: {
"1.0": { name: "g", system: "metric" },
28.35: { name: "oz", system: "imperial" },
453.6: { name: "lb", system: "imperial" },
"1000.0": { name: "kg", system: "metric" },
"1000000.0": { name: "T", system: "metric" },
},
volume: {
0.001: { name: "mL", system: "metric" },
"1.0": { name: "L", system: "metric" },
"1000.0": { name: "kL", system: "metric" },
},
};
});
beforeEach(() => {
unitPrices = new UnitPrices();
});
describe("get correct unit price duo unit/value for weight", function () {
const unit_type = "weight";
it("with scale: 1", function () {
const price = 1;
const scale = 1;
const unit_value = 1;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1000);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with scale and unit_value: 1000", function () {
const price = 1;
const scale = 1000;
const unit_value = 1000;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with scale: 1000 and unit_value: 2000", function () {
const price = 1;
const scale = 1000;
const unit_value = 2000;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.5);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with price: 2", function () {
const price = 2;
const scale = 1;
const unit_value = 1;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2000);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with price: 2, scale and unit_value: 1000", function () {
const price = 2;
const scale = 1000;
const unit_value = 1000;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with price: 2, scale: 1000 and unit_value: 2000", function () {
const price = 2;
const scale = 1000;
const unit_value = 2000;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
it("with price: 2, scale: 1000 and unit_value: 500", function () {
const price = 2;
const scale = 1000;
const unit_value = 500;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4);
expect(unitPrices.unit(scale, unit_type)).toEqual("kg");
});
});
describe("get correct unit price duo unit/value for volume", function () {
const unit_type = "volume";
it("with scale: 1", function () {
const price = 1;
const scale = 1;
const unit_value = 1;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
});
it("with price: 2 and unit_value: 0.5", function () {
const price = 2;
const scale = 1;
const unit_value = 0.5;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(4);
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
});
it("with price: 2, scale: 0.001 and unit_value: 0.01", function () {
const price = 2;
const scale = 0.001;
const unit_value = 0.01;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(200);
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
});
it("with price: 20000, scale: 1000 and unit_value: 10000", function () {
const price = 20000;
const scale = 1000;
const unit_value = 10000;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(2);
expect(unitPrices.unit(scale, unit_type)).toEqual("L");
});
it("with price: 2, scale: 1000 and unit_value: 10000 and variant_unit_name: box", function () {
const price = 20000;
const scale = 1000;
const unit_value = 10000;
const variant_unit_name = "Box";
expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(2);
expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("L");
});
});
describe("get correct unit price duo unit/value for items", function () {
const unit_type = "items";
const scale = null;
it("with price: 1 and unit_value: 1", function () {
const price = 1;
const unit_value = 1;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(1);
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
});
it("with price: 1 and unit_value: 10", function () {
const price = 1;
const unit_value = 10;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(0.1);
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
});
it("with price: 10 and unit_value: 1", function () {
const price = 10;
const unit_value = 1;
expect(unitPrices.price(price, scale, unit_type, unit_value)).toEqual(10);
expect(unitPrices.unit(scale, unit_type)).toEqual("item");
});
it("with price: 10 and unit_value: 1 and variant_unit_name: box", function () {
const price = 10;
const unit_value = 1;
const variant_unit_name = "Box";
expect(unitPrices.price(price, scale, unit_type, unit_value, variant_unit_name)).toEqual(10);
expect(unitPrices.unit(scale, unit_type, variant_unit_name)).toEqual("Box");
});
});
});

View File

@@ -1,56 +0,0 @@
/**
* @jest-environment jsdom
*/
import { Application } from "stimulus";
import product_controller from "../../../app/webpacker/controllers/product_controller";
describe("ProductController", () => {
beforeAll(() => {
const application = Application.start();
application.register("product", product_controller);
});
describe("variant_unit_with_scale", () => {
beforeEach(() => {
document.body.innerHTML = `
<div data-controller="product">
<input id="variant_unit" name="[products][0][variant_unit]" value="weight">
<input id="variant_unit_scale" name="[products][0][variant_unit_scale]" value="1.0">
<select id="variant_unit_with_scale" name="[products][0][variant_unit_with_scale]">
<option selected="selected" value="weight_1">Weight (g)</option>
<option value="weight_1000">Weight (kg)</option>
<option value="volume_4.54609">Volume (gal)</option>
<option value="items">Items</option>
</select>
</div>
`;
});
describe("change", () => {
it("weight_1000", () => {
variant_unit_with_scale.selectedIndex = 1;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("weight");
expect(variant_unit_scale.value).toBe("1000");
});
it("volume_4.54609", () => {
variant_unit_with_scale.selectedIndex = 2;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("volume");
expect(variant_unit_scale.value).toBe("4.54609");
});
it("items", () => {
variant_unit_with_scale.selectedIndex = 3;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("items");
expect(variant_unit_scale.value).toBe("");
});
})
});
});

View File

@@ -0,0 +1,86 @@
/**
* @jest-environment jsdom
*/
import { Application } from "stimulus";
import variant_controller from "controllers/variant_controller";
describe("VariantController", () => {
beforeAll(() => {
// Requires global var from page
global.ofn_available_units_sorted = {
weight: {
"1.0": { name: "g", system: "metric" },
"1000.0": { name: "kg", system: "metric" },
"1000000.0": { name: "T", system: "metric" },
},
volume: {
0.001: { name: "mL", system: "metric" },
"1.0": { name: "L", system: "metric" },
4.54609: { name: "gal", system: "imperial" },
"1000.0": { name: "kL", system: "metric" },
},
};
const mockedT = jest.fn();
mockedT.mockImplementation((string, opts) => string + ", " + JSON.stringify(opts));
global.I18n = { t: mockedT };
const application = Application.start();
application.register("variant", variant_controller);
});
afterAll(() => {
delete global.I18n;
});
describe("variant_unit_with_scale", () => {
beforeEach(() => {
document.body.innerHTML = `
<div data-controller="variant">
<input id="variant_unit" name="[products][0][variants_attributes][0][variant_unit]" value="weight">
<input id="variant_unit_scale" name="[products][0][variants_attributes][0][variant_unit_scale]" value="1.0">
<select id="variant_unit_with_scale" name="[products][0][variants_attributes][0][variant_unit_with_scale]">
<option selected="selected" value="weight_1">Weight (g)</option>
<option value="weight_1000">Weight (kg)</option>
<option value="volume_4.54609">Volume (gal)</option>
<option value="items">Items</option>
</select>
<input id="variant_unit_name" name="[products][0][variants_attributes][0][variant_unit_name]" type="text" >
<button id="unit_to_display" name="[products][0][variants_attributes][][0][unit_to_display]" type="submit" >2kg</button>
<input id="unit_value" name="[products][0][variants_attributes][0][unit_value]" value="2000.0" >
<input id="unit_description" name="[products][0][variants_attributes][0][unit_description]" >
<input id="unit_value_with_description" name="[products][0][variants_attributes][0][unit_value_with_description]" value="2" type="text" >
<input id="display_as" name="[products][0][variants_attributes][0][display_as]" placeholder="2kg" type="text" >
</div>
`;
});
describe("change", () => {
it("weight_1000", () => {
variant_unit_with_scale.selectedIndex = 1;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("weight");
expect(variant_unit_scale.value).toBe("1000");
});
it("volume_4.54609", () => {
variant_unit_with_scale.selectedIndex = 2;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("volume");
expect(variant_unit_scale.value).toBe("4.54609");
});
it("items", () => {
variant_unit_with_scale.selectedIndex = 3;
variant_unit_with_scale.dispatchEvent(new Event("change"));
expect(variant_unit.value).toBe("items");
expect(variant_unit_scale.value).toBe("");
});
});
});
});

View File

@@ -156,14 +156,20 @@ describe "filtering products for submission to database", ->
it "returns variant_unit_with_scale as variant_unit and variant_unit_scale", ->
testProduct =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'weight_1'
variants: [
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'weight_1'
]
expect(filterSubmitProducts([testProduct])).toEqual [
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variants_attributes: [
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
]
]
it "returns stock properties of a product if no variant is provided", ->
@@ -204,18 +210,15 @@ describe "filtering products for submission to database", ->
display_as: "bottle"
display_name: "nothing"
producer_id: 5
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
variant_unit_with_scale: 'volume_1'
]
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
variant_unit_with_scale: 'volume_1'
expect(filterSubmitProducts([testProduct])).toEqual [
id: 1
name: "TestProduct"
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
variants_attributes: [
id: 1
on_hand: 2
@@ -226,6 +229,9 @@ describe "filtering products for submission to database", ->
display_as: "bottle"
display_name: "nothing"
supplier_id: 5
variant_unit: 'volume'
variant_unit_scale: 1
variant_unit_name: 'loaf'
]
]
@@ -281,7 +287,6 @@ describe "AdminProductEditCtrl", ->
$scope.initialise()
expect($scope.q.query).toBe query
expect($scope.q.producerFilter).toBe producerFilter
expect($scope.q.categoryFilter).toBe categoryFilter
expect($scope.q.sorting).toBe sorting
expect($scope.q.importDateFilter).toBe importDateFilter
@@ -476,13 +481,13 @@ describe "AdminProductEditCtrl", ->
describe "determining whether a product has a unit", ->
it "returns true when it does", ->
product =
variant_unit_with_scale: 'weight_1000'
expect($scope.hasUnit(product)).toBe(true)
variant ={variant_unit_with_scale: 'weight_1000'}
expect($scope.hasUnit(variant)).toBe(true)
it "returns false when its unit is undefined", ->
product = {}
expect($scope.hasUnit(product)).toBe(false)
variant = {}
expect($scope.hasUnit(variant)).toBe(false)
describe "determining whether a variant has been saved", ->
@@ -505,51 +510,6 @@ describe "AdminProductEditCtrl", ->
window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"]
window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c)
it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", ->
testProduct =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'volume_1000'
$scope.packProduct(testProduct)
expect(testProduct).toEqual
id: 1
variant_unit: 'volume'
variant_unit_scale: 1000
variant_unit_with_scale: 'volume_1000'
it "extracts a null value into null variant_unit and variant_unit_scale", ->
testProduct =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: null
$scope.packProduct(testProduct)
expect(testProduct).toEqual
id: 1
variant_unit: null
variant_unit_scale: null
variant_unit_with_scale: null
it "extracts when variant_unit_with_scale is 'items'", ->
testProduct =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'items'
$scope.packProduct(testProduct)
expect(testProduct).toEqual
id: 1
variant_unit: 'items'
variant_unit_scale: null
variant_unit_with_scale: 'items'
it "packs each variant", ->
spyOn $scope, "packVariant"
testVariant = {id: 1}
@@ -559,120 +519,154 @@ describe "AdminProductEditCtrl", ->
$scope.packProduct(testProduct)
expect($scope.packVariant).toHaveBeenCalledWith(testProduct, testVariant)
expect($scope.packVariant).toHaveBeenCalledWith(testVariant)
describe "packing variants", ->
beforeEach ->
window.bigDecimal = jasmine.createSpyObj "bigDecimal", ["multiply"]
window.bigDecimal.multiply.and.callFake (a, b, c) -> (a * b).toFixed(c)
it "extracts variant_unit_with_scale into variant_unit and variant_unit_scale", ->
testVariant =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'volume_1000'
$scope.packVariant(testVariant)
expect(testVariant).toEqual
id: 1
variant_unit: 'volume'
variant_unit_scale: 1000
variant_unit_with_scale: 'volume_1000'
it "extracts when variant_unit_with_scale is 'items'", ->
testVariant =
id: 1
variant_unit: 'weight'
variant_unit_scale: 1
variant_unit_with_scale: 'items'
$scope.packVariant(testVariant)
expect(testVariant).toEqual
id: 1
variant_unit: 'items'
variant_unit_scale: null
variant_unit_with_scale: 'items'
it "extracts unit_value and unit_description from unit_value_with_description", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "250.5 (bottle)"}
BulkProducts.products = [testProduct]
$scope.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 250.5
unit_description: "(bottle)"
unit_value_with_description: "250.5 (bottle)"
it "extracts into unit_value when only a number is provided", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "250.5"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250.5"}
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 250.5
unit_description: ''
unit_value_with_description: "250.5"
variant_unit_scale: 1.0
it "extracts into unit_description when only a string is provided", ->
testProduct = {id: 123}
testVariant = {unit_value_with_description: "Medium"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: null
unit_description: 'Medium'
unit_value_with_description: "Medium"
it "extracts into unit_description when a string starting with a number is provided", ->
testProduct = {id: 123}
testVariant = {unit_value_with_description: "1kg"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: null
unit_description: '1kg'
unit_value_with_description: "1kg"
it "sets blank values when no value provided", ->
testProduct = {id: 123}
testVariant = {unit_value_with_description: ""}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: null
unit_description: ''
unit_value_with_description: ""
it "sets nothing when the field is undefined", ->
testProduct = {id: 123}
testVariant = {}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
expect(testVariant).toEqual {}
$scope.packVariant(testVariant)
expect(testVariant).toEqual({})
it "sets zero when the field is zero", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "0"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "0"}
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 0
unit_description: ''
unit_value_with_description: "0"
variant_unit_scale: 1.0
it "converts value from chosen unit to base unit", ->
testProduct = {id: 123, variant_unit_scale: 1000}
testVariant = {unit_value_with_description: "250.5"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
testVariant = {variant_unit_scale: 1000, unit_value_with_description: "250.5"}
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 250500
unit_description: ''
unit_value_with_description: "250.5"
variant_unit_scale: 1000
it "does not convert value when using a non-scaled unit", ->
testProduct = {id: 123}
testVariant = {unit_value_with_description: "12"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 12
unit_description: ''
unit_value_with_description: "12"
it "converts unit_value into a float when a comma separated number is provided", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "250,5"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "250,5"}
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 250.5
unit_description: ''
unit_value_with_description: "250,5"
variant_unit_scale: 1.0
it "rounds off the unit_value upto 2 decimal places", ->
testProduct = {id: 123, variant_unit_scale: 1.0}
testVariant = {unit_value_with_description: "1234.567"}
BulkProducts.products = [testProduct]
$scope.packVariant(testProduct, testVariant)
testVariant = {variant_unit_scale: 1.0, unit_value_with_description: "1234.567"}
$scope.packVariant(testVariant)
expect(testVariant).toEqual
unit_value: 1234.57
unit_description: ''
unit_value_with_description: "1234.567"
variant_unit_scale: 1.0
describe "filtering products", ->

View File

@@ -213,35 +213,67 @@ describe "LineItemsCtrl", ->
expect(scope.fulfilled()).toEqual ''
it "returns '' if selectedUnitsVariant has no property 'group_buy_unit_size' or group_buy_unit_size is 0", ->
scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 0 }
scope.selectedUnitsProduct = { group_buy_unit_size: 0 }
scope.selectedUnitsVariant = { variant_unit: "weight" }
expect(scope.fulfilled()).toEqual ''
scope.selectedUnitsProduct = { variant_unit: "weight" }
scope.selectedUnitsProduct = { }
scope.selectedUnitsVariant = { variant_unit: "weight" }
expect(scope.fulfilled()).toEqual ''
it "calls Math.round() if variant_unit is 'weight', 'volume', or items", ->
spyOn(Math,"round")
scope.selectedUnitsProduct = { variant_unit: "weight", group_buy_unit_size: 10 }
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
scope.selectedUnitsVariant = { variant_unit: "weight" }
scope.fulfilled()
expect(Math.round).toHaveBeenCalled()
scope.selectedUnitsProduct = { variant_unit: "volume", group_buy_unit_size: 10 }
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
scope.selectedUnitsVariant = { variant_unit: "volume" }
scope.fulfilled()
expect(Math.round).toHaveBeenCalled()
scope.selectedUnitsProduct = { variant_unit: "items", group_buy_unit_size: 10 }
scope.selectedUnitsProduct = { group_buy_unit_size: 10 }
scope.selectedUnitsVariant = { variant_unit: "items" }
scope.fulfilled()
expect(Math.round).toHaveBeenCalled()
describe "returns the quantity of fulfilled group buy units", ->
describe "returns the quantity of fulfilled group buy units", ->
runs = [
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 1000, variant_unit_scale: 1 }, arg: 1500, expected: 1.5 },
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1000 }, arg: 9, expected: 0.15 },
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 60000, variant_unit_scale: 1 }, arg: 9000, expected: 0.15 }
{ selectedUnitsProduct: { variant_unit: "weight", group_buy_unit_size: 5, variant_unit_scale: 28.35 }, arg: 12, expected: 2.4},
{ selectedUnitsProduct: { variant_unit: "volume", group_buy_unit_size: 5000, variant_unit_scale: 1 }, arg: 5, expected: 0.001}
];
runs.forEach ({selectedUnitsProduct, arg, expected}) ->
it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsProduct.variant_unit_scale , ->
{
selectedUnitsProduct: { group_buy_unit_size: 1000 },
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 },
arg: 1500,
expected: 1.5
}, {
selectedUnitsProduct: { group_buy_unit_size: 60000 } ,
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1000 } ,
arg: 9,
expected: 0.15
}, {
selectedUnitsProduct: { group_buy_unit_size: 60000 },
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 1 },
arg: 9000,
expected: 0.15
}, {
selectedUnitsProduct: { group_buy_unit_size: 5 },
selectedUnitsVariant: { variant_unit: "weight", variant_unit_scale: 28.35 },
arg: 12,
expected: 2.4
}, {
selectedUnitsProduct: { group_buy_unit_size: 5000 },
selectedUnitsVariant: { variant_unit: "volume", variant_unit_scale: 1 },
arg: 5,
expected: 0.001
}
]
runs.forEach ({selectedUnitsProduct, selectedUnitsVariant, arg, expected}) ->
it "returns the quantity of fulfilled group buy units, group_buy_unit_size: " + selectedUnitsProduct.group_buy_unit_size + ", arg: " + arg + ", scale: " + selectedUnitsVariant.variant_unit_scale , ->
scope.selectedUnitsProduct = selectedUnitsProduct
scope.selectedUnitsVariant = selectedUnitsVariant
expect(scope.fulfilled(arg)).toEqual expected
describe "allFinalWeightVolumesPresent()", ->
@@ -278,44 +310,44 @@ describe "LineItemsCtrl", ->
describe "sumUnitValues()", ->
it "returns the sum of the final_weight_volumes line_items if volume", ->
scope.filteredLineItems = [
{ final_weight_volume: 2, units_product: { variant_unit: "volume" } }
{ final_weight_volume: 7, units_product: { variant_unit: "volume" } }
{ final_weight_volume: 21, units_product: { variant_unit: "volume" } }
{ final_weight_volume: 2, units_variant: { variant_unit: "volume" } }
{ final_weight_volume: 7, units_variant: { variant_unit: "volume" } }
{ final_weight_volume: 21, units_variant: { variant_unit: "volume" } }
]
expect(scope.sumUnitValues()).toEqual 30
it "returns the sum of the quantity line_items if items", ->
scope.filteredLineItems = [
{ quantity: 2, units_product: { variant_unit: "items" } }
{ quantity: 7, units_product: { variant_unit: "items" } }
{ quantity: 21, units_product: { variant_unit: "items" } }
{ quantity: 2, units_variant: { variant_unit: "items" } }
{ quantity: 7, units_variant: { variant_unit: "items" } }
{ quantity: 21, units_variant: { variant_unit: "items" } }
]
expect(scope.sumUnitValues()).toEqual 30
it "returns the sum of the final_weight_volumes for line_items with both metric and imperial units", ->
scope.filteredLineItems = [
{ final_weight_volume: 907.2, units_product: { variant_unit: "weight", variant_unit_scale: 453.6 }, units_variant: { unit_value: 453.6 } }
{ final_weight_volume: 2000, units_product: { variant_unit: "weight", variant_unit_scale: 1000 }, units_variant: { unit_value: 1000 } }
{ final_weight_volume: 56.7, units_product: { variant_unit: "weight", variant_unit_scale: 28.35 }, units_variant: { unit_value: 28.35 } }
{ final_weight_volume: 2, units_product: { variant_unit: "volume", variant_unit_scale: 1.0 }, units_variant: { unit_value: 1.0 } }
{ final_weight_volume: 907.2, units_variant: { variant_unit: "weight", variant_unit_scale: 453.6, unit_value: 453.6 } }
{ final_weight_volume: 2000, units_variant: { variant_unit: "weight", variant_unit_scale: 1000, unit_value: 1000 } }
{ final_weight_volume: 56.7, units_variant: { variant_unit: "weight", variant_unit_scale: 28.35, unit_value: 28.35 } }
{ final_weight_volume: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1.0, unit_value: 1.0 } }
]
expect(scope.sumUnitValues()).toEqual 8
describe "sumMaxUnitValues()", ->
it "returns the sum of the product of unit_value and maxOf(max_quantity, pristine quantity) for specified line_items", ->
scope.filteredLineItems = [
{ id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
{ id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
{ id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "volume", variant_unit_scale: 1 } }
{ id: 1, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 1 }, max_quantity: 5 }
{ id: 2, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 2 }, max_quantity: 1 }
{ id: 3, units_variant: { variant_unit: "volume", variant_unit_scale: 1, unit_value: 3 }, max_quantity: 10 }
]
expect(scope.sumMaxUnitValues()).toEqual 37
it "returns the sum of the product of max_quantity for specified line_items if variant_unit is `items`", ->
scope.filteredLineItems = [
{ id: 1, units_variant: { unit_value: 1 }, max_quantity: 5, units_product: { variant_unit: "items" } }
{ id: 2, units_variant: { unit_value: 2 }, max_quantity: 1, units_product: { variant_unit: "items" } }
{ id: 3, units_variant: { unit_value: 3 }, max_quantity: 10, units_product: { variant_unit: "items" } }
{ id: 1, units_variant: { variant_unit: "items", unit_value: 1 }, max_quantity: 5 }
{ id: 2, units_variant: { variant_unit: "items", unit_value: 2 }, max_quantity: 1 }
{ id: 3, units_variant: { variant_unit: "items", unit_value: 3 }, max_quantity: 10 }
]
expect(scope.sumMaxUnitValues()).toEqual 16
@@ -331,45 +363,45 @@ describe "LineItemsCtrl", ->
expect(scope.formattedValueWithUnitName(1,{})).toEqual ''
it "returns the value, and does not call Math.round if variant_unit is 'items'", ->
unitsProduct = { variant_unit: "items" }
expect(scope.formattedValueWithUnitName(1, unitsProduct, unitsVariant)).toEqual "1 items"
unitsVariant.variant_unit= "items"
expect(scope.formattedValueWithUnitName(1, unitsVariant)).toEqual "1 items"
it "calls Math.round() if variant_unit is 'weight' or 'volume'", ->
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1 }
scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant)
unitsVariant = { unit_value: "1", variant_unit: "weight", variant_unit_scale: 1 }
scope.formattedValueWithUnitName(1, unitsVariant)
expect(Math.round).toHaveBeenCalled()
scope.selectedUnitsVariant = { variant_unit: "volume" }
scope.formattedValueWithUnitName(1,unitsProduct,unitsVariant)
scope.selectedUnitsVariant = {unit_value: "1", variant_unit: "volume" }
scope.formattedValueWithUnitName(1, unitsVariant)
expect(Math.round).toHaveBeenCalled()
it "calls Math.round with the value multiplied by 1000", ->
unitsProduct = { variant_unit: "weight", variant_unit_scale: 5 }
scope.formattedValueWithUnitName(10, unitsProduct,unitsVariant)
unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 5 }
scope.formattedValueWithUnitName(10, unitsVariant)
expect(Math.round).toHaveBeenCalledWith 10 * 1000
it "returns the result of Math.round divided by 1000, followed by the result of getUnitName", ->
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
unitsVariant = { unit_value: 1, variant_unit: "weight", variant_unit_scale: 1000 }
spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg"
expect(scope.formattedValueWithUnitName(2,unitsProduct,unitsVariant)).toEqual "2 kg"
expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 kg"
it "handle correclty the imperial units", ->
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
unitsVariant = { unit_value: "453.6" }
unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000, unit_value: "453.6" }
spyOn(VariantUnitManager, "getUnitName").and.returnValue "lb"
expect(scope.formattedValueWithUnitName(2, unitsProduct, unitsVariant)).toEqual "2 lb"
expect(scope.formattedValueWithUnitName(2, unitsVariant)).toEqual "2 lb"
describe "get group by size formatted value with unit name", ->
beforeEach ->
spyOn(VariantUnitManager, "getUnitName").and.returnValue "kg"
unitsProduct = { variant_unit: "weight", variant_unit_scale: 1000 }
unitsVariant = { variant_unit: "weight", variant_unit_scale: 1000 }
it "returns the formatted value with unit name", ->
expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsProduct)).toEqual "1 kg"
expect(scope.getGroupBySizeFormattedValueWithUnitName(1000, unitsVariant)).toEqual "1 kg"
it "handle the case when the value is actually null or empty", ->
expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsProduct)).toEqual ""
expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsProduct)).toEqual ""
expect(scope.getGroupBySizeFormattedValueWithUnitName(null, unitsVariant)).toEqual ""
expect(scope.getGroupBySizeFormattedValueWithUnitName("", unitsVariant)).toEqual ""
describe "updating the price upon updating the weight of a line item", ->

View File

@@ -1,29 +1,156 @@
describe "OptionValueNamer", ->
subject = null
OptionValueNamer = null
beforeEach ->
module('admin.products')
module "ofn.admin"
module "admin.products"
module ($provide)->
$provide.value "availableUnits", "g,kg,T,mL,L,kL"
null
inject (_OptionValueNamer_) ->
subject = new _OptionValueNamer_
beforeEach inject (_OptionValueNamer_) ->
OptionValueNamer = _OptionValueNamer_
describe "pluralize a variant unit name", ->
namer = null
beforeEach ->
namer = new OptionValueNamer({})
it "returns the same word if no plural is known", ->
expect(subject.pluralize("foo", 2)).toEqual "foo"
expect(namer.pluralize("foo", 2)).toEqual "foo"
it "returns the same word if we omit the quantity", ->
expect(subject.pluralize("loaf")).toEqual "loaf"
expect(namer.pluralize("loaf")).toEqual "loaf"
it "finds the plural of a word", ->
expect(subject.pluralize("loaf", 2)).toEqual "loaves"
expect(namer.pluralize("loaf", 2)).toEqual "loaves"
it "finds the singular of a word", ->
expect(subject.pluralize("loaves", 1)).toEqual "loaf"
expect(namer.pluralize("loaves", 1)).toEqual "loaf"
it "finds the zero form of a word", ->
expect(subject.pluralize("loaf", 0)).toEqual "loaves"
expect(namer.pluralize("loaf", 0)).toEqual "loaves"
it "ignores upper case", ->
expect(subject.pluralize("Loaf", 2)).toEqual "loaves"
expect(namer.pluralize("Loaf", 2)).toEqual "loaves"
describe "generating option value name", ->
v = namer = null
beforeEach ->
v = {}
namer = new OptionValueNamer(v)
it "when description is blank", ->
v.unit_description = null
spyOn(namer, "value_scaled").and.returnValue true
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
expect(namer.name()).toBe "valueunit"
it "when description is present", ->
v.unit_description = 'desc'
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
spyOn(namer, "value_scaled").and.returnValue true
expect(namer.name()).toBe "valueunit desc"
it "when value is blank and description is present", ->
v.unit_description = 'desc'
spyOn(namer, "option_value_value_unit").and.returnValue [null, null]
spyOn(namer, "value_scaled").and.returnValue true
expect(namer.name()).toBe "desc"
it "spaces value and unit when value is unscaled", ->
v.unit_description = null
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
spyOn(namer, "value_scaled").and.returnValue false
expect(namer.name()).toBe "value unit"
describe "determining if a variant's value is scaled", ->
v = namer = null
beforeEach ->
v = {}
namer = new OptionValueNamer(v)
it "returns true when the product has a scale", ->
v.variant_unit_scale = 1000
expect(namer.value_scaled()).toBe true
it "returns false otherwise", ->
expect(namer.value_scaled()).toBe false
describe "generating option value's value and unit", ->
v = namer = null
beforeEach ->
v = {}
namer = new OptionValueNamer(v)
it "generates simple values", ->
v.variant_unit = 'weight'
v.variant_unit_scale = 1.0
v.unit_value = 100
expect(namer.option_value_value_unit()).toEqual [100, 'g']
it "generates values when unit value is non-integer", ->
v.variant_unit = 'weight'
v.variant_unit_scale = 1.0
v.unit_value = 123.45
expect(namer.option_value_value_unit()).toEqual [123.45, 'g']
it "returns a value of 1 when unit value equals the scale", ->
v.variant_unit = 'weight'
v.variant_unit_scale = 1000.0
v.unit_value = 1000.0
expect(namer.option_value_value_unit()).toEqual [1, 'kg']
it "generates values for all weight scales", ->
for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']]
[scale, unit] = units
v.variant_unit = 'weight'
v.variant_unit_scale = scale
v.unit_value = 100 * scale
expect(namer.option_value_value_unit()).toEqual [100, unit]
it "generates values for all volume scales", ->
for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']]
[scale, unit] = units
v.variant_unit = 'volume'
v.variant_unit_scale = scale
v.unit_value = 100 * scale
expect(namer.option_value_value_unit()).toEqual [100, unit]
it "generates right values for volume with rounded values", ->
unit = 'L'
v.variant_unit = 'volume'
v.variant_unit_scale = 1.0
v.unit_value = 0.7
expect(namer.option_value_value_unit()).toEqual [700, 'mL']
it "chooses the correct scale when value is very small", ->
v.variant_unit = 'volume'
v.variant_unit_scale = 0.001
v.unit_value = 0.0001
expect(namer.option_value_value_unit()).toEqual [0.1, 'mL']
it "generates values for item units", ->
#TODO
# %w(packet box).each do |unit|
# p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit)
# v.stub(:product) { p }
# v.stub(:unit_value) { 100 }
# subject.option_value_value_unit.should == [100, unit.pluralize]
it "generates singular values for item units when value is 1", ->
v.variant_unit = 'items'
v.variant_unit_scale = null
v.variant_unit_name = 'packet'
v.unit_value = 1
expect(namer.option_value_value_unit()).toEqual [1, 'packet']
it "returns [nil, nil] when unit value is not set", ->
v.variant_unit = 'items'
v.variant_unit_scale = null
v.variant_unit_name = 'foo'
v.unit_value = null
expect(namer.option_value_value_unit()).toEqual [null, null]

View File

@@ -41,64 +41,64 @@ describe "unitsCtrl", ->
describe "interpretting unit_value_with_description", ->
beforeEach ->
scope.product.master = {}
scope.product = {}
describe "when a variant_unit_scale is present", ->
beforeEach ->
scope.product.variant_unit_scale = 1
it "splits by whitespace in to unit_value and unit_description", ->
scope.product.master.unit_value_with_description = "12 boxes"
scope.product.unit_value_with_description = "12 boxes"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 12
expect(scope.product.master.unit_description).toEqual "boxes"
expect(scope.product.unit_value).toEqual 12
expect(scope.product.unit_description).toEqual "boxes"
it "uses whole string as unit_value when only numerical characters are present", ->
scope.product.master.unit_value_with_description = "12345"
scope.product.unit_value_with_description = "12345"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 12345
expect(scope.product.master.unit_description).toEqual ''
expect(scope.product.unit_value).toEqual 12345
expect(scope.product.unit_description).toEqual ''
it "uses whole string as description when string does not start with a number", ->
scope.product.master.unit_value_with_description = "boxes 12"
scope.product.unit_value_with_description = "boxes 12"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual null
expect(scope.product.master.unit_description).toEqual "boxes 12"
expect(scope.product.unit_value).toEqual null
expect(scope.product.unit_description).toEqual "boxes 12"
it "does not require whitespace to split unit value and description", ->
scope.product.master.unit_value_with_description = "12boxes"
scope.product.unit_value_with_description = "12boxes"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 12
expect(scope.product.master.unit_description).toEqual "boxes"
expect(scope.product.unit_value).toEqual 12
expect(scope.product.unit_description).toEqual "boxes"
it "once a whitespace occurs, all subsequent numerical characters are counted as description", ->
scope.product.master.unit_value_with_description = "123 54 boxes"
scope.product.unit_value_with_description = "123 54 boxes"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 123
expect(scope.product.master.unit_description).toEqual "54 boxes"
expect(scope.product.unit_value).toEqual 123
expect(scope.product.unit_description).toEqual "54 boxes"
it "handle final point as decimal separator", ->
scope.product.master.unit_value_with_description = "22.22"
scope.product.unit_value_with_description = "22.22"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 22.22
expect(scope.product.master.unit_description).toEqual ""
expect(scope.product.unit_value).toEqual 22.22
expect(scope.product.unit_description).toEqual ""
it "handle comma as decimal separator", ->
scope.product.master.unit_value_with_description = "22,22"
scope.product.unit_value_with_description = "22,22"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 22.22
expect(scope.product.master.unit_description).toEqual ""
expect(scope.product.unit_value).toEqual 22.22
expect(scope.product.unit_description).toEqual ""
it "handle comma as decimal separator with description", ->
scope.product.master.unit_value_with_description = "22,22 things"
scope.product.unit_value_with_description = "22,22 things"
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 22.22
expect(scope.product.master.unit_description).toEqual "things"
expect(scope.product.unit_value).toEqual 22.22
expect(scope.product.unit_description).toEqual "things"
it "handles nice rounded division", ->
# this is a bit absurd, but it assure use that bigDecimal is called
window.bigDecimal.multiply.and.returnValue 0.7
scope.product.master.unit_value_with_description = "700"
scope.product.unit_value_with_description = "700"
scope.product.variant_unit_scale = 0.001
scope.processUnitValueWithDescription()
expect(scope.product.master.unit_value).toEqual 0.7
expect(scope.product.unit_value).toEqual 0.7

View File

@@ -53,126 +53,128 @@ describe "BulkProducts service", ->
describe "loading variant unit", ->
describe "setting product variant_unit_with_scale field", ->
it "sets by combining variant_unit and variant_unit_scale", ->
it "HERE 2 sets by combining variant_unit and variant_unit_scale", ->
product =
variant_unit: "volume"
variant_unit_scale: .001
variants:[
id: 10
variant_unit: "volume"
variant_unit_scale: .001
]
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "volume_0.001"
expect(product.variants[0].variant_unit_with_scale).toEqual "volume_0.001"
it "sets to null when variant_unit is null", ->
product = {variant_unit: null, variant_unit_scale: 1000}
product =
variants: [
{variant_unit: null, variant_unit_scale: 1000}
]
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toBeNull()
expect(product.variants[0].variant_unit_with_scale).toBeNull()
it "sets to variant_unit when variant_unit_scale is null", ->
product = {variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
product =
variants: [
{variant_unit: 'items', variant_unit_scale: null, variant_unit_name: 'foo'}
]
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
expect(product.variants[0].variant_unit_with_scale).toEqual "items"
it "sets to variant_unit when variant_unit is 'items'", ->
product = {variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
product =
variants: [
{variant_unit: 'items', variant_unit_scale: 1000, variant_unit_name: 'foo'}
]
BulkProducts.loadVariantUnit product
expect(product.variant_unit_with_scale).toEqual "items"
it "loads data for variants (incl. master)", ->
spyOn BulkProducts, "loadVariantUnitValues"
spyOn BulkProducts, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
BulkProducts.loadVariantUnit product
expect(BulkProducts.loadVariantUnitValues).toHaveBeenCalledWith product
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.master
expect(product.variants[0].variant_unit_with_scale).toEqual "items"
it "loads data for variants (excl. master)", ->
spyOn BulkProducts, "loadVariantUnitValue"
product =
variant_unit_scale: 1.0
master: {id: 1, unit_value: 1, unit_description: '(one)'}
variants: [{id: 2, unit_value: 2, unit_description: '(two)'}]
BulkProducts.loadVariantUnitValues product
variants: [
{id: 2, variant_unit_scale: 1.0, unit_value: 2, unit_description: '(two)'}
]
BulkProducts.loadVariantUnitValues product.variants
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product, product.variants[0]
expect(BulkProducts.loadVariantUnitValue).not.toHaveBeenCalledWith product, product.master
expect(BulkProducts.loadVariantUnitValue).toHaveBeenCalledWith product.variants[0]
describe "setting variant unit_value_with_description", ->
it "sets by combining unit_value and unit_description", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1, unit_description: '(bottle)'}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
{id: 1, variant_unit_scale: 1.0, unit_value: 1, unit_description: '(bottle)'}
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0]).toEqual
id: 1
variant_unit_scale: 1.0,
variant_unit_with_scale: null,
unit_value: 1
unit_description: '(bottle)'
unit_value_with_description: '1 (bottle)'
it "uses unit_value when description is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 1}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
{id: 1, variant_unit_scale: 1.0, unit_value: 1}
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0].unit_value_with_description).toEqual '1'
it "uses unit_description when value is missing", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_description: 'Small'}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
{id: 1, variant_unit_scale: 1.0, unit_description: 'Small'}
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0].unit_value_with_description).toEqual 'Small'
it "converts values from base value to chosen unit", ->
product =
variant_unit_scale: 1000.0
variants: [{id: 1, unit_value: 2500}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
id: 1, variant_unit_scale: 1000.0, unit_value: 2500
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0].unit_value_with_description).toEqual '2.5'
it "converts values from base value to chosen unit without breaking precision", ->
product =
variant_unit_scale: 0.001
variants: [{id: 1, unit_value: 0.35}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
{id: 1,variant_unit_scale: 0.001, unit_value: 0.35}
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0].unit_value_with_description).toEqual '350'
it "displays a unit_value of zero", ->
product =
variant_unit_scale: 1.0
variants: [{id: 1, unit_value: 0}]
BulkProducts.loadVariantUnitValues product, product.variants[0]
variants: [
{id: 1, variant_unit_scale: 1.0, unit_value: 0}
]
BulkProducts.loadVariantUnitValues product.variants
expect(product.variants[0].unit_value_with_description).toEqual '0'
describe "calculating the scaled unit value for a variant", ->
it "returns the scaled value when variant has a unit_value", ->
product = {variant_unit_scale: 0.001}
variant = {unit_value: 5}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5000
variant = {variant_unit_scale: 0.001, unit_value: 5}
expect(BulkProducts.variantUnitValue(variant)).toEqual 5000
it "returns the scaled value rounded off upto 2 decimal points", ->
product = {variant_unit_scale: 28.35}
variant = {unit_value: 1234.5}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 43.54
variant = {variant_unit_scale: 28.35, unit_value: 1234.5}
expect(BulkProducts.variantUnitValue(variant)).toEqual 43.54
it "returns the unscaled value when the product has no scale", ->
product = {}
variant = {unit_value: 5}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 5
expect(BulkProducts.variantUnitValue(variant)).toEqual 5
it "returns zero when the value is zero", ->
product = {}
variant = {unit_value: 0}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual 0
expect(BulkProducts.variantUnitValue(variant)).toEqual 0
it "returns null when the variant has no unit_value", ->
product = {}
variant = {}
expect(BulkProducts.variantUnitValue(product, variant)).toEqual null
expect(BulkProducts.variantUnitValue(variant)).toEqual null
describe "fetching a product by id", ->

View File

@@ -1,134 +0,0 @@
describe "Option Value Namer", ->
OptionValueNamer = null
beforeEach ->
module "ofn.admin"
module "admin.products"
module ($provide)->
$provide.value "availableUnits", "g,kg,T,mL,L,kL"
null
beforeEach inject (_OptionValueNamer_) ->
OptionValueNamer = _OptionValueNamer_
describe "generating option value name", ->
v = namer = null
beforeEach ->
v = {}
namer = new OptionValueNamer(v)
it "when description is blank", ->
v.unit_description = null
spyOn(namer, "value_scaled").and.returnValue true
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
expect(namer.name()).toBe "valueunit"
it "when description is present", ->
v.unit_description = 'desc'
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
spyOn(namer, "value_scaled").and.returnValue true
expect(namer.name()).toBe "valueunit desc"
it "when value is blank and description is present", ->
v.unit_description = 'desc'
spyOn(namer, "option_value_value_unit").and.returnValue [null, null]
spyOn(namer, "value_scaled").and.returnValue true
expect(namer.name()).toBe "desc"
it "spaces value and unit when value is unscaled", ->
v.unit_description = null
spyOn(namer, "option_value_value_unit").and.returnValue ["value", "unit"]
spyOn(namer, "value_scaled").and.returnValue false
expect(namer.name()).toBe "value unit"
describe "determining if a variant's value is scaled", ->
v = p = namer = null
beforeEach ->
p = {}
v = { product: p }
namer = new OptionValueNamer(v)
it "returns true when the product has a scale", ->
p.variant_unit_scale = 1000
expect(namer.value_scaled()).toBe true
it "returns false otherwise", ->
expect(namer.value_scaled()).toBe false
describe "generating option value's value and unit", ->
v = p = namer = null
beforeEach ->
p = {}
v = { product: p }
namer = new OptionValueNamer(v)
it "generates simple values", ->
p.variant_unit = 'weight'
p.variant_unit_scale = 1.0
v.unit_value = 100
expect(namer.option_value_value_unit()).toEqual [100, 'g']
it "generates values when unit value is non-integer", ->
p.variant_unit = 'weight'
p.variant_unit_scale = 1.0
v.unit_value = 123.45
expect(namer.option_value_value_unit()).toEqual [123.45, 'g']
it "returns a value of 1 when unit value equals the scale", ->
p.variant_unit = 'weight'
p.variant_unit_scale = 1000.0
v.unit_value = 1000.0
expect(namer.option_value_value_unit()).toEqual [1, 'kg']
it "generates values for all weight scales", ->
for units in [[1.0, 'g'], [1000.0, 'kg'], [1000000.0, 'T']]
[scale, unit] = units
p.variant_unit = 'weight'
p.variant_unit_scale = scale
v.unit_value = 100 * scale
expect(namer.option_value_value_unit()).toEqual [100, unit]
it "generates values for all volume scales", ->
for units in [[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']]
[scale, unit] = units
p.variant_unit = 'volume'
p.variant_unit_scale = scale
v.unit_value = 100 * scale
expect(namer.option_value_value_unit()).toEqual [100, unit]
it "generates right values for volume with rounded values", ->
unit = 'L'
p.variant_unit = 'volume'
p.variant_unit_scale = 1.0
v.unit_value = 0.7
expect(namer.option_value_value_unit()).toEqual [700, 'mL']
it "chooses the correct scale when value is very small", ->
p.variant_unit = 'volume'
p.variant_unit_scale = 0.001
v.unit_value = 0.0001
expect(namer.option_value_value_unit()).toEqual [0.1, 'mL']
it "generates values for item units", ->
#TODO
# %w(packet box).each do |unit|
# p = double(:product, variant_unit: 'items', variant_unit_scale: nil, variant_unit_name: unit)
# v.stub(:product) { p }
# v.stub(:unit_value) { 100 }
# subject.option_value_value_unit.should == [100, unit.pluralize]
it "generates singular values for item units when value is 1", ->
p.variant_unit = 'items'
p.variant_unit_scale = null
p.variant_unit_name = 'packet'
v.unit_value = 1
expect(namer.option_value_value_unit()).toEqual [1, 'packet']
it "returns [nil, nil] when unit value is not set", ->
p.variant_unit = 'items'
p.variant_unit_scale = null
p.variant_unit_name = 'foo'
v.unit_value = null
expect(namer.option_value_value_unit()).toEqual [null, null]

View File

@@ -34,8 +34,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
let(:variant) { item.variant }
it "contains a sum of total items" do
variant.product.update!(variant_unit: "items", variant_unit_name: "bottle")
variant.update!(unit_value: 6) # six-pack
variant.update!(variant_unit: "items", variant_unit_name: "bottle", unit_value: 6) # six-pack
item.update!(final_weight_volume: nil) # reset unit information
item.update!(quantity: 3)
@@ -44,8 +43,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
end
it "contains a sum of total weight" do
variant.product.update!(variant_unit: "weight")
variant.update!(unit_value: 200) # grams
variant.update!(variant_unit: "weight", unit_value: 200) # grams
item.update!(final_weight_volume: nil) # reset unit information
item.update!(quantity: 3)
@@ -57,8 +55,8 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
# This is not possible with the current code but was possible years ago.
# So I'm using `update_columns` to save invalid data.
# We still have lots of that data in our databases though.
variant.product.update(variant_unit: "items", variant_unit_name: "container")
variant.update_columns(unit_value: nil, unit_description: "vacuum")
variant.update_columns(variant_unit: "items", variant_unit_name: "container",
unit_value: nil, unit_description: "vacuum")
item.update!(final_weight_volume: nil) # reset unit information
expect(table_headers[4]).to eq "Total Units"
@@ -69,8 +67,7 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
expect(report).to receive(:display_summary_row?).and_return(true)
# assures product appears first on report table
variant.product.update!(name: "Alpha-Product #000")
variant.product.update!(variant_unit: "weight")
variant.update!(unit_value: 200) # grams
variant.update!(variant_unit: "weight", unit_value: 200) # grams
item.update!(final_weight_volume: nil) # reset unit information
item.update!(quantity: 3)
@@ -89,8 +86,8 @@ RSpec.describe Reporting::Reports::OrdersAndFulfillment::OrderCycleSupplierTotal
# This is not possible with the current code but was possible years ago.
# So I'm using `update_columns` to save invalid data.
# We still have lots of that data in our databases though.
variant.product.update(variant_unit: "items", variant_unit_name: "container")
variant.update_columns(unit_value: nil, unit_description: "vacuum")
variant.update_columns(variant_unit: "items", variant_unit_name: "container",
unit_value: nil, unit_description: "vacuum")
item.update!(final_weight_volume: nil) # reset unit information
# This second line item will have a default a bigint value.

View File

@@ -5,58 +5,28 @@ require 'spec_helper'
RSpec.describe Spree::Core::ProductDuplicator do
describe "unit" do
let(:product) do
double 'Product',
name: "foo",
product_properties: [property],
variants: [variant],
image:,
variant_unit: 'item'
instance_double(
Spree::Product,
name: "foo",
product_properties: [property],
variants: [variant],
image:,
variant_unit: 'item'
)
end
let(:new_product) do
double 'New Product',
save!: true
end
let(:property) do
double 'Property'
end
let(:new_property) do
double 'New Property'
end
let(:new_product) { instance_double(Spree::Product, save!: true) }
let(:property) { instance_double(Spree::ProductProperty) }
let(:new_property) { instance_double(Spree::ProductProperty) }
let(:variant) do
double 'Variant 1',
sku: "67890",
price: 19.50,
currency: "AUD",
images: [image_variant]
end
let(:new_variant) do
double 'New Variant 1',
sku: "67890"
end
let(:image) do
double 'Image',
attachment: double('Attachment')
end
let(:new_image) do
double 'New Image'
end
let(:image_variant) do
double 'Image Variant',
attachment: double('Attachment')
end
let(:new_image_variant) do
double 'New Image Variant',
attachment: double('Attachment')
instance_double(
Spree::Variant, sku: "67890", price: 19.50, currency: "AUD", images: [image_variant]
)
end
let(:new_variant) { instance_double(Spree::Variant, sku: "67890") }
let(:image) { instance_double(Spree::Image, attachment: double('Attachment')) }
let(:new_image) { instance_double(Spree::Image) }
let(:image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) }
let(:new_image_variant) { instance_double(Spree::Image, attachment: double('Attachment')) }
before do
expect(product).to receive(:dup).and_return(new_product)
@@ -73,7 +43,6 @@ RSpec.describe Spree::Core::ProductDuplicator do
expect(new_product).to receive(:product_properties=).with([new_property])
expect(new_product).to receive(:created_at=).with(nil)
expect(new_product).to receive(:price=).with(0)
expect(new_product).to receive(:unit_value=).with(nil)
expect(new_product).to receive(:updated_at=).with(nil)
expect(new_product).to receive(:deleted_at=).with(nil)
expect(new_product).to receive(:variants=).with([new_variant])
@@ -100,14 +69,17 @@ RSpec.describe Spree::Core::ProductDuplicator do
describe "errors" do
context "with invalid product" do
# Name has a max length of 255 char, when cloning a product the cloned product has a name
# starting with "COPY OF <product.name>". So we set a name with 254 char to make sure the
# cloned product will be invalid
let(:product) {
# name is a required field
create(:product).tap{ |p| p.update_columns(variant_unit: nil) }
create(:product).tap{ |v| v.update_columns(name: "l" * 254) }
}
subject { Spree::Core::ProductDuplicator.new(product).duplicate }
it "raises RecordInvalid error" do
expect{ subject }.to raise_error(ActiveRecord::RecordInvalid)
expect{ subject }.to raise_error(ActiveRecord::ActiveRecordError)
end
end

View File

@@ -124,7 +124,7 @@ RSpec.describe ProductImport::EntryValidator do
unit_value: 500,
variant_unit_scale: 1,
variant_unit: 'weight',
variants: [create(:variant, supplier: enterprise, unit_value: 500)]
supplier_id: enterprise.id
)
}
@@ -136,7 +136,7 @@ RSpec.describe ProductImport::EntryValidator do
unit_value: 1000,
variant_unit_scale: 1000,
variant_unit: 'weight',
variants: [create(:variant, supplier: enterprise, unit_value: 1000)]
supplier_id: enterprise.id
)
}

View File

@@ -0,0 +1,91 @@
# frozen_string_literal: false
require 'spec_helper'
RSpec.describe ProductImport::SpreadsheetEntry do
let(:enterprise) { create(:enterprise) }
let(:entry) {
ProductImport::SpreadsheetEntry.new(
"units" => "500",
"unit_type" => "kg",
"name" => "Tomato",
"enterprise" => enterprise,
"enterprise_id" => enterprise.id,
"producer" => enterprise,
"producer_id" => enterprise.id,
"distributor" => enterprise,
"price" => "1.0",
"on_hand" => "1",
"display_name" => display_name,
)
}
let(:display_name) { "" }
# TODO test match on display_name
describe "#match_variant?" do
it "returns true if matching" do
variant = create(:variant, unit_value: 500)
expect(entry.match_variant?(variant)).to be(true)
end
it "returns false if not machting" do
variant = create(:variant, unit_value: 250)
expect(entry.match_variant?(variant)).to be(false)
end
context "with same display_name" do
let(:display_name) { "Good" }
it "returns true" do
variant = create(:variant, unit_value: 500, display_name: "Good")
expect(entry.match_variant?(variant)).to be(true)
end
end
context "with different display_name" do
let(:display_name) { "Bad" }
it "returns false" do
variant = create(:variant, unit_value: 500, display_name: "Good")
expect(entry.match_variant?(variant)).to be(false)
end
end
end
describe "#match_inventory_variant?" do
it "returns true if matching" do
variant = create(:variant, unit_value: 500_000)
expect(entry.match_inventory_variant?(variant)).to be(true)
end
it "returns false if not machting" do
variant = create(:variant, unit_value: 500)
expect(entry.match_inventory_variant?(variant)).to be(false)
end
context "with same display_name" do
let(:display_name) { "Good" }
it "returns true" do
variant = create(:variant, unit_value: 500_000, display_name: "Good")
expect(entry.match_inventory_variant?(variant)).to be(true)
end
end
context "with different display_name" do
let(:display_name) { "Bad" }
it "returns false" do
variant = create(:variant, unit_value: 500_000, display_name: "Good")
expect(entry.match_inventory_variant?(variant)).to be(false)
end
end
end
end

View File

@@ -160,60 +160,60 @@ RSpec.describe ProductImport::ProductImporter do
carrots = Spree::Product.find_by(name: 'Carrots')
carrots_variant = carrots.variants.first
expect(carrots.on_hand).to eq 5
expect(carrots.variant_unit).to eq 'weight'
expect(carrots.variant_unit_scale).to eq 1
expect(carrots_variant.supplier).to eq enterprise
expect(carrots_variant.price).to eq 3.20
expect(carrots_variant.unit_value).to eq 500
expect(carrots_variant.variant_unit).to eq 'weight'
expect(carrots_variant.variant_unit_scale).to eq 1
expect(carrots_variant.on_demand).not_to eq true
expect(carrots_variant.import_date).to be_within(1.minute).of Time.zone.now
potatoes = Spree::Product.find_by(name: 'Potatoes')
potatoes_variant = potatoes.variants.first
expect(potatoes.on_hand).to eq 6
expect(potatoes.variant_unit).to eq 'weight'
expect(potatoes.variant_unit_scale).to eq 1000
expect(potatoes_variant.supplier).to eq enterprise
expect(potatoes_variant.price).to eq 6.50
expect(potatoes_variant.unit_value).to eq 2000
expect(potatoes_variant.variant_unit).to eq 'weight'
expect(potatoes_variant.variant_unit_scale).to eq 1000
expect(potatoes_variant.on_demand).not_to eq true
expect(potatoes_variant.import_date).to be_within(1.minute).of Time.zone.now
pea_soup = Spree::Product.find_by(name: 'Pea Soup')
pea_soup_variant = pea_soup.variants.first
expect(pea_soup.on_hand).to eq 8
expect(pea_soup.variant_unit).to eq 'volume'
expect(pea_soup.variant_unit_scale).to eq 0.001
expect(pea_soup_variant.supplier).to eq enterprise
expect(pea_soup_variant.price).to eq 5.50
expect(pea_soup_variant.unit_value).to eq 0.75
expect(pea_soup_variant.variant_unit).to eq 'volume'
expect(pea_soup_variant.variant_unit_scale).to eq 0.001
expect(pea_soup_variant.on_demand).not_to eq true
expect(pea_soup_variant.import_date).to be_within(1.minute).of Time.zone.now
salad = Spree::Product.find_by(name: 'Salad')
salad_variant = salad.variants.first
expect(salad.on_hand).to eq 7
expect(salad.variant_unit).to eq 'items'
expect(salad.variant_unit_scale).to eq nil
expect(salad_variant.supplier).to eq enterprise
expect(salad_variant.price).to eq 4.50
expect(salad_variant.unit_value).to eq 1
expect(salad_variant.variant_unit).to eq 'items'
expect(salad_variant.variant_unit_scale).to eq nil
expect(salad_variant.on_demand).not_to eq true
expect(salad_variant.import_date).to be_within(1.minute).of Time.zone.now
buns = Spree::Product.find_by(name: 'Hot Cross Buns')
buns_variant = buns.variants.first
expect(buns.on_hand).to eq 7
expect(buns.variant_unit).to eq 'items'
expect(buns.variant_unit_scale).to eq nil
expect(buns_variant.supplier).to eq enterprise
expect(buns_variant.price).to eq 3.50
expect(buns_variant.unit_value).to eq 1
expect(buns_variant.variant_unit).to eq 'items'
expect(buns_variant.variant_unit_scale).to eq nil
expect(buns_variant.on_demand).to eq true
expect(buns_variant.import_date).to be_within(1.minute).of Time.zone.now
end
@@ -575,12 +575,15 @@ RSpec.describe ProductImport::ProductImporter do
end
end
describe "updating non-updatable fields on existing products" do
describe "updating non-updatable fields on existing variants" do
let(:csv_data) {
CSV.generate do |csv|
csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type"]
csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg"]
csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg"]
csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type",
"shipping_category"]
csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg",
shipping_category.name]
csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg",
shipping_category.name]
end
}
let(:importer) { import_data csv_data }

View File

@@ -699,11 +699,11 @@ module Spree
describe "getting unit for display" do
let(:o) { create(:order) }
let(:p1) { create(:product, name: 'Clear Honey', variant_unit_scale: 1) }
let(:v1) { create(:variant, product: p1, unit_value: 500) }
let(:p1) { create(:product, name: 'Clear Honey') }
let(:v1) { create(:variant, product: p1, variant_unit_scale: 1, unit_value: 500) }
let(:li1) { create(:line_item, order: o, product: p1, variant: v1) }
let(:p2) { create(:product, name: 'Clear United States Honey', variant_unit_scale: 453.6) }
let(:v2) { create(:variant, product: p2, unit_value: 453.6) }
let(:p2) { create(:product, name: 'Clear United States Honey') }
let(:v2) { create(:variant, product: p2, variant_unit_scale: 453.6, unit_value: 453.6) }
let(:li2) { create(:line_item, order: o, product: p2, variant: v2) }
before do
@@ -723,8 +723,11 @@ module Spree
end
context "when the line_item has a final_weight_volume set" do
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') }
let!(:p0) { create(:simple_product) }
let!(:v) {
create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1,
unit_value: 10, unit_description: 'bar')
}
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:li) { create(:line_item, product: p, final_weight_volume: 5) }
@@ -742,8 +745,11 @@ module Spree
end
context "when the variant already has a value set" do
let!(:p0) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p0, unit_value: 10, unit_description: 'bar') }
let!(:p0) { create(:simple_product) }
let!(:v) {
create(:variant, product: p0, variant_unit: 'weight', variant_unit_scale: 1,
unit_value: 10, unit_description: 'bar')
}
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:li) { create(:line_item, product: p, final_weight_volume: 5) }

View File

@@ -34,5 +34,25 @@ module Spree
expect(variant.reload.price).to eq 10.25
end
end
describe "#price=" do
subject { Spree::Price.new }
context "with a number" do
it "returns the same number" do
subject.price = 12.5
expect(subject.price).to eq(12.5)
end
end
context "with empty string" do
it "sets the price to nil" do
subject.price = ""
expect(subject.price).to be_nil
end
end
end
end
end

View File

@@ -19,8 +19,8 @@ module Spree
end
it 'fails to duplicate invalid product' do
# Existing product is invalid:
product.update_columns(variant_unit: nil)
# cloned product will be invalid
product.update_columns(name: "l" * 254)
expect{ product.duplicate }.to raise_error(ActiveRecord::ActiveRecordError)
end
@@ -123,27 +123,6 @@ module Spree
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_length_of(:sku).is_at_most(255) }
context "unit value" do
it "requires a unit value when variant unit is weight" do
expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name',
unit_value: nil)).not_to be_valid
expect(build(:simple_product, variant_unit: 'weight', variant_unit_name: 'name',
unit_value: 0)).not_to be_valid
end
it "requires a unit value when variant unit is volume" do
expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name',
unit_value: nil)).not_to be_valid
expect(build(:simple_product, variant_unit: 'volume', variant_unit_name: 'name',
unit_value: 0)).not_to be_valid
end
it "does not require a unit value when variant unit is items" do
expect(build(:simple_product, variant_unit: 'items', variant_unit_name: 'name',
unit_value: nil)).to be_valid
end
end
context "when the product has variants" do
let(:product) do
product = create(:simple_product)
@@ -153,29 +132,6 @@ module Spree
it { is_expected.to validate_numericality_of(:price).is_greater_than_or_equal_to(0) }
it "requires a unit" do
product.variant_unit = nil
expect(product).not_to be_valid
end
%w(weight volume).each do |unit|
context "when unit is #{unit}" do
it "is valid when unit scale is set and unit name is not" do
product.variant_unit = unit
product.variant_unit_scale = 1
product.variant_unit_name = nil
expect(product).to be_valid
end
it "is invalid when unit scale is not set" do
product.variant_unit = unit
product.variant_unit_scale = nil
product.variant_unit_name = nil
expect(product).not_to be_valid
end
end
end
context "saving a new product" do
let!(:product){ Spree::Product.new }
let!(:shipping_category){ create(:shipping_category) }
@@ -184,62 +140,127 @@ module Spree
before do
create(:stock_location)
end
it "copies properties to the first standard variant" do
product.primary_taxon_id = taxon.id
product.name = "Product1"
product.variant_unit = "weight"
product.variant_unit_scale = 1000
product.unit_value = 1
product.unit_description = "some product"
product.price = 4.27
product.shipping_category_id = shipping_category.id
product.supplier_id = supplier.id
product.save!
end
product.save(context: :create_and_create_standard_variant)
it "copies properties to the first standard variant" do
expect(product.variants.reload.length).to eq 1
standard_variant = product.variants.reload.first
expect(standard_variant).to be_valid
expect(standard_variant.variant_unit).to eq("weight")
expect(standard_variant.variant_unit_scale).to eq(1000)
expect(standard_variant.unit_value).to eq(1)
expect(standard_variant.unit_description).to eq("some product")
expect(standard_variant.price).to eq 4.27
expect(standard_variant.shipping_category).to eq shipping_category
expect(standard_variant.primary_taxon).to eq taxon
expect(standard_variant.supplier).to eq supplier
end
end
context "when the unit is items" do
it "is valid when unit name is set and unit scale is not" do
product.variant_unit = 'items'
product.variant_unit_name = 'loaf'
product.variant_unit_scale = nil
expect(product).to be_valid
context "with variant attributes" do
it {
is_expected.to validate_presence_of(:variant_unit)
.on(:create_and_create_standard_variant)
}
it {
is_expected.to validate_presence_of(:supplier_id)
.on(:create_and_create_standard_variant)
}
it {
is_expected.to validate_presence_of(:primary_taxon_id)
.on(:create_and_create_standard_variant)
}
describe "unit_value" do
subject { build(:simple_product, variant_unit: "items") }
it {
is_expected.to validate_numericality_of(:unit_value).is_greater_than(0)
.on(:create_and_create_standard_variant)
}
it {
is_expected.not_to validate_presence_of(:unit_value)
.on(:create_and_create_standard_variant)
}
["weight", "volume"].each do |variant_unit|
context "when variant_unit is #{variant_unit}" do
subject { build(:simple_product, variant_unit:) }
it {
is_expected.to validate_presence_of(:unit_value)
.on(:create_and_create_standard_variant)
}
end
end
describe "unit_description" do
it {
is_expected.not_to validate_presence_of(:unit_description)
.on(:create_and_create_standard_variant)
}
context "when variant_unit is et and unit_value is nil" do
subject {
build(:simple_product, variant_unit: "items", unit_value: nil,
unit_description: "box")
}
it {
is_expected.to validate_presence_of(:unit_description)
.on(:create_and_create_standard_variant)
}
end
end
describe "variant_unit_scale" do
it {
is_expected.not_to validate_presence_of(:variant_unit_scale)
.on(:create_and_create_standard_variant)
}
["weight", "volume"].each do |variant_unit|
context "when variant_unit is #{variant_unit}" do
subject { build(:simple_product, variant_unit:) }
it {
is_expected.to validate_presence_of(:variant_unit_scale)
.on(:create_and_create_standard_variant)
}
end
end
end
describe "variant_unit_name" do
subject { build(:simple_product, variant_unit: "volume") }
it {
is_expected.not_to validate_presence_of(:variant_unit_name)
.on(:create_and_create_standard_variant)
}
context "when variant_unit is items" do
subject { build(:simple_product, variant_unit: "items") }
it {
is_expected.to validate_presence_of(:variant_unit_name)
.on(:create_and_create_standard_variant)
}
end
end
end
end
it "is invalid when unit name is not set" do
product.variant_unit = 'items'
product.variant_unit_name = nil
product.variant_unit_scale = nil
expect(product).not_to be_valid
end
end
end
context "a basic product" do
let(:product) { build_stubbed(:simple_product) }
it "requires variant unit fields" do
product.variant_unit = nil
product.variant_unit_name = nil
product.variant_unit_scale = nil
expect(product).not_to be_valid
end
it "requires a unit scale when variant unit is weight" do
product.variant_unit = 'weight'
product.variant_unit_scale = nil
product.variant_unit_name = nil
expect(product).not_to be_valid
end
end
@@ -328,30 +349,6 @@ module Spree
end
end
end
it "updates units when saved change to variant unit" do
product.variant_unit = 'items'
product.variant_unit_scale = nil
product.variant_unit_name = 'loaf'
product.save!
expect(product.variant_unit_name).to eq 'loaf'
product.update(variant_unit_name: 'bag')
expect(product.variant_unit_name).to eq 'bag'
product.variant_unit = 'weight'
product.variant_unit_scale = 1
product.variant_unit_name = 'g'
product.save!
expect(product.variant_unit).to eq 'weight'
product.update(variant_unit: 'volume')
expect(product.variant_unit).to eq 'volume'
end
end
describe "scopes" do
@@ -682,28 +679,6 @@ module Spree
end
end
describe "variant units" do
context "when the product already has a variant unit set" do
let!(:p) {
create(:simple_product,
variant_unit: 'weight',
variant_unit_scale: 1,
variant_unit_name: nil)
}
it "updates its variants unit values" do
v = create(:variant, unit_value: 1, product: p)
p.reload
expect(v.unit_presentation).to eq "1g"
p.update!(variant_unit: 'volume', variant_unit_scale: 0.001)
expect(v.reload.unit_presentation).to eq "1L"
end
end
end
describe "deletion" do
let(:product) { create(:simple_product) }
let(:variant) { create(:variant, product:) }

View File

@@ -44,21 +44,164 @@ RSpec.describe Spree::Variant do
end
end
# add test for the other validation
context "validations" do
it "should validate price is greater than 0" do
variant.price = -1
expect(variant).not_to be_valid
describe "validations" do
describe "variant_unit" do
subject(:variant) { build(:variant) }
it { is_expected.to validate_presence_of :variant_unit }
context "when the unit is items" do
subject(:variant) { build(:variant, variant_unit: "items", variant_unit_name: "box") }
it "is valid with only unit value set" do
variant.unit_value = 1
variant.unit_description = nil
expect(variant).to be_valid
end
it "is valid with only unit description set" do
variant.unit_value = nil
variant.unit_description = 'Medium'
expect(variant).to be_valid
end
it "sets unit_value to 1.0 before validation if it's nil" do
variant.unit_value = nil
variant.unit_description = nil
expect(variant).to be_valid
expect(variant.unit_value).to eq 1.0
end
end
context "when the product's unit is non-weight" do
subject(:variant) { build(:variant, variant_unit: "volume") }
it "sets weight to decimal before save if it's integer" do
variant.weight = 1
variant.save!
expect(variant.weight).to eq 1.0
end
it "sets weight to 0.0 before save if it's nil" do
variant.weight = nil
variant.save!
expect(variant.weight).to eq 0.0
end
it "sets weight to 0.0 if input is a non numerical string" do
variant.weight = "BANANAS!"
variant.save!
expect(variant.weight).to eq 0.0
end
it "sets weight to correct decimal value if input is numerical string" do
variant.weight = "2"
variant.save!
expect(variant.weight).to eq 2.0
end
end
end
it "should validate price is 0" do
variant.price = 0
expect(variant).to be_valid
describe "price" do
it { is_expected.to validate_presence_of :price }
it "should validate price is greater than 0" do
variant.price = -1
expect(variant).not_to be_valid
end
it "should validate price is 0" do
variant.price = 0
expect(variant).to be_valid
end
it "should validate unit_value is greater than 0" do
variant.unit_value = 0
expect(variant).not_to be_valid
end
end
it "should validate unit_value is greater than 0" do
variant.unit_value = 0
expect(variant).not_to be_valid
describe "unit_value" do
subject(:variant) { build(:variant, variant_unit: "item", unit_value: "") }
it { is_expected.not_to validate_presence_of(:unit_value) }
%w(weight volume).each do |unit|
context "when variant_unit is #{unit}" do
subject(:variant) { build(:variant, variant_unit: unit) }
it { is_expected.to validate_presence_of(:unit_value) }
it { is_expected.to validate_numericality_of(:unit_value).is_greater_than(0) }
end
end
end
describe "unit_description" do
subject(:variant) { build(:variant) }
it { expect(variant).to be_valid }
it { is_expected.not_to validate_presence_of(:unit_description) }
context "when variant_unit is set and unit_value is nil" do
subject(:variant) {
build(:variant, variant_unit: "item", unit_value: nil, unit_description: "box")
}
it { is_expected.to validate_presence_of(:unit_description) }
end
end
describe "variant_unit_scale" do
subject(:variant) { build(:variant, variant_unit: "box") }
it { is_expected.not_to validate_presence_of :variant_unit_scale }
%w(weight volume).each do |unit|
context "when variant_unit is #{unit}" do
subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) }
it { is_expected.to validate_presence_of :variant_unit_scale }
end
end
end
describe "variant_unit_name" do
subject(:variant) { build(:variant) }
it { is_expected.not_to validate_presence_of :variant_unit_name }
context "when variant_unit is items" do
subject(:variant) { build(:variant, variant_unit: "items") }
it { is_expected.to validate_presence_of :variant_unit_name }
end
end
describe "variant_unit_scale" do
subject(:variant) { build(:variant, variant_unit: "box") }
it { is_expected.not_to validate_presence_of :variant_unit_scale }
%w(weight volume).each do |unit|
context "when variant_unit is #{unit}" do
subject(:variant) { build(:variant, variant_unit: unit, variant_unit_scale: 1.0) }
it { is_expected.to validate_presence_of :variant_unit_scale }
end
end
end
describe "variant_unit_name" do
subject(:variant) { build(:variant) }
it { is_expected.not_to validate_presence_of :variant_unit_name }
context "when variant_unit is items" do
subject(:variant) { build(:variant, variant_unit: "items") }
it { is_expected.to validate_presence_of :variant_unit_name }
end
end
describe "tax category" do
@@ -529,8 +672,8 @@ RSpec.describe Spree::Variant do
context "handling nil values for related naming attributes" do
it "returns empty string or product name" do
product.name = "Apple"
product.variant_unit = "items"
product.display_as = nil
variant.variant_unit = "items"
variant.display_as = nil
variant.display_name = nil
@@ -540,8 +683,8 @@ RSpec.describe Spree::Variant do
it "uses the display name correctly" do
product.name = "Apple"
product.variant_unit = "items"
product.display_as = nil
variant.variant_unit = "items"
variant.display_as = nil
variant.unit_presentation = nil
variant.display_name = "Green"
@@ -554,8 +697,8 @@ RSpec.describe Spree::Variant do
describe "calculating the price with enterprise fees" do
it "returns the price plus the fees" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
distributor = instance_double(Enterprise)
order_cycle = instance_double(OrderCycle)
variant = Spree::Variant.new price: 100
expect(variant).to receive(:fees_for).with(distributor, order_cycle) { 23 }
@@ -565,8 +708,8 @@ RSpec.describe Spree::Variant do
describe "calculating the fees" do
it "delegates to EnterpriseFeeCalculator" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
distributor = instance_double(Enterprise)
order_cycle = instance_double(OrderCycle)
variant = Spree::Variant.new
expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator)
@@ -578,10 +721,10 @@ RSpec.describe Spree::Variant do
describe "calculating fees broken down by fee type" do
it "delegates to EnterpriseFeeCalculator" do
distributor = double(:distributor)
order_cycle = double(:order_cycle)
distributor = instance_double(Enterprise)
order_cycle = instance_double(OrderCycle)
variant = Spree::Variant.new
fees = double(:fees)
fees = instance_double(EnterpriseFee)
expect_any_instance_of(OpenFoodNetwork::EnterpriseFeeCalculator)
.to receive(:fees_by_type_for).with(variant) { fees }
@@ -590,90 +733,6 @@ RSpec.describe Spree::Variant do
end
end
context "when the product has variants" do
let!(:product) { create(:simple_product) }
let!(:variant) { create(:variant, product:) }
%w(weight volume).each do |unit|
context "when the product's unit is #{unit}" do
before do
product.update_attribute :variant_unit, unit
product.reload
end
it "is valid when unit value is set and unit description is not" do
variant.unit_value = 1
variant.unit_description = nil
expect(variant).to be_valid
end
it "is invalid when unit value is not set" do
variant.unit_value = nil
expect(variant).not_to be_valid
end
end
end
context "when the product's unit is items" do
before do
product.update_attribute :variant_unit, 'items'
product.reload
variant.reload
end
it "is valid with only unit value set" do
variant.unit_value = 1
variant.unit_description = nil
expect(variant).to be_valid
end
it "is valid with only unit description set" do
variant.unit_value = nil
variant.unit_description = 'Medium'
expect(variant).to be_valid
end
it "sets unit_value to 1.0 before validation if it's nil" do
variant.unit_value = nil
variant.unit_description = nil
expect(variant).to be_valid
expect(variant.unit_value).to eq 1.0
end
end
context "when the product's unit is non-weight" do
before do
product.update_attribute :variant_unit, 'volume'
product.reload
variant.reload
end
it "sets weight to decimal before save if it's integer" do
variant.weight = 1
variant.save!
expect(variant.weight).to eq 1.0
end
it "sets weight to 0.0 before save if it's nil" do
variant.weight = nil
variant.save!
expect(variant.weight).to eq 0.0
end
it "sets weight to 0.0 if input is a non numerical string" do
variant.weight = "BANANAS!"
variant.save!
expect(variant.weight).to eq 0.0
end
it "sets weight to correct decimal value if input is numerical string" do
variant.weight = "2"
variant.save!
expect(variant.weight).to eq 2.0
end
end
end
describe "unit value/description" do
let(:v) { Spree::Variant.new(unit_presentation: "small" ) }
@@ -739,42 +798,41 @@ RSpec.describe Spree::Variant do
describe "setting the variant's weight from the unit value" do
it "sets the variant's weight when unit is weight" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: 0)
p.update! variant_unit: 'weight', variant_unit_scale: 1
v.update! unit_value: 10, unit_description: 'foo'
v = create(:variant, weight: 0)
v.update!(
variant_unit: 'weight', variant_unit_scale: 1, unit_value: 10, unit_description: 'foo'
)
expect(v.reload.weight).to eq(0.01)
end
it "does nothing when unit is not weight" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: 123)
p.update! variant_unit: 'volume', variant_unit_scale: 1
v.update! unit_value: 10, unit_description: 'foo'
v = create(:variant, weight: 123, variant_unit: 'volume')
v.update! variant_unit: 'volume', variant_unit_scale: 1, unit_value: 10,
unit_description: 'foo'
expect(v.reload.weight).to eq(123)
end
it "does nothing when unit_value is not set" do
p = create(:simple_product, variant_unit: 'volume')
v = create(:variant, product: p, weight: 123)
p.update! variant_unit: 'weight', variant_unit_scale: 1
v = create(:variant, weight: 123, variant_unit: 'volume')
# Although invalid, this calls the before_validation callback, which would
# error if not handling unit_value == nil case
expect(v.update(unit_value: nil, unit_description: 'foo')).to be false
expect(
v.update(variant_unit: "weight", variant_unit_scale: 1, unit_value: nil,
unit_description: "foo")
).to be false
expect(v.reload.weight).to eq(123)
end
end
context "when the variant already has a value set" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) { create(:variant, product: p, unit_value: 5, unit_description: 'bar') }
let!(:v) {
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
unit_description: 'bar')
}
it "assigns the new option value" do
expect(v.unit_presentation).to eq "5g bar"
@@ -786,28 +844,30 @@ RSpec.describe Spree::Variant do
end
context "when the variant does not have a display_as value set" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) {
create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: '')
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
unit_description: 'bar', display_as: '')
}
it "requests the new value from OptionValueName" do
expect_any_instance_of(VariantUnits::OptionValueNamer)
.to receive(:name).exactly(1).times.and_call_original
v.update(unit_value: 10, unit_description: 'foo')
expect(v.unit_presentation).to eq "10g foo"
end
end
context "when the variant has a display_as value set" do
let!(:p) { create(:simple_product, variant_unit: 'weight', variant_unit_scale: 1) }
let!(:v) {
create(:variant, product: p, unit_value: 5, unit_description: 'bar', display_as: 'FOOS!')
create(:variant, variant_unit: 'weight', variant_unit_scale: 1, unit_value: 5,
unit_description: 'bar', display_as: 'FOOS!')
}
it "does not request the new value from OptionValueName" do
expect_any_instance_of(VariantUnits::OptionValueNamer).not_to receive(:name)
v.update!(unit_value: 10, unit_description: 'foo')
expect(v.unit_presentation).to eq("FOOS!")
end
end
@@ -873,12 +933,11 @@ RSpec.describe Spree::Variant do
end
describe "#ensure_unit_value" do
let(:product) { create(:product, variant_unit: "weight") }
let(:variant) { create(:variant, product_id: product.id) }
let(:variant) { create(:variant, variant_unit: "weight") }
context "when a product's variant_unit value is changed from weight to items" do
context "when variant_unit value is changed from weight to items" do
it "sets the variant's unit_value to 1" do
product.update(variant_unit: "items")
variant.update(variant_unit: "items")
expect(variant.unit_value).to eq 1
end
@@ -908,4 +967,40 @@ RSpec.describe Spree::Variant do
end
end
end
describe "after save callback" do
let(:variant) { create(:variant) }
it "updates units and unit_presenation when saved change to variant unit" do
variant.variant_unit = 'items'
variant.variant_unit_scale = nil
variant.variant_unit_name = 'loaf'
variant.save!
expect(variant.variant_unit_name).to eq 'loaf'
expect(variant.unit_presentation).to eq "1 loaf"
variant.update(variant_unit_name: 'bag')
expect(variant.variant_unit_name).to eq 'bag'
expect(variant.unit_presentation).to eq "1 bag"
variant.variant_unit = 'weight'
variant.variant_unit_scale = 1
variant.variant_unit_name = 'g'
variant.save!
expect(variant.variant_unit).to eq 'weight'
expect(variant.unit_presentation).to eq "1g"
variant.update(variant_unit: 'volume')
expect(variant.variant_unit).to eq 'volume'
expect(variant.unit_presentation).to eq "1L"
variant.update(display_as: 'My display')
expect(variant.unit_presentation).to eq "My display"
end
end
end

View File

@@ -69,22 +69,27 @@ RSpec.describe Sets::ProductSet do
unit_description: 'some description'
)
end
let(:variant) { product.variants.first }
let(:collection_hash) do
{
0 => {
id: product.id,
variant_unit: 'weight',
variant_unit_scale: 1
variants_attributes: [{
id: variant.id.to_s,
variant_unit: 'weight',
variant_unit_scale: 1
}]
}
}
end
it 'updates the product without error' do
expect(product_set.save).to eq true
expect(product_set.saved_count).to eq 1
# updating variant doesn't increment saved_count
# expect(product_set.saved_count).to eq 1
expect(product.reload.attributes).to include(
expect(variant.reload.attributes).to include(
'variant_unit' => 'weight'
)
@@ -305,8 +310,8 @@ RSpec.describe Sets::ProductSet do
{ id: product.variants.first.id.to_s }, # default variant unchanged
# omit ID for new variant
{
sku: "new sku", price: "5.00", unit_value: "5",
supplier_id: supplier.id, primary_taxon_id: create(:taxon).id
sku: "new sku", price: "5.00", unit_value: "5", variant_unit: "weight",
variant_unit_scale: 1, supplier_id: supplier.id, primary_taxon_id: create(:taxon).id
},
]
}
@@ -318,9 +323,12 @@ RSpec.describe Sets::ProductSet do
expect(product_set.errors).to be_empty
}.to change { product.variants.count }.by(1)
expect(product.variants.last.sku).to eq "new sku"
expect(product.variants.last.price).to eq 5.00
expect(product.variants.last.unit_value).to eq 5
variant = product.variants.last
expect(variant.sku).to eq "new sku"
expect(variant.price).to eq 5.00
expect(variant.unit_value).to eq 5
expect(variant.variant_unit).to eq "weight"
expect(variant.variant_unit_scale).to eq 1
end
context "variant has error" do

View File

@@ -3,53 +3,46 @@
require 'spec_helper'
RSpec.describe UnitPrice do
subject { UnitPrice.new(variant) }
let(:variant) { Spree::Variant.new }
let(:product) { instance_double(Spree::Product) }
before do
allow(variant).to receive(:product) { product }
allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL")
end
describe "#unit" do
context "metric" do
before do
allow(product).to receive(:variant_unit_scale) { 1.0 }
end
it "returns kg for weight" do
allow(product).to receive(:variant_unit) { "weight" }
expect(subject.unit).to eq("kg")
variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "weight")
expect(UnitPrice.new(variant).unit).to eq("kg")
end
it "returns L for volume" do
allow(product).to receive(:variant_unit) { "volume" }
expect(subject.unit).to eq("L")
variant = Spree::Variant.new(variant_unit_scale: 1.0, variant_unit: "volume")
expect(UnitPrice.new(variant).unit).to eq("L")
end
end
context "imperial" do
it "returns lbs" do
allow(product).to receive(:variant_unit_scale) { 453.6 }
allow(product).to receive(:variant_unit) { "weight" }
expect(subject.unit).to eq("lb")
variant = Spree::Variant.new(variant_unit_scale: 453.6, variant_unit: "weight")
expect(UnitPrice.new(variant).unit).to eq("lb")
end
end
context "items" do
it "returns items if no unit is specified" do
allow(product).to receive(:variant_unit_name) { nil }
allow(product).to receive(:variant_unit_scale) { nil }
allow(product).to receive(:variant_unit) { "items" }
expect(subject.unit).to eq("Item")
variant = Spree::Variant.new(variant_unit_name: nil, variant_unit_scale: nil,
variant_unit: "items")
expect(UnitPrice.new(variant).unit).to eq("Item")
end
it "returns the unit if a unit is specified" do
allow(product).to receive(:variant_unit_name) { "bunch" }
allow(product).to receive(:variant_unit_scale) { nil }
allow(product).to receive(:variant_unit) { "items" }
expect(subject.unit).to eq("bunch")
variant = Spree::Variant.new(variant_unit_name: "bunch", variant_unit_scale: nil,
variant_unit: "items")
expect(UnitPrice.new(variant).unit).to eq("bunch")
end
end
end
@@ -57,49 +50,47 @@ RSpec.describe UnitPrice do
describe "#denominator" do
context "metric" do
it "returns 0.5 for a 500g variant" do
allow(product).to receive(:variant_unit_scale) { 1.0 }
allow(product).to receive(:variant_unit) { "weight" }
variant.unit_value = 500
expect(subject.denominator).to eq(0.5)
variant = Spree::Variant.new(variant_unit_scale: 1.0, unit_value: 500,
variant_unit: "weight")
expect(UnitPrice.new(variant).denominator).to eq(0.5)
end
it "returns 2 for a 2kg variant" do
allow(product).to receive(:variant_unit_scale) { 1000 }
allow(product).to receive(:variant_unit) { "weight" }
variant.unit_value = 2000
expect(subject.denominator).to eq(2)
variant = Spree::Variant.new(variant_unit_scale: 1000, unit_value: 2000,
variant_unit: "weight")
expect(UnitPrice.new(variant).denominator).to eq(2)
end
it "returns 0.5 for a 500mL variant" do
allow(product).to receive(:variant_unit_scale) { 0.001 }
allow(product).to receive(:variant_unit) { "volume" }
variant.unit_value = 0.5
expect(subject.denominator).to eq(0.5)
variant = Spree::Variant.new(variant_unit_scale: 0.001, unit_value: 0.5,
variant_unit: "volume")
expect(UnitPrice.new(variant).denominator).to eq(0.5)
end
end
context "imperial" do
it "returns 2 for a 2 pound variant" do
allow(product).to receive(:variant_unit_scale) { 453.6 }
allow(product).to receive(:variant_unit) { "weight" }
variant.unit_value = 2 * 453.6
expect(subject.denominator).to eq(2)
variant = Spree::Variant.new(variant_unit_scale: 453.6, unit_value: 2 * 453.6,
variant_unit: "weight")
expect(UnitPrice.new(variant).denominator).to eq(2)
end
end
context "items" do
it "returns 1 if no unit is specified" do
allow(product).to receive(:variant_unit_scale) { nil }
allow(product).to receive(:variant_unit) { "items" }
variant.unit_value = 1
expect(subject.denominator).to eq(1)
variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 1, variant_unit: "items")
expect(UnitPrice.new(variant).denominator).to eq(1)
end
it "returns 2 for multi-item units" do
allow(product).to receive(:variant_unit_scale) { nil }
allow(product).to receive(:variant_unit) { "items" }
variant.unit_value = 2
expect(subject.denominator).to eq(2)
variant = Spree::Variant.new(variant_unit_scale: nil, unit_value: 2, variant_unit: "items")
expect(UnitPrice.new(variant).denominator).to eq(2)
end
end
end

View File

@@ -5,9 +5,8 @@ require 'spec_helper'
module VariantUnits
RSpec.describe OptionValueNamer do
describe "generating option value name" do
subject { OptionValueNamer.new(v) }
let(:v) { Spree::Variant.new }
let(:p) { Spree::Product.new }
let(:subject) { OptionValueNamer.new(v) }
it "when description is blank" do
allow(v).to receive(:unit_description) { nil }
@@ -40,18 +39,14 @@ module VariantUnits
describe "determining if a variant's value is scaled" do
it "returns true when the product has a scale" do
p = Spree::Product.new variant_unit_scale: 1000
v = Spree::Variant.new
allow(v).to receive(:product) { p }
v = Spree::Variant.new variant_unit_scale: 1000
subject = OptionValueNamer.new v
expect(subject.__send__(:value_scaled?)).to be true
end
it "returns false otherwise" do
p = Spree::Product.new
v = Spree::Variant.new
allow(v).to receive(:product) { p }
subject = OptionValueNamer.new v
expect(subject.__send__(:value_scaled?)).to be false
@@ -59,115 +54,109 @@ module VariantUnits
end
describe "generating option value's value and unit" do
let(:v) { Spree::Variant.new }
let(:subject) { OptionValueNamer.new v }
before do
allow(Spree::Config).to receive(:available_units).and_return("g,lb,oz,kg,T,mL,L,kL")
end
it "generates simple values" do
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 100 }
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
unit_value: 100)
expect(subject.__send__(:option_value_value_unit)).to eq [100, 'g']
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, 'g']
end
it "generates values when unit value is non-integer" do
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 123.45 }
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
unit_value: 123.45)
expect(subject.__send__(:option_value_value_unit)).to eq [123.45, 'g']
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [123.45, 'g']
end
it "returns a value of 1 when unit value equals the scale" do
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1000.0)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 1000.0 }
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1000.0,
unit_value: 1000.0)
expect(subject.__send__(:option_value_value_unit)).to eq [1, 'kg']
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'kg']
end
it "returns only values that are in the same measurement systems" do
p = double(:product, variant_unit: 'weight', variant_unit_scale: 1.0)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 500 }
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: 1.0,
unit_value: 500)
# 500g would convert to > 1 pound, but we don't want the namer to use
# pounds since it's in a different measurement system.
expect(subject.__send__(:option_value_value_unit)).to eq [500, 'g']
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [500, 'g']
end
it "generates values for all weight scales" do
[[1.0, 'g'], [28.35, 'oz'], [453.6, 'lb'], [1000.0, 'kg'],
[1_000_000.0, 'T']].each do |scale, unit|
p = double(:product, variant_unit: 'weight', variant_unit_scale: scale)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 10.0 * scale }
expect(subject.__send__(:option_value_value_unit)).to eq [10, unit]
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: scale,
unit_value: 10.0 * scale)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [10, unit]
end
end
it "generates values for all volume scales" do
[[0.001, 'mL'], [1.0, 'L'], [1000.0, 'kL']].each do |scale, unit|
p = double(:product, variant_unit: 'volume', variant_unit_scale: scale)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 3 * scale }
expect(subject.__send__(:option_value_value_unit)).to eq [3, unit]
v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: scale,
unit_value: 3 * scale)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [3, unit]
end
end
it "chooses the correct scale when value is very small" do
p = double(:product, variant_unit: 'volume', variant_unit_scale: 0.001)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 0.0001 }
expect(subject.__send__(:option_value_value_unit)).to eq [0.1, 'mL']
v = instance_double(Spree::Variant, variant_unit: 'volume', variant_unit_scale: 0.001,
unit_value: 0.0001)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [0.1, 'mL']
end
it "generates values for item units" do
%w(packet box).each do |unit|
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: unit)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 100 }
expect(subject.__send__(:option_value_value_unit)).to eq [100, unit.pluralize]
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: unit, unit_value: 100)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [100, unit.pluralize]
end
end
it "generates singular values for item units when value is 1" do
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: 'packet')
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
allow(v).to receive(:unit_value) { 1 }
expect(subject.__send__(:option_value_value_unit)).to eq [1, 'packet']
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: 'packet', unit_value: 1)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [1, 'packet']
end
it "returns [nil, nil] when unit value is not set" do
p = double(:product, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: 'foo')
allow(v).to receive(:product) { p }
allow(v).to receive(:unit_value) { nil }
expect(subject.__send__(:option_value_value_unit)).to eq [nil, nil]
v = instance_double(Spree::Variant, variant_unit: 'items', variant_unit_scale: nil,
variant_unit_name: 'foo', unit_value: nil)
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [nil, nil]
end
it "truncates value to 2 decimals maximum" do
oz_scale = 28.35
p = double(:product, variant_unit: 'weight', variant_unit_scale: oz_scale)
allow(v).to receive(:product) { p }
allow(p).to receive(:persisted?) { true }
v = instance_double(Spree::Variant, variant_unit: 'weight', variant_unit_scale: oz_scale,
unit_value: (12.5 * oz_scale).round(2))
# The unit_value is stored rounded to 2 decimals
allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) }
expect(subject.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6), 'oz']
# allow(v).to receive(:unit_value) { (12.5 * oz_scale).round(2) }
option_value_namer = OptionValueNamer.new v
expect(option_value_namer.__send__(:option_value_value_unit)).to eq [BigDecimal(12.5, 6),
'oz']
end
end
end

Some files were not shown because too many files have changed in this diff Show More