Merge pull request #12562 from rioug/product-supplier-id

[Product Refactor]  Move supplier to Variant
This commit is contained in:
Rachel Arnould
2024-07-03 13:03:58 +02:00
committed by GitHub
218 changed files with 2210 additions and 1735 deletions

View File

@@ -6,13 +6,6 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
Layout/EmptyLineBetweenDefs:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Layout/EmptyLines:
@@ -108,7 +101,7 @@ Lint/UselessMethodDefinition:
Exclude:
- 'app/models/spree/gateway.rb'
# Offense count: 24
# Offense count: 23
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
Metrics/AbcSize:
Exclude:
@@ -121,7 +114,6 @@ Metrics/AbcSize:
- 'app/helpers/spree/admin/navigation_helper.rb'
- 'app/models/enterprise_group.rb'
- 'app/models/enterprise_relationship.rb'
- 'app/models/product_import/entry_processor.rb'
- 'app/models/spree/ability.rb'
- 'app/models/spree/address.rb'
- 'app/models/spree/order/checkout.rb'
@@ -253,7 +245,7 @@ Metrics/MethodLength:
- 'lib/spree/localized_number.rb'
- 'lib/tasks/sample_data/product_factory.rb'
# Offense count: 48
# Offense count: 49
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ModuleLength:
Exclude:
@@ -283,6 +275,7 @@ Metrics/ModuleLength:
- 'spec/controllers/payment_gateways/stripe_controller_spec.rb'
- 'spec/controllers/spree/admin/adjustments_controller_spec.rb'
- 'spec/controllers/spree/admin/payment_methods_controller_spec.rb'
- 'spec/controllers/spree/admin/variants_controller_spec.rb'
- 'spec/lib/open_food_network/address_finder_spec.rb'
- 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb'
- 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb'
@@ -893,7 +886,7 @@ Style/ReturnNilInPredicateMethodDefinition:
- 'app/serializers/api/admin/customer_serializer.rb'
- 'engines/order_management/app/services/order_management/subscriptions/validator.rb'
# Offense count: 204
# Offense count: 207
Style/Send:
Exclude:
- 'spec/controllers/admin/subscriptions_controller_spec.rb'

View File

@@ -47,7 +47,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
removeClearedValues()
params = {
'q[name_cont]': $scope.q.query,
'q[supplier_id_eq]': $scope.q.producerFilter,
'q[variants_supplier_id_eq]': $scope.q.producerFilter,
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter,
'q[s]': $scope.sorting,
import_date: $scope.q.importDateFilter,
@@ -217,7 +217,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
products: productsToSubmit
filters:
'q[name_cont]': $scope.q.query
'q[supplier_id_eq]': $scope.q.producerFilter
'q[variants_supplier_id_eq]': $scope.q.producerFilter
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter
'q[s]': $scope.sorting
import_date: $scope.q.importDateFilter
@@ -314,9 +314,6 @@ filterSubmitProducts = (productsToFilter) ->
if product.hasOwnProperty("name")
filteredProduct.name = product.name
hasUpdatableProperty = true
if product.hasOwnProperty("producer_id")
filteredProduct.supplier_id = product.producer_id
hasUpdatableProperty = true
if product.hasOwnProperty("price")
filteredProduct.price = product.price
hasUpdatableProperty = true
@@ -379,6 +376,9 @@ filterSubmitVariant = (variant) ->
if variant.hasOwnProperty("display_as")
filteredVariant.display_as = variant.display_as
hasUpdatableProperty = true
if variant.hasOwnProperty("producer_id")
filteredVariant.supplier_id = variant.producer_id
hasUpdatableProperty = true
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}

View File

@@ -1,12 +1,16 @@
# Used like a regular angular filter where an object is passed
# Adds the additional special case that a value of 0 for the filter
# acts as a bypass for that particular attribute
# NOTE the name doesn't reflect what the filter does, it only fiters on the variant.producer_id
angular.module("admin.indexUtils").filter "attrFilter", ($filter) ->
return (objects, filters) ->
Object.keys(filters).reduce (filtered, attr) ->
filter = filters[attr]
return filtered if !filter? || filter == 0
return $filter('filter')(filtered, (object) ->
object[attr] == filter
)
, objects
filter = filters["producer_id"]
return objects if !filter? || filter == 0
return $filter('filter')(objects, (product) ->
for variant in product.variants
return true if variant["producer_id"] == filter
false
, true)

View File

@@ -27,7 +27,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
"order_bill_address_full_name_reversed",
"order_bill_address_full_name_with_comma",
"order_bill_address_full_name_with_comma_reversed",
"variant_product_supplier_name",
"variant_supplier_name",
"order_email",
"order_number",
"product_name"].join("_or_") + "_cont"
@@ -81,7 +81,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
"q[order_shipment_state_not_eq]": "shipped",
"q[order_completed_at_not_null]": "true",
"q[order_distributor_id_eq]": $scope.distributorFilter,
"q[variant_product_supplier_id_eq]": $scope.supplierFilter,
"q[variant_supplier_id_eq]": $scope.supplierFilter,
"q[order_order_cycle_id_eq]": $scope.orderCycleFilter,
"q[order_completed_at_gteq]": if formattedStartDate then formattedStartDate else undefined,
"q[order_completed_at_lt]": if formattedEndDate then formattedEndDate else undefined,
@@ -105,7 +105,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
Dereferencer.dereferenceAttr $scope.line_items, "supplier", Enterprises.byID
$scope.loadOrders()
RequestMonitor.load $q.all([$scope.orders.$promise]).then ->
Dereferencer.dereferenceAttr $scope.line_items, "order", Orders.byID
Dereferencer.dereferenceAttr $scope.line_items, "order", Orders.byID
Dereferencer.dereferenceAttr $scope.orders, "distributor", Enterprises.byID
Dereferencer.dereferenceAttr $scope.orders, "order_cycle", OrderCycles.byID
$scope.bulk_order_form.$setPristine()
@@ -133,7 +133,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
return $http(
method: 'GET'
url: "/admin/orders/#{order.number}/fire?e=cancel&send_cancellation_email=#{sendEmailCancellation}&restock_items=#{restock_items}")
$scope.deleteLineItem = (lineItem) ->
if lineItem.order.item_count == 1
ofnCancelOrderAlert((confirm, sendEmailCancellation, restock_items) ->
@@ -167,7 +167,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.cancelOrder(order, sendEmailCancellation, restock_items).then(-> $scope.refreshData())
else
Promise.all(LineItems.delete(item) for item in items).then(-> $scope.refreshData())
, "js.admin.deleting_item_will_cancel_order")
, "js.admin.deleting_item_will_cancel_order")
else
ofnDeleteLineItemsAlert(() ->
Promise.all(LineItems.delete(item) for item in lineItemsToDelete).then(-> $scope.refreshData())
@@ -199,7 +199,7 @@ 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")
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
else
1
@@ -252,7 +252,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
scale = $scope.getScale(unitsProduct, unitsVariant)
if scale
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
else
else
''
$scope.fulfilled = (sumOfUnitValues) ->

View File

@@ -2,10 +2,10 @@ angular.module("admin.variantOverrides").directive "trackInheritance", (VariantO
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
# This is a bit hacky, but it allows us to load the inherit property on the VO, but then not submit it
scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant.id
scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty && viewValue
DirtyVariantOverrides.inherit scope.hub_id, scope.variant.id, scope.variantOverrides[scope.hub_id][scope.variant.id].id
DirtyVariantOverrides.inherit scope.hub_id, scope.variant, scope.variantOverrides[scope.hub_id][scope.variant.id].id
scope.displayDirty()
viewValue

View File

@@ -2,4 +2,8 @@ angular.module("admin.variantOverrides").filter "hubPermissions", ($filter) ->
return (products, hubPermissions, hub_id) ->
return [] if !hub_id
return [] if !hubPermissions[hub_id]
return $filter('filter')(products, ((product) -> hubPermissions[hub_id].indexOf(product.producer_id) > -1), true)
return $filter('filter')(products, ((product) ->
for variant in product.variants
return hubPermissions[hub_id].indexOf(variant.producer_id) > -1
), true)

View File

@@ -12,11 +12,11 @@ angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http
@add(hub_id, variant_id, vo_id)
@dirtyVariantOverrides[hub_id][variant_id][attr] = value
inherit: (hub_id, variant_id, vo_id) ->
@add(hub_id, variant_id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant_id))
inherit: (hub_id, variant, vo_id) ->
@add(hub_id, variant.id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant))
delete blankVo[attr] for attr, value of blankVo when attr not in @requiredAttrs()
@dirtyVariantOverrides[hub_id][variant_id] = blankVo
@dirtyVariantOverrides[hub_id][variant.id] = blankVo
count: ->
count = 0

View File

@@ -13,17 +13,18 @@ angular.module("admin.variantOverrides").factory "VariantOverrides", (variantOve
@variantOverrides[hub.id] ||= {}
for product in products
for variant in product.variants
@inherit(hub.id, variant.id) unless @variantOverrides[hub.id][variant.id]
@inherit(hub.id, variant) unless @variantOverrides[hub.id][variant.id]
inherit: (hub_id, variant_id) ->
inherit: (hub_id, variant) ->
# This method is called from the trackInheritance directive, to reinstate inheritance
@variantOverrides[hub_id][variant_id] ||= {}
angular.extend @variantOverrides[hub_id][variant_id], @newFor hub_id, variant_id
@variantOverrides[hub_id][variant.id] ||= {}
angular.extend @variantOverrides[hub_id][variant.id], @newFor(hub_id, variant)
newFor: (hub_id, variant_id) ->
newFor: (hub_id, variant) ->
# These properties need to match those checked in VariantOverrideSet.deletable?
hub_id: hub_id
variant_id: variant_id
variant_id: variant.id
producer_id: variant.producer_id
sku: null
price: null
count_on_hand: null

View File

@@ -4,15 +4,18 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.query = ""
$scope.taxonSelectors = FilterSelectorsService.createSelectors()
$scope.propertySelectors = FilterSelectorsService.createSelectors()
$scope.producerPropertySelectors = FilterSelectorsService.createSelectors()
$scope.filtersActive = true
$scope.page = 1
$scope.per_page = 10
$scope.order_cycle = OrderCycle.order_cycle
$scope.supplied_taxons = null
$scope.supplied_properties = null
$scope.supplied_producer_properties = null
$scope.showFilterSidebar = false
$scope.activeTaxons = []
$scope.activeProperties = []
$scope.activeProducerProperties = []
# Update filters after initial load of shop tab
$timeout =>
@@ -45,6 +48,12 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.supplied_properties[property.id] = Properties.properties_by_id[property.id]
)
OrderCycleResource.producerProperties params, (data)=>
$scope.supplied_producer_properties = {}
data.map( (property) ->
$scope.supplied_producer_properties[property.id] = Properties.properties_by_id[property.id]
)
$scope.loadMore = ->
if ($scope.page * $scope.per_page) <= Products.products.length
$scope.loadMoreProducts()
@@ -52,6 +61,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.$watch 'query', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeTaxons', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeProperties', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeProducerProperties', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.loadProducts = ->
$scope.page = 1
@@ -66,8 +76,9 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
id: $scope.order_cycle.order_cycle_id,
page: page || $scope.page,
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[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_variants_supplier_name_cont]': $scope.query,
'q[with_properties][]': $scope.activeProperties,
'q[with_variants_supplier_properties][]': $scope.activeProducerProperties,
'q[variants_primary_taxon_id_in_any][]': $scope.activeTaxons
}
@@ -86,6 +97,12 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
Properties.properties_by_id[property_id].name
).join($scope.filtersJoinWord()) if $scope.activeProperties?
$scope.appliedProducerPropertiesList = ->
$scope.activeProducerProperties.map( (property_id) ->
Properties.properties_by_id[property_id].name
).join($scope.filtersJoinWord()) if $scope.activeProducerProperties?
$scope.filtersJoinWord = ->
$sce.trustAsHtml(" <span class='join-word'>#{t('products_or')}</span> ")
@@ -99,6 +116,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.clearFilters = ->
$scope.taxonSelectors.clearAll()
$scope.propertySelectors.clearAll()
$scope.producerPropertySelectors.clearAll()
$scope.refreshStaleData = ->
# If the products template has already been loaded but the controller is being initialized
@@ -109,7 +127,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.loadProducts()
$scope.filtersCount = () ->
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive()
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive() + $scope.producerPropertySelectors.totalActive()
$scope.toggleFilterSidebar = ->
$scope.showFilterSidebar = !$scope.showFilterSidebar

View File

@@ -18,4 +18,11 @@ angular.module('Darkswarm').factory 'OrderCycleResource', ($resource) ->
url: '/api/v0/order_cycles/:id/properties.json'
params:
id: '@id'
'producerProperties':
method: 'GET'
isArray: true
url: '/api/v0/order_cycles/:id/producer_properties.json'
params:
id: '@id'
})

View File

@@ -39,7 +39,7 @@ angular.module('Darkswarm').factory 'Products', (OrderCycleResource, OrderCycle,
dereference: ->
for product in @fetched_products
product.supplier = Shopfront.producers_by_id[product.supplier.id]
product.supplier = Shopfront.producers_by_id[product.variants[0].supplier.id]
Dereferencer.dereference product.taxons, Taxons.taxons_by_id
product.properties = angular.copy(product.properties_with_values)

View File

@@ -189,10 +189,7 @@ module Admin
.visible_enterprises
if enterprises.present?
enterprises.includes(
supplied_products:
[:supplier, :variants, :image]
)
enterprises.includes(supplied_products: [:variants, :image])
end
when :index
if spree_current_user.admin?

View File

@@ -149,7 +149,7 @@ module Admin
def ransack_query
query = {}
query.merge!(supplier_id_in: @producer_id) if @producer_id.present?
query.merge!(variants_supplier_id_in: @producer_id) if @producer_id.present?
if @search_term.present?
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
@@ -163,13 +163,13 @@ module Admin
def product_query_includes
[
:image,
:supplier,
{ variants: [
:default_price,
:primary_taxon,
:product,
:stock_items,
:tax_category,
:supplier,
] },
]
end

View File

@@ -7,9 +7,11 @@ module Api
include ApiActionCaching
skip_authorization_check
skip_before_action :authenticate_user, :ensure_api_key, only: [:taxons, :properties]
skip_before_action :authenticate_user, :ensure_api_key, only: [
:taxons, :properties, :producer_properties
]
caches_action :taxons, :properties,
caches_action :taxons, :properties, :producer_properties,
expires_in: CacheService::FILTERS_EXPIRY,
cache_path: proc { |controller| controller.request.url }
@@ -41,7 +43,13 @@ module Api
def properties
render plain: ActiveModel::ArraySerializer.new(
product_properties | producer_properties, each_serializer: Api::PropertySerializer
product_properties, each_serializer: Api::PropertySerializer
).to_json
end
def producer_properties
render plain: ActiveModel::ArraySerializer.new(
load_producer_properties, each_serializer: Api::PropertySerializer
).to_json
end
@@ -58,7 +66,7 @@ module Api
select('DISTINCT spree_properties.*')
end
def producer_properties
def load_producer_properties
producers = Enterprise.
joins(:supplied_products).
where(spree_products: { id: distributed_products })
@@ -86,8 +94,9 @@ module Api
end
def distributed_products
OrderCycles::DistributedProductsService.new(distributor, order_cycle,
customer).products_relation
OrderCycles::DistributedProductsService.new(
distributor, order_cycle, customer
).products_supplier_relation.pluck(:id)
end
end
end

View File

@@ -54,7 +54,7 @@ module Api
end
def overridable
@products = product_finder.paged_products_for_producers
@products = product_finder.products_for_producers
render_paged_products @products, ::Api::Admin::ProductSimpleSerializer
end

View File

@@ -8,6 +8,7 @@ module Spree
before_action :setup_property, only: [:index]
def index
@supplier = @product.variants.first.supplier
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
end

View File

@@ -12,6 +12,7 @@ module Spree
include EnterprisesHelper
before_action :load_data
before_action :load_producers, only: [:index, :new]
before_action :load_form_data, only: [:index, :new, :create, :edit, :update]
before_action :load_spree_api_key, only: [:index, :variant_overrides]
before_action :strip_new_properties, only: [:create, :update]
@@ -41,6 +42,7 @@ module Spree
flash[:success] = flash_message_for(@object, :successfully_created)
redirect_after_save
else
load_producers
# Re-fill the form with deleted params on product
@on_hand = request.params[:product][:on_hand]
@on_demand = request.params[:product][:on_demand]
@@ -52,14 +54,9 @@ module Spree
def update
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
original_supplier_id = @product.supplier_id
delete_stock_params_and_set_after do
params[:product] ||= {} if params[:clear_product_properties]
if @object.update(permitted_resource_params)
if original_supplier_id != @product.supplier_id
ExchangeVariantDeleter.new.delete(@product)
end
flash[:success] = flash_message_for(@object, :successfully_updated)
end
redirect_to spree.edit_admin_product_url(@object, @url_filters)
@@ -157,12 +154,15 @@ module Spree
end
def load_form_data
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
managed_product_enterprises.is_primary_producer.by_name
@taxons = Spree::Taxon.order(:name)
@import_dates = product_import_dates.uniq.to_json
end
def load_producers
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
managed_product_enterprises.is_primary_producer.by_name
end
def product_import_dates
options = [{ id: '0', name: '' }]
product_import_dates_query.collect(&:import_date).
@@ -173,12 +173,10 @@ module Spree
def product_import_dates_query
Spree::Variant.
select('DISTINCT spree_variants.import_date').
joins(:product).
where(spree_products: { supplier_id: editable_enterprises.collect(&:id) }).
select('import_date').distinct.
where(supplier_id: editable_enterprises.collect(&:id)).
where.not(spree_variants: { import_date: nil }).
where(spree_variants: { deleted_at: nil }).
order('spree_variants.import_date DESC')
order('import_date DESC')
end
def strip_new_properties

View File

@@ -46,7 +46,13 @@ module Spree
def update
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
original_supplier_id = @object.supplier_id
if @object.update(permitted_resource_params)
if original_supplier_id != @object.supplier_id
ExchangeVariantDeleter.new.delete(@object)
end
flash[:success] = flash_message_for(@object, :successfully_updated)
redirect_to spree.admin_product_variants_url(params[:product_id], @url_filters)
else
@@ -113,6 +119,8 @@ module Spree
private
def load_data
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
managed_product_enterprises.is_primary_producer.by_name
@tax_categories = TaxCategory.order(:name)
@shipping_categories = ShippingCategory.order(:name)
end

View File

@@ -60,11 +60,12 @@ class ProducerMailer < ApplicationMailer
def line_items_from(order_cycle, producer)
@line_items ||= Spree::LineItem.
includes(variant: [:product]).
includes(variant: :product).
joins(variant: :product).
from_order_cycle(order_cycle).
sorted_by_name_and_unit_value.
merge(Spree::Product.with_deleted.in_supplier(producer)).
merge(Spree::Order.by_state(["complete", "resumed"]))
merge(Spree::Variant.with_deleted.where(supplier: producer)).
merge(Spree::Order.by_state(["complete", "resumed"])).
sorted_by_name_and_unit_value
end
def total_from_line_items(line_items)
@@ -81,7 +82,7 @@ class ProducerMailer < ApplicationMailer
line_items.map do |line_item|
{
sku: line_item.variant.sku,
supplier_name: line_item.product.supplier.name,
supplier_name: line_item.variant.supplier.name,
product_and_full_name: line_item.product_and_full_name,
quantity: line_item.quantity,
first_name: line_item.order.billing_address.first_name,

View File

@@ -39,13 +39,13 @@ class Enterprise < ApplicationRecord
class_name: 'EnterpriseGroup'
has_many :producer_properties, foreign_key: 'producer_id', dependent: :destroy
has_many :properties, through: :producer_properties
has_many :supplied_products, class_name: 'Spree::Product',
foreign_key: 'supplier_id',
dependent: :destroy
has_many :supplied_variants, through: :supplied_products, source: :variants
has_many :supplied_variants,
class_name: 'Spree::Variant', foreign_key: 'supplier_id', dependent: :destroy
has_many :supplied_products, through: :supplied_variants, source: :product
has_many :distributed_orders, class_name: 'Spree::Order',
foreign_key: 'distributor_id',
dependent: :restrict_with_exception
belongs_to :address, class_name: 'Spree::Address'
belongs_to :business_address, optional: true, class_name: 'Spree::Address', dependent: :destroy
has_many :enterprise_fees, dependent: :restrict_with_exception
@@ -167,7 +167,7 @@ class Enterprise < ApplicationRecord
scope :is_distributor, -> { where.not(sells: 'none') }
scope :is_hub, -> { where(sells: 'any') }
scope :supplying_variant_in, lambda { |variants|
joins(supplied_products: :variants).
joins(:supplied_variants).
where(spree_variants: { id: variants }).
select('DISTINCT enterprises.*')
}
@@ -205,14 +205,14 @@ class Enterprise < ApplicationRecord
select('DISTINCT enterprises.*')
}
scope :distributing_products, lambda { |product_ids|
scope :distributing_variants, lambda { |variants_ids|
exchanges = joins("
INNER JOIN exchanges
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = false)
").
joins('INNER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)').
joins('INNER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)').
where(spree_variants: { product_id: product_ids }).select('DISTINCT enterprises.id')
where(spree_variants: { id: variants_ids }).select('DISTINCT enterprises.id')
where(id: exchanges)
}
@@ -598,7 +598,7 @@ class Enterprise < ApplicationRecord
# Touch distributors without them touching their distributors.
# We avoid an infinite loop and don't need to touch the whole distributor tree.
def touch_distributors
Enterprise.distributing_products(supplied_products.select(:id)).
Enterprise.distributing_variants(supplied_variants.select(:id)).
where.not(enterprises: { id: }).
update_all(updated_at: Time.zone.now)
end

View File

@@ -108,6 +108,6 @@ class EnterpriseRelationship < ApplicationRecord
def child_variant_overrides
VariantOverride.unscoped.for_hubs(child)
.joins(variant: :product).where(spree_products: { supplier_id: parent })
.joins(:variant).where(spree_variants: { supplier_id: parent } )
end
end

View File

@@ -54,10 +54,7 @@ module ProductImport
if settings.importing_into_inventory?
VariantOverride.for_hubs([enterprise_id]).count
else
Spree::Variant.
joins(:product).
where(spree_products: { supplier_id: enterprise_id }).
count
Spree::Variant.where(supplier_id: enterprise_id).count
end
@enterprise_products[enterprise_id] = products_count
@@ -169,7 +166,6 @@ module ProductImport
product.assign_attributes(
entry.assignable_attributes.except('id', 'on_hand', 'on_demand', 'display_name')
)
product.supplier_id = entry.producer_id
if product.save
ensure_variant_updated(product, entry)
@@ -228,10 +224,13 @@ 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.import_date = @import_time
variant.supplier_id = entry.producer_id
variant.save
# on_demand and on_hand require a stock level, which is created after the variant is created
variant.on_demand = entry.on_demand if entry.on_demand
variant.on_hand = entry.on_hand if entry.on_hand
variant.import_date = @import_time
variant.save
end
end
end

View File

@@ -73,6 +73,7 @@ module ProductImport
# Variant needs a product. Product needs to be assigned first in order for
# delegate to work. name= will fail otherwise.
new_variant = Spree::Variant.new(product_id:, **variant_attributes)
new_variant.supplier_id = entry.producer_id
new_variant.save
if new_variant.persisted?
@@ -287,9 +288,7 @@ module ProductImport
end
def inventory_validation(entry)
products = Spree::Product.where(supplier_id: entry.producer_id,
name: entry.name,
deleted_at: nil)
products = Spree::Product.in_supplier(entry.producer_id).where(name: entry.name)
if products.empty?
mark_as_invalid(entry, attribute: 'name',
@@ -358,9 +357,7 @@ module ProductImport
end
def product_validation(entry)
products = Spree::Product.where(supplier_id: entry.enterprise_id,
name: entry.name,
deleted_at: nil)
products = Spree::Product.in_supplier(entry.enterprise_id).where(name: entry.name)
if products.empty?
mark_as_new_product(entry)
@@ -384,7 +381,6 @@ module ProductImport
new_product.assign_attributes(
entry.assignable_attributes.except('id', 'on_hand', 'on_demand', 'display_name')
)
new_product.supplier_id = entry.producer_id
entry.on_hand = 0 if entry.on_hand.nil?
if new_product.valid?

View File

@@ -189,7 +189,9 @@ module Spree
:seo, :group_buy_options,
:bulk_update, :clone, :delete,
:destroy], Spree::Product do |product|
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include?(
product.variants.first.supplier
)
end
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone], :products_v3
@@ -198,11 +200,11 @@ module Spree
can [:admin, :index, :read, :edit,
:update, :search, :delete, :destroy], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).
managed_product_enterprises.include? variant.product.supplier
managed_product_enterprises.include? variant.supplier
end
can [:admin, :index, :read, :update, :bulk_update, :bulk_reset], VariantOverride do |vo|
next false unless vo.hub.present? && vo.variant&.product&.supplier.present?
next false unless vo.hub.present? && vo.variant&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
@@ -210,14 +212,14 @@ module Spree
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? vo.variant.product.supplier
include? vo.variant.supplier
hub_auth && producer_auth
end
can [:admin, :create, :update], InventoryItem do |ii|
next false unless ii.enterprise.present? &&
ii.variant&.product&.supplier.present?
ii.variant&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
@@ -225,7 +227,7 @@ module Spree
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? ii.variant.product.supplier
include? ii.variant.supplier
hub_auth && producer_auth
end

View File

@@ -16,7 +16,7 @@ module Spree
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
has_one :product, through: :variant
has_one :supplier, through: :product
has_one :supplier, through: :variant
belongs_to :tax_category, class_name: "Spree::TaxCategory", optional: true
has_many :adjustments, as: :adjustable, dependent: :destroy
@@ -85,13 +85,11 @@ module Spree
where(order_cycles: { id: order_cycle })
}
# Here we are simply joining the line item to its variant and product
# We dont use joins here to avoid the default scopes,
# and with that, include deleted variants and deleted products
# Here we are simply joining the line item to its variant
# We dont use joins here to avoid the default scopes, and with that, include deleted variants
scope :supplied_by_any, lambda { |enterprises|
product_ids = Spree::Product.unscoped.where(supplier_id: enterprises).select(:id)
variant_ids = Spree::Variant.unscoped.where(product_id: product_ids).select(:id)
where(spree_line_items: { variant_id: variant_ids })
variant_ids = Spree::Variant.unscoped.where(supplier: enterprises).select(:id)
where(variant_id: variant_ids)
}
scope :with_tax, -> {

View File

@@ -5,19 +5,15 @@ require 'open_food_network/property_merge'
# PRODUCTS
# Products represent an entity for sale in a store.
# Products can have variations, called variants
# Products properties include description, permalink, availability,
# shipping category, etc. that do not change by variant.
#
# MASTER VARIANT
# Every product has one master variant, which stores master price and sku, size and weight, etc.
# Price, SKU, size, weight, etc. are all delegated to the master variant.
# Contains on_hand inventory levels only when there are no variants for the product.
# Products properties include description, meta_keywork, etc. that do not change by variant.
#
# VARIANTS
# All variants can access the product properties directly (via reverse delegation).
# Every product has at least one variant (standard variant), which stores price and availability,
# shipping category, sku, size and weight, etc.
# All variants can access the product name, description, and meta_keyword directly (via reverse
# delegation).
# Inventory units are tied to Variant.
# The master variant can have inventory units, but not option values.
# All other variants have option values and may have inventory units.
# All variants have option values and may have inventory units.
# Sum of on_hand each variant's inventory level determine "on_hand" level for the product.
#
module Spree
@@ -26,15 +22,14 @@ module Spree
include LogDestroyPerformer
self.belongs_to_required_by_default = false
self.ignored_columns += [:supplier_id]
acts_as_paranoid
searchable_attributes :supplier_id, :meta_keywords, :sku
searchable_associations :supplier, :properties, :variants
searchable_attributes :meta_keywords, :sku
searchable_associations :properties, :variants
searchable_scopes :active, :with_properties
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
has_one :image, class_name: "Spree::Image", as: :viewable, dependent: :destroy
has_many :product_properties, dependent: :destroy
@@ -45,7 +40,6 @@ module Spree
has_many :prices, -> { order('spree_variants.id, currency') }, through: :variants
has_many :stock_items, through: :variants
has_many :supplier_properties, through: :supplier, source: :properties
has_many :variant_images, -> { order(:position) }, source: :images,
through: :variants
@@ -68,28 +62,27 @@ module Spree
accepts_nested_attributes_for :image
accepts_nested_attributes_for :product_properties,
allow_destroy: true,
reject_if: lambda { |pp| pp[:property_name].blank? }
reject_if: ->(pp) { pp[:property_name].blank? }
# 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
:shipping_category_id, :primary_taxon_id, :supplier_id
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
scope :with_properties, ->(*property_ids) {
left_outer_joins(:product_properties).
left_outer_joins(:supplier_properties).
where(inherits_properties: true).
where(producer_properties: { property_id: property_ids }).
or(
where(spree_product_properties: { property_id: property_ids })
)
where(spree_product_properties: { property_id: property_ids })
}
scope :with_order_cycles_outer, -> {
scope :with_order_cycles_outer, lambda {
joins("
LEFT OUTER JOIN spree_variants AS o_spree_variants
ON (o_spree_variants.product_id = spree_products.id)").
@@ -111,9 +104,7 @@ module Spree
where(import_date: import_date.all_day))
}
scope :with_order_cycles_inner, -> {
joins(variants: { exchanges: :order_cycle })
}
scope :with_order_cycles_inner, -> { joins(variants: { exchanges: :order_cycle }) }
scope :visible_for, lambda { |enterprise|
joins('
@@ -126,8 +117,9 @@ module Spree
distinct
}
# -- Scopes
scope :in_supplier, lambda { |supplier| where(supplier_id: supplier) }
scope :in_supplier, lambda { |supplier|
distinct.joins(:variants).where(spree_variants: { supplier: })
}
# Products distributed via the given distributor through an OC
scope :in_distributor, lambda { |distributor|
@@ -144,18 +136,6 @@ module Spree
distinct
}
# Products supplied by a given enterprise or distributed via that enterprise through an OC
scope :in_supplier_or_distributor, lambda { |enterprise|
enterprise = enterprise.respond_to?(:id) ? enterprise.id : enterprise.to_i
with_order_cycles_outer.
where("
spree_products.supplier_id = ?
OR (o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)
", enterprise, false, enterprise).
select('distinct spree_products.*')
}
# Products distributed by the given order cycle
scope :in_order_cycle, lambda { |order_cycle|
with_order_cycles_inner.
@@ -170,27 +150,17 @@ module Spree
where.not(order_cycles: { id: nil })
}
scope :by_producer, -> { joins(:supplier).order('enterprises.name') }
scope :by_name, -> { order('name') }
scope :by_producer, -> { joins(variants: :supplier).order('enterprises.name') }
scope :by_name, -> { order('spree_products.name') }
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
where(supplier_id: user.enterprises.select("enterprises.id"))
in_supplier(user.enterprises)
end
}
scope :stockable_by, lambda { |enterprise|
return where('1=0') if enterprise.blank?
permitted_producer_ids = EnterpriseRelationship.joins(:parent).permitting(enterprise.id)
.with_permission(:add_to_order_cycle)
.where(enterprises: { is_primary_producer: true })
.pluck(:parent_id)
where(spree_products: { supplier_id: [enterprise.id] | permitted_producer_ids })
}
scope :active, lambda { where(spree_products: { deleted_at: nil }) }
def self.group_by_products_id
@@ -236,7 +206,10 @@ module Spree
ps = product_properties.all
if inherits_properties
ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier.producer_properties)
# NOTE: Set the supplier as the first variant supplier. If variants have different supplier,
# result might not be correct
supplier = variants.first.supplier
ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier&.producer_properties || [])
end
ps.
@@ -263,8 +236,6 @@ module Spree
def destruction
transaction do
touch_distributors
ExchangeVariant.
where(exchange_variants: { variant_id: variants.with_deleted.
select(:id) }).destroy_all
@@ -285,6 +256,7 @@ module Spree
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
@@ -319,11 +291,26 @@ module Spree
def update_units
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
variants.each(&:update_units)
variants.each do |v|
if v.persisted?
v.update_units
else
v.assign_units
end
end
end
def touch_distributors
Enterprise.distributing_products(id).each(&:touch)
def touch_supplier
return if variants.empty?
# Assume the product supplier is the supplier of the first variant
# Will breack if product has mutiple variants with different supplier
first_variant = variants.first
# The variant is invalid if no supplier is present, but this method can be triggered when
# importing product. In this scenario the variant has not been updated with the supplier yet
# hence the check.
first_variant.supplier.touch if first_variant.supplier.present?
end
def validate_image

View File

@@ -6,6 +6,8 @@ module Spree
has_many :products, through: :product_properties
has_many :producer_properties, dependent: :destroy
after_touch :touch_producer_properties
validates :name, :presentation, presence: true
scope :sorted, -> { order(:name) }
@@ -13,5 +15,11 @@ module Spree
def property
self
end
private
def touch_producer_properties
producer_properties.each(&:touch)
end
end
end

View File

@@ -57,7 +57,7 @@ module Spree
taxons = {}
Spree::Taxon.
joins(products: :supplier).
joins(variants: :supplier).
select('spree_taxons.*, enterprises.id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new

View File

@@ -13,8 +13,8 @@ module Spree
acts_as_paranoid
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id
searchable_associations :product, :default_price, :primary_taxon
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id, :supplier_id
searchable_associations :product, :default_price, :primary_taxon, :supplier
searchable_scopes :active, :deleted
NAME_FIELDS = ["display_name", "display_as", "weight", "unit_value", "unit_description"].freeze
@@ -23,12 +23,15 @@ module Spree
meta_keywords
variants_display_as
variants_display_name
supplier_name).join('_or_')}_cont".freeze
variants_supplier_name).join('_or_')}_cont".freeze
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
belongs_to :product, -> {
with_deleted
}, touch: true, class_name: 'Spree::Product', optional: false
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
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
@@ -58,6 +61,7 @@ module Spree
has_many :variant_overrides, dependent: :destroy
has_many :inventory_items, dependent: :destroy
has_many :semantic_links, dependent: :delete_all
has_many :supplier_properties, through: :supplier, source: :properties
localize_number :price, :weight
@@ -94,7 +98,7 @@ module Spree
after_save :save_default_price
# default variant scope only lists non-deleted variants
scope :deleted, lambda { where.not(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }
@@ -139,11 +143,9 @@ module Spree
.where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
}
scope :stockable_by, lambda { |enterprise|
return where("1=0") if enterprise.blank?
joins(:product).
where(spree_products: { id: Spree::Product.stockable_by(enterprise).select(:id) })
scope :with_properties, lambda { |property_ids|
left_outer_joins(:supplier_properties).
where(producer_properties: { property_id: property_ids })
}
# Define sope as class method to allow chaining with other scopes filtering id.
@@ -250,8 +252,16 @@ module Spree
end
def destruction
exchange_variants.reload.destroy_all
yield
transaction do
# Even tough Enterprise will touch associated variant distributors when touched,
# the variant will be removed from the exchange by the time it's triggered,
# so it won't be able to find the deleted variant's distributors.
# This why we do it here
touch_distributors
exchange_variants.reload.destroy_all
yield
end
end
def ensure_unit_value
@@ -268,5 +278,9 @@ module Spree
def convert_variant_weight_to_decimal
self.weight = weight.to_d
end
def touch_distributors
Enterprise.distributing_variants(id).each(&:touch)
end
end
end

View File

@@ -31,16 +31,17 @@ class ProductScopeQuery
product_scope.find(@params[:product_id])
end
def paged_products_for_producers
def products_for_producers
producer_ids = OpenFoodNetwork::Permissions.new(@user).
variant_override_producers.by_name.select('enterprises.id')
# Use `order("enterprises.name")` instead of `by_producer scope`, the scope adds a join
# on variants which messes our query
Spree::Product.where(nil).
merge(product_scope).
includes(variants: [:product, :default_price, :stock_items]).
where(supplier_id: producer_ids).
by_producer.by_name.
ransack(@params[:q]).result
includes(variants: [:product, :default_price, :stock_items, :supplier]).
where(variants: { supplier_id: producer_ids }).
ransack(@params[:q]).result(distinct: true)
end
def product_scope

View File

@@ -108,7 +108,7 @@ class ProductsReflex < ApplicationReflex
def ransack_query
query = {}
query.merge!(supplier_id_in: @producer_id) if @producer_id.present?
query.merge!(variants_supplier_id_in: @producer_id) if @producer_id.present?
if @search_term.present?
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end

View File

@@ -7,7 +7,7 @@ module Api
attributes :name, :supplier_name, :image_url, :variants
def supplier_name
object.supplier&.name
object.variants.first.supplier&.name
end
def image_url

View File

@@ -9,7 +9,7 @@ module Api
has_one :order, serializer: Api::Admin::IdSerializer
def supplier
{ id: object.product.supplier_id }
{ id: object.supplier.id }
end
def units_product

View File

@@ -7,8 +7,6 @@ module Api
:inherits_properties, :on_hand, :price, :import_date, :image_url,
:thumb_url, :variants
has_one :supplier, key: :producer_id, embed: :id
def variants
ActiveModel::ArraySerializer.new(
object.variants,

View File

@@ -3,13 +3,9 @@
module Api
module Admin
class ProductSimpleSerializer < ActiveModel::Serializer
attributes :id, :name, :producer_id
attributes :id, :name
has_many :variants, key: :variants, serializer: Api::Admin::VariantSimpleSerializer
def producer_id
object.supplier_id
end
end
end
end

View File

@@ -9,6 +9,7 @@ module Api
:price, :on_demand, :on_hand, :in_stock, :stock_location_id, :stock_location_name
has_one :primary_taxon, key: :category_id, embed: :id
has_one :supplier, key: :producer_id, embed: :id
def name
if object.full_name.present?
@@ -31,7 +32,7 @@ module Api
end
def producer_name
object.product.supplier.name
object.supplier.name
end
def image

View File

@@ -6,7 +6,7 @@ module Api
attributes :id, :name, :import_date,
:options_text, :unit_value, :unit_description, :unit_to_display,
:display_as, :display_name, :name_to_display,
:price, :on_demand, :on_hand
:price, :on_demand, :on_hand, :producer_id
has_many :variant_overrides
@@ -27,6 +27,10 @@ module Api
def price
object.price.nil? ? 0.to_f : object.price
end
def producer_id
object.supplier_id
end
end
end
end

View File

@@ -10,7 +10,6 @@ class Api::ProductSerializer < ActiveModel::Serializer
has_many :variants, serializer: Api::VariantSerializer
has_one :image, serializer: Api::ImageSerializer
has_one :supplier, serializer: Api::IdSerializer
# return an unformatted descripton
def description

View File

@@ -9,6 +9,8 @@ class Api::VariantSerializer < ActiveModel::Serializer
:tag_list, :thumb_url,
:unit_price_price, :unit_price_unit
has_one :supplier, serializer: Api::IdSerializer
delegate :price, to: :object
def fees

View File

@@ -3,6 +3,5 @@
class Invoice
class ProductSerializer < ActiveModel::Serializer
attributes :name
has_one :supplier, serializer: Invoice::EnterpriseSerializer
end
end

View File

@@ -4,5 +4,6 @@ class Invoice
class VariantSerializer < ActiveModel::Serializer
attributes :id, :display_name, :options_text
has_one :product, serializer: Invoice::ProductSerializer
has_one :supplier, serializer: Invoice::EnterpriseSerializer
end
end

View File

@@ -30,7 +30,7 @@ class ExchangeProductsRenderer
end
def supplied_products(enterprises_query_matcher)
products_relation = Spree::Product.where(supplier_id: enterprises_query_matcher).order(:name)
products_relation = Spree::Product.in_supplier(enterprises_query_matcher).order(:name)
filter_visible(products_relation)
end
@@ -95,7 +95,7 @@ class ExchangeProductsRenderer
return enterprises if enterprises.empty?
enterprises.includes(
supplied_products: [:supplier, :variants, :image]
supplied_products: [{ variants: :supplier }, :image]
)
end
end

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
class ExchangeVariantDeleter
def delete(product)
ExchangeVariant.
where(variant_id: product.variants.select(:id)).
delete_all
def delete(variant)
ExchangeVariant.where(variant_id: variant.id).delete_all
end
end

View File

@@ -11,11 +11,8 @@ module OrderCycles
@customer = customer
end
def products_relation
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
# TODO refactor products_taxons_relation and products_supplier_relation
# 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
@@ -31,6 +28,28 @@ module OrderCycles
group("spree_products.id, first_variant.primary_taxon_id")
end
# Joins on the first product variant to allow us to filter product by supplier. This is so
# enterprise can display product sorted by supplier in a custom order on their shopfront.
#
# Caveat, the supplier sorting won't work properly if there are multiple variant with different
# supplier for a given product.
#
def products_supplier_relation
Spree::Product.where(id: stocked_products).
joins("LEFT JOIN (SELECT DISTINCT ON(product_id) id, product_id, supplier_id
FROM spree_variants WHERE deleted_at IS NULL) first_variant
ON spree_products.id = first_variant.product_id").
select("spree_products.*, first_variant.supplier_id").
group("spree_products.id, first_variant.supplier_id")
end
def supplier_property_join(query)
query.joins("
JOIN enterprises ON enterprises.id = first_variant.supplier_id
LEFT OUTER JOIN producer_properties ON producer_properties.producer_id = enterprises.id
")
end
def variants_relation
order_cycle.
variants_distributed_by(distributor).

View File

@@ -83,7 +83,7 @@ module Permissions
Spree::Order.with_line_items_variants_and_products_outer.
where(
distributor_id: granted_distributor_ids,
spree_products: { supplier_id: enterprises_with_associated_orders }
spree_variants: { supplier_id: enterprises_with_associated_orders }
).
where_clause.__send__(:predicates).
reduce(:and)

View File

@@ -4,11 +4,11 @@ module PermittedAttributes
class Product
def self.attributes
[
:id, :name, :description, :supplier_id, :price,
:id, :name, :description, :price,
:variant_unit, :variant_unit_scale, :variant_unit_with_scale, :unit_value,
:unit_description, :variant_unit_name,
:display_as, :sku, :group_buy, :group_buy_unit_size,
:taxon_ids, :primary_taxon_id, :tax_category_id,
:taxon_ids, :primary_taxon_id, :tax_category_id, :supplier_id,
:meta_keywords, :notes, :inherits_properties,
{ product_properties_attributes: [:id, :property_name, :value],
variants_attributes: [PermittedAttributes::Variant.attributes],

View File

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

View File

@@ -2,7 +2,8 @@
require 'open_food_network/scope_product_to_hub'
class ProductsRenderer
class ProductsRenderer # rubocop:disable Metrics/ClassLength
include Pagy::Backend
class NoProducts < RuntimeError; end
@@ -34,12 +35,12 @@ class ProductsRenderer
return unless order_cycle
@products ||= begin
results = distributed_products.
products_taxons_relation.
results = products_relation.
order(Arel.sql(products_order))
filter_and_paginate(results).
each { |product| product_scoper.scope(product) } # Scope results with variant_overrides
results = filter(results)
# Scope results with variant_overrides
paginate(results).each { |product| product_scoper.scope(product) }
end
end
@@ -51,10 +52,57 @@ class ProductsRenderer
OpenFoodNetwork::EnterpriseFeeCalculator.new distributor, order_cycle
end
def filter_and_paginate(query)
results = query.ransack(args[:q]).result
# TODO refactor this, distributed_products should be able to give use the relation based
# on the sorting method, same for ordering. It would prevent the SQL implementation from
# leaking here
def products_relation
if distributor.preferred_shopfront_product_sorting_method == "by_category" &&
distributor.preferred_shopfront_taxon_order.present?
return distributed_products.products_taxons_relation
end
_pagy, paginated_results = pagy_arel(
distributed_products.products_supplier_relation
end
# TODO: refactor to address CyclomaticComplexity
def filter(query) # rubocop:disable Metrics/CyclomaticComplexity
supplier_properties = args[:q]&.slice("with_variants_supplier_properties")
ransack_results = query.ransack(args[:q]).result.to_a
return ransack_results if supplier_properties.blank?
with_properties = args[:q]&.dig("with_properties")
supplier_properties_results = []
if supplier_properties.present?
# We can't search on an association's scope with ransack, a work around is to define
# the a scope on the parent (Spree::Product) but because we are joining on "first_variant"
# to get the supplier it doesn't work, so we do the filtering manually here
# see:
# OrderCycleDistributedProducts#products_supplier_relation
# OrderCycleDistributedProducts#supplier_property_join
supplier_property_ids = supplier_properties["with_variants_supplier_properties"]
supplier_properties_results = distributed_products.supplier_property_join(query).
where(producer_properties: { property_id: supplier_property_ids }).
where(inherits_properties: true)
end
if supplier_properties_results.present? && with_properties.present?
# apply "OR" between property search
return ransack_results | supplier_properties_results
end
# Intersect the result to apply "AND" with other search criteria
return ransack_results.intersection(supplier_properties_results) \
unless supplier_properties_results.empty?
# We should get here but just in case we return the ransack results
ransack_results
end
def paginate(results)
_pagy, paginated_results = pagy_array(
results,
page: args[:page] || 1,
items: args[:per_page] || DEFAULT_PER_PAGE
@@ -67,12 +115,13 @@ class ProductsRenderer
OrderCycles::DistributedProductsService.new(distributor, order_cycle, customer)
end
# TODO refactor, see above
def products_order
if distributor.preferred_shopfront_product_sorting_method == "by_producer" &&
distributor.preferred_shopfront_producer_order.present?
order_by_producer = distributor
.preferred_shopfront_producer_order
.split(",").map { |id| "spree_products.supplier_id=#{id} DESC" }
.split(",").map { |id| "first_variant.supplier_id=#{id} DESC" }
.join(", ")
"#{order_by_producer}, spree_products.name ASC, spree_products.id ASC"
elsif distributor.preferred_shopfront_product_sorting_method == "by_category" &&
@@ -87,7 +136,6 @@ class ProductsRenderer
end
end
def variants_for_shop
@variants_for_shop ||= begin
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)

View File

@@ -55,8 +55,6 @@ module Sets
def update_product(product, attributes)
return false unless update_product_only_attributes(product, attributes)
ExchangeVariantDeleter.new.delete(product) if product.saved_change_to_supplier_id?
update_product_variants(product, attributes)
end
@@ -107,6 +105,8 @@ module Sets
if variant.present?
variant.assign_attributes(variant_attributes.except(:id))
variant.save if variant.changed?
ExchangeVariantDeleter.new.delete(variant) if variant.saved_change_to_supplier_id?
else
variant = create_variant(product, variant_attributes)
end

View File

@@ -25,13 +25,8 @@
-# empty
%td.col-on_hand.align-right
-# empty
%td.col-producer.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :supplier_id,
aria_label: t('.producer_field_name'),
options: producer_options,
selected_option: product.supplier_id,
placeholder_value: t('admin.products_v3.filters.search_for_producers')))
%td.col-on_hand.align-right
-# empty
%td.col-category.align-left
-# empty
%td.col-tax_category.align-left

View File

@@ -4,17 +4,17 @@
action: 'rails-nested-form:add->bulk-form#registerElements rails-nested-form:remove->bulk-form#toggleFormChanged' },
class: (defined?(should_slide_in) && should_slide_in) ? 'slide-in' : '' }
%tr
= render partial: 'product_row', locals: { f: product_form, product:, producer_options:, product_index: }
= render partial: 'product_row', locals: { f: product_form, product:, product_index: }
- product.variants.each_with_index do |variant, variant_index|
= form.fields_for("products][#{product_index}][variants_attributes][", variant, index: variant_index) do |variant_form|
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options: }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options:, producer_options: }
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", product.variants.build) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }
%tr.condensed{ 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': "true" }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form, category_options:, tax_category_options: }
= render partial: 'variant_row', locals: { variant: new_variant_form.object, f: new_variant_form, category_options:, tax_category_options:, producer_options: }
%tr{ 'data-nested-form-target': "target" }
%tr.condensed

View File

@@ -39,8 +39,13 @@
= f.label :on_demand do
= f.check_box :on_demand, 'data-action': 'change->toggle-control#disableIfPresent change->popout#closeIfChecked'
= t(:on_demand)
%td.col-producer.align-left
-# empty producer name
%td.col-producer.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :supplier_id,
aria_label: t('.producer_field_name'),
options: producer_options,
selected_option: variant.supplier_id,
placeholder_value: t('admin.products_v3.filters.search_for_producers')))
%td.col-category.field.naked_inputs
= render(SearchableDropdownComponent.new(form: f,
name: :primary_taxon_id,

View File

@@ -1,5 +1,5 @@
%tr.product.even
%td.producer{ "ng-show": 'columns.producer.visible', "ng-bind-html": '::producersByID[product.producer_id].name' }
%td.producer{ "ng-show": 'columns.producer.visible' }
%td.product{ "ng-show": 'columns.product.visible', "ng-bind": '::product.name' }
%td.sku{ "ng-show": 'columns.sku.visible' }
%td.price{ "ng-show": 'columns.price.visible' }

View File

@@ -1,5 +1,5 @@
%tr.variant{ id: "v_{{variant.id}}", "ng-repeat": 'variant in product.variants | inventoryVariants:hub_id:views' }
%td.producer{ "ng-show": 'columns.producer.visible' }
%td.producer{ "ng-show": 'columns.producer.visible', "ng-bind": '::producersByID[variant.producer_id].name' }
%td.product{ "ng-show": 'columns.product.visible' }
%span{ "ng-bind": '::variant.display_name || ""' }
.variant-override-unit{ "ng-bind": '::variant.unit_to_display' }

View File

@@ -7,6 +7,7 @@
= render 'admin/variant_overrides/loading_flash'
= render 'admin/variant_overrides/controls'
= render 'admin/variant_overrides/no_results'
// filteredProducts is defined in admin/variant_overrides/products
%div{ "ng-cloak": true, "ng-show": 'hub_id && filteredProducts.length > 0' }
= render 'admin/variant_overrides/new_products'
= render 'admin/variant_overrides/hidden_products'

View File

@@ -40,7 +40,7 @@
= line_items.first.variant.sku
- if @distributors_pickup_times.many?
%td
= line_items.first.product.supplier.name
= line_items.first.variant.supplier.name
%td
= product_and_full_name
%td.text-right

View File

@@ -15,7 +15,7 @@ Orders summary
= t :producer_mail_order_text
\
- @grouped_line_items.each_pair do |product_and_full_name, line_items|
#{line_items.first.variant.sku} - #{raw(line_items.first.product.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)}
#{line_items.first.variant.sku} - #{raw(line_items.first.variant.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)}
\
\
#{t :total}: #{@total}

View File

@@ -1,10 +1,12 @@
= cache_with_locale do
%span{ "ng-show" => "query && ( appliedPropertiesList() || appliedTaxonsList() )" }
%span{ "ng-show" => "query && ( appliedPropertiesList() || appliedProducerPropertiesList() || appliedTaxonsList() )" }
= t :products_filters_in
%span.applied-properties{'ng-bind-html' => 'appliedPropertiesList()'}
%span{ "ng-show" => "appliedPropertiesList() && appliedProducerPropertiesList()" }
= t :products_or
%span.applied-properties{'ng-bind-html' => 'appliedProducerPropertiesList()'}
%span{ "ng-show" => "appliedPropertiesList() && appliedTaxonsList()" }
%span{ "ng-show" => "(appliedPropertiesList() || appliedProducerPropertiesList()) && appliedTaxonsList()" }
= t :products_and
%span.applied-taxons{'ng-bind-html' => 'appliedTaxonsList()'}

View File

@@ -4,3 +4,7 @@
.filter-shopfront.property-selectors{ "ng-show": 'supplied_properties != null' }
%filter-selector{ 'selector-set' => "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}
.filter-shopfront.property-selectors{ng: {show: 'supplied_producer_properties != null'}}
%filter-selector{ 'selector-set' => "producerPropertySelectors", objects: "supplied_producer_properties", "active-selectors" => "activeProducerProperties"}

View File

@@ -1,5 +1,5 @@
= cache_with_locale do
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedTaxonsList()" }
.row.animate-slide{ "ng-show" => "query || appliedPropertiesList() || appliedProducerPropertiesList() || appliedTaxonsList()" }
.small-12.columns
.alert-box.search-alert.ng-scope
%div{"ng-show" => "Products.products.length > 0"}

View File

@@ -16,7 +16,7 @@
= render 'spree/shared/line_item_name', line_item: item
%br
%small
%em= item.variant.product.supplier.name
%em= item.variant.supplier.name
%td{:align => "right"}
= item.quantity
%td{:align => "right"}

View File

@@ -19,7 +19,7 @@
= render 'spree/shared/line_item_name', line_item: item
%br
%small
%em= item.variant.product.supplier.name
%em= item.variant.supplier.name
%td{:align => "right"}
= item.quantity
%td{:align => "right"}

View File

@@ -27,7 +27,7 @@
= render partial: 'product_property_fields', locals: { f: pp_form }
= f.check_box :inherits_properties
= f.label :inherits_properties, t('.inherits_properties_checkbox_hint', supplier: @product.supplier.name)
= f.label :inherits_properties, t('.inherits_properties_checkbox_hint', supplier: @supplier.name)
%br
%br
@@ -39,7 +39,7 @@
%th= t('admin.description')
%th.actions
%tbody#producer_properties
- @product.supplier.producer_properties.each do |producer_property|
- @supplier.producer_properties.each do |producer_property|
%tr
%td= producer_property.property.presentation
%td= producer_property.value

View File

@@ -27,12 +27,6 @@
= f.text_field :variant_unit_name, {placeholder: t('admin.products.unit_name_placeholder')}
= f.error_message_on :variant_unit_name
= f.field_container :supplier do
= f.label :supplier, t(:spree_admin_supplier)
%br
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2"})
= f.error_message_on :supplier
.clear
.clear

View File

@@ -6,7 +6,6 @@
%a{class: 'image-modal'}
%img{'ng-src' => '{{ product.thumb_url }}'}
%td.producer{ 'ng-show' => 'columns.producer.visible' }
%select.fullwidth{ "data-controller": "tom-select", 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
%td.sku{ 'ng-show' => 'columns.sku.visible' }
%input{ 'ng-model' => "product.sku", :name => 'product_sku', 'ofn-track-product' => 'sku', :type => 'text' }
%td.name{ 'ng-show' => 'columns.name.visible' }

View File

@@ -4,6 +4,7 @@
%a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last", 'ofn-with-tip' => t('.new_variant') }
%td{ 'ng-show' => 'columns.image.visible' }
%td{ 'ng-show' => 'columns.producer.visible' }
%select.fullwidth{ "data-controller": "tom-select", 'ng-model' => 'variant.producer_id', :name => 'producer_id', 'ofn-track-variant' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' }
%td{ 'ng-show' => 'columns.sku.visible' }
%input{ 'ng-model' => "variant.sku", :name => 'variant_sku', 'ofn-track-variant' => 'sku', :type => 'text' }
%td{ 'ng-show' => 'columns.name.visible' }

View File

@@ -76,4 +76,8 @@
= 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, t(:spree_admin_supplier)
= f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"})
.clear

View File

@@ -20,7 +20,7 @@
= render 'spree/shared/line_item_name', line_item: item
%br
%small
%em= item.variant.product.supplier.name
%em= item.variant.supplier.name
%td
- if item.variant.sku.blank?
\-

View File

@@ -1,15 +1,16 @@
# frozen_string_literal: true
require 'pagy/extras/arel'
require 'pagy/extras/array'
require 'pagy/extras/items'
require 'pagy/extras/overflow'
# Pagy Variables
# See https://ddnexus.github.io/pagy/api/pagy#variables
Pagy::DEFAULT[:items] = 100
Pagy::DEFAULT[:items] = 100
# Items extra: Allow the client to request a custom number of items per page with an optional selector UI
# 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
Pagy::DEFAULT[:items_param] = :per_page
Pagy::DEFAULT[:max_items] = 100

View File

@@ -69,7 +69,6 @@ en:
name: "Product Name"
price: "Price"
primary_taxon: "Product Category"
supplier: "Supplier"
shipping_category_id: "Shipping Category"
variant_unit: "Variant Unit"
variant_unit_name: "Variant Unit Name"
@@ -77,6 +76,7 @@ en:
spree/variant:
primary_taxon: "Product Category"
shipping_category_id: "Shipping Category"
supplier: "Supplier"
spree/credit_card:
base: "Credit Card"
number: "Number"
@@ -917,7 +917,6 @@ en:
search_for_tax_categories: "Search for tax categories"
category_field_name: "Category"
tax_category_field_name: "Tax Category"
product_row:
producer_field_name: "Producer"
clone:
success: Successfully cloned the product

View File

@@ -60,6 +60,7 @@ Openfoodnetwork::Application.routes.draw do
get :products, on: :member
get :taxons, on: :member
get :properties, on: :member
get :producer_properties, on: :member
end
resources :exchanges, only: [:show], to: 'exchange_products#index' do

View File

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

View File

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

View File

@@ -977,10 +977,12 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_25_024328) do
t.bigint "tax_category_id"
t.bigint "shipping_category_id"
t.bigint "primary_taxon_id"
t.bigint "supplier_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"
t.index ["supplier_id"], name: "index_spree_variants_on_supplier_id"
t.index ["tax_category_id"], name: "index_spree_variants_on_tax_category_id"
t.check_constraint "unit_value > 0::double precision", name: "positive_unit_value"
end
@@ -1221,6 +1223,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_06_25_024328) do
add_foreign_key "spree_taxons", "spree_taxons", column: "parent_id", name: "spree_taxons_parent_id_fk"
add_foreign_key "spree_users", "spree_addresses", column: "bill_address_id", name: "spree_users_bill_address_id_fk"
add_foreign_key "spree_users", "spree_addresses", column: "ship_address_id", name: "spree_users_ship_address_id_fk"
add_foreign_key "spree_variants", "enterprises", column: "supplier_id"
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"

View File

@@ -20,12 +20,7 @@ module Catalog
attr_reader :excluded_items_ids, :enterprise_ids
def enterprise_variants_relation
relation = Spree::Variant
.joins(:product)
.where(
spree_products: { supplier_id: enterprise_ids },
spree_variants: { deleted_at: nil }
)
relation = Spree::Variant.where(supplier_id: enterprise_ids)
return relation if excluded_items_ids.blank?

View File

@@ -10,8 +10,8 @@ module Catalog
describe '#reset' do
let(:supplier_ids) { enterprise.id }
let(:product) { create(:product) }
let(:enterprise) { product.supplier }
let(:variant) { product.variants.first }
let(:enterprise) { variant.supplier }
before { variant.on_hand = 2 }

View File

@@ -20,8 +20,12 @@ module DfcProvider
)
product = variant.product
if variant.new_record?
variant.supplier = current_enterprise
variant.save!
end
product.save! if product.new_record?
variant.save! if variant.new_record?
supplied_product = SuppliedProductBuilder.supplied_product(variant)
render json: DfcIo.export(supplied_product)

View File

@@ -3,7 +3,7 @@
class DfcBuilder
def self.catalog_item(variant)
id = urls.enterprise_catalog_item_url(
enterprise_id: variant.product.supplier_id,
enterprise_id: variant.supplier_id,
id: variant.id,
)
product = SuppliedProductBuilder.supplied_product(variant)

View File

@@ -3,7 +3,7 @@
class OfferBuilder < DfcBuilder
def self.build(variant)
id = urls.enterprise_offer_url(
enterprise_id: variant.product.supplier_id,
enterprise_id: variant.supplier_id,
id: variant.id,
)

View File

@@ -3,11 +3,11 @@
class SuppliedProductBuilder < DfcBuilder
def self.supplied_product(variant)
id = urls.enterprise_supplied_product_url(
enterprise_id: variant.product.supplier_id,
enterprise_id: variant.supplier_id,
id: variant.id,
)
product_uri = urls.enterprise_url(
variant.product.supplier_id,
variant.supplier_id,
spree_product_id: variant.product_id
)
@@ -29,14 +29,15 @@ class SuppliedProductBuilder < DfcBuilder
if product
Spree::Variant.new(
product:,
supplier:,
price: 0,
).tap do |variant|
apply(supplied_product, variant)
end
else
product = import_product(supplied_product)
product.supplier = supplier
product.ensure_standard_variant
product.variants.first.supplier = supplier
product.variants.first
end.tap do |variant|
link = supplied_product.semanticId

View File

@@ -15,7 +15,7 @@ RSpec.describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml",
let(:product) {
create(
:base_product,
id: 90_000, supplier: enterprise, name: "Apple", description: "Red",
id: 90_000, name: "Apple", description: "Red",
variants: [variant],
primary_taxon: non_local_vegetable
)
@@ -27,7 +27,7 @@ RSpec.describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, sku: "AR") }
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, sku: "AR", supplier: enterprise) }
before { login_as user }

View File

@@ -27,7 +27,7 @@ RSpec.describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_aut
let!(:product) {
create(
:product_with_image,
id: 90_000, supplier: enterprise, name: "Apple", description: "Round",
id: 90_000, name: "Apple", description: "Round",
variants: [variant],
primary_taxon: non_local_vegetable
)
@@ -39,7 +39,9 @@ RSpec.describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_aut
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, sku: "APP") }
let(:variant) {
build(:base_variant, id: 10_001, unit_value: 1, sku: "APP", supplier: enterprise)
}
before { login_as user }

View File

@@ -9,11 +9,11 @@ RSpec.describe "Offers", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc:
create(
:product,
id: 90_000,
supplier: enterprise, name: "Pesto", description: "Basil Pesto",
name: "Pesto", description: "Basil Pesto",
variants: [variant],
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1) }
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, supplier: enterprise) }
before { login_as user }

View File

@@ -9,11 +9,13 @@ RSpec.describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswa
create(
:product_with_image,
id: 90_000,
supplier: enterprise, name: "Pesto", description: "Basil Pesto",
name: "Pesto", description: "Basil Pesto",
variants: [variant]
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, primary_taxon: taxon) }
let(:variant) {
build(:base_variant, id: 10_001, unit_value: 1, primary_taxon: taxon, supplier: enterprise)
}
let(:taxon) {
build(
:taxon,

View File

@@ -8,7 +8,7 @@ RSpec.describe DfcBuilder do
describe ".catalog_item" do
it "assigns a semantic id" do
variant.id = 5
variant.product.supplier_id = 7
variant.supplier_id = 7
item = DfcBuilder.catalog_item(variant)
@@ -19,7 +19,7 @@ RSpec.describe DfcBuilder do
it "refers to a supplied product" do
variant.id = 5
variant.product.supplier_id = 7
variant.supplier_id = 7
item = DfcBuilder.catalog_item(variant)

View File

@@ -5,7 +5,7 @@ require_relative "../spec_helper"
RSpec.describe EnterpriseBuilder do
subject(:builder) { described_class }
let(:enterprise) {
build(
create(
:enterprise,
id: 10_000, name: "Fabi's Farm",
description: "The place where stuff grows", abn: "123 456 789 0",
@@ -13,7 +13,7 @@ RSpec.describe EnterpriseBuilder do
)
}
let(:variant) {
create(:product, supplier: enterprise, name: "Apple").variants.first
create(:product, supplier_id: enterprise.id, name: "Apple").variants.first
}
describe ".enterprise" do

View File

@@ -7,13 +7,13 @@ RSpec.describe SuppliedProductBuilder do
subject(:builder) { described_class }
let(:variant) {
build(:variant, id: 5, product: spree_product, primary_taxon: taxon)
create(:variant, id: 5, product: spree_product, primary_taxon: taxon, supplier:)
}
let(:spree_product) {
create(:product, id: 6, supplier:)
create(:product, id: 6)
}
let(:supplier) {
build(:supplier_enterprise, id: 7)
create(:supplier_enterprise, id: 7)
}
let(:taxon) {
build(
@@ -43,7 +43,7 @@ RSpec.describe SuppliedProductBuilder do
variant.product.name = "Apple"
product = builder.supplied_product(variant)
expect(product.name).to eq "Apple"
expect(product.name).to match /Apple/
end
it "assigns the variant name if present" do
@@ -51,7 +51,7 @@ RSpec.describe SuppliedProductBuilder do
variant.display_name = "Granny Smith"
product = builder.supplied_product(variant)
expect(product.name).to eq "Apple - Granny Smith"
expect(product.name).to match /Apple - Granny Smith/
end
context "product_type mapping" do
@@ -244,7 +244,7 @@ RSpec.describe SuppliedProductBuilder do
it "doesn't return a product of another enterprise" do
variant.save!
create(:product, id: 8, supplier: create(:enterprise))
create(:product, id: 8, supplier_id: create(:enterprise).id)
supplied_product.spree_product_uri =
"http://test.host/api/dfc/enterprises/7?spree_product_id=8"

View File

@@ -8,15 +8,14 @@ module OrderManagement
# - Variants of hub
# - Variants that are in outgoing exchanges where the hub is receiver
def self.eligible_variants(distributor)
variant_conditions = ["spree_products.supplier_id IN (?)",
permitted_producer_ids(distributor)]
query = Spree::Variant.where(supplier_id: permitted_producer_ids(distributor))
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
if exchange_variant_ids.present?
variant_conditions[0] << " OR spree_variants.id IN (?)"
variant_conditions << exchange_variant_ids
query = query.or(Spree::Variant.where(id: exchange_variant_ids))
end
Spree::Variant.joins(:product).where(*variant_conditions)
query
end
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)

View File

@@ -8,18 +8,18 @@ module OrderManagement
describe "creating a new subscription" do
let!(:shop) { create(:distributor_enterprise) }
let!(:customer) { create(:customer, enterprise: shop) }
let!(:product1) { create(:product, supplier: shop) }
let!(:product2) { create(:product, supplier: shop) }
let!(:product3) { create(:product, supplier: shop) }
let!(:product1) { create(:product) }
let!(:product2) { create(:product) }
let!(:product3) { create(:product) }
let!(:variant1) {
create(:variant, product: product1, unit_value: '100', price: 12.00)
create(:variant, product: product1, unit_value: '100', price: 12.00, supplier: shop)
}
let!(:variant2) {
create(:variant, product: product2, unit_value: '1000', price: 6.00)
create(:variant, product: product2, unit_value: '1000', price: 6.00, supplier: shop)
}
let!(:variant3) {
create(:variant, product: product2, unit_value: '1000',
price: 2.50, on_hand: 1)
price: 2.50, on_hand: 1, supplier: shop)
}
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
let!(:order_cycle1) {

View File

@@ -8,8 +8,7 @@ module OrderManagement
describe "variant eligibility for subscription" do
let!(:shop) { create(:distributor_enterprise) }
let!(:producer) { create(:supplier_enterprise) }
let!(:product) { create(:product, supplier: producer) }
let!(:variant) { product.variants.first }
let!(:variant) { create(:variant, supplier: producer) }
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
let!(:subscription) { create(:subscription, shop:, schedule:) }
@@ -45,7 +44,7 @@ module OrderManagement
context "if the supplier is permitted for the shop" do
let!(:enterprise_relationship) {
create(:enterprise_relationship, child: shop,
parent: product.supplier,
parent: variant.supplier,
permissions_list: [:add_to_order_cycle])
}
@@ -60,7 +59,7 @@ module OrderManagement
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
order_cycle.exchanges.create(sender: variant.supplier,
receiver: shop,
incoming: true, variants: [variant])
}
@@ -72,7 +71,7 @@ module OrderManagement
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
order_cycle.exchanges.create(sender: variant.supplier,
receiver: shop,
incoming: false,
variants: [variant])
@@ -123,7 +122,7 @@ module OrderManagement
context "if it is an incoming exchange where the shop is the receiver" do
let!(:incoming_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
order_cycle.exchanges.create(sender: variant.supplier,
receiver: shop,
incoming: true,
variants: [variant])
@@ -138,7 +137,7 @@ module OrderManagement
context "if it is an outgoing exchange where the shop is the receiver" do
let!(:outgoing_exchange) {
order_cycle.exchanges.create(sender: product.supplier,
order_cycle.exchanges.create(sender: variant.supplier,
receiver: shop,
incoming: false,
variants: [variant])

View File

@@ -151,7 +151,7 @@ module OpenFoodNetwork
end
def all_variants_supplied_by(producer)
Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', producer)
Spree::Variant.where(supplier: producer)
end
def no_variants
@@ -163,9 +163,9 @@ module OpenFoodNetwork
user_manages_coordinator_or(enterprise)
end.map(&:id)
Spree::Variant.includes(product: :supplier).
select("spree_variants.id, spree_variants.product_id, spree_products.supplier_id").
joins(:product).where(spree_products: { supplier_id: valid_suppliers })
Spree::Variant.includes(:supplier).
select(:id, :product_id, :supplier_id).
where(supplier_id: valid_suppliers)
end
# Find the variants that a user is permitted see within outgoing exchanges
@@ -185,13 +185,10 @@ module OpenFoodNetwork
permitted_variants = variants_from_suppliers(producer_ids)
# PLUS my incoming producers' variants that are already in an outgoing exchange of this hub,
# so things don't break. TODO: Remove this when all P-OC are sorted out
active_variants = Spree::Variant.joins(:exchanges, :product).
where("exchanges.receiver_id = (?)
AND spree_products.supplier_id IN (?)
AND incoming = 'f'",
hub.id,
managed_producer_ids)
# so things don't break.
# TODO: Remove this when all P-OC are sorted out
active_variants = Spree::Variant.joins(:exchanges).
where(exchanges: { receiver: hub, incoming: false }, supplier_id: managed_producer_ids)
Spree::Variant.where(id: permitted_variants | active_variants)
end
@@ -238,7 +235,7 @@ module OpenFoodNetwork
end
def variants_from_suppliers(supplier_ids)
Spree::Variant.joins(:product).where(spree_products: { supplier_id: supplier_ids })
Spree::Variant.where(supplier_id: supplier_ids)
end
def active_outgoing_variants(hub)

View File

@@ -62,28 +62,26 @@ module OpenFoodNetwork
def editable_products
return Spree::Product.all if admin?
Spree::Product.where(supplier_id: @user.enterprises).or(
Spree::Product.where(supplier_id: related_enterprises_granting(:manage_products))
product_with_variants.where(spree_variants: { supplier_id: @user.enterprises }).or(
product_with_variants.where(
spree_variants: { supplier_id: related_enterprises_granting(:manage_products) }
)
)
end
def visible_products
return Spree::Product.all if admin?
Spree::Product.where(
supplier_id: @user.enterprises
).or(
Spree::Product.where(
supplier_id: related_enterprises_granting(:manage_products) |
related_enterprises_granting(:add_to_order_cycle)
product_with_variants.where(spree_variants: { supplier_id: @user.enterprises }).or(
product_with_variants.where(
spree_variants: {
supplier_id: related_enterprises_granting(:manage_products) |
related_enterprises_granting(:add_to_order_cycle)
}
)
)
end
def product_ids_supplied_by(supplier_ids)
Spree::Product.where(supplier_id: supplier_ids).select(:id)
end
def managed_product_enterprises
managed_and_related_enterprises_granting :manage_products
end
@@ -176,5 +174,9 @@ module OpenFoodNetwork
def managed_enterprise_products
Spree::Product.managed_by(@user)
end
def product_with_variants
Spree::Product.joins(:variants)
end
end
end

View File

@@ -15,7 +15,7 @@ module Reporting
@orders ||= search_orders
end
def list(line_item_includes = [variant: [product: :supplier]])
def list(line_item_includes = [variant: [:supplier, :product]])
line_items = order_permissions.visible_line_items.in_orders(orders.result)
.order("supplier.name", "product.name", "variant.display_name")

View File

@@ -19,8 +19,8 @@ module Reporting
reflect query.join(association(Spree::Variant, :product))
end
def joins_product_supplier
reflect query.join(association(Spree::Product, :supplier, supplier_alias))
def joins_variant_supplier
reflect query.join(association(Spree::Variant, :supplier, supplier_alias))
end
def joins_variant_shipping_category

View File

@@ -22,7 +22,7 @@ module Reporting
[
{
order: [:bill_address],
variant: { product: :supplier }
variant: [:product, :supplier]
}
]
end

View File

@@ -201,7 +201,6 @@ module Reporting
# Includes:
# * Line item
# * Variant
# * Product
# * Tax category of product, if enterprise fee tells to inherit
def include_line_item_source_details
join_scope(
@@ -224,13 +223,6 @@ module Reporting
JOIN_STRING
)
join_scope(
<<~JOIN_STRING
LEFT OUTER JOIN spree_products
ON (spree_products.id = spree_variants.product_id)
JOIN_STRING
)
join_scope(
<<~JOIN_STRING
LEFT OUTER JOIN spree_tax_categories AS product_tax_categories
@@ -324,7 +316,7 @@ module Reporting
def filter_by_distribution(params)
filter_scope(spree_orders: { distributor_id: params.distributor_ids }) \
if params.distributor_ids.present?
filter_scope(spree_products: { supplier_id: params.producer_ids }) \
filter_scope(spree_variants: { supplier_id: params.producer_ids }) \
if params.producer_ids.present?
filter_scope(spree_orders: { order_cycle_id: params.order_cycle_ids }) \
if params.order_cycle_ids.present?

View File

@@ -49,11 +49,11 @@ module Reporting
end
def supplier_name
proc { |line_items| line_items.first.variant.product.supplier.name }
proc { |line_items| line_items.first.variant.supplier.name }
end
def supplier_charges_sales_tax?
proc { |line_items| line_items.first.variant.product.supplier.charges_sales_tax }
proc { |line_items| line_items.first.variant.supplier.charges_sales_tax }
end
def product_name

View File

@@ -93,7 +93,7 @@ module Reporting
end
def line_item_includes
[{ variant: { product: :supplier },
[{ variant: [:product, :supplier],
order: [:bill_address, :ship_address, :order_cycle, :adjustments, :payments,
:user, :distributor, :shipments] }]
end

View File

@@ -40,7 +40,7 @@ module Reporting
:adjustments,
{ shipments: { shipping_rates: :shipping_method } }
],
variant: { product: :supplier }
variant: [:product, :supplier]
}]
end
end

View File

@@ -42,7 +42,7 @@ module Reporting
end
def line_item_includes
[{ variant: { product: :supplier } }]
[{ variant: [:supplier, :product] }]
end
def query_result

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