Merge branch 'master' into tb-rescue-not-found-error-for-enterprise

This commit is contained in:
Trésor Bireke
2025-04-01 01:10:17 +02:00
committed by GitHub
150 changed files with 5813 additions and 1207 deletions

View File

@@ -15,18 +15,15 @@ module Admin
def index
# Fetch DFC catalog JSON for preview
api = DfcRequest.new(spree_current_user)
@catalog_url = params.require(:catalog_url)
@catalog_url = params.require(:catalog_url).strip
@catalog_json = api.call(@catalog_url)
graph = DfcIo.import(@catalog_json)
catalog = DfcCatalog.new(graph)
catalog = DfcCatalog.from_json(@catalog_json)
# Render table and let user decide which ones to import.
@items = catalog.products.map do |subject|
[
subject,
@enterprise.supplied_variants.linked_to(subject.semanticId)&.product
]
end
@items = list_products(catalog)
rescue URI::InvalidURIError
flash[:error] = t ".invalid_url"
redirect_to admin_product_import_path
rescue Faraday::Error,
Addressable::URI::InvalidURIError,
ActionController::ParameterMissing => e
@@ -45,8 +42,7 @@ module Admin
ids = params.require(:semanticIds)
# Load DFC catalog JSON
graph = DfcIo.import(params.require(:catalog_json))
catalog = DfcCatalog.new(graph)
catalog = DfcCatalog.from_json(params.require(:catalog_json))
catalog.apply_wholesale_values!
# Import all selected products for given enterprise.
@@ -74,5 +70,15 @@ module Admin
.managed_product_enterprises.is_primary_producer
.find(params.require(:enterprise_id))
end
# List internal and external products for the preview.
def list_products(catalog)
catalog.products.map do |subject|
[
subject,
@enterprise.supplied_variants.linked_to(subject.semanticId)&.product
]
end
end
end
end

View File

@@ -117,7 +117,7 @@ module Api
end
def shipment_params
return {} unless params.has_key? :shipment
return {} unless params.key? :shipment
params.require(:shipment).permit(:tracking, :selected_shipping_rate_id)
end

View File

@@ -15,4 +15,12 @@ module LinkHelper
prefix + url
end
end
def new_tab_option
if feature?(:open_in_same_tab, spree_current_user)
{}
else
{ target: "_blank" }
end
end
end

View File

@@ -122,7 +122,7 @@ module Spree
end
else
if html_options['data-update'].nil? && html_options[:remote]
object_name, action = url.split('/')[-2..-1]
object_name, action = url.split('/')[-2..]
html_options['data-update'] = [action, object_name.singularize].join('_')
end

View File

@@ -55,12 +55,11 @@ class BackorderJob < ApplicationJob
ordered_quantities[item] = retail_quantity
end
return if backorder.lines.empty?
place_order(user, order, orderer, backorder)
items.each do |item|
variant = item.variant
variant.on_hand += ordered_quantities[item] if variant.on_demand
end
adjust_stock(items, ordered_quantities)
end
# We look at linked variants which are either stock controlled or
@@ -80,6 +79,9 @@ class BackorderJob < ApplicationJob
needed_quantity = needed_quantity(line_item)
solution = broker.best_offer(variant.semantic_links[0].semantic_id)
# If this product was removed from the catalog, we can't order it.
return 0 unless solution.offer
# The number of wholesale packs we need to order to fulfill the
# needed quantity.
# For example, we order 2 packs of 12 cans if we need 15 cans.
@@ -135,4 +137,14 @@ class BackorderJob < ApplicationJob
order.exchange.semantic_links.create!(semantic_id: placed_order.semanticId)
end
def adjust_stock(items, ordered_quantities)
items.each do |item|
variant = item.variant
quantity = ordered_quantities[item]
next if quantity.zero?
variant.on_hand += quantity if variant.on_demand
end
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
# Run any pre-conditions and mark order cycle as open.
#
# Currently, an order cycle is considered open in the shopfront when orders_open_at >= now.
# But now there are some pre-conditions for opening an order cycle, so we would like to change that.
# Instead, the presence of opened_at (and absence of processed_at) should indicate it is open.
class OpenOrderCycleJob < ApplicationJob
sidekiq_options retry_for: 10.minutes
def perform(order_cycle_id)
ActiveRecord::Base.transaction do
# Fetch order cycle if it's still unopened, and lock DB row until finished
order_cycle = OrderCycle.lock.find_by!(id: order_cycle_id, opened_at: nil)
sync_remote_variants(order_cycle)
# Mark as opened
opened_at = Time.zone.now
order_cycle.update_columns(opened_at:)
# And notify any subscribers
OrderCycles::WebhookService.create_webhook_job(order_cycle, 'order_cycle.opened', opened_at)
end
end
private
def sync_remote_variants(order_cycle)
# Sync any remote variants for each supplier
order_cycle.suppliers.each do |supplier|
links = variant_links_for(order_cycle, supplier)
next if links.empty?
# Find authorised user to access remote products
dfc_user = supplier.owner # we assume the owner's account is the one used to import from dfc.
import_variants(links, dfc_user)
end
end
# Fetch all remote variants for this supplier in the order cycle
def variant_links_for(order_cycle, supplier)
variants = order_cycle.exchanges.incoming.from_enterprise(supplier)
.joins(:exchange_variants).select('exchange_variants.variant_id')
SemanticLink.where(subject_id: variants)
end
def import_variants(links, dfc_user)
# Find any catalogues associated with the variants
catalogs = links.group_by do |link|
FdcUrlBuilder.new(link.semantic_id).catalog_url
end
# Import selected variants from each catalog
catalogs.each do |catalog_url, catalog_links|
catalog = DfcCatalog.load(dfc_user, catalog_url)
catalog.apply_wholesale_values!
catalog_links.each do |link|
catalog_item = catalog.item(link.semantic_id)
SuppliedProductImporter.update_product(catalog_item, link.subject) if catalog_item
end
end
end
end

View File

@@ -25,8 +25,7 @@ class OrderCycleClosingJob < ApplicationJob
def mark_as_processed
OrderCycle.where(id: recently_closed_order_cycles).update_all(
processed_at: Time.zone.now,
updated_at: Time.zone.now
processed_at: Time.zone.now
)
end
end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
# Trigger jobs for any order cycles that recently opened
class OrderCycleOpenedJob < ApplicationJob
def perform
ActiveRecord::Base.transaction do
recently_opened_order_cycles.find_each do |order_cycle|
OrderCycles::WebhookService.create_webhook_job(order_cycle, 'order_cycle.opened')
end
mark_as_opened(recently_opened_order_cycles)
end
end
private
def recently_opened_order_cycles
@recently_opened_order_cycles ||= OrderCycle
.where(opened_at: nil)
.where(orders_open_at: 1.hour.ago..Time.zone.now)
.lock.order(:id)
end
def mark_as_opened(order_cycles)
now = Time.zone.now
order_cycles.update_all(opened_at: now, updated_at: now)
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
# Trigger jobs for any order cycles that recently opened
class TriggerOrderCyclesToOpenJob < ApplicationJob
def perform
recently_opened_order_cycles.find_each do |order_cycle|
OpenOrderCycleJob.perform_later(order_cycle.id)
end
end
private
def recently_opened_order_cycles
OrderCycle
.where(opened_at: nil)
.where(orders_open_at: 1.hour.ago..Time.zone.now)
end
end

View File

@@ -13,11 +13,11 @@ class WebhookDeliveryJob < ApplicationJob
queue_as :default
def perform(url, event, payload)
def perform(url, event, payload, at: Time.zone.now)
body = {
id: job_id,
at: Time.zone.now.to_s,
event:,
at: at.to_s,
data: payload,
}

View File

@@ -16,13 +16,14 @@ class PaymentMailer < ApplicationMailer
end
def authorization_required(payment)
@payment = payment
shop_owner = @payment.order.distributor.owner
@order = payment.order
shop_owner = @order.distributor.owner
subject = I18n.t('spree.payment_mailer.authorization_required.subject',
order: @payment.order)
order: @order)
I18n.with_locale valid_locale(shop_owner) do
mail(to: shop_owner.email,
subject:)
subject:,
reply_to: @order.email)
end
end
end

View File

@@ -23,7 +23,8 @@ module Spree
I18n.with_locale valid_locale(@order.distributor.owner) do
subject = I18n.t('spree.order_mailer.cancel_email_for_shop.subject')
mail(to: @order.distributor.contact.email,
subject:)
subject:,
reply_to: @order.email)
end
end
@@ -43,7 +44,8 @@ module Spree
I18n.with_locale valid_locale(@order.user) do
subject = mail_subject(t('spree.order_mailer.confirm_email.subject'), resend)
mail(to: @order.distributor.contact.email,
subject:)
subject:,
reply_to: @order.email)
end
end

View File

@@ -1,18 +0,0 @@
# frozen_string_literal: true
require 'active_support/concern'
module ProductStock
extend ActiveSupport::Concern
def on_demand
raise 'Cannot determine product on_demand value of product with multiple variants' if
variants.size > 1
variants.first.on_demand
end
def on_hand
variants.map(&:on_hand).reduce(:+)
end
end

View File

@@ -31,19 +31,25 @@ class Enterprise < ApplicationRecord
has_many :relationships_as_parent, class_name: 'EnterpriseRelationship',
foreign_key: 'parent_id',
inverse_of: :parent,
dependent: :destroy
has_many :relationships_as_child, class_name: 'EnterpriseRelationship',
foreign_key: 'child_id',
inverse_of: :child,
dependent: :destroy
has_and_belongs_to_many :groups, join_table: 'enterprise_groups_enterprises',
class_name: 'EnterpriseGroup'
has_many :producer_properties, foreign_key: 'producer_id', dependent: :destroy
has_many :producer_properties, foreign_key: 'producer_id',
inverse_of: :producer,
dependent: :destroy
has_many :properties, through: :producer_properties
has_many :supplied_variants,
class_name: 'Spree::Variant', foreign_key: 'supplier_id', dependent: :destroy
class_name: 'Spree::Variant', foreign_key: 'supplier_id',
inverse_of: :supplier, dependent: :destroy
has_many :supplied_products, through: :supplied_variants, source: :product
has_many :distributed_orders, class_name: 'Spree::Order',
foreign_key: 'distributor_id',
inverse_of: :distributor,
dependent: :restrict_with_exception
belongs_to :address, class_name: 'Spree::Address'

View File

@@ -19,10 +19,12 @@ class OrderCycle < ApplicationRecord
# :incoming_exchanges and :outgoing_exchanges.
has_many :cached_incoming_exchanges, -> {
where incoming: true
}, class_name: "Exchange", dependent: :destroy
}, class_name: "Exchange", inverse_of: :order_cycle,
dependent: :destroy
has_many :cached_outgoing_exchanges, -> {
where incoming: false
}, class_name: "Exchange", dependent: :destroy
}, class_name: "Exchange", inverse_of: :order_cycle,
dependent: :destroy
has_many :orders, class_name: 'Spree::Order', dependent: :restrict_with_exception
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges

View File

@@ -2,7 +2,7 @@
module Spree
class Country < ApplicationRecord
has_many :states, -> { order('name ASC') }, dependent: :destroy
has_many :states, -> { order('name ASC') }, inverse_of: :country, dependent: :destroy
validates :name, :iso_name, presence: true

View File

@@ -84,7 +84,7 @@ module Spree
end
def refund(payment, amount)
refund_type = payment.amount == amount.to_f ? "Full" : "Partial"
refund_type = payment.amount == amount.to_d ? "Full" : "Partial"
refund_transaction = provider.build_refund_transaction(
TransactionID: payment.source.transaction_id,
RefundType: refund_type,
@@ -97,7 +97,7 @@ module Spree
refund_transaction_response = provider.refund_transaction(refund_transaction)
if refund_transaction_response.success?
payment.source.update(
refunded_at: Time.now,
refunded_at: Time.zone.now,
refund_transaction_id: refund_transaction_response.RefundTransactionID,
state: "refunded",
refund_type:

View File

@@ -4,7 +4,8 @@ module Spree
class InventoryUnit < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant",
inverse_of: :inventory_units
belongs_to :order, class_name: "Spree::Order"
belongs_to :shipment, class_name: "Spree::Shipment"
belongs_to :return_authorization, class_name: "Spree::ReturnAuthorization",

View File

@@ -13,7 +13,7 @@ module Spree
belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items
has_one :order_cycle, through: :order
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant", inverse_of: :line_items
has_one :product, through: :variant
has_one :supplier, through: :variant
belongs_to :tax_category, class_name: "Spree::TaxCategory", optional: true

View File

@@ -47,6 +47,7 @@ module Spree
has_many :payments, dependent: :destroy
has_many :return_authorizations, dependent: :destroy, inverse_of: :order
has_many :adjustments, -> { order "#{Spree::Adjustment.table_name}.created_at ASC" },
inverse_of: :adjustable,
as: :adjustable,
dependent: :destroy
@@ -65,6 +66,7 @@ module Spree
.order("#{Spree::Adjustment.table_name}.created_at ASC")
},
class_name: 'Spree::Adjustment',
inverse_of: :order,
dependent: :destroy
has_many :invoices, dependent: :restrict_with_exception
belongs_to :order_cycle, optional: true

View File

@@ -22,6 +22,7 @@ module Spree
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0").completed },
class_name: "Spree::Payment", foreign_key: :source_id,
inverse_of: :source,
dependent: :restrict_with_exception
has_many :log_entries, as: :source, dependent: :destroy
@@ -115,10 +116,6 @@ module Spree
amount - offsets_total
end
def can_credit?
credit_allowed.positive?
end
def build_source
return if source_attributes.nil?
return unless payment_method&.payment_source_class

View File

@@ -6,7 +6,7 @@ module Spree
acts_as_paranoid without_default_scope: true
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices
validate :check_price
validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

View File

@@ -18,7 +18,6 @@ require 'open_food_network/property_merge'
#
module Spree
class Product < ApplicationRecord
include ProductStock
include LogDestroyPerformer
self.belongs_to_required_by_default = false
@@ -41,6 +40,7 @@ module Spree
has_many :product_properties, dependent: :destroy
has_many :properties, through: :product_properties
has_many :variants, -> { order("spree_variants.id ASC") }, class_name: 'Spree::Variant',
inverse_of: :product,
dependent: :destroy
has_many :prices, -> { order('spree_variants.id, currency') }, through: :variants

View File

@@ -6,7 +6,7 @@ module Spree
acts_as_paranoid
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :stock_items
has_many :stock_movements, dependent: :destroy
validates :variant_id, uniqueness: { scope: [:deleted_at] }

View File

@@ -32,7 +32,8 @@ module Spree
belongs_to :product, -> {
with_deleted
}, touch: true, class_name: 'Spree::Product', optional: false
}, touch: true, class_name: 'Spree::Product', optional: false,
inverse_of: :variants
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
@@ -46,12 +47,14 @@ module Spree
has_many :stock_items, dependent: :destroy, inverse_of: :variant
has_many :images, -> { order(:position) }, as: :viewable,
dependent: :destroy,
class_name: "Spree::Image"
class_name: "Spree::Image",
inverse_of: :viewable
accepts_nested_attributes_for :images
has_one :default_price,
-> { with_deleted.where(currency: CurrentConfig.get(:currency)) },
class_name: 'Spree::Price',
inverse_of: :variant,
dependent: :destroy
has_many :prices,
class_name: 'Spree::Price',

View File

@@ -2,7 +2,7 @@
class SubscriptionLineItem < ApplicationRecord
belongs_to :subscription, inverse_of: :subscription_line_items
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant'
belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: false
validates :quantity, presence: true, numericality: { only_integer: true }

View File

@@ -3,7 +3,7 @@
module Api
module Admin
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :sku, :inherits_properties, :on_hand, :price, :import_date, :image_url,
attributes :id, :name, :sku, :inherits_properties, :price, :import_date, :image_url,
:thumb_url, :variants
def variants

View File

@@ -81,7 +81,7 @@ class EmbeddedPageService
def current_referer_without_www
return unless current_referer
current_referer.start_with?('www.') ? current_referer[4..-1] : current_referer
current_referer.start_with?('www.') ? current_referer[4..] : current_referer
end
def set_embedded_layout

View File

@@ -5,9 +5,9 @@
module OrderCycles
class WebhookService
def self.create_webhook_job(order_cycle, event)
def self.create_webhook_job(order_cycle, event, at)
webhook_payload = order_cycle
.slice(:id, :name, :orders_open_at, :orders_close_at, :coordinator_id)
.slice(:id, :name, :orders_open_at, :opened_at, :orders_close_at, :coordinator_id)
.merge(coordinator_name: order_cycle.coordinator.name)
# Endpoints for coordinator owner
@@ -17,7 +17,7 @@ module OrderCycles
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map(&:webhook_endpoints)
webhook_endpoints.each do |endpoint|
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload)
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload, at:)
end
end
end

View File

@@ -92,7 +92,10 @@ module Orders
# Verifies if the in-memory payment state is different from the one stored in the database
# This is be done without reloading the payment so that in-memory data is not changed
def different_from_db_payment_state?(in_memory_payment_state, payment_id)
in_memory_payment_state != Spree::Payment.find(payment_id).state
# Re-load payment from the DB (unless it was cleared by clear_invalid_payments)
db_payment = Spree::Payment.find_by(id: payment_id)
db_payment.present? && in_memory_payment_state != db_payment.state
end
end
end

View File

@@ -2,7 +2,6 @@
require 'open_food_network/scope_product_to_hub'
class ProductsRenderer
include Pagy::Backend

View File

@@ -1,4 +1,5 @@
.flex
-# Prevent Turbo pre-fetch which changes cart state
.flex{'data-turbo-prefetch': "false"}
.columns.three.text-center.checkout-tab{"class": [("selected" if checkout_step?(:details)), ("success" unless checkout_step?(:details))]}
%div
%span.checkout-tab-number

View File

@@ -1,2 +1,2 @@
= t('spree.payment_mailer.authorization_required.message', order_number: @payment.order.number)
= link_to spree.edit_admin_order_url(@payment.order), spree.edit_admin_order_url(@payment.order)
= t('spree.payment_mailer.authorization_required.message', order_number: @order.number)
= link_to spree.edit_admin_order_url(@order), spree.edit_admin_order_url(@order)

View File

@@ -1,3 +1,3 @@
= t('spree.payment_mailer.authorization_required.message', order_number: @payment.order.number)
= t('spree.payment_mailer.authorization_required.message', order_number: @order.number)
= link_to spree.edit_admin_order_url(@payment.order), spree.edit_admin_order_url(@payment.order)
= link_to spree.edit_admin_order_url(@order), spree.edit_admin_order_url(@order)

View File

@@ -9,7 +9,7 @@
- if admin_user? or enterprise_user?
%li
%a{href: spree.admin_dashboard_path, target:'_blank'}
%a{href: spree.admin_dashboard_path, **new_tab_option}
%i.ofn-i_021-tools
= t 'label_administration'

View File

@@ -1,6 +1,6 @@
- if admin_user? or enterprise_user?
%li
%a{href: spree.admin_dashboard_path, target:'_blank'}
%a{href: spree.admin_dashboard_path, **new_tab_option}
%i.ofn-i_021-tools
= t 'label_admin'

View File

@@ -1,9 +1,9 @@
%table.index.edit-note-table
%tr.edit-note.hidden.total
%td{ colspan: "5", data: { controller: "input-char-count" }, style: "position: relative;" }
%label
%label{ for: "note" }
= t(".note_label")
= text_field_tag :note, @order.note, { maxLength: 280, data: { "input-char-count-target": "input" } }
= text_area_tag :note, @order.note, { id: "note", rows: 3, maxLength: 280, data: { "input-char-count-target": "input" }, style: "width: 100%;" }
%span.edit-note-count{ data: { "input-char-count-target": "count" }, style: "position: absolute; right: 7px; top: 7px; font-size: 11px;" }
%td.actions
@@ -15,7 +15,8 @@
- if order.note.present?
%strong
= t(".note_label")
= order.note
%pre{ style: "font-family: inherit;" }
= order.note
- else
= t(".no_note_present")