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:
Rahul Singh 2025-02-19 16:48:34 +05:30 committed by GitHub
parent a85e0becf7
commit acd092fc90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 148 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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