Added GTIN and Condition to variant for structured data use (#6097)
* Adding Conditions and GTIN to products and variant Summary This commit adds GTIN and Conditions to products to allow a more efficient integration of structured data and the related services such as Google Shopping or Facebook Marketplaces. Changes - Add Conditions and GTIN to products and variants - Added the fields to the Admin Interface - Add the values to API - Provides samples for GTIN (format EAN 8) and Condition (New) to all sample products. - Added Testing of the related fields - feat(i18n): Differentiate GTIN and Condition labels for product and variant forms. * Update solidus-api.oas.yml Reflect the changes to the conditions values. --------- Co-authored-by: Thomas von Deyen <thomas@vondeyen.com>
This commit is contained in:
parent
a85e0becf7
commit
acd092fc90
@ -36,8 +36,14 @@
|
||||
<%= render component("ui/forms/field").text_field(f, :meta_title) %>
|
||||
<%= render component("ui/forms/field").text_field(f, :meta_description) %>
|
||||
<%= render component("ui/forms/field").text_area(f, :meta_keywords) %>
|
||||
<%= render component("ui/forms/field").text_field(f, :gtin) %>
|
||||
<%= render component("ui/forms/field").select(
|
||||
f,
|
||||
:condition,
|
||||
condition_options,
|
||||
include_blank: t('spree.unset'),
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render component('ui/panel').new(title: "Media") do |panel| %>
|
||||
<% panel.with_action(
|
||||
name: t(".manage_images"),
|
||||
|
@ -25,4 +25,10 @@ class SolidusAdmin::Products::Show::Component < SolidusAdmin::BaseComponent
|
||||
["#{_1} (#{_2})", _3]
|
||||
end
|
||||
end
|
||||
|
||||
def condition_options
|
||||
@condition_options ||= Spree::Variant.conditions.map do |key, value|
|
||||
[t("spree.condition.#{key}"), value]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ module Spree
|
||||
preference :product_property_attributes, :array, default: [:id, :product_id, :property_id, :value, :property_name]
|
||||
|
||||
preference :variant_attributes, :array, default: [
|
||||
:id, :name, :sku, :weight, :height, :width, :depth, :is_master,
|
||||
:id, :name, :sku, :gtin, :condition, :weight, :height, :width, :depth, :is_master,
|
||||
:slug, :description, :track_inventory
|
||||
]
|
||||
|
||||
|
@ -74,6 +74,8 @@ paths:
|
||||
product:
|
||||
name: The Majestic Product
|
||||
price: '19.99'
|
||||
gtin: 12345678
|
||||
condition: new
|
||||
shipping_category_id: 8
|
||||
product_properties_attributes:
|
||||
- property_name: fabric
|
||||
@ -86,6 +88,8 @@ paths:
|
||||
- price: 19.99
|
||||
cost_price: 17
|
||||
sku: SKU-3
|
||||
gtin: 12345678
|
||||
condition: new
|
||||
track_inventory: true
|
||||
options:
|
||||
- name: size
|
||||
|
@ -179,6 +179,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div data-hook="admin_product_form_gtin">
|
||||
<%= f.field_container :gtin do %>
|
||||
<%= f.label :gtin %>
|
||||
<%= f.text_field :gtin %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div data-hook="admin_product_form_condition">
|
||||
<%= f.field_container :condition do %>
|
||||
<%= f.label :condition %>
|
||||
<%= f.select :condition,
|
||||
Spree::Variant.conditions.map { |key, value| [t("spree.condition.#{key}"), value] },
|
||||
{ include_blank: t('spree.unset') },
|
||||
class: 'custom-select'
|
||||
%>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="shipping_specs" class="row">
|
||||
<% [:height, :width, :depth, :weight].each_with_index do |field, index| %>
|
||||
<div id="shipping_specs_<%= field %>_field" class="col-6">
|
||||
|
@ -7,6 +7,23 @@
|
||||
<%= f.text_field :sku, class: 'fullwidth' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<div class="field" data-hook="gtin">
|
||||
<%= f.label :gtin %>
|
||||
<%= f.text_field :gtin, class: 'fullwidth' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<div class="field" data-hook="condition">
|
||||
<%= f.label :condition %>
|
||||
<%= f.select :condition,
|
||||
Spree::Variant.conditions.map { |key, value| [t("spree.condition.#{key}"), value] },
|
||||
{ include_blank: t('spree.unset') },
|
||||
class: 'custom-select fullwidth' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="field checkbox" data-hook="track_inventory">
|
||||
<label>
|
||||
|
@ -85,6 +85,8 @@ module Spree
|
||||
:track_inventory,
|
||||
:weight,
|
||||
:width,
|
||||
:gtin,
|
||||
:condition
|
||||
]
|
||||
MASTER_ATTRIBUTES.each do |attr|
|
||||
delegate :"#{attr}", :"#{attr}=", to: :find_or_build_master
|
||||
|
@ -28,6 +28,10 @@ module Spree
|
||||
attr_writer :rebuild_vat_prices
|
||||
include Spree::DefaultPrice
|
||||
|
||||
# Consider that not all platforms digest structured data in the same way,
|
||||
# you might have to modify the output on the frontend or in feeds accordingly.
|
||||
enum :condition, { damaged: "damaged", new: "new", refurbished: "refurbished", used: "used" }, prefix: true
|
||||
|
||||
belongs_to :product, -> { with_discarded }, touch: true, class_name: 'Spree::Product', inverse_of: :variants_including_master, optional: false
|
||||
belongs_to :tax_category, class_name: 'Spree::TaxCategory', optional: true
|
||||
belongs_to :shipping_category, class_name: "Spree::ShippingCategory", optional: true
|
||||
|
@ -166,11 +166,13 @@ en:
|
||||
variant: Variant
|
||||
spree/product:
|
||||
available_on: Available On
|
||||
condition: Master Condition
|
||||
cost_currency: Cost Currency
|
||||
cost_price: Cost Price
|
||||
depth: Depth
|
||||
description: Description
|
||||
discontinue_on: Discontinue on
|
||||
gtin: Master GTIN
|
||||
height: Height
|
||||
master_price: Master Price
|
||||
meta_description: Meta Description
|
||||
@ -418,9 +420,11 @@ en:
|
||||
password_confirmation: Password Confirmation
|
||||
spree_roles: Roles
|
||||
spree/variant:
|
||||
condition: Condition
|
||||
cost_currency: Cost Currency
|
||||
cost_price: Cost Price
|
||||
depth: Depth
|
||||
gtin: GTIN
|
||||
height: Height
|
||||
price: Price
|
||||
shipping_category: Variant Shipping Category
|
||||
@ -1134,6 +1138,11 @@ en:
|
||||
company: Company
|
||||
complete: complete
|
||||
complete_order: Complete Order
|
||||
condition:
|
||||
damaged: Damaged
|
||||
new: New
|
||||
refurbished: Refurbished
|
||||
used: Used
|
||||
configuration: Configuration
|
||||
configurations: Configurations
|
||||
confirm: Confirm
|
||||
@ -2315,6 +2324,7 @@ en:
|
||||
unfinalize_all_adjustments: Unfinalize All Adjustments
|
||||
unlock: Unlock
|
||||
unrecognized_card_type: Unrecognized Card Type
|
||||
unset: Unset
|
||||
unshippable_items: Unshippable Items
|
||||
update: Update
|
||||
updated_successfully: Updated Successfully
|
||||
|
@ -0,0 +1,6 @@
|
||||
class AddGtinAndConditionToSpreeVariant < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :spree_variants, :gtin, :string
|
||||
add_column :spree_variants, :condition, :string
|
||||
end
|
||||
end
|
@ -81,7 +81,8 @@ module Spree
|
||||
:meta_keywords, :price, :sku, :deleted_at,
|
||||
:option_values_hash, :weight, :height, :width, :depth,
|
||||
:shipping_category_id, :tax_category_id,
|
||||
:taxon_ids, :option_type_ids, :cost_currency, :cost_price, :primary_taxon_id
|
||||
:taxon_ids, :option_type_ids, :cost_currency, :cost_price, :primary_taxon_id,
|
||||
:gtin, :condition
|
||||
]
|
||||
|
||||
@@property_attributes = [:name, :presentation]
|
||||
|
@ -108,6 +108,21 @@ RSpec.describe Spree::Product, type: :model do
|
||||
expect(product.reload.master.default_price.price).to eq 12
|
||||
end
|
||||
end
|
||||
|
||||
context "when master variant attributes change" do
|
||||
before do
|
||||
product.master.gtin = "1234567890123"
|
||||
product.master.condition = "new"
|
||||
end
|
||||
|
||||
it_behaves_like "a change occurred"
|
||||
|
||||
it "saves the master with updated gtin and condition" do
|
||||
product.save!
|
||||
expect(product.reload.master.gtin).to eq "1234567890123"
|
||||
expect(product.reload.master.condition).to eq "new"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "product has no variants" do
|
||||
|
@ -44,6 +44,38 @@ RSpec.describe Spree::Variant, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'enums' do
|
||||
it 'has the correct enum values' do
|
||||
expect(described_class.conditions).to eq({
|
||||
"damaged" => "damaged",
|
||||
"new" => "new",
|
||||
"refurbished" => "refurbished",
|
||||
"used" => "used"
|
||||
})
|
||||
end
|
||||
|
||||
it 'correctly assigns and reads condition values' do
|
||||
variant = create(:variant, condition: 'new')
|
||||
expect(variant.reload.condition).to eq('new')
|
||||
end
|
||||
|
||||
it 'does not accept invalid enum values' do
|
||||
expect { create(:variant, condition: 'invalid_value') }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'gtin and condition attributes' do
|
||||
let(:variant) { create(:variant, gtin: '1234567890123', condition: 'new') }
|
||||
|
||||
it 'has a GTIN' do
|
||||
expect(variant.gtin).to eq('1234567890123')
|
||||
end
|
||||
|
||||
it 'has a condition' do
|
||||
expect(variant.condition).to eq('new')
|
||||
end
|
||||
end
|
||||
|
||||
context "validations" do
|
||||
it "should validate price is greater than 0" do
|
||||
variant = build(:variant, price: -1)
|
||||
|
@ -27,6 +27,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 19.99,
|
||||
eur_price: 16,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -38,6 +40,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 19.99,
|
||||
eur_price: 16,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -49,6 +53,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 29.99,
|
||||
eur_price: 27,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 1,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -60,6 +66,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 19.99,
|
||||
eur_price: 16,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -71,6 +79,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 29.99,
|
||||
eur_price: 27,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 1,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -82,6 +92,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 29.99,
|
||||
eur_price: 27,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.8,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -93,6 +105,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 26.99,
|
||||
eur_price: 23,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -104,6 +118,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 9.99,
|
||||
eur_price: 7,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 1,
|
||||
height: 5,
|
||||
width: 5,
|
||||
@ -115,6 +131,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 15.99,
|
||||
eur_price: 14,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -126,6 +144,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 15.99,
|
||||
eur_price: 14,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -137,6 +157,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 15.99,
|
||||
eur_price: 14,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
@ -149,6 +171,8 @@ products = [
|
||||
shipping_category:,
|
||||
price: 24,
|
||||
eur_price: 22,
|
||||
gtin: 12345678,
|
||||
condition: "new",
|
||||
weight: 0.5,
|
||||
height: 20,
|
||||
width: 10,
|
||||
|
Loading…
x
Reference in New Issue
Block a user