spree_core 6.0.0.rc3 → 6.0.0.rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/mailers/spree/email_template_mailer.rb +5 -2
- data/app/models/concerns/spree/has_external_references.rb +7 -6
- data/app/models/concerns/spree/preference_schema/json_schema.rb +8 -1
- data/app/models/concerns/spree/purchase/addresses.rb +95 -32
- data/app/models/concerns/spree/purchase/taxation.rb +44 -3
- data/app/models/concerns/spree/translatable_resource.rb +49 -0
- data/app/models/concerns/spree/webhook_payload_redaction.rb +1 -0
- data/app/models/spree/ability.rb +8 -0
- data/app/models/spree/address.rb +6 -0
- data/app/models/spree/cart.rb +4 -6
- data/app/models/spree/company.rb +1 -1
- data/app/models/spree/data_request.rb +34 -1
- data/app/models/spree/delivery_rate.rb +12 -4
- data/app/models/spree/delivery_rate_provider/base.rb +1 -1
- data/app/models/spree/delivery_rate_provider/estimate.rb +5 -4
- data/app/models/spree/fulfillment.rb +1 -0
- data/app/models/spree/gateway/bogus.rb +7 -0
- data/app/models/spree/line_item.rb +3 -8
- data/app/models/spree/oauth_access_grant.rb +11 -0
- data/app/models/spree/oauth_access_token.rb +16 -0
- data/app/models/spree/oauth_application.rb +97 -0
- data/app/models/spree/oauth_consent_request.rb +23 -0
- data/app/models/spree/oauth_protected_resource.rb +21 -0
- data/app/models/spree/oauth_redirect.rb +12 -0
- data/app/models/spree/option_type.rb +5 -0
- data/app/models/spree/option_value.rb +5 -0
- data/app/models/spree/order.rb +6 -2
- data/app/models/spree/payment.rb +14 -2
- data/app/models/spree/payment_capture_event.rb +2 -0
- data/app/models/spree/policy.rb +6 -0
- data/app/models/spree/price.rb +16 -33
- data/app/models/spree/price_rule.rb +3 -16
- data/app/models/spree/price_rules/market_rule.rb +0 -6
- data/app/models/spree/pricing_provider/base.rb +1 -1
- data/app/models/spree/product.rb +1 -1
- data/app/models/spree/purchase_order_item.rb +1 -1
- data/app/models/spree/return.rb +1 -0
- data/app/models/spree/sales_context.rb +162 -0
- data/app/models/spree/search_provider/database.rb +1 -1
- data/app/models/spree/stock/estimator.rb +4 -20
- data/app/models/spree/store.rb +8 -0
- data/app/models/spree/tax_exemption/item_override.rb +14 -0
- data/app/models/spree/tax_exemption.rb +26 -0
- data/app/models/spree/tax_exemption_certificate.rb +13 -2
- data/app/models/spree/tax_exemption_reason.rb +62 -0
- data/app/models/spree/tax_line.rb +5 -4
- data/app/models/spree/tax_provider/base.rb +24 -4
- data/app/models/spree/tax_provider/internal.rb +36 -29
- data/app/models/spree/tax_rate.rb +9 -35
- data/app/models/spree/translations/batch.rb +14 -4
- data/app/services/spree/carts/destroy.rb +7 -2
- data/app/services/spree/carts/price_items.rb +2 -8
- data/app/services/spree/carts/split_by_seller.rb +6 -1
- data/app/services/spree/carts/update.rb +35 -11
- data/app/services/spree/data_requests/confirm.rb +37 -0
- data/app/services/spree/data_requests/create.rb +61 -13
- data/app/services/spree/email_templates/preview.rb +24 -3
- data/app/services/spree/seeds/store_resources.rb +1 -0
- data/app/services/spree/seeds/tax_exemption_reasons.rb +28 -0
- data/app/services/spree/tax/resolve_exemptions.rb +11 -2
- data/app/services/spree/update_check.rb +8 -1
- data/app/tools/spree/agent_tools/create_export.rb +116 -0
- data/app/tools/spree/agent_tools/create_import.rb +117 -0
- data/app/tools/spree/agent_tools/create_media.rb +81 -0
- data/app/tools/spree/agent_tools/create_resource.rb +36 -0
- data/app/tools/spree/agent_tools/curate_products.rb +90 -0
- data/app/tools/spree/agent_tools/delete_resource.rb +28 -0
- data/app/tools/spree/agent_tools/describe_import_mapping.rb +73 -0
- data/app/tools/spree/agent_tools/describe_reporting.rb +39 -0
- data/app/tools/spree/agent_tools/describe_resource.rb +81 -0
- data/app/tools/spree/agent_tools/get_import_status.rb +71 -0
- data/app/tools/spree/agent_tools/get_resource.rb +97 -0
- data/app/tools/spree/agent_tools/import_tool.rb +80 -0
- data/app/tools/spree/agent_tools/propose_import_mapping.rb +138 -0
- data/app/tools/spree/agent_tools/query_report.rb +124 -0
- data/app/tools/spree/agent_tools/resource_write.rb +99 -0
- data/app/tools/spree/agent_tools/search_resources.rb +155 -0
- data/app/tools/spree/agent_tools/set_stock.rb +52 -0
- data/app/tools/spree/agent_tools/update_resource.rb +42 -0
- data/app/tools/spree/agent_tools/upload_file.rb +104 -0
- data/app/workflows/spree/carts/complete.rb +115 -2
- data/app/workflows/spree/catalogs/set_price_list.rb +2 -2
- data/app/workflows/spree/customers/anonymize.rb +38 -17
- data/app/workflows/spree/data_requests/fulfill.rb +20 -6
- data/app/workflows/spree/orders/cancel.rb +8 -0
- data/app/workflows/spree/orders/complete.rb +8 -0
- data/app/{services → workflows}/spree/orders/create.rb +50 -26
- data/app/workflows/spree/purchase_orders/create.rb +3 -0
- data/app/workflows/spree/returns/refund.rb +4 -0
- data/app/{services → workflows}/spree/stock_levels/bulk_upsert.rb +49 -7
- data/app/workflows/spree/variants/create.rb +7 -4
- data/config/locales/en.yml +18 -0
- data/db/migrate/20250902143122_fix_policies_store_association.rb +6 -4
- data/db/migrate/20260515000001_add_store_id_to_spree_newsletter_subscribers.rb +11 -2
- data/db/migrate/20260904100001_add_applied_tax_exemptions_to_spree_orders.rb +16 -0
- data/db/migrate/20260914230000_add_store_to_spree_option_types.rb +12 -2
- data/db/migrate/20261005120000_create_spree_oauth_tables.rb +67 -0
- data/db/migrate/20261016000001_remove_default_from_spree_tax_rates_included_in_price.rb +9 -0
- data/db/migrate/20261016100001_create_spree_tax_exemption_reasons.rb +21 -0
- data/db/migrate/20261017100001_add_tax_exemption_reason_to_spree_tax_exemption_certificates.rb +9 -0
- data/db/sample_data/orders.rb +1 -1
- data/lib/spree/agent_tool.rb +188 -0
- data/lib/spree/agent_tools/api_dispatch.rb +204 -0
- data/lib/spree/agent_tools/context.rb +191 -0
- data/lib/spree/agent_tools/default_catalog.rb +180 -0
- data/lib/spree/agent_tools/membership_map.rb +61 -0
- data/lib/spree/agent_tools/record_summary.rb +219 -0
- data/lib/spree/agent_tools/registry.rb +131 -0
- data/lib/spree/agent_tools/resource_map.rb +352 -0
- data/lib/spree/agent_tools/workflow_endpoints.rb +88 -0
- data/lib/spree/agent_tools/workflow_schema.rb +279 -0
- data/lib/spree/agent_tools/workflow_tool.rb +654 -0
- data/lib/spree/agent_tools.rb +24 -0
- data/lib/spree/core/configuration.rb +1 -1
- data/lib/spree/core/controller_helpers/store.rb +3 -13
- data/lib/spree/core/dependencies.rb +9 -0
- data/lib/spree/core/engine.rb +37 -1
- data/lib/spree/core/model_extensions.rb +111 -0
- data/lib/spree/core/permissions/default_catalog.rb +6 -1
- data/lib/spree/core/pricing/price_resolution.rb +4 -4
- data/lib/spree/core/version.rb +1 -1
- data/lib/spree/core.rb +27 -2
- data/lib/spree/service_module.rb +11 -0
- data/lib/spree/testing_support/factories/data_request_factory.rb +4 -0
- data/lib/spree/testing_support/factories/tax_exemption_certificate_factory.rb +13 -1
- data/lib/spree/testing_support/factories/tax_exemption_reason_factory.rb +8 -0
- data/lib/spree/upgrades/5_6_to_6_0/manifest.yml +38 -0
- data/lib/tasks/markets.rake +47 -0
- data/lib/tasks/tax_exemption_reasons.rake +48 -0
- metadata +69 -7
- data/app/models/concerns/spree/vat_price_calculation.rb +0 -99
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
class OauthAccessGrant < Spree.base_class
|
|
3
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant
|
|
4
|
+
|
|
5
|
+
self.table_name = 'spree_oauth_access_grants'
|
|
6
|
+
|
|
7
|
+
belongs_to :application,
|
|
8
|
+
class_name: 'Spree::OauthApplication',
|
|
9
|
+
optional: false
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# The credential a client sends. Its resource owner is a real user, so a
|
|
3
|
+
# call carries that person's own permissions resolved fresh on every
|
|
4
|
+
# request — narrowing a role or deleting the user takes effect immediately,
|
|
5
|
+
# without touching the token. This is what an API key cannot do: a key is a
|
|
6
|
+
# standing grant with nobody behind it.
|
|
7
|
+
class OauthAccessToken < Spree.base_class
|
|
8
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken
|
|
9
|
+
|
|
10
|
+
self.table_name = 'spree_oauth_access_tokens'
|
|
11
|
+
|
|
12
|
+
belongs_to :application,
|
|
13
|
+
class_name: 'Spree::OauthApplication',
|
|
14
|
+
optional: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# A registered OAuth client: an MCP client today, a marketplace app or a
|
|
3
|
+
# merchant-approved integration later. Clients are pre-registered rather
|
|
4
|
+
# than dynamically registered — RFC 7591 is deprecated, and the consumer
|
|
5
|
+
# clients worth supporting need no registration endpoint.
|
|
6
|
+
class OauthApplication < Spree.base_class
|
|
7
|
+
include ::Doorkeeper::Orm::ActiveRecord::Mixins::Application
|
|
8
|
+
|
|
9
|
+
self.table_name = 'spree_oauth_applications'
|
|
10
|
+
|
|
11
|
+
has_prefix_id :oauthapp
|
|
12
|
+
|
|
13
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
14
|
+
|
|
15
|
+
has_many :access_grants,
|
|
16
|
+
class_name: 'Spree::OauthAccessGrant',
|
|
17
|
+
foreign_key: :application_id,
|
|
18
|
+
dependent: :destroy
|
|
19
|
+
has_many :access_tokens,
|
|
20
|
+
class_name: 'Spree::OauthAccessToken',
|
|
21
|
+
foreign_key: :application_id,
|
|
22
|
+
dependent: :destroy
|
|
23
|
+
|
|
24
|
+
# Tokens that can still produce access.
|
|
25
|
+
#
|
|
26
|
+
# Not simply `not_expired`: an access token lasts hours while the refresh
|
|
27
|
+
# token beside it lasts far longer, so a client whose access has lapsed
|
|
28
|
+
# can mint another at will. Listing only unexpired access would hide a
|
|
29
|
+
# connection from the screen that exists to revoke it — the one place a
|
|
30
|
+
# merchant can actually stop it.
|
|
31
|
+
has_many :live_access_tokens,
|
|
32
|
+
-> { where(revoked_at: nil) },
|
|
33
|
+
class_name: 'Spree::OauthAccessToken',
|
|
34
|
+
foreign_key: :application_id,
|
|
35
|
+
inverse_of: :application,
|
|
36
|
+
dependent: nil
|
|
37
|
+
|
|
38
|
+
# Whether anything can still reach the store with this registration.
|
|
39
|
+
#
|
|
40
|
+
# The screen lists every registered client, so a row with no live token
|
|
41
|
+
# may be one nobody has connected yet or one whose access was revoked —
|
|
42
|
+
# and an empty permission list reads the same either way. A merchant
|
|
43
|
+
# pressing Revoke has to see that it worked.
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def connected?
|
|
47
|
+
live_access_tokens.any?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# What this application may currently do, as permission keys.
|
|
51
|
+
#
|
|
52
|
+
# Read from the tokens rather than the registration: a client is
|
|
53
|
+
# registered once and granted per consent, so the registration's own
|
|
54
|
+
# `scopes` column stays empty.
|
|
55
|
+
#
|
|
56
|
+
# @return [Array<String>]
|
|
57
|
+
def live_token_scopes
|
|
58
|
+
live_access_tokens.flat_map { |token| token.scopes.to_a }.uniq.sort
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# When this application last obtained a token that still works.
|
|
62
|
+
#
|
|
63
|
+
# Not the last call it made: a token is minted once and then used for
|
|
64
|
+
# hours, and nothing records a call. Reporting this as "last used" would
|
|
65
|
+
# let a merchant read an actively running agent as idle, which is the
|
|
66
|
+
# opposite of what the connected-apps screen is for.
|
|
67
|
+
#
|
|
68
|
+
# @return [ActiveSupport::TimeWithZone, nil]
|
|
69
|
+
def last_authorized_at
|
|
70
|
+
live_access_tokens.map(&:created_at).max
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Who approved this application's access, and when.
|
|
74
|
+
#
|
|
75
|
+
# Read from the oldest live token, because that is the consent still in
|
|
76
|
+
# force — a later token refreshes an existing grant rather than replacing
|
|
77
|
+
# who gave it. On the screen that decides whether to revoke, "Claude can
|
|
78
|
+
# read your orders" is only half the story without the person who allowed
|
|
79
|
+
# it.
|
|
80
|
+
#
|
|
81
|
+
# @return [Object, nil] a {Spree.admin_user_class} record
|
|
82
|
+
def authorized_by
|
|
83
|
+
oldest_live_token&.resource_owner
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [ActiveSupport::TimeWithZone, nil]
|
|
87
|
+
def authorized_at
|
|
88
|
+
oldest_live_token&.created_at
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def oldest_live_token
|
|
94
|
+
live_access_tokens.min_by(&:created_at)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# What a client is asking a merchant to approve, as the consent screen
|
|
3
|
+
# needs it. Never persisted — approving it writes an
|
|
4
|
+
# {Spree::OauthAccessGrant} instead.
|
|
5
|
+
class OauthConsentRequest
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
include ActiveModel::Attributes
|
|
8
|
+
|
|
9
|
+
attribute :client_name, :string
|
|
10
|
+
# Every permission the requested scopes imply, expanded, so the screen
|
|
11
|
+
# lists what is actually being handed over rather than the shorthand the
|
|
12
|
+
# client asked for.
|
|
13
|
+
attribute :scopes, default: -> { [] }
|
|
14
|
+
# Of those, the ones this person may hand over. A grant can only narrow
|
|
15
|
+
# the approver's own authority, so anything they do not hold is not
|
|
16
|
+
# offered.
|
|
17
|
+
attribute :grantable_scopes, default: -> { [] }
|
|
18
|
+
attribute :resource, :string
|
|
19
|
+
attribute :redirect_uri, :string
|
|
20
|
+
|
|
21
|
+
validates :client_name, :redirect_uri, presence: true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# RFC 9728 metadata: which authorization server guards a protected surface
|
|
3
|
+
# and what it will accept. Never persisted — assembled per request, since
|
|
4
|
+
# the origin a client reached is the one it can use.
|
|
5
|
+
class OauthProtectedResource
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
include ActiveModel::Attributes
|
|
8
|
+
|
|
9
|
+
attribute :resource, :string
|
|
10
|
+
attribute :authorization_servers, default: -> { [] }
|
|
11
|
+
# Exactly what the authorization server accepts. Advertising one scope
|
|
12
|
+
# more than it knows turns every connection into "the requested scope is
|
|
13
|
+
# invalid", because a client with no scope challenge asks for everything
|
|
14
|
+
# listed here.
|
|
15
|
+
attribute :scopes_supported, default: -> { [] }
|
|
16
|
+
attribute :bearer_methods_supported, default: -> { ['header'] }
|
|
17
|
+
attribute :resource_documentation, :string
|
|
18
|
+
|
|
19
|
+
validates :resource, presence: true
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Where to send the browser once the merchant has decided, carrying either
|
|
3
|
+
# the authorization code or the refusal. Never persisted.
|
|
4
|
+
class OauthRedirect
|
|
5
|
+
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Attributes
|
|
7
|
+
|
|
8
|
+
attribute :redirect_uri, :string
|
|
9
|
+
|
|
10
|
+
validates :redirect_uri, presence: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -12,6 +12,11 @@ module Spree
|
|
|
12
12
|
|
|
13
13
|
TRANSLATABLE_FIELDS = Spree::LabelTranslatable::TRANSLATABLE_FIELDS
|
|
14
14
|
|
|
15
|
+
# Option types are managed under the catalog, so the products permission guards them — see Admin::OptionTypesController.
|
|
16
|
+
def self.translation_write_permission
|
|
17
|
+
'write_products'
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
# Option values are translated alongside their type — the translations
|
|
16
21
|
# endpoint nests their matrices so the editor fetches both in one read.
|
|
17
22
|
def self.translatable_children
|
|
@@ -7,6 +7,11 @@ module Spree
|
|
|
7
7
|
include Spree::Metadata
|
|
8
8
|
include Spree::LabelTranslatable
|
|
9
9
|
|
|
10
|
+
# Values are written through their option type, so they ride the same permission.
|
|
11
|
+
def self.translation_write_permission
|
|
12
|
+
'write_products'
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
TRANSLATABLE_FIELDS = Spree::LabelTranslatable::TRANSLATABLE_FIELDS
|
|
11
16
|
|
|
12
17
|
#
|
data/app/models/spree/order.rb
CHANGED
|
@@ -51,9 +51,10 @@ module Spree
|
|
|
51
51
|
include Spree::NumberIdentifier
|
|
52
52
|
|
|
53
53
|
publishes_lifecycle_events
|
|
54
|
-
publishes_events :approved, :canceled, :delivered, :fulfilled, :paid, :placed,
|
|
54
|
+
publishes_events :approved, :canceled, :delivered, :fulfilled, :paid, :placed,
|
|
55
55
|
:resend_confirmation_email, :resend_digital_links_email
|
|
56
56
|
publishes_event :completed, deprecated_alias_of: 'order.placed'
|
|
57
|
+
publishes_event :shipped, deprecated_alias_of: 'order.fulfilled'
|
|
57
58
|
include Spree::HasCustomFields
|
|
58
59
|
include Spree::Metadata
|
|
59
60
|
include Spree::HasExternalReferences
|
|
@@ -151,7 +152,7 @@ module Spree
|
|
|
151
152
|
self.whitelisted_ransackable_attributes = %w[
|
|
152
153
|
completed_at email number status payment_status payment_state fulfillment_status shipment_state delivery_total
|
|
153
154
|
total item_total total_quantity considered_risky channel_id currency coupon_code customer_id seller_id
|
|
154
|
-
order_group_id po_number
|
|
155
|
+
order_group_id po_number market_id locale
|
|
155
156
|
]
|
|
156
157
|
self.whitelisted_ransackable_scopes = %w[complete incomplete refunded partially_refunded search]
|
|
157
158
|
self.ransackable_scope_types = %w[complete incomplete refunded partially_refunded].index_with('boolean')
|
|
@@ -920,6 +921,9 @@ module Spree
|
|
|
920
921
|
|
|
921
922
|
shipment_ids = fulfillments.map(&:id)
|
|
922
923
|
DeliveryRate.where(fulfillment_id: shipment_ids).delete_all
|
|
924
|
+
# A connector's key for a fulfillment that is going away would otherwise
|
|
925
|
+
# outlive it and block the key from naming the rebuilt one.
|
|
926
|
+
Spree::ExternalReference.where(resource_type: Spree::Fulfillment.name, resource_id: shipment_ids).delete_all if shipment_ids.any?
|
|
923
927
|
|
|
924
928
|
fulfillments.delete_all
|
|
925
929
|
|
data/app/models/spree/payment.rb
CHANGED
|
@@ -326,16 +326,28 @@ module Spree
|
|
|
326
326
|
true
|
|
327
327
|
end
|
|
328
328
|
|
|
329
|
+
# Matches +Payment.risky+: any result other than a match counts, whatever
|
|
330
|
+
# message the gateway sent with it.
|
|
329
331
|
def is_cvv_risky?
|
|
330
332
|
return false if cvv_response_code == 'M'
|
|
331
333
|
return false if cvv_response_code.nil?
|
|
332
|
-
return false if cvv_response_message.present?
|
|
333
334
|
|
|
334
335
|
true
|
|
335
336
|
end
|
|
336
337
|
|
|
338
|
+
# The payment's page in the provider's dashboard. Only https links are
|
|
339
|
+
# returned, since staff open it from the admin and a gateway may build it
|
|
340
|
+
# from values the provider sent back.
|
|
341
|
+
#
|
|
342
|
+
# @return [String, nil]
|
|
337
343
|
def gateway_dashboard_payment_url
|
|
338
|
-
payment_method.try(:gateway_dashboard_payment_url, self)
|
|
344
|
+
url = payment_method.try(:gateway_dashboard_payment_url, self)
|
|
345
|
+
return if url.blank?
|
|
346
|
+
|
|
347
|
+
uri = URI.parse(url)
|
|
348
|
+
url if uri.is_a?(URI::HTTPS) && uri.host.present?
|
|
349
|
+
rescue URI::InvalidURIError
|
|
350
|
+
nil
|
|
339
351
|
end
|
|
340
352
|
|
|
341
353
|
def captured_amount
|
data/app/models/spree/policy.rb
CHANGED
|
@@ -4,6 +4,12 @@ module Spree
|
|
|
4
4
|
|
|
5
5
|
extend FriendlyId
|
|
6
6
|
include Spree::TranslatableResource
|
|
7
|
+
|
|
8
|
+
# Policies are store settings — see Admin::PoliciesController.
|
|
9
|
+
def self.translation_write_permission
|
|
10
|
+
'write_settings'
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
include Spree::SanitizableRichText
|
|
8
14
|
|
|
9
15
|
UNIQUENESS_SCOPE = %i[owner_id owner_type].freeze
|
data/app/models/spree/price.rb
CHANGED
|
@@ -9,7 +9,6 @@ module Spree
|
|
|
9
9
|
|
|
10
10
|
has_prefix_id :price
|
|
11
11
|
|
|
12
|
-
include Spree::VatPriceCalculation
|
|
13
12
|
include Spree::StorePreferences
|
|
14
13
|
include Spree::CanonicalDecimals
|
|
15
14
|
|
|
@@ -135,44 +134,28 @@ module Spree
|
|
|
135
134
|
alias_attribute :price, :amount
|
|
136
135
|
alias_attribute :compare_at_price, :compare_at_amount
|
|
137
136
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# chose. Lists narrowed by anything else (a quantity, a group) were not set
|
|
143
|
-
# for a country, so their prices restate like any other.
|
|
144
|
-
#
|
|
145
|
-
# @return [Boolean]
|
|
146
|
-
def final_for_destination?
|
|
147
|
-
rules = price_list&.price_rules.to_a
|
|
148
|
-
return false unless rules.any?(&:geographic?)
|
|
149
|
-
|
|
150
|
-
# Under 'any' the list can win on a non-geographic rule alone, so a
|
|
151
|
-
# geographic rule being present does not mean this buyer's geography is why
|
|
152
|
-
# the price applied. Under 'all' every rule matched, so it does.
|
|
153
|
-
price_list.match_policy == 'all' || rules.all?(&:geographic?)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def price_including_vat_for(price_options)
|
|
157
|
-
return price if final_for_destination?
|
|
158
|
-
|
|
159
|
-
options = price_options.merge(tax_category: variant.tax_category)
|
|
160
|
-
gross_amount(price, options)
|
|
137
|
+
# @deprecated Prices are charged as entered for every buyer since 6.0. Use {#price}; removed in 6.1.
|
|
138
|
+
def price_including_vat_for(_price_options = {})
|
|
139
|
+
Spree::Deprecation.warn('Spree::Price#price_including_vat_for is deprecated and will be removed in Spree 6.1. Prices are charged as entered; use #price instead.')
|
|
140
|
+
price
|
|
161
141
|
end
|
|
162
142
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
gross_amount(compare_at_price, options)
|
|
143
|
+
# @deprecated Use {#compare_at_price}; removed in 6.1.
|
|
144
|
+
def compare_at_price_including_vat_for(_price_options = {})
|
|
145
|
+
Spree::Deprecation.warn('Spree::Price#compare_at_price_including_vat_for is deprecated and will be removed in Spree 6.1. Prices are charged as entered; use #compare_at_price instead.')
|
|
146
|
+
compare_at_price
|
|
168
147
|
end
|
|
169
148
|
|
|
170
|
-
|
|
171
|
-
|
|
149
|
+
# @deprecated Use {#display_price}; removed in 6.1.
|
|
150
|
+
def display_price_including_vat_for(_price_options = {})
|
|
151
|
+
Spree::Deprecation.warn('Spree::Price#display_price_including_vat_for is deprecated and will be removed in Spree 6.1. Prices are charged as entered; use #display_price instead.')
|
|
152
|
+
display_price
|
|
172
153
|
end
|
|
173
154
|
|
|
174
|
-
|
|
175
|
-
|
|
155
|
+
# @deprecated Use {#display_compare_at_price}; removed in 6.1.
|
|
156
|
+
def display_compare_at_price_including_vat_for(_price_options = {})
|
|
157
|
+
Spree::Deprecation.warn('Spree::Price#display_compare_at_price_including_vat_for is deprecated and will be removed in Spree 6.1. Prices are charged as entered; use #display_compare_at_price instead.')
|
|
158
|
+
display_compare_at_price
|
|
176
159
|
end
|
|
177
160
|
|
|
178
161
|
# returns the name of the price in a format of variant name and currency
|
|
@@ -16,18 +16,6 @@ module Spree
|
|
|
16
16
|
raise NotImplementedError, "#{self.class.name} must implement #applicable?"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Whether this rule narrows its price list by geography. A list carrying one
|
|
20
|
-
# states the price for that geography outright, so its prices are charged as
|
|
21
|
-
# entered and never restated for the buyer's VAT rate
|
|
22
|
-
# (Spree::VatPriceCalculation). A rule that narrows by anything else — a
|
|
23
|
-
# quantity, a group, a channel — leaves its prices open to restatement, since
|
|
24
|
-
# they were not set for a country.
|
|
25
|
-
#
|
|
26
|
-
# @return [Boolean]
|
|
27
|
-
def geographic?
|
|
28
|
-
false
|
|
29
|
-
end
|
|
30
|
-
|
|
31
19
|
# Whether this rule narrows by something about the purchase rather than
|
|
32
20
|
# about the buyer. A catalog-owned price list has already been matched on
|
|
33
21
|
# audience by the time the resolver holds it — the catalog assignment is
|
|
@@ -45,10 +33,9 @@ module Spree
|
|
|
45
33
|
false
|
|
46
34
|
end
|
|
47
35
|
|
|
48
|
-
# Instance-side reader, so rules
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# as MarketRule does for `geographic?`.
|
|
36
|
+
# Instance-side reader, so rules can be classified per row
|
|
37
|
+
# (`price_rules.select(&:contextual?)`). A kind whose answer depends on how it is
|
|
38
|
+
# configured overrides this.
|
|
52
39
|
#
|
|
53
40
|
# @return [Boolean]
|
|
54
41
|
def contextual?
|
|
@@ -23,12 +23,6 @@ module Spree
|
|
|
23
23
|
preferred_market_ids.map(&:to_s).include?(context.market.id.to_s)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# An empty market list matches every buyer (see #applicable?), so it names
|
|
27
|
-
# no geography — and that is the state a freshly created rule row is in.
|
|
28
|
-
def geographic?
|
|
29
|
-
preferred_market_ids.present?
|
|
30
|
-
end
|
|
31
|
-
|
|
32
26
|
def self.description
|
|
33
27
|
'Apply pricing based on the market'
|
|
34
28
|
end
|
|
@@ -6,7 +6,7 @@ module Spree
|
|
|
6
6
|
# A provider returns a {Spree::Price}: {Internal} returns the persisted
|
|
7
7
|
# catalog row, an external provider an **unsaved, readonly** instance. The
|
|
8
8
|
# Active Record shape is deliberately the contract, so everything
|
|
9
|
-
# downstream —
|
|
9
|
+
# downstream — the line item, +discounted?+, the serializers —
|
|
10
10
|
# keeps working without knowing where the number came from. +readonly!+ is
|
|
11
11
|
# what stops an external answer being written back into the catalog.
|
|
12
12
|
#
|
data/app/models/spree/product.rb
CHANGED
|
@@ -887,7 +887,7 @@ module Spree
|
|
|
887
887
|
next unless variant.should_track_inventory?
|
|
888
888
|
next if variant.sku.blank? && variant != default_variant
|
|
889
889
|
|
|
890
|
-
variant.stock_levels.each do |stock_level|
|
|
890
|
+
variant.stock_levels.sort_by(&:id).each do |stock_level|
|
|
891
891
|
location = stock_level.stock_location
|
|
892
892
|
next if location.nil? || location == default_stock_location
|
|
893
893
|
next unless location.store_id == store.id && location_owner_ids.include?(location.seller_id)
|
|
@@ -11,7 +11,7 @@ module Spree
|
|
|
11
11
|
belongs_to :purchase_order, class_name: 'Spree::PurchaseOrder', inverse_of: :items, touch: true
|
|
12
12
|
|
|
13
13
|
validates :quantity_ordered, numericality: { greater_than: 0, only_integer: true }
|
|
14
|
-
validates :unit_cost, numericality: { greater_than_or_equal_to: 0 }
|
|
14
|
+
validates :unit_cost, presence: true, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
|
|
15
15
|
validates :variant_id, uniqueness: { scope: :purchase_order_id }
|
|
16
16
|
|
|
17
17
|
delegate :currency, to: :purchase_order, allow_nil: true
|
data/app/models/spree/return.rb
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
# Who buys, on which channel, in which market: what a buyer can be sold, and
|
|
3
|
+
# at what price, depends on all of these together. A market fixes the
|
|
4
|
+
# currency and the languages a sale may use.
|
|
5
|
+
class SalesContext
|
|
6
|
+
include ActiveModel::Model
|
|
7
|
+
include ActiveModel::Attributes
|
|
8
|
+
|
|
9
|
+
attribute :store
|
|
10
|
+
attribute :market
|
|
11
|
+
attribute :channel
|
|
12
|
+
attribute :customer
|
|
13
|
+
attribute :company
|
|
14
|
+
attribute :country
|
|
15
|
+
attribute :currency, :string
|
|
16
|
+
attribute :locale, :string
|
|
17
|
+
# A placed sale keeps the company it was stamped with, even none, so only
|
|
18
|
+
# a sale still being put together looks up the customer's company.
|
|
19
|
+
attribute :resolve_company, :boolean, default: true
|
|
20
|
+
|
|
21
|
+
validate :currency_sold_in_a_market
|
|
22
|
+
validate :currency_matches_market
|
|
23
|
+
validate :locale_supported_by_market
|
|
24
|
+
|
|
25
|
+
# @param order [Spree::Order]
|
|
26
|
+
# @return [Spree::SalesContext]
|
|
27
|
+
def self.from_order(order)
|
|
28
|
+
new(
|
|
29
|
+
store: order.store, market: order.market, channel: order.channel, customer: order.customer,
|
|
30
|
+
company: order.resolved_company, resolve_company: false, country: order.tax_country,
|
|
31
|
+
currency: order.currency, locale: order.locale
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The market chosen for the sale, else one selling the requested currency,
|
|
36
|
+
# else the store's default.
|
|
37
|
+
#
|
|
38
|
+
# @return [Spree::Market, nil]
|
|
39
|
+
def market
|
|
40
|
+
chosen = super
|
|
41
|
+
return chosen if chosen
|
|
42
|
+
return @resolved_market if defined?(@resolved_market)
|
|
43
|
+
|
|
44
|
+
@resolved_market = resolve_market
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def channel
|
|
48
|
+
super || store&.default_channel
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] the requested currency, else the market's, else the store's
|
|
52
|
+
def currency
|
|
53
|
+
requested_currency || market&.currency || store&.default_currency
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [String, nil] the requested language, else the market's default
|
|
57
|
+
def locale
|
|
58
|
+
super.presence || market&.default_locale
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The company chosen for the sale, else the one the customer stands in
|
|
62
|
+
# alone — the same answer pricing and catalog visibility reach.
|
|
63
|
+
#
|
|
64
|
+
# @return [Spree::Company, nil]
|
|
65
|
+
def company
|
|
66
|
+
chosen = super
|
|
67
|
+
return chosen if chosen || !resolve_company
|
|
68
|
+
return @standing_company if defined?(@standing_company)
|
|
69
|
+
|
|
70
|
+
@standing_company =
|
|
71
|
+
if customer.is_a?(Spree.customer_class) && store
|
|
72
|
+
Spree::Company.sole_standing_for(store: store, customer: customer)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Variants the buyer may order on the channel: published there and inside
|
|
77
|
+
# their catalogs.
|
|
78
|
+
#
|
|
79
|
+
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
80
|
+
def orderable_variants
|
|
81
|
+
@orderable_variants ||= Spree::Variant.for_products(
|
|
82
|
+
Spree.products_for_context_service.call(
|
|
83
|
+
store: store, channel: channel, customer: customer, company: company,
|
|
84
|
+
base: store.products.not_discontinued_on(channel)
|
|
85
|
+
).value
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Orderable variants the buyer can be quoted in the sale's currency: a base
|
|
90
|
+
# price, or a price from a list that applies to them, so a product sold to
|
|
91
|
+
# a buyer only under their agreement is offered to that buyer alone.
|
|
92
|
+
#
|
|
93
|
+
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
94
|
+
def sellable_variants
|
|
95
|
+
prices = Spree::Price.where(variant_id: orderable_variants.select(:id), currency: currency)
|
|
96
|
+
.where.not(amount: nil)
|
|
97
|
+
.where(price_list_id: [nil, *applicable_price_list_ids])
|
|
98
|
+
|
|
99
|
+
orderable_variants.where(id: prices.select(:variant_id))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Spree::Pricing::Context] the context prices are resolved in
|
|
103
|
+
def pricing_context
|
|
104
|
+
@pricing_context ||= Spree::Pricing::Context.new(
|
|
105
|
+
currency: currency, store: store, country: country, market: market,
|
|
106
|
+
channel: channel, user: customer, company: company
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def requested_currency
|
|
113
|
+
attributes['currency'].presence&.upcase
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_market
|
|
117
|
+
return unless store
|
|
118
|
+
|
|
119
|
+
candidates = [(Spree::Current.market if Spree::Current.store == store), store.default_market].compact
|
|
120
|
+
return candidates.first unless requested_currency
|
|
121
|
+
|
|
122
|
+
candidates.find { |candidate| candidate.currency == requested_currency } ||
|
|
123
|
+
store.markets.find_by(currency: requested_currency)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Catalog price lists the buyer's catalogs carry, and standalone lists
|
|
127
|
+
# whose rules match the sale, judged the way the internal pricing provider
|
|
128
|
+
# judges them.
|
|
129
|
+
def applicable_price_list_ids
|
|
130
|
+
@applicable_price_list_ids ||= begin
|
|
131
|
+
catalogs = Spree::Catalog.groups_for_buyer(store: store, customer: customer, company: company, channel: channel)
|
|
132
|
+
catalog_lists = catalogs.flatten.filter_map(&:price_list).select do |list|
|
|
133
|
+
list.currently_active? && list.contextual_rules_applicable?(pricing_context)
|
|
134
|
+
end
|
|
135
|
+
standalone_lists = Spree::PriceList.for_context(pricing_context).select { |list| list.applicable?(pricing_context) }
|
|
136
|
+
|
|
137
|
+
(catalog_lists + standalone_lists).map(&:id).uniq
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def currency_sold_in_a_market
|
|
142
|
+
return if requested_currency.nil? || market || store.nil? || !store.markets.exists?
|
|
143
|
+
|
|
144
|
+
errors.add(:currency, :currency_not_supported_by_store, message: I18n.t('spree.currency_not_supported_by_store'))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def currency_matches_market
|
|
148
|
+
return if market.nil? || currency == market.currency
|
|
149
|
+
|
|
150
|
+
errors.add(
|
|
151
|
+
:currency, :currency_does_not_match_market,
|
|
152
|
+
message: I18n.t('spree.currency_does_not_match_market', currency: market.currency)
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def locale_supported_by_market
|
|
157
|
+
return if market.nil? || locale.blank? || market.supported_locales_list.include?(locale)
|
|
158
|
+
|
|
159
|
+
errors.add(:locale, :locale_not_supported_by_market, message: I18n.t('spree.locale_not_supported_by_market'))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|