Compare commits

..

1 Commits

Author SHA1 Message Date
Maikel
ac7a36416b Revert "Allow negative stock levels for on-demand products" 2024-06-19 15:22:25 +10:00
513 changed files with 12468 additions and 10837 deletions

View File

@@ -1,14 +0,0 @@
name: Auto Author Assign
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
assign-author:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v2.1.0

View File

@@ -59,7 +59,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
@@ -186,7 +185,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
@@ -216,9 +214,9 @@ jobs:
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: failed-admin-tests-screenshots
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
@@ -264,7 +262,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
@@ -294,9 +291,9 @@ jobs:
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: failed-consumer-tests-screenshots
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
@@ -343,7 +340,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
@@ -371,6 +367,15 @@ jobs:
run: |
bin/rake knapsack_pro:rspec
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
test_the_rest:
runs-on: ubuntu-22.04
services:
@@ -413,7 +418,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
@@ -466,7 +470,6 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile

View File

@@ -115,11 +115,6 @@ Rails/OutputSafety:
Exclude:
- spec/**/*
Rails/RedundantActiveRecordAllMethod:
AllowedReceivers:
- ActionMailer::Preview
- ActiveSupport::TimeZone
Rails/SkipsModelValidations:
AllowedMethods:
- touch
@@ -129,13 +124,6 @@ Rails/SkipsModelValidations:
- update_column
- update_columns
Rails/UnknownEnv:
Environments:
- development
- production
- staging
- test
Rails/WhereExists:
EnforcedStyle: where # Cf. conversion https://github.com/openfoodfoundation/openfoodnetwork/pull/12363

View File

@@ -6,12 +6,35 @@
# 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:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 6
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented, indented_relative_to_receiver
Layout/MultilineMethodCallIndentation:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude:
- 'app/services/products_renderer.rb'
# Offense count: 16
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
@@ -127,7 +150,7 @@ Metrics/BlockNesting:
Exclude:
- 'app/models/spree/payment/processing.rb'
# Offense count: 46
# Offense count: 47
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Exclude:
@@ -163,6 +186,7 @@ Metrics/ClassLength:
- 'app/models/spree/variant.rb'
- 'app/models/spree/zone.rb'
- 'app/reflexes/admin/orders_reflex.rb'
- 'app/reflexes/products_reflex.rb'
- 'app/serializers/api/cached_enterprise_serializer.rb'
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
- 'app/services/cart_service.rb'
@@ -229,7 +253,7 @@ Metrics/MethodLength:
- 'lib/spree/localized_number.rb'
- 'lib/tasks/sample_data/product_factory.rb'
# Offense count: 49
# Offense count: 48
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ModuleLength:
Exclude:
@@ -259,7 +283,6 @@ 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'
@@ -392,6 +415,7 @@ RSpecRails/HaveHttpStatus:
- 'spec/controllers/stripe/webhooks_controller_spec.rb'
- 'spec/controllers/user_passwords_controller_spec.rb'
- 'spec/controllers/user_registrations_controller_spec.rb'
- 'spec/requests/admin/images_spec.rb'
- 'spec/requests/api/routes_spec.rb'
- 'spec/requests/checkout/stripe_sca_spec.rb'
- 'spec/requests/home_controller_spec.rb'
@@ -601,6 +625,52 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/spree/admin/zones_controller.rb'
- 'app/controllers/spree/users_controller.rb'
# Offense count: 32
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/Pluck:
Exclude:
- 'app/controllers/admin/variant_overrides_controller.rb'
- 'app/services/cart_service.rb'
- 'lib/reporting/report_headers_builder.rb'
- 'spec/controllers/admin/bulk_line_items_controller_spec.rb'
- 'spec/controllers/admin/subscriptions_controller_spec.rb'
- 'spec/controllers/api/v0/order_cycles_controller_spec.rb'
- 'spec/controllers/api/v0/orders_controller_spec.rb'
- 'spec/controllers/api/v0/products_controller_spec.rb'
- 'spec/controllers/api/v0/shops_controller_spec.rb'
- 'spec/controllers/api/v0/states_controller_spec.rb'
- 'spec/controllers/api/v0/taxons_controller_spec.rb'
- 'spec/helpers/spree/admin/orders_helper_spec.rb'
- 'spec/lib/reports/lettuce_share_report_spec.rb'
- 'spec/lib/reports/users_and_enterprises_report_spec.rb'
- 'spec/serializers/api/admin/for_order_cycle/supplied_product_serializer_spec.rb'
- 'spec/support/api_helper.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: conservative, aggressive
Rails/PluckInWhere:
Exclude:
- 'app/models/spree/variant.rb'
# Offense count: 4
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedReceivers.
# AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone
Rails/RedundantActiveRecordAllMethod:
Exclude:
- 'app/models/spree/tax_rate.rb'
- 'app/models/spree/user.rb'
- 'app/models/spree/variant.rb'
- 'spec/system/admin/product_import_spec.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/RelativeDateConstant:
Exclude:
- 'lib/tasks/data/remove_transient_data.rb'
# Offense count: 56
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Include.
@@ -621,6 +691,26 @@ Rails/ResponseParsedBody:
- 'spec/controllers/spree/credit_cards_controller_spec.rb'
- 'spec/controllers/user_registrations_controller_spec.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/RootPathnameMethods:
Exclude:
- 'spec/lib/reports/orders_and_fulfillment/order_cycle_customer_totals_report_spec.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/SelectMap:
Exclude:
- 'app/models/enterprise.rb'
# Offense count: 4
# Configuration parameters: ForbiddenMethods, AllowedMethods.
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
Rails/SkipsModelValidations:
Exclude:
- 'app/models/variant_override.rb'
- 'spec/models/spree/line_item_spec.rb'
# Offense count: 7
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
@@ -649,6 +739,24 @@ Rails/UniqueValidationWithoutIndex:
- 'app/models/spree/tax_category.rb'
- 'app/models/spree/zone.rb'
# Offense count: 1
# Configuration parameters: Severity, Environments.
# Environments: development, test, production
Rails/UnknownEnv:
Exclude:
- 'app/models/spree/app_configuration.rb'
# Offense count: 7
# Configuration parameters: Severity.
Rails/UnusedRenderContent:
Exclude:
- 'app/controllers/admin/bulk_line_items_controller.rb'
- 'app/controllers/admin/tag_rules_controller.rb'
- 'app/controllers/api/v0/enterprise_fees_controller.rb'
- 'app/controllers/api/v0/products_controller.rb'
- 'app/controllers/api/v0/taxons_controller.rb'
- 'app/controllers/api/v0/variants_controller.rb'
# Offense count: 1
Security/Open:
Exclude:
@@ -840,6 +948,13 @@ Style/RedundantInitialize:
Exclude:
- 'spec/models/spree/gateway_spec.rb'
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/RedundantInterpolation:
Exclude:
- 'lib/tasks/karma.rake'
- 'spec/base_spec_helper.rb'
# Offense count: 19
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns.
@@ -849,6 +964,39 @@ Style/ReturnNilInPredicateMethodDefinition:
- 'app/serializers/api/admin/customer_serializer.rb'
- 'engines/order_management/app/services/order_management/subscriptions/validator.rb'
# Offense count: 204
Style/Send:
Exclude:
- 'spec/controllers/admin/subscriptions_controller_spec.rb'
- 'spec/controllers/payment_gateways/paypal_controller_spec.rb'
- 'spec/controllers/spree/admin/base_controller_spec.rb'
- 'spec/controllers/spree/orders_controller_spec.rb'
- 'spec/helpers/order_cycles_helper_spec.rb'
- 'spec/jobs/subscription_confirm_job_spec.rb'
- 'spec/jobs/subscription_placement_job_spec.rb'
- 'spec/lib/open_food_network/address_finder_spec.rb'
- 'spec/lib/open_food_network/enterprise_fee_applicator_spec.rb'
- 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb'
- 'spec/lib/open_food_network/order_cycle_form_applicator_spec.rb'
- 'spec/lib/open_food_network/permissions_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/reports/xero_invoices_report_spec.rb'
- 'spec/lib/stripe/webhook_handler_spec.rb'
- 'spec/models/calculator/weight_spec.rb'
- 'spec/models/enterprise_spec.rb'
- 'spec/models/exchange_spec.rb'
- 'spec/models/spree/order_inventory_spec.rb'
- 'spec/models/spree/payment_spec.rb'
- 'spec/models/spree/return_authorization_spec.rb'
- 'spec/models/tag_rule/filter_order_cycles_spec.rb'
- 'spec/models/tag_rule/filter_payment_methods_spec.rb'
- 'spec/models/tag_rule/filter_products_spec.rb'
- 'spec/models/tag_rule/filter_shipping_methods_spec.rb'
- 'spec/services/cart_service_spec.rb'
- 'spec/services/products_renderer_spec.rb'
- 'spec/services/variant_units/option_value_namer_spec.rb'
- 'spec/support/localized_number_helper.rb'
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SlicingWithRange:

View File

@@ -16,6 +16,7 @@ gem "image_processing"
gem 'activemerchant', '>= 1.78.0'
gem 'angular-rails-templates', '>= 0.3.0'
gem 'awesome_nested_set'
gem 'ransack', '~> 4.1.0'
gem 'responders'
gem 'webpacker', '~> 5'

View File

@@ -161,6 +161,8 @@ GEM
activerecord (>= 3.1.0, < 8)
ast (2.4.2)
attr_required (1.0.2)
awesome_nested_set (3.6.0)
activerecord (>= 4.0.0, < 7.2)
aws-eventstream (1.3.0)
aws-partitions (1.929.0)
aws-sdk-core (3.196.1)
@@ -356,7 +358,7 @@ GEM
ruby-vips (>= 2.0.17, < 3)
immigrant (0.3.6)
activerecord (>= 3.0)
invisible_captcha (2.3.0)
invisible_captcha (2.2.0)
rails (>= 5.2)
io-console (0.7.2)
ipaddress (0.8.3)
@@ -415,7 +417,7 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.0.2)
matrix (0.4.2)
method_source (1.1.0)
mime-types (3.5.2)
@@ -447,7 +449,7 @@ GEM
net-smtp (0.5.0)
net-protocol
newrelic_rpm (9.9.0)
nio4r (2.7.1)
nio4r (2.7.0)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@@ -861,6 +863,7 @@ DEPENDENCIES
angularjs-file-upload-rails (~> 2.4.1)
angularjs-rails (= 1.8.0)
arel-helpers (~> 2.12)
awesome_nested_set
aws-sdk-s3
bigdecimal (= 3.0.2)
bootsnap

View File

@@ -10,6 +10,7 @@
//= require jquery.ui.all
//= require jquery.powertip
//= require jquery.cookie
//= require jquery.jstree/jquery.jstree
//= require jquery.vAlign
//= require angular
//= require angular-resource

View File

@@ -47,7 +47,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
removeClearedValues()
params = {
'q[name_cont]': $scope.q.query,
'q[variants_supplier_id_eq]': $scope.q.producerFilter,
'q[supplier_id_eq]': $scope.q.producerFilter,
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter,
'q[s]': $scope.sorting,
import_date: $scope.q.importDateFilter,
@@ -126,11 +126,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
DisplayProperties.setShowVariants 0, showVariants
$scope.addVariant = (product) ->
# Set new variant category to same as last product variant category to keep compactibility with deleted variant callback to set new variant category
newVariantId = $scope.nextVariantId();
newVariantCategoryId = product.variants[product.variants.length - 1]?.category_id
product.variants.push
id: newVariantId
id: $scope.nextVariantId()
unit_value: null
unit_description: null
on_demand: false
@@ -139,9 +136,8 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
on_hand: null
price: null
tax_category_id: null
category_id: newVariantCategoryId
category_id: null
DisplayProperties.setShowVariants product.id, true
DirtyProducts.addVariantProperty(product.id, newVariantId, 'category_id', newVariantCategoryId)
$scope.nextVariantId = ->
@@ -221,7 +217,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
products: productsToSubmit
filters:
'q[name_cont]': $scope.q.query
'q[variants_supplier_id_eq]': $scope.q.producerFilter
'q[supplier_id_eq]': $scope.q.producerFilter
'q[variants_primary_taxon_id_eq]': $scope.q.categoryFilter
'q[s]': $scope.sorting
import_date: $scope.q.importDateFilter
@@ -318,6 +314,9 @@ 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
@@ -380,9 +379,6 @@ 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,16 +1,12 @@
# 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) ->
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)
Object.keys(filters).reduce (filtered, attr) ->
filter = filters[attr]
return filtered if !filter? || filter == 0
return $filter('filter')(filtered, (object) ->
object[attr] == filter
)
, objects

View File

@@ -31,7 +31,7 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, $injec
savePreferences: (action_name) =>
$http
method: "PUT"
url: "/admin/column_preferences/bulk_update.json"
url: "/admin/column_preferences/bulk_update"
data:
action_name: action_name
column_preferences: (preference for column_name, preference of @columns)

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_supplier_name",
"variant_product_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_supplier_id_eq]": $scope.supplierFilter,
"q[variant_product_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

@@ -3,7 +3,6 @@ angular.module('admin.orderCycles')
$controller('AdminOrderCycleBasicCtrl', {$scope: $scope, ocInstance: ocInstance})
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.order_cycle_id = order_cycle_id
$scope.order_cycle = OrderCycle.load(order_cycle_id)
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id)
@@ -19,8 +18,6 @@ angular.module('admin.orderCycles')
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.update(destination, $scope.order_cycle_form)
@@ -28,4 +25,4 @@ angular.module('admin.orderCycles')
if $scope.order_cycle_form?.$dirty
t('admin.unsaved_confirm_leave')
NavigationCheck.register(warnAboutUnsavedChanges)
NavigationCheck.register(warnAboutUnsavedChanges)

View File

@@ -1,9 +1,8 @@
angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($scope, $rootScope, $controller, $location, Enterprise, EnterpriseFee, OrderCycle, ExchangeProduct, ocInstance) ->
angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($scope, $rootScope, $controller, $location, Enterprise, OrderCycle, ExchangeProduct, ocInstance) ->
$controller('AdminOrderCycleExchangesCtrl', {$scope: $scope, ocInstance: ocInstance, $location: $location})
$scope.view = 'incoming'
# NB: weirdly at this next line $scope.order_cycle.id comes out undefined so we use $scope.order_cycle_id instead
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true)
$scope.exchangeTotalVariants = (exchange) ->
return unless $scope.enterprises? && $scope.enterprises[exchange.enterprise_id]?

View File

@@ -22,8 +22,6 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.mirrorIncomingToOutgoingProducts()
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()

View File

@@ -6,8 +6,6 @@ angular.module('admin.orderCycles').factory('EnterpriseFee', ($resource) ->
params:
order_cycle_id: '@order_cycle_id'
coordinator_id: '@coordinator_id'
per_item: '@per_item'
per_order: '@per_order'
})
{

View File

@@ -161,11 +161,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
update: (destination, form) ->
oc = new OrderCycleResource({
order_cycle: this.dataForSubmit(),
confirm: this.order_cycle.confirm,
trigger_action: this.order_cycle.trigger_action
})
oc = new OrderCycleResource({order_cycle: this.dataForSubmit()})
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
form.$setPristine() if form
if destination?
@@ -175,8 +171,6 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
, (response) ->
if response.data.errors?
StatusMessage.display('failure', response.data.errors[0])
else if (response.data.trigger_action)
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
else
StatusMessage.display('failure', t('js.order_cycles.update_failure'))

View File

@@ -0,0 +1,21 @@
root = exports ? this
root.taxon_tree_menu = (obj, context) ->
base_url = Spree.url(Spree.routes.taxonomy_taxons)
admin_base_url = Spree.url(Spree.routes.admin_taxonomy_taxons)
edit_url = Spree.url(Spree.routes.admin_taxonomy_taxons + '/' + obj.attr("id") + "/edit");
create:
label: "<i class='icon-plus'></i> " + Spree.translations.add,
action: (obj) -> context.create(obj)
rename:
label: "<i class='icon-pencil'></i> " + Spree.translations.rename,
action: (obj) -> context.rename(obj)
remove:
label: "<i class='icon-trash'></i> " + Spree.translations.remove,
action: (obj) -> context.remove(obj)
edit:
separator_before: true,
label: "<i class='icon-edit'></i> " + Spree.translations.edit,
action: (obj) -> window.location = edit_url.toString()

View File

@@ -0,0 +1,139 @@
handle_ajax_error = (XMLHttpRequest, textStatus, errorThrown) ->
$.jstree.rollback(last_rollback)
$("#ajax_error").show().html("<strong>" + server_error + "</strong><br />" + taxonomy_tree_error)
handle_move = (e, data) ->
last_rollback = data.rlbk
position = data.rslt.cp
node = data.rslt.o
new_parent = data.rslt.np
url = new URL(Spree.routes.admin_taxonomy_taxons)
url.pathname = url.pathname + '/' + node.attr("id")
data = {
_method: "put",
"taxon[position]": position,
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: data,
error: handle_ajax_error
true
handle_create = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.name
position = data.rslt.position
new_parent = data.rslt.parent
data = {
"taxon[name]": name,
"taxon[position]": position
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: base_url.toString(),
data: data,
error: handle_ajax_error,
success: (data,result) ->
node.attr('id', data.id)
handle_rename = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.new_name
# change the name inside the main input field as well if taxon is the root one
document.getElementById("taxonomy_name").value = name if node.parents("[id]").attr("id") == "taxonomy_tree"
url = new URL(base_url)
url.pathname = url.pathname + '/' + node.attr("id")
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: {_method: "put", "taxon[name]": name },
error: handle_ajax_error
handle_delete = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
delete_url = new URL(base_url)
delete_url.pathname = delete_url.pathname + '/' + node.attr("id")
if confirm(Spree.translations.are_you_sure_delete)
$.ajax
type: "POST",
dataType: "json",
url: delete_url.toString(),
data: {_method: "delete"},
error: handle_ajax_error
else
$.jstree.rollback(last_rollback)
last_rollback = null
root = exports ? this
root.setup_taxonomy_tree = (taxonomy_id) ->
if taxonomy_id != undefined
# this is defined within admin/taxonomies/edit
root.base_url = Spree.url(Spree.routes.taxonomy_taxons)
$.ajax
url: base_url.pathname.replace("/taxons", "/jstree"),
success: (taxonomy) ->
last_rollback = null
conf =
json_data:
data: taxonomy,
ajax:
url: (e) ->
base_url.pathname + '/' + e.attr('id') + '/jstree'
themes:
theme: "apple",
url: "/assets/jquery.jstree/themes/apple/style.css"
strings:
new_node: new_taxon,
loading: Spree.translations.loading + "..."
crrm:
move:
check_move: (m) ->
position = m.cp
node = m.o
new_parent = m.np
# no parent or cant drag and drop
if !new_parent || node.attr("rel") == "root"
return false
# can't drop before root
if new_parent.attr("id") == "taxonomy_tree" && position == 0
return false
true
contextmenu:
items: (obj) ->
taxon_tree_menu(obj, this)
plugins: ["themes", "json_data", "dnd", "crrm", "contextmenu"]
$("#taxonomy_tree").jstree(conf)
.bind("move_node.jstree", handle_move)
.bind("remove.jstree", handle_delete)
.bind("create.jstree", handle_create)
.bind("rename.jstree", handle_rename)
.bind "loaded.jstree", ->
$(this).jstree("core").toggle_node($('.jstree-icon').first())
$("#taxonomy_tree a").on "dblclick", (e) ->
$("#taxonomy_tree").jstree("rename", this)
# surpress form submit on enter/return
$(document).keypress (e) ->
if e.keyCode == 13
e.preventDefault()

View File

@@ -10,9 +10,7 @@ angular.module("admin.utils").factory "StatusMessage", ->
statusMessage:
text: ""
style: {},
type: null,
actionName: null
style: {}
invalidMessage: ""
@@ -25,15 +23,11 @@ angular.module("admin.utils").factory "StatusMessage", ->
active: ->
@statusMessage.text != ''
display: (type, text, actionName = null) ->
display: (type, text) ->
@statusMessage.text = text
@statusMessage.type = type
@statusMessage.actionName = actionName
@statusMessage.style = @types[type].style
null
clear: ->
@statusMessage.text = ''
@statusMessage.style = {}
@statusMessage.type = null
@statusMessage.actionName = null

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
scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant.id
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty && viewValue
DirtyVariantOverrides.inherit scope.hub_id, scope.variant, scope.variantOverrides[scope.hub_id][scope.variant.id].id
DirtyVariantOverrides.inherit scope.hub_id, scope.variant.id, scope.variantOverrides[scope.hub_id][scope.variant.id].id
scope.displayDirty()
viewValue

View File

@@ -2,8 +2,4 @@ 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) ->
for variant in product.variants
return hubPermissions[hub_id].indexOf(variant.producer_id) > -1
), true)
return $filter('filter')(products, ((product) -> hubPermissions[hub_id].indexOf(product.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, vo_id) ->
@add(hub_id, variant.id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant))
inherit: (hub_id, variant_id, vo_id) ->
@add(hub_id, variant_id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant_id))
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,18 +13,17 @@ angular.module("admin.variantOverrides").factory "VariantOverrides", (variantOve
@variantOverrides[hub.id] ||= {}
for product in products
for variant in product.variants
@inherit(hub.id, variant) unless @variantOverrides[hub.id][variant.id]
@inherit(hub.id, variant.id) unless @variantOverrides[hub.id][variant.id]
inherit: (hub_id, variant) ->
inherit: (hub_id, variant_id) ->
# 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)
@variantOverrides[hub_id][variant_id] ||= {}
angular.extend @variantOverrides[hub_id][variant_id], @newFor hub_id, variant_id
newFor: (hub_id, variant) ->
newFor: (hub_id, variant_id) ->
# These properties need to match those checked in VariantOverrideSet.deletable?
hub_id: hub_id
variant_id: variant.id
producer_id: variant.producer_id
variant_id: variant_id
sku: null
price: null
count_on_hand: null

View File

@@ -4,18 +4,15 @@ 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 =>
@@ -48,12 +45,6 @@ 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()
@@ -61,7 +52,6 @@ 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
@@ -76,9 +66,8 @@ 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_variants_supplier_name_cont]': $scope.query,
'q[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_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
}
@@ -97,12 +86,6 @@ 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> ")
@@ -116,7 +99,6 @@ 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
@@ -127,7 +109,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.loadProducts()
$scope.filtersCount = () ->
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive() + $scope.producerPropertySelectors.totalActive()
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive()
$scope.toggleFilterSidebar = ->
$scope.showFilterSidebar = !$scope.showFilterSidebar

View File

@@ -18,11 +18,4 @@ 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.variants[0].supplier.id]
product.supplier = Shopfront.producers_by_id[product.supplier.id]
Dereferencer.dereference product.taxons, Taxons.taxons_by_id
product.properties = angular.copy(product.properties_with_values)

View File

@@ -5,9 +5,8 @@
%div.menu{ 'ng-show' => "expanded" }
.menu_items
.menu_item{ "ng-repeat": "column in columns", "ng-click": "toggle(column);" }
%input{ type: "checkbox", "ng-checked": "column.visible" }
%span
{{ column.name }}
%input.redesigned-input{ type: "checkbox", "ng-checked": "column.visible" }
{{ column.name }}
%hr
%div.menu_item.text-center
%input.fullwidth.orange{ type: "button", "ng-value": "saved() ? 'Saved': 'Saving'", "ng-show": "saved() || saving", "ng-disabled": "saved()" }

View File

@@ -1,7 +1,7 @@
#save-bar.animate-show{ "ng-show": 'dirty || persist || StatusMessage.active()' }
.container
.seven.columns.alpha
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style', data: { 'order-cycle-form-target': 'statusMessage' }, "ng-attr-data-type": "{{StatusMessage.statusMessage.type}}", "ng-attr-data-action-name": "{{StatusMessage.statusMessage.actionName}}" }
%h5#status-message{ "ng-show": "StatusMessage.invalidMessage == ''", "ng-style": 'StatusMessage.statusMessage.style' }
{{ StatusMessage.statusMessage.text || "&nbsp;" }}
%h5#status-message{ style: 'color: #C85136', "ng-show": "StatusMessage.invalidMessage !== ''" }
{{ StatusMessage.invalidMessage || "&nbsp;" }}

View File

@@ -24,19 +24,6 @@
max-width: 100%;
height: auto;
}
.flex-column {
display: flex;
flex-direction: column;
}
.gap-1 {
gap: 1rem;
}
.gap-2 {
gap: 2rem;
}
}
/* prevent arrow on selected admin menu item appearing above modal */

View File

@@ -9,5 +9,5 @@
%div.menu_items
- @options.each do |option|
%label.menu_item{ "data-multiple-checked-select-target": "option", "data-value": option[1], "data-label": option[0] }
%input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] }
%span= option[0]
%input.redesigned-input{ type: "checkbox", checked: @selected.include?(option[1]), name: "#{@name}[]", value: option[1] }
= option[0]

View File

@@ -35,7 +35,7 @@ module Admin
order.with_lock do
if order.contents.update_item(@line_item, line_item_params)
# No Content, does not trigger ng resource auto-update
head :no_content
render body: nil, status: :no_content
else
render json: { errors: @line_item.errors }, status: :precondition_failed
end
@@ -49,7 +49,7 @@ module Admin
authorize! :update, order
order.contents.remove(@line_item.variant)
head :no_content # No Content, does not trigger ng resource auto-update
render body: nil, status: :no_content # No Content, does not trigger ng resource auto-update
end
private

View File

@@ -4,25 +4,17 @@ module Admin
class ColumnPreferencesController < Admin::ResourceController
before_action :load_collection, only: [:bulk_update]
respond_to :json
def bulk_update
@cp_set.collection.each { |cp| authorize! :bulk_update, cp }
respond_to do |format|
if @cp_set.save
format.json {
render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer
}
format.turbo_stream {
flash.now[:success] = t('.success')
render :bulk_update, locals: { action: permitted_params[:action_name] }
}
else
format.json { render json: { errors: @cp_set.errors }, status: :bad_request }
format.turbo_stream {
flash.now[:error] = @cp_set.errors.full_messages.to_sentence
render :bulk_update, locals: { action: permitted_params[:action_name] }
}
end
if @cp_set.save
render json: @cp_set.collection, each_serializer: Api::Admin::ColumnPreferenceSerializer
elsif @cp_set.errors.present?
render json: { errors: @cp_set.errors }, status: :bad_request
else
render body: nil, status: :internal_server_error
end
end
@@ -36,26 +28,11 @@ module Admin
end
def load_collection
collection_attributes = nil
respond_to do |format|
format.json do
collection_attributes = Hash[permitted_params[:column_preferences].
each_with_index.map { |cp, i| [i, cp] }]
collection_attributes.select!{ |_i, cp|
cp[:action_name] == permitted_params[:action_name]
}
end
format.all do
# Inject action name and user ID for each column_preference
collection_attributes = permitted_params[:column_preferences].to_h.each_value { |cp|
cp[:action_name] = permitted_params[:action_name]
cp[:user_id] = spree_current_user.id
}
end
end
@cp_set = Sets::ColumnPreferenceSet.new(@column_preferences, collection_attributes:)
collection_hash = Hash[permitted_params[:column_preferences].
each_with_index.map { |cp, i| [i, cp] }]
collection_hash.select!{ |_i, cp| cp[:action_name] == permitted_params[:action_name] }
@cp_set = Sets::ColumnPreferenceSet.new(@column_preferences,
collection_attributes: collection_hash)
end
def collection

View File

@@ -1,22 +0,0 @@
# frozen_string_literal: true
module Admin
class ConnectedAppSettingsController < Spree::Admin::BaseController
def update
Spree::Config.set(connected_apps_enabled:)
respond_to do |format|
format.html {
flash[:success] = t(:successfully_updated, resource: t('.resource'))
redirect_to main_app.edit_admin_connected_app_settings_path
}
end
end
private
def connected_apps_enabled
params.require(:preferences).require(:connected_apps_enabled).compact_blank.join(",")
end
end
end

View File

@@ -5,12 +5,12 @@ module Admin
def create
authorize! :admin, enterprise
attributes = {}
attributes[:type] = connected_app_params[:type] if connected_app_params[:type]
app = ConnectedApp.create!(enterprise_id: enterprise.id)
app = ConnectedApp.create!(enterprise_id: enterprise.id, **attributes)
app.connect(api_key: spree_current_user.spree_api_key,
channel: SessionChannel.for_request(request))
ConnectAppJob.perform_later(
app, spree_current_user.spree_api_key,
channel: SessionChannel.for_request(request),
)
render_panel
end
@@ -18,9 +18,15 @@ module Admin
def destroy
authorize! :admin, enterprise
app = enterprise.connected_apps.find(params.require(:id))
app = enterprise.connected_apps.first
app.destroy
WebhookDeliveryJob.perform_later(
app.data["destroy"],
"disconnect-app",
nil
)
render_panel
end
@@ -33,9 +39,5 @@ module Admin
def render_panel
redirect_to "#{edit_admin_enterprise_path(enterprise)}#/connected_apps_panel"
end
def connected_app_params
params.permit(:type)
end
end
end

View File

@@ -35,7 +35,13 @@ module Admin
private
def fetch_catalog(url)
DfcRequest.new(spree_current_user).call(url)
if url =~ /food-data-collaboration/
fdc_json = FdcRequest.new(spree_current_user).call(url)
fdc_message = JSON.parse(fdc_json)
fdc_message["products"]
else
DfcRequest.new(spree_current_user).call(url)
end
end
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.

View File

@@ -65,9 +65,7 @@ module Admin
order_cycle ||= OrderCycle.new(coordinator:) if coordinator.present?
enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user,
order_cycle).visible_enterprises
fees = EnterpriseFee.for_enterprises(enterprises).order('enterprise_id', 'fee_type', 'name')
filter_fees(fees)
EnterpriseFee.for_enterprises(enterprises).order('enterprise_id', 'fee_type', 'name')
else
collection = EnterpriseFee.managed_by(spree_current_user).order('enterprise_id',
'fee_type', 'name')
@@ -76,12 +74,6 @@ module Admin
end
end
def filter_fees(fees)
fees = fees.per_item if params[:per_item]
fees = fees.per_order if params[:per_order]
fees
end
def collection_actions
[:index, :for_order_cycle, :bulk_update]
end

View File

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

View File

@@ -11,7 +11,6 @@ module Admin
before_action :remove_protected_attrs, only: [:update]
before_action :require_order_cycle_set_params, only: [:bulk_update]
around_action :protect_invalid_destroy, only: :destroy
before_action :verify_datetime_change, only: :update
def index
respond_to do |format|
@@ -236,7 +235,7 @@ module Admin
else
begin
yield
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::DeleteRestrictionError
rescue ActiveRecord::InvalidForeignKey
redirect_to main_app.admin_order_cycles_url
flash[:error] = I18n.t('admin.order_cycles.destroy_errors.orders_present')
end
@@ -295,22 +294,5 @@ module Admin
collection_attributes: [:id] + PermittedAttributes::OrderCycle.basic_attributes
).to_h.with_indifferent_access
end
# Check that order cycle datetime values changed if it has existing orders
def verify_datetime_change
return unless params[:order_cycle][:confirm]
return unless @order_cycle.orders.exists?
return if same_dates(@order_cycle.orders_open_at, order_cycle_params[:orders_open_at]) &&
same_dates(@order_cycle.orders_close_at, order_cycle_params[:orders_close_at])
render json: { trigger_action: params[:order_cycle][:trigger_action] },
status: :unprocessable_entity
end
def same_dates(date, string)
false unless date && string
DateTime.parse(string).to_fs(:short) == date.to_fs(:short)
end
end
end

View File

@@ -4,8 +4,6 @@ require 'roo'
module Admin
class ProductImportController < Spree::Admin::BaseController
TMPDIR_PREFIX = "product_import-"
before_action :validate_upload_presence, except: %i[index guide validate_data]
def index
@@ -103,7 +101,8 @@ module Admin
def save_uploaded_file(upload)
extension = File.extname(upload.original_filename)
File.open(File.join(mktmpdir, "import#{extension}"), 'wb') do |f|
directory = Dir.mktmpdir 'product_import'
File.open(File.join(directory, "import#{extension}"), 'wb') do |f|
data = UploadSanitizer.new(upload.read).call
f.write(data)
f.path
@@ -127,14 +126,6 @@ module Admin
ProductImport::ProductImporter
end
def mktmpdir
Dir::Tmpname.create(TMPDIR_PREFIX, Rails.root.join('tmp') ) { |tmpname| Dir.mkdir(tmpname) }
end
def tmpdir_base
Rails.root.join('tmp', TMPDIR_PREFIX).to_s
end
def file_path
@file_path ||= validate_file_path(sanitize_file_path(params[:filepath]))
end
@@ -143,9 +134,8 @@ module Admin
FilePathSanitizer.new.sanitize(file_path, on_error: method(:raise_invalid_file_path))
end
# Ensure file is under the safe tmp directory
def validate_file_path(file_path)
return file_path if file_path.to_s.match?(%r{^#{tmpdir_base}[A-Za-z0-9-]*/import\.csv$})
return file_path if file_path.to_s.match?(TEMP_FILE_PATH_REGEX)
raise_invalid_file_path
end
@@ -155,5 +145,6 @@ module Admin
notice: I18n.t(:product_import_no_data_in_spreadsheet_notice)
raise 'Invalid File Path'
end
TEMP_FILE_PATH_REGEX = %r{^/tmp/product_import[A-Za-z0-9-]*/import\.csv$}
end
end

View File

@@ -11,8 +11,6 @@ module Admin
def index
fetch_products
render "index", locals: { producers:, categories:, tax_category_options:, flash: }
session[:products_return_to_url] = request.url
end
def bulk_update
@@ -40,9 +38,7 @@ module Admin
{ id: params[:id] }
).find_product
@record.destroyed_by = spree_current_user
status = :ok
if @record.destroy
flash.now[:success] = t('.delete_product.success')
else
@@ -72,29 +68,6 @@ module Admin
end
end
def clone
@product = Spree::Product.find(params[:id])
status = :ok
begin
@cloned_product = @product.duplicate
flash.now[:success] = t('.success')
@product_index = "-#{@cloned_product.id}"
@producer_options = producers
@category_options = categories
@tax_category_options = tax_category_options
rescue ActiveRecord::ActiveRecordError => _e
flash.now[:error] = t('.error')
status = :unprocessable_entity
@product_index = "-1" # Create a unique enough index
end
respond_with do |format|
format.turbo_stream { render :clone, status: }
end
end
def index_url(params)
"/admin/products?#{params.to_query}" # todo: fix routing so this can be automaticly generated
end
@@ -151,7 +124,7 @@ module Admin
def ransack_query
query = {}
query.merge!(variants_supplier_id_in: @producer_id) if @producer_id.present?
query.merge!(supplier_id_in: @producer_id) if @producer_id.present?
if @search_term.present?
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
@@ -165,13 +138,13 @@ module Admin
def product_query_includes
[
:image,
:supplier,
{ variants: [
:default_price,
:primary_taxon,
:product,
:stock_items,
:tax_category,
:supplier,
] },
]
end

View File

@@ -61,9 +61,6 @@ module Admin
def render_in_background
cable_ready[ScopedChannel.for_id(params[:uuid])]
.inner_html(
selector: "#report-go",
html: helpers.button(t(:go), "report__submit-btn", "submit", disabled: true)
).inner_html(
selector: "#report-table",
html: render_to_string(partial: "admin/reports/loading")
).scroll_into_view(

View File

@@ -5,7 +5,7 @@ module Admin
respond_to :json
respond_override destroy: { json: {
success: lambda { head :no_content }
success: lambda { render body: nil, status: :no_content }
} }
def map_by_tag

View File

@@ -88,7 +88,7 @@ module Admin
end
def modified_variant_overrides_ids
variant_overrides_params.pluck(:id)
variant_overrides_params.map { |vo| vo[:id] }
end
def collection_actions

View File

@@ -9,7 +9,7 @@ module Api
authorize! :destroy, enterprise_fee
if enterprise_fee.destroy
head :no_content
render plain: I18n.t(:successfully_removed), status: :no_content
else
render plain: enterprise_fee.errors.full_messages.first, status: :forbidden
end

View File

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

View File

@@ -44,7 +44,7 @@ module Api
authorize! :delete, @product
@product.destroyed_by = current_api_user
@product.destroy
head :no_content
render json: @product, serializer: Api::Admin::ProductSerializer, status: :no_content
end
def bulk_products
@@ -54,7 +54,7 @@ module Api
end
def overridable
@products = product_finder.products_for_producers
@products = product_finder.paged_products_for_producers
render_paged_products @products, ::Api::Admin::ProductSimpleSerializer
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
module Api
module V0
class TaxonomiesController < Api::V0::BaseController
respond_to :json
skip_authorization_check only: :jstree
def jstree
@taxonomy = Spree::Taxonomy.find(params[:id])
render json: @taxonomy.root, serializer: Api::TaxonJstreeSerializer
end
end
end
end

View File

@@ -5,10 +5,12 @@ module Api
class TaxonsController < Api::V0::BaseController
respond_to :json
skip_authorization_check only: [:index, :show]
skip_authorization_check only: [:index, :show, :jstree]
def index
@taxons = if params[:ids]
@taxons = if taxonomy
taxonomy.root.children
elsif params[:ids]
Spree::Taxon.where(id: raw_params[:ids].split(","))
else
Spree::Taxon.ransack(raw_params[:q]).result
@@ -16,9 +18,23 @@ module Api
render json: @taxons, each_serializer: Api::TaxonSerializer
end
def jstree
@taxon = taxon
render json: @taxon.children, each_serializer: Api::TaxonJstreeSerializer
end
def create
authorize! :create, Spree::Taxon
@taxon = Spree::Taxon.new(taxon_params)
@taxon.taxonomy_id = params[:taxonomy_id]
taxonomy = Spree::Taxonomy.find_by(id: params[:taxonomy_id])
if taxonomy.nil?
@taxon.errors.add(:taxonomy_id, I18n.t(:invalid_taxonomy_id, scope: 'spree.api'))
invalid_resource!(@taxon) && return
end
@taxon.parent_id = taxonomy.root.id unless params.dig(:taxon, :parent_id)
if @taxon.save
render json: @taxon, serializer: Api::TaxonSerializer, status: :created
@@ -39,19 +55,25 @@ module Api
def destroy
authorize! :delete, Spree::Taxon
taxon.destroy
head :no_content
render json: taxon, serializer: Api::TaxonSerializer, status: :no_content
end
private
def taxonomy
return if params[:taxonomy_id].blank?
@taxonomy ||= Spree::Taxonomy.find(params[:taxonomy_id])
end
def taxon
@taxon = Spree::Taxon.find(params[:id])
@taxon ||= taxonomy.taxons.find(params[:id])
end
def taxon_params
return if params[:taxon].blank?
params.require(:taxon).permit([:name, :position])
params.require(:taxon).permit([:name, :parent_id, :position])
end
end
end

View File

@@ -44,7 +44,7 @@ module Api
authorize! :delete, @variant
VariantDeleter.new.delete(@variant)
head :no_content
render json: @variant, serializer: Api::VariantSerializer, status: :no_content
end
private

View File

@@ -19,18 +19,15 @@ module Spree
before_action :authorize_admin
before_action :set_locale
before_action :warn_invalid_order_cycles, if: :page_load_request?
before_action :warn_invalid_order_cycles, if: :html_request?
# Warn the user when they have an active order cycle with hubs that are not ready
# for checkout (ie. does not have valid shipping and payment methods).
def warn_invalid_order_cycles
return if flash[:notice].present? || session[:displayed_order_cycle_warning]
return if flash[:notice].present?
warning = OrderCycles::WarningService.new(spree_current_user).call
return if warning.blank?
flash.now[:notice] = warning
session[:displayed_order_cycle_warning] = true
flash[:notice] = warning if warning.present?
end
protected
@@ -84,12 +81,6 @@ module Spree
private
def page_load_request?
return false if request.format.include?('turbo')
html_request?
end
def html_request?
request.format.html?
end

View File

@@ -37,14 +37,13 @@ module Spree
if @object.save
flash[:success] = flash_message_for(@object, :successfully_created)
respond_to do |format|
format.html { redirect_to location_after_save }
format.turbo_stream { render :update }
end
redirect_to location_after_save
else
respond_with_error(@object.errors)
respond_with(@object)
end
rescue ActiveStorage::IntegrityError
@object.errors.add :attachment, :integrity_error
respond_with(@object)
end
def update
@@ -54,13 +53,16 @@ module Spree
if @object.update(permitted_resource_params)
flash[:success] = flash_message_for(@object, :successfully_updated)
respond_to do |format|
respond_with do |format|
format.html { redirect_to location_after_save }
format.turbo_stream
end
else
respond_with_error(@object.errors)
respond_with(@object)
end
rescue ActiveStorage::IntegrityError
@object.errors.add :attachment, :integrity_error
respond_with(@object)
end
def destroy
@@ -110,14 +112,6 @@ module Spree
:attachment, :viewable_id, :alt
)
end
def respond_with_error(errors)
@errors = errors.map(&:full_message)
respond_to do |format|
format.html { respond_with(@object) }
format.turbo_stream { render :edit }
end
end
end
end
end

View File

@@ -15,8 +15,6 @@ module Spree
invoice_pdf = filepath(invoice_id)
send_file(invoice_pdf, type: 'application/pdf', disposition: :inline)
rescue ActionController::MissingFile
render "errors/not_found", status: :not_found, formats: :html
end
def generate

View File

@@ -8,7 +8,6 @@ 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

@@ -10,10 +10,8 @@ module Spree
include OpenFoodNetwork::SpreeApiKeyLoader
include OrderCyclesHelper
include EnterprisesHelper
helper ::Admin::ProductsHelper
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]
@@ -43,7 +41,6 @@ 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]
@@ -55,9 +52,14 @@ 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)
@@ -155,13 +157,10 @@ module Spree
end
def load_form_data
@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
@taxons = Spree::Taxon.order(:name)
@import_dates = product_import_dates.uniq.to_json
end
def product_import_dates
@@ -174,10 +173,12 @@ module Spree
def product_import_dates_query
Spree::Variant.
select('import_date').distinct.
where(supplier_id: editable_enterprises.collect(&:id)).
select('DISTINCT spree_variants.import_date').
joins(:product).
where(spree_products: { supplier_id: editable_enterprises.collect(&:id) }).
where.not(spree_variants: { import_date: nil }).
order('import_date DESC')
where(spree_variants: { deleted_at: nil }).
order('spree_variants.import_date DESC')
end
def strip_new_properties

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Spree
module Admin
class TaxonomiesController < ::Admin::ResourceController
respond_to :json, only: [:get_children]
def get_children
@taxons = Taxon.find(params[:parent_id]).children
end
private
def location_after_save
if @taxonomy.created_at == @taxonomy.updated_at
spree.edit_admin_taxonomy_url(@taxonomy)
else
spree.admin_taxonomies_url
end
end
def permitted_resource_params
params.require(:taxonomy).permit(:name)
end
end
end
end

View File

@@ -2,70 +2,122 @@
module Spree
module Admin
class TaxonsController < ::Admin::ResourceController
before_action :set_taxon, except: %i[create index new]
class TaxonsController < Spree::Admin::BaseController
respond_to :html, :json, :js
def index
@taxons = Taxon.order(:name)
def edit
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.find(params[:id])
@permalink_part = @taxon.permalink.split("/").last
end
def new
@taxon = Taxon.new
end
def edit; end
def create
@taxon = Spree::Taxon.new(taxon_params)
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.build(params[:taxon])
if @taxon.save
flash[:success] = flash_message_for(@taxon, :successfully_created)
redirect_to edit_admin_taxon_path(@taxon.id)
respond_with(@taxon) do |format|
format.json { render json: @taxon.to_json }
end
else
render :new, status: :unprocessable_entity
flash[:error] = Spree.t('errors.messages.could_not_create_taxon')
respond_with(@taxon) do |format|
format.html do
if redirect_to @taxonomy
spree.edit_admin_taxonomy_url(@taxonomy)
else
spree.admin_taxonomies_url
end
end
end
end
end
def update
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.find(params[:id])
parent_id = params[:taxon][:parent_id]
new_position = params[:taxon][:position]
if parent_id || new_position # taxon is being moved
new_parent = parent_id.nil? ? @taxon.parent : Taxon.find(parent_id.to_i)
new_position = new_position.nil? ? -1 : new_position.to_i
# Bellow is a very complicated way of finding where in nested set we
# should actually move the taxon to achieve sane results,
# JS is giving us the desired position, which was awesome for previous setup,
# but now it's quite complicated to find where we should put it as we have
# to differenciate between moving to the same branch, up down and into
# first position.
new_siblings = new_parent.children
if new_position <= 0 && new_siblings.empty?
@taxon.move_to_child_of(new_parent)
elsif new_parent.id != @taxon.parent_id
if new_position.zero?
@taxon.move_to_left_of(new_siblings.first)
else
@taxon.move_to_right_of(new_siblings[new_position - 1])
end
elsif new_position < new_siblings.index(@taxon)
@taxon.move_to_left_of(new_siblings[new_position]) # we move up
else
@taxon.move_to_right_of(new_siblings[new_position - 1]) # we move down
end
# Reset legacy position, if any extensions still rely on it
new_parent.children.reload.each do |t|
t.update_columns(
position: t.position,
updated_at: Time.zone.now
)
end
if parent_id
@taxon.reload
@taxon.set_permalink
@taxon.save!
@update_children = true
end
end
if params.key? "permalink_part"
parent_permalink = @taxon.permalink.split("/")[0...-1].join("/")
parent_permalink += "/" if parent_permalink.present?
params[:taxon][:permalink] = parent_permalink + params[:permalink_part]
end
# check if we need to rename child taxons if parent name or permalink changes
if params[:taxon][:name] != @taxon.name || params[:taxon][:permalink] != @taxon.permalink
@update_children = true
end
if @taxon.update(taxon_params)
flash[:success] = flash_message_for(@taxon, :successfully_updated)
redirect_to edit_admin_taxon_path(@taxon.id)
else
render :edit, status: :unprocessable_entity
end
# rename child taxons
if @update_children
@taxon.descendants.each do |taxon|
taxon.reload
taxon.set_permalink
taxon.save!
end
end
respond_with(@taxon) do |format|
format.html { redirect_to spree.edit_admin_taxonomy_url(@taxonomy) }
format.json { render json: @taxon.to_json }
end
end
def destroy
status = if @taxon.destroy
flash_message = t('.delete_taxon.success')
status = :ok
else
flash_message = t('.delete_taxon.error')
status = :unprocessable_entity
end
respond_to do |format|
format.html {
flash[:success] = flash_message if status == :ok
flash[:error] = flash_message if status == :unprocessable_entity
redirect_to admin_taxons_path
}
format.turbo_stream {
flash[:success] = flash_message if status == :ok
flash[:error] = flash_message if status == :unprocessable_entity
render :destroy_taxon, status:
}
end
@taxon = Taxon.find(params[:id])
@taxon.destroy
respond_with(@taxon) { |format| format.json { render json: '' } }
end
private
def set_taxon
@taxon = Taxon.find(params[:id])
end
def taxon_params
params.require(:taxon).permit(
:name, :position, :icon, :description, :permalink,
:name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id,
:meta_description, :meta_keywords, :meta_title, :dfc_id
)
end

View File

@@ -46,13 +46,7 @@ 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
@@ -119,8 +113,6 @@ 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

@@ -47,8 +47,14 @@ module Spree
@user = Spree::User.new(user_params)
if @user.save
flash[:success] = t('devise.user_registrations.spree_user.signed_up_but_unconfirmed')
render cable_ready: cable_car.redirect_to(url: main_app.root_path)
render cable_ready: cable_car.inner_html(
"#signup-feedback",
partial("layouts/alert",
locals: {
type: "success",
message: t('devise.user_registrations.spree_user.signed_up_but_unconfirmed')
})
)
else
render status: :unprocessable_entity, cable_ready: cable_car.morph(
"#signup-tab",

View File

@@ -26,8 +26,7 @@ module Admin
show_enterprise_fees = can?(:manage_enterprise_fees,
enterprise) && (is_shop || enterprise.is_primary_producer)
show_connected_apps = can?(:manage_connected_apps, enterprise) &&
feature?(:connected_apps, spree_current_user, enterprise) &&
Spree::Config.connected_apps_enabled.present?
feature?(:connected_apps, spree_current_user, enterprise)
build_enterprise_side_menu_items(
is_shop:,
@@ -39,11 +38,6 @@ module Admin
)
end
def connected_apps_enabled
connected_apps_enabled = Spree::Config.connected_apps_enabled&.split(',') || []
ConnectedApp::TYPES & connected_apps_enabled
end
private
def build_enterprise_side_menu_items(
@@ -72,8 +66,8 @@ module Admin
{ name: 'inventory_settings', icon_class: "icon-list-ol", show: is_shop },
{ name: 'tag_rules', icon_class: "icon-random", show: is_shop },
{ name: 'shop_preferences', icon_class: "icon-shopping-cart", show: is_shop },
{ name: 'white_label', icon_class: "icon-leaf", show: true },
{ name: 'users', icon_class: "icon-user", show: true },
{ name: 'white_label', icon_class: "icon-leaf", show: true },
{ name: 'connected_apps', icon_class: "icon-puzzle-piece", show: show_connected_apps },
]
end

View File

@@ -9,33 +9,5 @@ module Admin
new_admin_product_image_path(product.id)
end
end
def prepare_new_variant(product)
product.variants.build
end
def unit_value_with_description(variant)
precised_unit_value = nil
if variant.unit_value
scaled_unit_value = variant.unit_value / (variant.product.variant_unit_scale || 1)
precised_unit_value = number_with_precision(
scaled_unit_value,
precision: nil,
strip_insignificant_zeros: true,
significant: false,
)
end
[precised_unit_value, variant.unit_description].compact_blank.join(" ")
end
def products_return_to_url(url_filters)
if feature?(:admin_style_v3, spree_current_user)
return session[:products_return_to_url] || admin_products_url
end
"#{admin_products_path}#{url_filters.empty? ? '' : "#?#{url_filters.to_query}"}"
end
end
end

View File

@@ -2,7 +2,7 @@
module I18nHelper
def locale_options
OpenFoodNetwork::I18nConfig.selectable_locales.map do |locale|
OpenFoodNetwork::I18nConfig.available_locales.map do |locale|
[t('language_name', locale:), locale]
end
end

View File

@@ -10,8 +10,4 @@ module MailerHelper
link_to ofn, "https://www.openfoodnetwork.org"
end
end
def order_reply_email(order)
order.distributor.email_address.presence || order.distributor.contact.email
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module Spree
module Admin
module TaxonsHelper
def taxon_path(taxon)
taxon.ancestors.reverse.collect(&:name).join( " >> ")
end
end
end
end

View File

@@ -19,8 +19,8 @@ class ConnectAppJob < ApplicationJob
selector = "#connected-app-discover-regen.enterprise_#{enterprise.id}"
html = ApplicationController.render(
partial: "admin/enterprises/form/connected_apps/discover_regen",
locals: { enterprise:, connected_app: enterprise.connected_apps.discover_regen.first },
partial: "admin/enterprises/form/connected_apps",
locals: { enterprise: },
)
cable_ready[channel].morph(selector:, html:).broadcast

View File

@@ -39,14 +39,10 @@ class ReportJob < ApplicationJob
end
def broadcast_result(channel, format, blob)
cable_ready[channel]
.inner_html(
selector: "#report-go",
html: Spree::Admin::BaseController.helpers.button(I18n.t(:go), "report__submit-btn")
).inner_html(
selector: "#report-table",
html: actioncable_content(format, blob)
).broadcast
cable_ready[channel].inner_html(
selector: "#report-table",
html: actioncable_content(format, blob)
).broadcast
end
def broadcast_error(channel)
@@ -57,14 +53,7 @@ class ReportJob < ApplicationJob
end
def actioncable_content(format, blob)
if format.to_sym == :html
return blob.result if blob.byte_size < 10**6 # 1 MB
return render(
partial: "admin/reports/display",
locals: { file_url: blob.expiring_service_url }
)
end
return blob.result if format.to_sym == :html
render(partial: "admin/reports/download", locals: { file_url: blob.expiring_service_url })
end

View File

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

@@ -0,0 +1,19 @@
# frozen_string_literal: true
# Rails 5 introduced some breaking changes to these built-in methods, and the new versions
# no longer work correctly in relation to decrementing stock with LineItems / VariantOverrides.
# The following methods re-instate the pre-Rails-5 versions, which work as expected.
# https://apidock.com/rails/v4.2.9/ActiveRecord/Persistence/increment%21
# https://apidock.com/rails/v4.2.9/ActiveRecord/Persistence/decrement%21
module LineItemStockChanges
extend ActiveSupport::Concern
def increment!(attribute, by = 1)
increment(attribute, by).update_attribute(attribute, self[attribute])
end
def decrement!(attribute, by = 1)
decrement(attribute, by).update_attribute(attribute, self[attribute])
end
end

View File

@@ -10,12 +10,9 @@ module LogDestroyPerformer
after_destroy :log_who_destroyed
def log_who_destroyed
message = if destroyed_by.nil?
"#{self.class} #{id} deleted"
else
"#{self.class} #{id} deleted by #{destroyed_by.id} <#{destroyed_by.email}>"
end
Rails.logger.info message
return if destroyed_by.nil?
Rails.logger.info "#{self.class} #{id} deleted by #{destroyed_by.id}"
end
end
end

View File

@@ -4,34 +4,8 @@
#
# Here we store keys and links to access the app.
class ConnectedApp < ApplicationRecord
TYPES = ['discover_regen', 'affiliate_sales_data'].freeze
belongs_to :enterprise
after_destroy :disconnect
scope :discover_regen, -> { where(type: "ConnectedApp") }
scope :affiliate_sales_data, -> { where(type: "ConnectedApps::AffiliateSalesData") }
scope :connecting, -> { where(data: nil) }
scope :ready, -> { where.not(data: nil) }
def connecting?
data.nil?
end
def ready?
!connecting?
end
def connect(api_key:, channel:)
ConnectAppJob.perform_later(self, api_key, channel:)
end
def disconnect
WebhookDeliveryJob.perform_later(
data["destroy"],
"disconnect-app",
nil
)
end
end

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
# An enterprise can opt-in for their data to be included in the affiliate_sales_data endpoint
#
module ConnectedApps
class AffiliateSalesData < ConnectedApp
def connect(_opts)
update! data: true # not-nil value indicates it is ready
end
def disconnect; end
end
end

View File

@@ -4,14 +4,4 @@ class CustomTab < ApplicationRecord
belongs_to :enterprise
validates :title, presence: true, length: { maximum: 20 }
# Remove any unsupported HTML.
def content
HtmlSanitizer.sanitize(super)
end
# Remove any unsupported HTML.
def content=(html)
super(HtmlSanitizer.sanitize(html))
end
end

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: false
require "mini_magick"
class Enterprise < ApplicationRecord
SELLS = %w(unspecified none own any).freeze
ENTERPRISE_SEARCH_RADIUS = 100
@@ -41,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_variants,
class_name: 'Spree::Variant', foreign_key: 'supplier_id', dependent: :destroy
has_many :supplied_products, through: :supplied_variants, source: :product
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 :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
@@ -169,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_variants).
joins(supplied_products: :variants).
where(spree_variants: { id: variants }).
select('DISTINCT enterprises.*')
}
@@ -207,14 +205,14 @@ class Enterprise < ApplicationRecord
select('DISTINCT enterprises.*')
}
scope :distributing_variants, lambda { |variants_ids|
scope :distributing_products, lambda { |product_ids|
exchanges = joins("
INNER JOIN exchanges
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = false)
ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')
").
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: { id: variants_ids }).select('DISTINCT enterprises.id')
where(spree_variants: { product_id: product_ids }).select('DISTINCT enterprises.id')
where(id: exchanges)
}
@@ -429,9 +427,10 @@ class Enterprise < ApplicationRecord
test_permalink = UrlGenerator.to_url(test_permalink)
test_permalink = "my-enterprise" if test_permalink.blank?
existing = Enterprise.
select(:permalink).
order(:permalink).
where("permalink LIKE ?", "#{test_permalink}%").
pluck(:permalink)
map(&:permalink)
if existing.include?(test_permalink)
used_indices = existing.map do |p|
@@ -599,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_variants(supplied_variants.select(:id)).
Enterprise.distributing_products(supplied_products.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).where(spree_variants: { supplier_id: parent } )
.joins(variant: :product).where(spree_products: { supplier_id: parent })
end
end

View File

@@ -24,7 +24,6 @@ class OrderCycle < ApplicationRecord
where incoming: false
}, class_name: "Exchange", dependent: :destroy
has_many :orders, class_name: 'Spree::Order', dependent: :restrict_with_exception
has_many :suppliers, -> { distinct }, source: :sender, through: :cached_incoming_exchanges
has_many :distributors, -> { distinct }, source: :receiver, through: :cached_outgoing_exchanges
has_many :order_cycle_schedules, dependent: :destroy

View File

@@ -54,7 +54,10 @@ module ProductImport
if settings.importing_into_inventory?
VariantOverride.for_hubs([enterprise_id]).count
else
Spree::Variant.where(supplier_id: enterprise_id).count
Spree::Variant.
joins(:product).
where(spree_products: { supplier_id: enterprise_id }).
count
end
@enterprise_products[enterprise_id] = products_count
@@ -162,10 +165,11 @@ module ProductImport
return
end
product = Spree::Product.new(supplier_id: entry.enterprise_id)
product = Spree::Product.new
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)
@@ -224,13 +228,10 @@ 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,7 +73,6 @@ 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?
@@ -288,7 +287,9 @@ module ProductImport
end
def inventory_validation(entry)
products = Spree::Product.in_supplier(entry.producer_id).where(name: entry.name)
products = Spree::Product.where(supplier_id: entry.producer_id,
name: entry.name,
deleted_at: nil)
if products.empty?
mark_as_invalid(entry, attribute: 'name',
@@ -357,7 +358,9 @@ module ProductImport
end
def product_validation(entry)
products = Spree::Product.in_supplier(entry.enterprise_id).where(name: entry.name)
products = Spree::Product.where(supplier_id: entry.enterprise_id,
name: entry.name,
deleted_at: nil)
if products.empty?
mark_as_new_product(entry)
@@ -377,10 +380,11 @@ module ProductImport
end
def mark_as_new_product(entry)
new_product = Spree::Product.new(supplier_id: entry.enterprise_id)
new_product = Spree::Product.new
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

@@ -287,6 +287,8 @@ module ProductImport
end
def delete_uploaded_file
return unless @file.path == Rails.root.join("tmp/product_import").to_s
File.delete(@file)
end

View File

@@ -39,6 +39,7 @@ module Spree
can [:index, :read], StockLocation
can [:index, :read], StockMovement
can [:index, :read], Taxon
can [:index, :read], Taxonomy
can [:index, :read], Variant
can [:index, :read], Zone
end
@@ -188,22 +189,20 @@ module Spree
:seo, :group_buy_options,
:bulk_update, :clone, :delete,
:destroy], Spree::Product do |product|
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include?(
product.variants.first.supplier
)
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier
end
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone], :products_v3
can [:admin, :index, :bulk_update, :destroy, :destroy_variant], :products_v3
can [:create], Spree::Variant
can [:admin, :index, :read, :edit,
:update, :search, :delete, :destroy], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).
managed_product_enterprises.include? variant.supplier
managed_product_enterprises.include? variant.product.supplier
end
can [:admin, :index, :read, :update, :bulk_update, :bulk_reset], VariantOverride do |vo|
next false unless vo.hub.present? && vo.variant&.supplier.present?
next false unless vo.hub.present? && vo.variant&.product&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
@@ -211,14 +210,14 @@ module Spree
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? vo.variant.supplier
include? vo.variant.product.supplier
hub_auth && producer_auth
end
can [:admin, :create, :update], InventoryItem do |ii|
next false unless ii.enterprise.present? &&
ii.variant&.supplier.present?
ii.variant&.product&.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
@@ -226,7 +225,7 @@ module Spree
producer_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_producers.
include? ii.variant.supplier
include? ii.variant.product.supplier
hub_auth && producer_auth
end

View File

@@ -139,8 +139,5 @@ module Spree
# Available units
preference :available_units, :string, default: "g,kg,T,mL,L,kL"
# Connected Apps
preference :connected_apps_enabled, :string, default: nil
end
end

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: true
require "mini_magick"
module Spree
class Image < Asset
has_one_attached :attachment, service: image_service do |attachment|

View File

@@ -5,6 +5,7 @@ require 'open_food_network/scope_variant_to_hub'
module Spree
class LineItem < ApplicationRecord
include VariantUnits::VariantAndLineItemNaming
include LineItemStockChanges
searchable_attributes :price, :quantity, :order_id, :variant_id, :tax_category_id
searchable_associations :order, :order_cycle, :variant, :product, :supplier, :tax_category
@@ -15,7 +16,7 @@ module Spree
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
has_one :product, through: :variant
has_one :supplier, through: :variant
has_one :supplier, through: :product
belongs_to :tax_category, class_name: "Spree::TaxCategory", optional: true
has_many :adjustments, as: :adjustable, dependent: :destroy
@@ -84,11 +85,13 @@ module Spree
where(order_cycles: { id: order_cycle })
}
# 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
# 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
scope :supplied_by_any, lambda { |enterprises|
variant_ids = Spree::Variant.unscoped.where(supplier: enterprises).select(:id)
where(variant_id: variant_ids)
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 })
}
scope :with_tax, -> {

View File

@@ -5,15 +5,19 @@ 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, meta_keywork, etc. that do not change by variant.
# 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.
#
# VARIANTS
# 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).
# All variants can access the product properties directly (via reverse delegation).
# Inventory units are tied to Variant.
# All variants have option values and may have inventory units.
# The master variant can have inventory units, but not option values.
# All other 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
@@ -22,14 +26,15 @@ module Spree
include LogDestroyPerformer
self.belongs_to_required_by_default = false
self.ignored_columns += [:supplier_id]
acts_as_paranoid
searchable_attributes :meta_keywords, :sku
searchable_associations :properties, :variants
searchable_attributes :supplier_id, :meta_keywords, :sku
searchable_associations :supplier, :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
@@ -40,6 +45,7 @@ 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
@@ -62,28 +68,28 @@ module Spree
accepts_nested_attributes_for :image
accepts_nested_attributes_for :product_properties,
allow_destroy: true,
reject_if: ->(pp) { pp[:property_name].blank? }
reject_if: lambda { |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, :supplier_id
:shipping_category_id, :primary_taxon_id
after_validation :validate_variant_attrs, on: :create
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(spree_product_properties: { property_id: property_ids })
where(producer_properties: { property_id: property_ids }).
or(
where(spree_product_properties: { property_id: property_ids })
)
}
scope :with_order_cycles_outer, lambda {
scope :with_order_cycles_outer, -> {
joins("
LEFT OUTER JOIN spree_variants AS o_spree_variants
ON (o_spree_variants.product_id = spree_products.id)").
@@ -105,7 +111,9 @@ 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('
@@ -118,9 +126,8 @@ module Spree
distinct
}
scope :in_supplier, lambda { |supplier|
distinct.joins(:variants).where(spree_variants: { supplier: })
}
# -- Scopes
scope :in_supplier, lambda { |supplier| where(supplier_id: supplier) }
# Products distributed via the given distributor through an OC
scope :in_distributor, lambda { |distributor|
@@ -137,6 +144,18 @@ 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.
@@ -151,17 +170,27 @@ module Spree
where.not(order_cycles: { id: nil })
}
scope :by_producer, -> { joins(variants: :supplier).order('enterprises.name') }
scope :by_name, -> { order('spree_products.name') }
scope :by_producer, -> { joins(:supplier).order('enterprises.name') }
scope :by_name, -> { order('name') }
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
in_supplier(user.enterprises)
where(supplier_id: user.enterprises.select("enterprises.id"))
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
@@ -207,10 +236,7 @@ module Spree
ps = product_properties.all
if inherits_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 || [])
ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier.producer_properties)
end
ps.
@@ -237,6 +263,8 @@ module Spree
def destruction
transaction do
touch_distributors
ExchangeVariant.
where(exchange_variants: { variant_id: variants.with_deleted.
select(:id) }).destroy_all
@@ -257,7 +285,6 @@ 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
@@ -265,7 +292,6 @@ module Spree
def variant_unit_with_scale
scale_clean = ActiveSupport::NumberHelper.number_to_rounded(variant_unit_scale,
precision: nil,
significant: false,
strip_insignificant_zeros: true)
[variant_unit, scale_clean].compact_blank.join("_")
end
@@ -290,44 +316,14 @@ module Spree
private
def validate_variant_attrs
# Avoid running validation when we can't set variant attrs
# eg clone product. Will raise error if clonning a product with no variant
return if variants.first&.valid?
unless Spree::Taxon.find_by(id: primary_taxon_id)
errors.add(:primary_taxon_id,
I18n.t('activerecord.errors.models.spree/product.must_exist'))
end
return if Enterprise.find_by(id: supplier_id)
errors.add(:supplier_id,
I18n.t('activerecord.errors.models.spree/product.must_exist'))
end
def update_units
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
variants.each do |v|
if v.persisted?
v.update_units
else
v.assign_units
end
end
variants.each(&:update_units)
end
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?
def touch_distributors
Enterprise.distributing_products(id).each(&:touch)
end
def validate_image

View File

@@ -6,8 +6,6 @@ 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) }
@@ -15,11 +13,5 @@ module Spree
def property
self
end
private
def touch_producer_properties
producer_properties.each(&:touch)
end
end
end

View File

@@ -31,7 +31,7 @@ module Spree
return [] if order.distributor && !order.distributor.charges_sales_tax
return [] unless order.tax_zone
includes(zone: { zone_members: :zoneable }).load.select do |rate|
all.includes(zone: { zone_members: :zoneable }).load.select do |rate|
rate.potentially_applicable?(order.tax_zone)
end
end

View File

@@ -2,11 +2,19 @@
module Spree
class Taxon < ApplicationRecord
self.belongs_to_required_by_default = false
acts_as_nested_set dependent: :destroy
belongs_to :taxonomy, class_name: 'Spree::Taxonomy', touch: true
has_many :variants, class_name: "Spree::Variant", foreign_key: "primary_taxon_id",
inverse_of: :primary_taxon, dependent: :restrict_with_error
has_many :products, through: :variants, dependent: nil
before_create :set_permalink
validates :name, presence: true
# Indicate which filters should be used for this taxon
@@ -23,13 +31,33 @@ module Spree
end
end
def set_permalink
if parent.present?
self.permalink = [parent.permalink, permalink_end].join('/')
elsif permalink.blank?
self.permalink = UrlGenerator.to_url(name)
end
end
# For #2759
def to_param
permalink
end
def pretty_name
ancestor_chain = ancestors.inject("") do |name, ancestor|
name + "#{ancestor.name} -> "
end
ancestor_chain + name.to_s
end
# Find all the taxons of supplied products for each enterprise, indexed by enterprise.
# Format: {enterprise_id => [taxon_id, ...]}
def self.supplied_taxons
taxons = {}
Spree::Taxon.
joins(variants: :supplier).
joins(products: :supplier).
select('spree_taxons.*, enterprises.id AS enterprise_id').
each do |t|
taxons[t.enterprise_id.to_i] ||= Set.new
@@ -62,5 +90,13 @@ module Spree
ts[t.enterprise_id.to_i] << t.id
end
end
private
def permalink_end
return UrlGenerator.to_url(name) if permalink.blank?
permalink.split('/').last
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Spree
class Taxonomy < ApplicationRecord
validates :name, presence: true
has_many :taxons, dependent: :nullify
has_one :root, -> { where parent_id: nil }, class_name: "Spree::Taxon", dependent: :destroy
after_save :set_name
default_scope -> { order("#{table_name}.position") }
private
def set_name
if root
root.update_columns(
name:,
updated_at: Time.zone.now
)
else
self.root = Taxon.create!(taxonomy_id: id, name:)
end
end
end
end

View File

@@ -96,7 +96,7 @@ module Spree
end
def build_enterprise_roles
Enterprise.find_each do |enterprise|
Enterprise.all.find_each do |enterprise|
unless enterprise_roles.find_by enterprise_id: enterprise.id
enterprise_roles.build(enterprise:)
end
@@ -156,12 +156,6 @@ module Spree
self.disabled_at = value == '1' ? Time.zone.now : nil
end
def affiliate_enterprises
return [] unless Flipper.enabled?(:affiliate_sales_data, self)
Enterprise.joins(:connected_apps).merge(ConnectedApps::AffiliateSalesData.ready)
end
protected
def password_required?

View File

@@ -13,8 +13,8 @@ module Spree
acts_as_paranoid
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id, :supplier_id
searchable_associations :product, :default_price, :primary_taxon, :supplier
searchable_attributes :sku, :display_as, :display_name, :primary_taxon_id
searchable_associations :product, :default_price, :primary_taxon
searchable_scopes :active, :deleted
NAME_FIELDS = ["display_name", "display_as", "weight", "unit_value", "unit_description"].freeze
@@ -23,15 +23,12 @@ module Spree
meta_keywords
variants_display_as
variants_display_name
variants_supplier_name).join('_or_')}_cont".freeze
supplier_name).join('_or_')}_cont".freeze
belongs_to :product, -> {
with_deleted
}, touch: true, class_name: 'Spree::Product', optional: false
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
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
@@ -61,7 +58,6 @@ 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
@@ -69,7 +65,7 @@ module Spree
validate :check_currency
validates :price, numericality: { greater_than_or_equal_to: 0 }, presence: true
validates :tax_category, presence: true,
if: proc { Spree::Config.products_require_tax_category }
if: proc { Spree::Config[:products_require_tax_category] }
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product&.variant_unit)
@@ -87,6 +83,7 @@ module Spree
before_validation :ensure_unit_value
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
before_validation :convert_variant_weight_to_decimal
before_validation :assign_related_taxon, if: ->(v) { v.primary_taxon.blank? }
before_save :assign_units, if: ->(variant) {
variant.new_record? || variant.changed_attributes.keys.intersection(NAME_FIELDS).any?
@@ -97,7 +94,7 @@ module Spree
after_save :save_default_price
# default variant scope only lists non-deleted variants
scope :deleted, -> { where.not(deleted_at: nil) }
scope :deleted, lambda { where.not(deleted_at: nil) }
scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }
@@ -142,9 +139,11 @@ module Spree
.where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
}
scope :with_properties, lambda { |property_ids|
left_outer_joins(:supplier_properties).
where(producer_properties: { property_id: property_ids })
scope :stockable_by, lambda { |enterprise|
return where("1=0") if enterprise.blank?
joins(:product).
where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
}
# Define sope as class method to allow chaining with other scopes filtering id.
@@ -216,6 +215,10 @@ module Spree
private
def assign_related_taxon
self.primary_taxon ||= product.variants.last&.primary_taxon
end
def check_currency
return unless currency.nil?
@@ -235,7 +238,7 @@ module Spree
end
def create_stock_items
StockLocation.find_each do |stock_location|
StockLocation.all.find_each do |stock_location|
stock_location.propagate_variant(self)
end
end
@@ -247,16 +250,8 @@ module Spree
end
def destruction
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
exchange_variants.reload.destroy_all
yield
end
def ensure_unit_value
@@ -273,9 +268,5 @@ 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

@@ -52,14 +52,11 @@ class VariantOverride < ApplicationRecord
return
end
# rubocop:disable Rails/SkipsModelValidations
# Cf. conversation https://github.com/openfoodfoundation/openfoodnetwork/pull/12647
if quantity > 0
increment! :count_on_hand, quantity
elsif quantity < 0
decrement! :count_on_hand, -quantity
end
# rubocop:enable Rails/SkipsModelValidations
end
def default_stock?

View File

@@ -31,18 +31,16 @@ class ProductScopeQuery
product_scope.find(@params[:product_id])
end
def products_for_producers
def paged_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, :supplier]).
where(variants: { supplier_id: producer_ids }).
order("enterprises.name, spree_products.name").
ransack(@params[:q]).result(distinct: true)
includes(variants: [:product, :default_price, :stock_items]).
where(supplier_id: producer_ids).
by_producer.by_name.
ransack(@params[:q]).result
end
def product_scope

View File

@@ -33,16 +33,23 @@ module Admin
end
def bulk_invoice(params)
visible_orders = bulk_load_orders(params)
visible_orders = editable_orders.invoiceable.where(id: params[:bulk_ids])
return if notify_if_abn_related_issue(visible_orders)
if Spree::Config.enterprise_number_required_on_invoices?
distributors_without_abn = Enterprise.where(
id: visible_orders.select(:distributor_id),
abn: nil,
)
file_id = "#{Time.zone.now.to_i}-#{SecureRandom.hex(2)}"
if distributors_without_abn.exists?
render_business_number_required_error(distributors_without_abn)
return
end
end
cable_ready.append(
selector: "#orders-index",
html: render(partial: "spree/admin/orders/bulk/invoice_modal",
locals: { invoice_url: "/admin/orders/invoices/#{file_id}" })
html: render(partial: "spree/admin/orders/bulk/invoice_modal")
).broadcast
# Preserve order of bulk_ids.
@@ -52,7 +59,7 @@ module Admin
BulkInvoiceJob.perform_later(
visible_order_ids,
"tmp/invoices/#{file_id}.pdf",
"tmp/invoices/#{Time.zone.now.to_i}-#{SecureRandom.hex(2)}.pdf",
channel: SessionChannel.for_request(request),
current_user_id: current_user.id
)
@@ -127,35 +134,5 @@ module Admin
enterprise_name: distributor_names.join(", "))
morph_admin_flashes
end
def bulk_load_orders(params)
editable_orders.invoiceable.where(id: params[:bulk_ids])
end
def notify_if_abn_related_issue(orders)
return false unless abn_required?
distributors = distributors_without_abn(orders)
return false if distributors.empty?
render_business_number_required_error(distributors)
true
end
def abn_required?
Spree::Config.enterprise_number_required_on_invoices?
end
def distributors_without_abn(orders)
abn = if OpenFoodNetwork::FeatureToggle.enabled?(:invoices)
[nil, ""]
else
[nil]
end
Enterprise.where(
id: orders.select(:distributor_id),
abn:,
)
end
end
end

View File

@@ -0,0 +1,177 @@
# frozen_string_literal: true
class ProductsReflex < ApplicationReflex
include Pagy::Backend
before_reflex :init_filters_params, :init_pagination_params
def change_per_page
@per_page = element.value.to_i
@page = 1
fetch_and_render_products_with_flash
end
def clear_search
@search_term = nil
@producer_id = nil
@category_id = nil
@page = 1
fetch_and_render_products_with_flash
end
private
def init_filters_params
# params comes from the form
# _params comes from the url
# priority is given to params from the form (if present) over url params
@search_term = params[:search_term] || params[:_search_term]
@producer_id = params[:producer_id] || params[:_producer_id]
@category_id = params[:category_id] || params[:_category_id]
end
def init_pagination_params
# prority is given to element dataset (if present) over url params
@page = element.dataset.page || params[:_page] || 1
@per_page = element.dataset.perpage || params[:_per_page] || 15
end
def fetch_and_render_products_with_flash
fetch_products
render_products
end
def render_products
cable_ready.replace(
selector: "#products-content",
html: render(partial: "admin/products_v3/content",
locals: { products: @products, pagy: @pagy, search_term: @search_term,
producer_options: producers, producer_id: @producer_id,
category_options: categories, tax_category_options:,
category_id: @category_id, flashes: flash })
)
cable_ready.replace_state(
url: current_url,
)
morph :nothing
end
def render_products_form_with_flash
locals = { products: @products }
locals[:error_counts] = @error_counts if @error_counts.present?
locals[:flashes] = flash if flash.any?
cable_ready.replace(
selector: "#products-form",
html: render(partial: "admin/products_v3/table", locals:)
)
morph :nothing
# dunno why this doesn't work. The HTML stops after the first `<col>` element, wtf?!
# morph "#products-form", render(partial: "admin/products_v3/table", locals:)
end
def producers
producers = OpenFoodNetwork::Permissions.new(current_user)
.managed_product_enterprises.is_primary_producer.by_name
producers.map { |p| [p.name, p.id] }
end
def categories
Spree::Taxon.order(:name).map { |c| [c.name, c.id] }
end
def tax_category_options
Spree::TaxCategory.order(:name).pluck(:name, :id)
end
def fetch_products
product_query = OpenFoodNetwork::Permissions.new(current_user)
.editable_products.merge(product_scope).ransack(ransack_query).result(distinct: true)
@pagy, @products = pagy(product_query.order(:name), items: @per_page, page: @page,
size: [1, 2, 2, 1])
end
def product_scope
scope = if current_user.has_spree_role?("admin") || current_user.enterprises.present?
Spree::Product
else
Spree::Product.active
end
scope.includes(product_query_includes)
end
def ransack_query
query = {}
query.merge!(supplier_id_in: @producer_id) if @producer_id.present?
if @search_term.present?
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
query.merge!(variants_primary_taxon_id_in: @category_id) if @category_id.present?
query
end
# Optimise by pre-loading required columns
def product_query_includes
# TODO: add other fields used in columns? (eg supplier: [:name])
[
# variants: [
# :default_price,
# :stock_locations,
# :stock_items,
# :variant_overrides
# ]
]
end
def current_url
url = URI(request.original_url)
url.query = url.query.present? ? "#{url.query}&" : ""
# add params with _ to avoid conflicts with params from the form
url.query += "_page=#{@page}"
url.query += "&_per_page=#{@per_page}"
url.query += "&_search_term=#{@search_term}" if @search_term.present?
url.query += "&_producer_id=#{@producer_id}" if @producer_id.present?
url.query += "&_category_id=#{@category_id}" if @category_id.present?
url.to_s
end
# Similar to spree/admin/products_controller
def product_set_from_params
# Form field names:
# '[products][0][id]' (hidden field)
# '[products][0][name]'
# '[products][0][variants_attributes][0][id]' (hidden field)
# '[products][0][variants_attributes][0][display_name]'
#
# Resulting in params:
# "products" => {
# "0" => {
# "id" => "123"
# "name" => "Pommes",
# "variants_attributes" => {
# "0" => {
# "id" => "1234",
# "display_name" => "Large box",
# }
# }
# }
collection_hash = products_bulk_params[:products]
.transform_values { |product|
# Convert variants_attributes form hash to an array if present
product[:variants_attributes] &&= product[:variants_attributes].values
product
}.with_indifferent_access
Sets::ProductSet.new(collection_attributes: collection_hash)
end
def products_bulk_params
params.permit(products: ::PermittedAttributes::Product.attributes)
.to_h.with_indifferent_access
end
end

View File

@@ -7,7 +7,7 @@ module Api
attributes :name, :supplier_name, :image_url, :variants
def supplier_name
object.variants.first.supplier&.name
object.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.supplier.id }
{ id: object.product.supplier_id }
end
def units_product

View File

@@ -7,6 +7,8 @@ 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,9 +3,13 @@
module Api
module Admin
class ProductSimpleSerializer < ActiveModel::Serializer
attributes :id, :name
attributes :id, :name, :producer_id
has_many :variants, key: :variants, serializer: Api::Admin::VariantSimpleSerializer
def producer_id
object.supplier_id
end
end
end
end

View File

@@ -3,7 +3,7 @@
module Api
module Admin
class TaxonSerializer < ActiveModel::Serializer
attributes :id, :name
attributes :id, :name, :pretty_name
end
end
end

View File

@@ -9,7 +9,6 @@ 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?
@@ -32,7 +31,7 @@ module Api
end
def producer_name
object.supplier.name
object.product.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, :producer_id
:price, :on_demand, :on_hand
has_many :variant_overrides
@@ -27,10 +27,6 @@ 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,6 +10,7 @@ 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

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