Merge pull request #11369 from Matt-Yorkley/product-taxon

[Product Refactor] Primary Taxon
This commit is contained in:
Gaetan Craig-Riou
2024-04-03 10:31:49 +11:00
committed by GitHub
56 changed files with 222 additions and 190 deletions

View File

@@ -48,7 +48,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
params = {
'q[name_cont]': $scope.q.query,
'q[supplier_id_eq]': $scope.q.producerFilter,
'q[primary_taxon_id_eq]': $scope.q.categoryFilter,
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter,
'q[s]': $scope.sorting,
import_date: $scope.q.importDateFilter,
page: $scope.page,
@@ -136,6 +136,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
on_hand: null
price: null
tax_category_id: null
category_id: null
DisplayProperties.setShowVariants product.id, true
@@ -217,7 +218,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
filters:
'q[name_cont]': $scope.q.query
'q[supplier_id_eq]': $scope.q.producerFilter
'q[primary_taxon_id_eq]': $scope.q.categoryFilter
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter
'q[s]': $scope.sorting
import_date: $scope.q.importDateFilter
page: $scope.page
@@ -332,9 +333,6 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_demand = product.on_demand
hasUpdatableProperty = true
if product.hasOwnProperty("category_id")
filteredProduct.primary_taxon_id = product.category_id
hasUpdatableProperty = true
if product.hasOwnProperty("inherits_properties")
filteredProduct.inherits_properties = product.inherits_properties
hasUpdatableProperty = true
@@ -375,6 +373,9 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("tax_category_id")
filteredVariant.tax_category_id = variant.tax_category_id
hasUpdatableProperty = true
if variant.hasOwnProperty("category_id")
filteredVariant.primary_taxon_id = variant.category_id
hasUpdatableProperty = true
if variant.hasOwnProperty("display_as")
filteredVariant.display_as = variant.display_as
hasUpdatableProperty = true

View File

@@ -68,7 +68,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
per_page: $scope.per_page,
'q[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_supplier_name_cont]': $scope.query,
'q[with_properties][]': $scope.activeProperties,
'q[primary_taxon_id_in_any][]': $scope.activeTaxons
'q[variants_primary_taxon_id_in_any][]': $scope.activeTaxons
}
$scope.searchKeypress = (e)->

View File

@@ -70,22 +70,7 @@ module Api
end
def search_params
permitted_search_params = params.slice :q, :page, :per_page
if permitted_search_params.key? :q
permitted_search_params[:q].slice!(*permitted_ransack_params)
end
permitted_search_params
end
def permitted_ransack_params
[
"#{[:name, :meta_keywords, :variants_display_as,
:variants_display_name, :supplier_name]
.join('_or_')}_cont",
:with_properties, :primary_taxon_id_in_any
]
params.slice :q, :page, :per_page
end
def distributor

View File

@@ -24,7 +24,6 @@ module ProductImport
def self.non_updatable_fields
{
category: :primary_taxon_id,
description: :description,
unit_type: :variant_unit_scale,
variant_unit_name: :variant_unit_name,
@@ -69,7 +68,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', 'primary_taxon_id'
'variant_unit_scale'
)
# Variant needs a product. Product needs to be assigned first in order for
# delegate to work. name= will fail otherwise.
@@ -398,7 +397,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', 'primary_taxon_id')
'variant_unit_scale')
)
check_on_hand_nil(entry, existing_variant)

View File

@@ -28,16 +28,11 @@ module Spree
acts_as_paranoid
after_create :ensure_standard_variant
around_destroy :destruction
after_save :update_units
searchable_attributes :supplier_id, :primary_taxon_id, :meta_keywords, :sku
searchable_associations :supplier, :properties, :primary_taxon, :variants
searchable_attributes :supplier_id, :meta_keywords, :sku
searchable_associations :supplier, :properties, :variants
searchable_scopes :active, :with_properties
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
belongs_to :primary_taxon, class_name: 'Spree::Taxon', optional: false, touch: true
has_one :image, class_name: "Spree::Image", as: :viewable, dependent: :destroy
@@ -77,7 +72,11 @@ 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
:shipping_category_id, :primary_taxon_id
after_create :ensure_standard_variant
around_destroy :destruction
after_save :update_units
scope :with_properties, ->(*property_ids) {
left_outer_joins(:product_properties).
@@ -284,6 +283,7 @@ module Spree
variant.unit_description = unit_description
variant.tax_category_id = tax_category_id
variant.shipping_category_id = shipping_category_id
variant.primary_taxon_id = primary_taxon_id
variants << variant
end

View File

@@ -7,9 +7,12 @@ module Spree
acts_as_nested_set dependent: :destroy
belongs_to :taxonomy, class_name: 'Spree::Taxonomy', touch: true
has_many :products, class_name: "Spree::Product", foreign_key: "primary_taxon_id",
has_many :variants, class_name: "Spree::Variant", foreign_key: "primary_taxon_id",
inverse_of: :primary_taxon, dependent: :restrict_with_error
has_many :products, through: :variants, dependent: nil
before_create :set_permalink
validates :name, presence: true
@@ -77,7 +80,7 @@ module Spree
taxons = Spree::Taxon
.select("DISTINCT spree_taxons.id, ents_and_vars.enterprise_id")
.joins(products: :variants)
.joins(:variants)
.joins("
INNER JOIN (#{ents_and_vars.to_sql}) AS ents_and_vars
ON spree_variants.id = ents_and_vars.variant_id")

View File

@@ -13,8 +13,8 @@ module Spree
acts_as_paranoid
searchable_attributes :sku, :display_as, :display_name
searchable_associations :product, :default_price
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id
searchable_associations :product, :default_price, :primary_taxon
searchable_scopes :active, :deleted
NAME_FIELDS = ["display_name", "display_as", "weight", "unit_value", "unit_description"].freeze
@@ -28,6 +28,7 @@ module Spree
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', optional: false
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true, optional: false
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
@@ -82,6 +83,7 @@ module Spree
before_validation :ensure_unit_value
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
before_validation :convert_variant_weight_to_decimal
before_validation :assign_related_taxon, if: ->(v) { v.primary_taxon.blank? }
before_save :assign_units, if: ->(variant) {
variant.new_record? || variant.changed_attributes.keys.intersection(NAME_FIELDS).any?
@@ -208,6 +210,10 @@ module Spree
private
def assign_related_taxon
self.primary_taxon ||= product.variants.last&.primary_taxon
end
def check_currency
return unless currency.nil?

View File

@@ -20,7 +20,7 @@ class ProductScopeQuery
product_query.
ransack(query_params_with_defaults).
result
result(distinct: true)
end
def find_product

View File

@@ -152,7 +152,7 @@ class ProductsReflex < ApplicationReflex
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result
.editable_products.merge(product_scope).ransack(ransack_query).result(distinct: true)
@pagy, @products = pagy(product_query.order(:name), items: @per_page, page: @page,
size: [1, 2, 2, 1])
end
@@ -173,7 +173,7 @@ class ProductsReflex < ApplicationReflex
if @search_term.present?
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
query.merge!(primary_taxon_id_in: @category_id) if @category_id.present?
query.merge!(variants_primary_taxon_id_in: @category_id) if @category_id.present?
query
end

View File

@@ -8,7 +8,6 @@ module Api
:thumb_url, :variants
has_one :supplier, key: :producer_id, embed: :id
has_one :primary_taxon, key: :category_id, embed: :id
def variants
ActiveModel::ArraySerializer.new(

View File

@@ -8,6 +8,8 @@ module Api
:display_as, :display_name, :name_to_display, :variant_overrides_count,
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name
has_one :primary_taxon, key: :category_id, embed: :id
def name
if object.full_name.present?
"#{object.name} - #{object.full_name}"

View File

@@ -9,8 +9,6 @@ class Api::ProductSerializer < ActiveModel::Serializer
has_many :variants, serializer: Api::VariantSerializer
has_one :primary_taxon, serializer: Api::TaxonSerializer
has_one :image, serializer: Api::ImageSerializer
has_one :supplier, serializer: Api::IdSerializer

View File

@@ -15,6 +15,22 @@ module OrderCycles
Spree::Product.where(id: stocked_products).group("spree_products.id")
end
# Joins on the first product variant to allow us to filter product by taxon. This is so
# enterprise can display product sorted by category in a custom order on their shopfront.
#
# Caveat, the category sorting won't work properly if there are multiple variant with different
# category for a given product.
#
def products_taxons_relation
Spree::Product.where(id: stocked_products).
joins("LEFT JOIN (
SELECT DISTINCT ON(product_id) id, product_id, primary_taxon_id
FROM spree_variants WHERE deleted_at IS NULL
) first_variant ON spree_products.id = first_variant.product_id").
select("spree_products.*, first_variant.primary_taxon_id").
group("spree_products.id, first_variant.primary_taxon_id")
end
def variants_relation
order_cycle.
variants_distributed_by(distributor).

View File

@@ -7,7 +7,7 @@ module PermittedAttributes
: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
:weight, :height, :width, :depth, :taxon_ids, :primary_taxon_id
]
end
end

View File

@@ -35,7 +35,7 @@ class ProductsRenderer
@products ||= begin
results = distributed_products.
products_relation.
products_taxons_relation.
order(Arel.sql(products_order))
filter_and_paginate(results).
@@ -54,7 +54,7 @@ class ProductsRenderer
def filter_and_paginate(query)
results = query.ransack(args[:q]).result
_pagy, paginated_results = pagy(
_pagy, paginated_results = pagy_arel(
results,
page: args[:page] || 1,
items: args[:per_page] || DEFAULT_PER_PAGE
@@ -78,7 +78,7 @@ class ProductsRenderer
distributor.preferred_shopfront_taxon_order.present?
distributor
.preferred_shopfront_taxon_order
.split(",").map { |id| "spree_products.primary_taxon_id=#{id} DESC" }
.split(",").map { |id| "first_variant.primary_taxon_id=#{id} DESC" }
.join(", ") + ", spree_products.name ASC, spree_products.id ASC"
else
"spree_products.name ASC, spree_products.id"

View File

@@ -46,18 +46,12 @@ module Sets
# variant.update( { price: xx.x } )
#
def update_product_attributes(attributes)
split_taxon_ids!(attributes)
product = find_model(@collection, attributes[:id])
return if product.nil?
update_product(product, attributes)
end
def split_taxon_ids!(attributes)
attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present?
end
def update_product(product, attributes)
return false unless update_product_only_attributes(product, attributes)

View File

@@ -30,7 +30,7 @@
%td.align-left
.content= product.supplier&.name
%td.align-left
.content= product.primary_taxon&.name
-# empty
%td.align-left
%td.align-left
.content= product.inherits_properties ? 'YES' : 'NO' #TODO: consider using https://github.com/RST-J/human_attribute_values, else use I18n.t (also below)

View File

@@ -42,7 +42,7 @@
%td.align-left
.content= variant.product.supplier&.name # same as product
%td.align-left
-# empty
.content= variant.primary_taxon&.name
%td.align-left
.content= variant.tax_category&.name || "None" # TODO: convert to dropdown, else translate hardcoded string.
%td.align-left

View File

@@ -27,8 +27,6 @@
= f.text_field :variant_unit_name, {placeholder: t('admin.products.unit_name_placeholder')}
= f.error_message_on :variant_unit_name
= render 'spree/admin/products/primary_taxon_form', f: f
= f.field_container :supplier do
= f.label :supplier, t(:spree_admin_supplier)
%br

View File

@@ -1,6 +1,6 @@
= f.field_container :primary_taxon do
= f.label :primary_taxon, t('.product_category')
= f.label :primary_taxon_id, t('.product_category')
%span.required *
%br
= f.collection_select(:primary_taxon_id, Spree::Taxon.order(:name), :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
= f.error_message_on :primary_taxon
= f.error_message_on :primary_taxon_id

View File

@@ -24,7 +24,6 @@
%td.on_demand{ 'ng-show' => 'columns.on_demand.visible' }
%input.field{ 'ng-model' => 'product.on_demand', :name => 'on_demand', 'ofn-track-product' => 'on_demand', :type => 'checkbox', 'ng-hide' => 'hasVariants(product)' }
%td.category{ 'ng-if' => 'columns.category.visible' }
%input.fullwidth{ :type => 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'product.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-product' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' }
%td.tax_category{ 'ng-if' => 'columns.tax_category.visible' }
%td.inherits_properties{ 'ng-show' => 'columns.inherits_properties.visible' }
%input{ 'ng-model' => 'product.inherits_properties', :name => 'inherits_properties', 'ofn-track-product' => 'inherits_properties', type: "checkbox" }

View File

@@ -21,6 +21,7 @@
%td{ 'ng-show' => 'columns.on_demand.visible' }
%input.field{ 'ng-model' => 'variant.on_demand', :name => 'variant_on_demand', 'ofn-track-variant' => 'on_demand', :type => 'checkbox' }
%td{ 'ng-show' => 'columns.category.visible' }
%input.fullwidth{ type: 'text', id: "p{{product.id}}_category_id", 'ng-model' => 'variant.category_id', 'ofn-taxon-autocomplete' => '', 'ofn-track-variant' => 'category_id', 'multiple-selection' => 'false', placeholder: 'Category' }
%td{ 'ng-show' => 'columns.tax_category.visible' }
%select.select2{ name: 'variant_tax_category_id', "ofn-track-variant": 'tax_category_id', "ng-model": 'variant.tax_category_id', "ng-options": 'tax_category.id as tax_category.name for tax_category in tax_categories' }
%option{ value: '' }= t(:none)

View File

@@ -72,4 +72,8 @@
= 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('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" })
.clear

View File

@@ -1,15 +1,18 @@
# frozen_string_literal: true
require 'pagy/extras/arel'
require 'pagy/extras/items'
require 'pagy/extras/overflow'
# Pagy Variables
# See https://ddnexus.github.io/pagy/api/pagy#variables
Pagy::DEFAULT[:items] = 100
# Items extra: Allow the client to request a custom number of items per page with an optional selector UI
# See https://ddnexus.github.io/pagy/extras/items
require 'pagy/extras/items'
Pagy::DEFAULT[:items_param] = :per_page
Pagy::DEFAULT[:max_items] = 100
# For handling requests for non-existant pages eg: page 35 when there are only 4 pages of results
require 'pagy/extras/overflow'
Pagy::DEFAULT[:overflow] = :empty_page

View File

@@ -72,6 +72,9 @@ en:
variant_unit: "Variant Unit"
variant_unit_name: "Variant Unit Name"
unit_value: "Unit value"
spree/variant:
primary_taxon: "Product Category"
shipping_category_id: "Shipping Category"
spree/credit_card:
base: "Credit Card"
number: "Number"
@@ -1246,6 +1249,7 @@ en:
open_date: "Open Date"
close_date: "Close Date"
display_ordering_in_shopfront: "Display ordering in shopfront:"
shopfront_sort_by_product: "By product"
shopfront_sort_by_category: "By category"
shopfront_sort_by_producer: "By producer"
shopfront_sort_by_category_placeholder: "Category"

View File

@@ -0,0 +1,5 @@
class AddTaxonsToVariants < ActiveRecord::Migration[7.0]
def change
add_reference :spree_variants, :primary_taxon, foreign_key: { to_table: :spree_taxons }
end
end

View File

@@ -0,0 +1,5 @@
class RemoveTaxonConstraint < ActiveRecord::Migration[7.0]
def change
change_column_null :spree_products, :primary_taxon_id, true
end
end

View File

@@ -0,0 +1,15 @@
class MigrateProductTaxons < ActiveRecord::Migration[7.0]
def up
migrate_primary_taxon
end
def migrate_primary_taxon
ActiveRecord::Base.connection.execute(<<-SQL
UPDATE spree_variants
SET primary_taxon_id = spree_products.primary_taxon_id
FROM spree_products
WHERE spree_variants.product_id = spree_products.id
SQL
)
end
end

View File

@@ -698,7 +698,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_13_044159) do
t.float "variant_unit_scale"
t.string "variant_unit_name", limit: 255
t.text "notes"
t.integer "primary_taxon_id", null: false
t.integer "primary_taxon_id"
t.boolean "inherits_properties", default: true, null: false
t.string "sku", limit: 255, default: "", null: false
t.index ["deleted_at"], name: "index_products_on_deleted_at"
@@ -979,6 +979,8 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_13_044159) do
t.datetime "updated_at", default: -> { "now()" }, null: false
t.bigint "tax_category_id"
t.bigint "shipping_category_id"
t.bigint "primary_taxon_id"
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"
t.index ["sku"], name: "index_spree_variants_on_sku"
@@ -1225,6 +1227,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_13_044159) do
add_foreign_key "spree_variants", "spree_products", column: "product_id", name: "spree_variants_product_id_fk"
add_foreign_key "spree_variants", "spree_shipping_categories", column: "shipping_category_id"
add_foreign_key "spree_variants", "spree_tax_categories", column: "tax_category_id"
add_foreign_key "spree_variants", "spree_taxons", column: "primary_taxon_id"
add_foreign_key "spree_zone_members", "spree_zones", column: "zone_id", name: "spree_zone_members_zone_id_fk"
add_foreign_key "subscription_line_items", "spree_variants", column: "variant_id", name: "subscription_line_items_variant_id_fk"
add_foreign_key "subscription_line_items", "subscriptions", name: "subscription_line_items_subscription_id_fk"

View File

@@ -65,26 +65,25 @@ class SuppliedProductBuilder < DfcBuilder
Spree::Product.new(
name: supplied_product.name,
description: supplied_product.description,
price: 0, # will be in DFC Offer
primary_taxon: taxon(supplied_product)
price: 0 # will be in DFC Offer
).tap do |product|
QuantitativeValueBuilder.apply(supplied_product.quantity, product)
product.ensure_standard_variant
product.variants.first.primary_taxon = taxon(supplied_product)
end
end
def self.apply(supplied_product, variant)
variant.product.assign_attributes(
description: supplied_product.description,
primary_taxon: taxon(supplied_product)
)
variant.product.assign_attributes(description: supplied_product.description)
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
end
def self.product_type(variant)
taxon_dfc_id = variant.product.primary_taxon&.dfc_id
taxon_dfc_id = variant.primary_taxon&.dfc_id
DfcProductTypeFactory.for(taxon_dfc_id)
end

View File

@@ -10,11 +10,10 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
:product_with_image,
id: 90_000,
supplier: enterprise, name: "Pesto", description: "Basil Pesto",
variants: [variant],
primary_taxon: taxon
variants: [variant]
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1) }
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, primary_taxon: taxon) }
let(:taxon) {
build(
:taxon,
@@ -114,7 +113,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
product = Spree::Product.find(product_id)
expect(product.name).to eq "Apple"
expect(product.variants).to eq [variant]
expect(product.primary_taxon).to eq(non_local_vegetable)
expect(product.variants.first.primary_taxon).to eq(non_local_vegetable)
# Creates a variant for existing product
supplied_product[:'ofn:spree_product_id'] = product_id
@@ -244,7 +243,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
}.to change { variant.description }.to("DFC-Pesto updated")
.and change { variant.display_name }.to("Pesto novo")
.and change { variant.unit_value }.to(17)
.and change { variant.product.primary_taxon }.to(drink_taxon)
.and change { variant.primary_taxon }.to(drink_taxon)
end
end
end

View File

@@ -7,12 +7,10 @@ describe SuppliedProductBuilder do
subject(:builder) { described_class }
let(:variant) {
build(:variant, id: 5, product: spree_product)
build(:variant, id: 5, product: spree_product, primary_taxon: taxon)
}
let(:spree_product) {
create(:product, id: 6, supplier:).tap do |p|
p.primary_taxon = taxon
end
create(:product, id: 6, supplier:)
}
let(:supplier) {
build(:supplier_enterprise, id: 7)
@@ -121,7 +119,7 @@ describe SuppliedProductBuilder do
it "assigns the taxon matching the DFC product type" do
product = builder.import_product(supplied_product)
expect(product.primary_taxon).to eq(taxon)
expect(product.variants.first.primary_taxon).to eq(taxon)
end
end
end
@@ -191,58 +189,7 @@ describe SuppliedProductBuilder do
imported_product = imported_variant.product
expect(imported_product.id).to eq(spree_product.id)
expect(imported_product.description).to eq("Better Awesome tomato")
expect(imported_product.primary_taxon).to eq(new_taxon)
end
it "adds a new variant" do
expect(imported_variant.id).to be_nil
expect(imported_variant.product).to eq(spree_product)
expect(imported_variant.display_name).to eq("Tomato")
expect(imported_variant.unit_value).to eq(2000)
end
end
context "with spree_product_uri supplied" do
let(:imported_variant) { builder.import_variant(supplied_product, supplier) }
let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK }
let!(:new_taxon) {
create(
:taxon,
name: "Soft Drink",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#soft-drink"
)
}
context "when spree_product_uri match the server host" do
let(:supplied_product) do
variant.save! # referenced in spree_product_id
DfcProvider::SuppliedProduct.new(
"https://example.net/tomato",
name: "Tomato",
description: "Better Awesome tomato",
quantity: DataFoodConsortium::Connector::QuantitativeValue.new(
unit: DfcLoader.connector.MEASURES.KILOGRAM,
value: 2,
),
productType: product_type,
spree_product_uri: "http://test.host/api/dfc/enterprises/7?spree_product_id=6"
)
end
it "update an existing Spree::Product" do
imported_product = imported_variant.product
expect(imported_product.id).to eq(spree_product.id)
expect(imported_product.description).to eq("Better Awesome tomato")
expect(imported_product.primary_taxon).to eq(new_taxon)
end
it "adds a new variant" do
expect(imported_variant.id).to be_nil
expect(imported_variant.product).to eq(spree_product)
expect(imported_variant.display_name).to eq("Tomato")
expect(imported_variant.unit_value).to eq(2000)
end
expect(imported_variant.primary_taxon).to eq(new_taxon)
end
context "when spree_product_uri doesn't match the server host" do

View File

@@ -17,7 +17,7 @@ module Reporting
producer_suburb: proc { |variant| variant.product.supplier.address.city },
product: proc { |variant| variant.product.name },
product_properties: proc { |v| v.product.properties.map(&:name).join(", ") },
taxons: proc { |variant| variant.product.primary_taxon.name },
taxons: proc { |variant| variant.primary_taxon.name },
variant_value: proc { |variant| variant.full_name },
price: proc { |variant| variant.price },
group_buy_unit_quantity: proc { |variant| variant.product.group_buy_unit_size },

View File

@@ -32,7 +32,7 @@ module Reporting
total: proc { |_variant| '' },
gst: proc { |variant| gst(variant) },
grower: proc { |variant| grower_and_method(variant) },
taxon: proc { |variant| variant.product.primary_taxon.name }
taxon: proc { |variant| variant.primary_taxon.name }
}
end

View File

@@ -126,7 +126,7 @@ module Api
context "with taxon filters" do
it "filters by taxon" do
api_get :products, id: order_cycle.id, distributor: distributor.id,
q: { primary_taxon_id_in_any: [taxon2.id] }
q: { variants_primary_taxon_id_in_any: [taxon2.id] }
expect(product_ids).to include product2.id, product3.id
expect(product_ids).not_to include product1.id, product4.id

View File

@@ -25,6 +25,7 @@ describe Api::V0::ProductsController, type: :controller do
end
context "as a normal user" do
let(:taxon) { create(:taxon) }
let(:attachment) { fixture_file_upload("thinking-cat.jpg") }
before do
@@ -34,9 +35,11 @@ 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)
product.variants.create!(unit_value: "1", unit_description: "thing", price: 1,
primary_taxon: taxon)
product.variants.first.images.create!(attachment:)
product.set_property("spree", "rocks")
api_get :show, id: product.to_param
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
@@ -117,8 +120,7 @@ describe Api::V0::ProductsController, type: :controller do
expect(response.status).to eq(422)
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", "primary_taxon", "supplier", "variant_unit",
"price"])
expect(errors.keys).to match_array(["name", "supplier", "variant_unit", "price"])
end
it "can update a product" do
@@ -266,7 +268,8 @@ describe Api::V0::ProductsController, type: :controller do
end
it "filters results by product category" do
api_get :bulk_products, { page: 1, per_page: 15, q: { primary_taxon_id_eq: taxon.id } },
api_get :bulk_products,
{ page: 1, per_page: 15, q: { variants_primary_taxon_id_eq: taxon.id } },
format: :json
expect(returned_product_ids).to eq [product3.id, product2.id]
end

View File

@@ -127,6 +127,7 @@ describe Api::V0::VariantsController, type: :controller do
let(:product) { create(:product) }
let(:variant) { product.variants.first }
let(:taxon) { create(:taxon) }
let!(:variant2) { create(:variant, product:) }
context "deleted variants" do
@@ -144,7 +145,7 @@ 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" },
unit_description: "L", price: "1", primary_taxon_id: taxon.id },
product_id: variant.product.id
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)

View File

@@ -93,6 +93,7 @@ describe Spree::Admin::ProductsController, type: :controller do
variant_unit_name: nil
)
end
let!(:taxon) { create(:taxon) }
before { controller_login_as_enterprise_user([producer]) }
@@ -111,7 +112,8 @@ describe Spree::Admin::ProductsController, type: :controller do
"price" => "5.0",
"unit_value" => 4,
"unit_description" => "",
"display_name" => "name"
"display_name" => "name",
"primary_taxon_id" => taxon.id
}
]
}

View File

@@ -11,7 +11,9 @@ module Spree
describe "deleted variants" do
let(:product) { create(:product, name: 'Product A') }
let(:deleted_variant) do
deleted_variant = product.variants.create(unit_value: "2", price: 1)
deleted_variant = product.variants.create(
unit_value: "2", price: 1, primary_taxon: create(:taxon)
)
deleted_variant.delete
deleted_variant
end

View File

@@ -3,13 +3,18 @@
FactoryBot.define do
factory :base_product, class: Spree::Product do
sequence(:name) { |n| "Product ##{n} - #{Kernel.rand(9999)}" }
transient do
primary_taxon { nil }
end
primary_taxon_id { |p| (p.primary_taxon || Spree::Taxon.first || create(:taxon)).id }
description { generate(:random_description) }
price { 19.99 }
sku { 'ABC' }
deleted_at { nil }
supplier { Enterprise.is_primary_producer.first || FactoryBot.create(:supplier_enterprise) }
primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) }
unit_value { 1 }
unit_description { '' }
@@ -48,6 +53,7 @@ FactoryBot.define do
on_demand { false }
on_hand { 5 }
end
after(:create) do |product, evaluator|
product.variants.first.on_demand = evaluator.on_demand
product.variants.first.on_hand = evaluator.on_hand

View File

@@ -11,7 +11,8 @@ FactoryBot.define do
width { generate(:random_float) }
depth { generate(:random_float) }
product { |p| p.association(:base_product) }
primary_taxon { Spree::Taxon.first || FactoryBot.create(:taxon) }
product { |p| p.association(:product) }
# ensure stock item will be created for this variant
before(:create) { create(:stock_location) if Spree::StockLocation.count.zero? }
@@ -22,7 +23,6 @@ FactoryBot.define do
on_hand { 5 }
end
product { |p| p.association(:product) }
unit_value { 1 }
unit_description { '' }

View File

@@ -803,8 +803,8 @@ describe "AdminProductEditCtrl", ->
expect(product).toEqual
id: 123
variants: [
{id: -1, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null}
{id: -2, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null}
{id: -1, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: null}
{id: -2, price: null, unit_value: null, tax_category_id: null, unit_description: null, on_demand: false, on_hand: null, display_as: null, display_name: null, category_id: null}
]
it "shows the variant(s)", ->

View File

@@ -43,7 +43,7 @@ module Reporting
allow(variant).to receive_message_chain(:product, :name).and_return("Product Name")
allow(variant).to receive_message_chain(:product, :properties)
.and_return [double(name: "property1"), double(name: "property2")]
allow(variant).to receive_message_chain(:product, :primary_taxon).
allow(variant).to receive_message_chain(:primary_taxon).
and_return double(name: "taxon1")
allow(variant).to receive_message_chain(:product, :group_buy_unit_size).and_return(21)
allow(subject).to receive(:query_result).and_return [variant]

View File

@@ -12,6 +12,7 @@ describe Enterprise do
describe "with a supplied product" do
let(:product) { create(:simple_product, supplier: enterprise, primary_taxon_id: taxon.id) }
let(:variant) { product.variants.first }
let(:property) { product.product_properties.last }
let(:producer_property) { enterprise.producer_properties.last }
@@ -20,9 +21,9 @@ describe Enterprise do
enterprise.set_producer_property 'Biodynamic', 'ASDF 4321'
end
it "touches enterprise when a taxon on that product changes" do
it "touches enterprise when a taxon on that variant changes" do
expect {
later { product.update(primary_taxon_id: taxon2.id) }
later { variant.update(primary_taxon_id: taxon2.id) }
}.to change { enterprise.reload.updated_at }
end
@@ -47,6 +48,7 @@ describe Enterprise do
describe "with a distributed product" do
let(:product) { create(:simple_product, primary_taxon_id: taxon.id) }
let(:variant) { product.variants.first }
let(:oc) {
create(:simple_order_cycle, distributors: [enterprise],
variants: [product.variants.first])
@@ -63,9 +65,9 @@ describe Enterprise do
context "with an order cycle" do
before { oc }
it "touches enterprise when a taxon on that product changes" do
it "touches enterprise when a taxon on that variant changes" do
expect {
later { product.update(primary_taxon_id: taxon2.id) }
later { variant.update(primary_taxon_id: taxon2.id) }
}.to change { enterprise.reload.updated_at }
end

View File

@@ -284,7 +284,7 @@ describe ProductImport::ProductImporter do
carrots = Spree::Product.find_by(name: 'Good Carrots')
expect(carrots.on_hand).to eq 5
expect(carrots.variants.first.price).to eq 3.20
expect(carrots.primary_taxon.name).to eq "Vegetables"
expect(carrots.variants.first.primary_taxon.name).to eq "Vegetables"
expect(carrots.variants.first.shipping_category).to eq shipping_category
expect(carrots.supplier).to eq enterprise
expect(carrots.variants.first.unit_presentation).to eq "500g"
@@ -562,7 +562,7 @@ describe ProductImport::ProductImporter do
let(:csv_data) {
CSV.generate do |csv|
csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type"]
csv << ["Beetroot", enterprise3.name, "Meat", "5", "3.50", "500", "g"]
csv << ["Beetroot", enterprise3.name, "Vegetables", "5", "3.50", "500", "Kg"]
csv << ["Tomato", enterprise3.name, "Vegetables", "6", "5.50", "500", "Kg"]
end
}

View File

@@ -155,7 +155,6 @@ module Spree
describe "associations" do
it { is_expected.to belong_to(:supplier).required }
it { is_expected.to belong_to(:primary_taxon).required }
end
describe "validations and defaults" do
@@ -167,10 +166,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) }
it "requires a primary taxon" do
expect(build(:simple_product, primary_taxon: nil)).not_to be_valid
end
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',
@@ -229,10 +224,11 @@ module Spree
context "saving a new product" do
let!(:product){ Spree::Product.new }
let!(:shipping_category){ create(:shipping_category) }
let!(:taxon){ create(:taxon) }
before do
create(:stock_location)
product.primary_taxon = create(:taxon)
product.primary_taxon_id = taxon.id
product.supplier = create(:supplier_enterprise)
product.name = "Product1"
product.variant_unit = "weight"
@@ -248,6 +244,7 @@ module Spree
standard_variant = product.variants.reload.first
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
end
end
@@ -324,7 +321,7 @@ module Spree
let(:product) { create(:simple_product) }
describe "touching affected enterprises when the product is deleted" do
let(:product) { create(:simple_product) }
let(:product) { create(:simple_product, supplier: distributor) }
let(:supplier) { product.supplier }
let(:distributor) { create(:distributor_enterprise) }
let!(:oc) {

View File

@@ -41,11 +41,11 @@ module Spree
let!(:taxon1) { create(:taxon) }
let!(:taxon2) { create(:taxon) }
let!(:product) { create(:simple_product, primary_taxon: taxon1) }
let(:variant) { product.variants.first }
it "is touched when assignment of primary_taxon on a product changes" do
it "is touched when assignment of primary_taxon on a variant changes" do
expect do
product.primary_taxon = taxon2
product.save
variant.update(primary_taxon: taxon2)
end.to change { taxon2.reload.updated_at }
end
end

View File

@@ -12,7 +12,7 @@ describe ProductScopeQuery do
before { current_api_user.enterprise_roles.create(enterprise: supplier2) }
describe 'bulk update' do
describe '#bulk_products' do
let!(:product3) { create(:product, supplier: supplier2) }
it "returns a list of products" do
@@ -28,12 +28,29 @@ describe ProductScopeQuery do
expect(subject).not_to include(product2, product3)
end
it "filters results by product category" do
subject = ProductScopeQuery
.new(current_api_user, { q: { primary_taxon_id_eq: taxon.id } }).bulk_products
describe "by variant category" do
it "filters results by product category" do
create(:variant, product: product2, primary_taxon: taxon)
expect(subject).to include(product, product2)
expect(subject).not_to include(product3)
subject = ProductScopeQuery
.new(current_api_user, { q: { variants_primary_taxon_id_eq: taxon.id } })
.bulk_products
expect(subject).to match_array([product, product2])
expect(subject).not_to include(product3)
end
context "with mutiple variant in the same category" do
it "doesn't duplicate products" do
create(:variant, product: product2, primary_taxon: taxon)
subject = ProductScopeQuery
.new(current_api_user, { q: { variants_primary_taxon_id_eq: taxon.id } })
.bulk_products
expect(subject).to match_array([product, product2])
end
end
end
it "filters results by import_date" do

View File

@@ -15,7 +15,7 @@ describe ProductsReflex, type: :reflex, feature: :admin_style_v3 do
end
describe '#fetch' do
subject{ build_reflex(method_name: :fetch, **context) }
subject { build_reflex(method_name: :fetch, **context) }
describe "sorting" do
let!(:product_z) { create(:simple_product, name: "Zucchini") }
@@ -34,6 +34,27 @@ describe ProductsReflex, type: :reflex, feature: :admin_style_v3 do
end
end
describe '#filter' do
context "when filtering by category" do
let!(:product_a) { create(:simple_product, name: "Apples") }
let!(:product_z) do
create(:simple_product, name: "Zucchini").tap do |p|
p.variants.first.update(primary_taxon: category_c)
end
end
let(:category_c) { create(:taxon, name: "Category 1") }
it "returns product with a variant matching the given category" do
# Add a second variant to test we are not returning duplicate product
product_z.variants << create(:variant, primary_taxon: category_c)
reflex = run_reflex(:filter, params: { category_id: category_c.id } )
expect(reflex.get(:products).to_a).to eq([product_z])
end
end
end
describe '#bulk_update' do
let!(:variant_a1) {
product_a.variants.first.tap{ |v|

View File

@@ -28,7 +28,7 @@ describe Api::ProductSerializer do
it "serializes various attributes" do
expect(serializer.serializable_hash.keys).to eq [
:id, :name, :meta_keywords, :group_buy, :notes, :description, :description_html,
:properties_with_values, :variants, :primary_taxon, :image, :supplier
:properties_with_values, :variants, :image, :supplier
]
end

View File

@@ -162,12 +162,6 @@ describe ProductsRenderer do
expect(products_renderer.products_json).to include "998.0"
end
it "includes the primary taxon" do
taxon = create(:taxon)
allow_any_instance_of(Spree::Product).to receive(:primary_taxon).and_return taxon
expect(products_renderer.products_json).to include taxon.name
end
it "loads tag_list for variants" do
VariantOverride.create(variant:, hub: distributor, tag_list: 'lalala')
expect(products_renderer.products_json).to include "[\"lalala\"]"

View File

@@ -344,7 +344,6 @@ describe '
within "tr#p_#{p.id}" do
expect(page).to have_field "product_name", with: p.name
expect(page).to have_select "producer_id", selected: s1.name
expect(page).to have_select2 "p#{p.id}_category_id", selected: t2.name
expect(page).to have_select "variant_unit_with_scale", selected: "Volume (L)"
expect(page).to have_checked_field "inherits_properties"
expect(page).to have_field "product_sku", with: p.sku
@@ -352,7 +351,6 @@ describe '
fill_in "product_name", with: "Big Bag Of Potatoes"
select s2.name, from: 'producer_id'
select "Weight (kg)", from: "variant_unit_with_scale"
select2_select t1.name, from: "p#{p.id}_category_id"
uncheck "inherits_properties"
fill_in "product_sku", with: "NEW SKU"
end
@@ -365,7 +363,6 @@ describe '
expect(p.supplier).to eq s2
expect(p.variant_unit).to eq "weight"
expect(p.variant_unit_scale).to eq 1000 # Kg
expect(p.primary_taxon.permalink).to eq t1.permalink
expect(p.inherits_properties).to be false
expect(p.sku).to eq "NEW SKU"
end

View File

@@ -143,7 +143,7 @@ describe '
expect(product.variants.first.unit_value).to eq(5000)
expect(product.variants.first.unit_description).to eq("")
expect(product.variant_unit_name).to eq("")
expect(product.primary_taxon_id).to eq(taxon.id)
expect(product.variants.first.primary_taxon_id).to eq(taxon.id)
expect(product.variants.first.price.to_s).to eq('19.99')
expect(product.on_hand).to eq(5)
expect(product.variants.first.tax_category_id).to eq(tax_category.id)

View File

@@ -358,15 +358,15 @@ describe '
let(:taxon) { create(:taxon, name: 'Taxon Name') }
let(:product1) {
create(:simple_product, name: "Product Name", price: 100, supplier:,
primary_taxon: taxon)
primary_taxon_id: taxon.id)
}
let(:product2) {
create(:simple_product, name: "Product 2", price: 99.0, variant_unit: 'weight',
variant_unit_scale: 1, unit_value: '100', supplier:,
primary_taxon: taxon, sku: "product_sku")
primary_taxon_id: taxon.id, sku: "product_sku")
}
let(:variant1) { product1.variants.first }
let(:variant2) { create(:variant, product: product1, price: 80.0) }
let(:variant2) { create(:variant, product: product1, price: 80.0, primary_taxon: taxon) }
let(:variant3) { product2.variants.first }
before do
@@ -396,17 +396,17 @@ describe '
expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city,
"Product Name",
product1.properties.map(&:presentation).join(", "),
product1.primary_taxon.name, "1g", "100.0",
taxon.name, "1g", "100.0",
"none", "", "sku1", "No", "10"]
expect(page).to have_table_row [product1.supplier.name, product1.supplier.address.city,
"Product Name",
product1.properties.map(&:presentation).join(", "),
product1.primary_taxon.name, "1g", "80.0",
taxon.name, "1g", "80.0",
"none", "", "sku2", "No", "20"]
expect(page).to have_table_row [product2.supplier.name, product1.supplier.address.city,
"Product 2",
product1.properties.map(&:presentation).join(", "),
product2.primary_taxon.name, "100g", "99.0",
taxon.name, "100g", "99.0",
"none", "", "product_sku", "No", "9"]
end

View File

@@ -9,6 +9,8 @@ describe '
include AuthenticationHelper
include WebHelper
let!(:taxon) { create(:taxon) }
describe "new variant" do
it "creating a new variant" do
# Given a product with a unit-related option type
@@ -21,6 +23,7 @@ describe '
fill_in 'unit_value_human', with: '1'
fill_in 'variant_unit_description', with: 'foo'
select taxon.name, from: "variant_primary_taxon_id"
click_button 'Create'
# Then the variant should have been created
@@ -61,6 +64,7 @@ describe '
# Expect variant_weight to accept 3 decimal places
fill_in 'variant_weight', with: '1.234'
fill_in 'unit_value_human', with: 1
select taxon.name, from: "variant_primary_taxon_id"
click_button 'Create'
# Then the variant should have been created

View File

@@ -53,8 +53,9 @@ describe "Shops caching", caching: true do
let!(:property) { create(:property, presentation: "Cached Property") }
let!(:property2) { create(:property, presentation: "New Property") }
let!(:product) {
create(:product, primary_taxon: taxon, properties: [property])
create(:product, primary_taxon_id: taxon.id, properties: [property])
}
let(:variant) { product.variants.first }
let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first }
let(:test_domain) {
@@ -92,7 +93,7 @@ describe "Shops caching", caching: true do
expect(page).to have_content taxon.name
expect(page).to have_content property.presentation
product.update_attribute(:primary_taxon, taxon2)
variant.update_attribute(:primary_taxon, taxon2)
product.update_attribute(:properties, [property2])
visit enterprise_shop_path(distributor)