mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-12 18:36:49 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80554a14a | ||
|
|
a5fe5fb448 | ||
|
|
4c51d60bfd | ||
|
|
a0475ee8a4 | ||
|
|
4cdc604f45 | ||
|
|
dbf44c41b2 | ||
|
|
947914724a | ||
|
|
b5004f1cbf | ||
|
|
4596399bc2 | ||
|
|
685abccb61 | ||
|
|
9254928656 | ||
|
|
228997c35b | ||
|
|
667f44336d | ||
|
|
0a136ff2fb | ||
|
|
3f3577e73c | ||
|
|
cf1664bed3 | ||
|
|
b8aee4e857 | ||
|
|
cfe3435851 | ||
|
|
53e342ba1a | ||
|
|
6bdb14248c | ||
|
|
8e27291b15 | ||
|
|
138248e1c9 |
1
Gemfile
1
Gemfile
@@ -94,7 +94,6 @@ gem 'wkhtmltopdf-binary'
|
||||
gem 'foreigner'
|
||||
gem 'immigrant'
|
||||
gem 'roo', '~> 2.8.2'
|
||||
gem 'roo-xls', '~> 1.1.0'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
|
||||
@@ -587,10 +587,6 @@ GEM
|
||||
roo (2.8.2)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (>= 1.2.1, < 2.0.0)
|
||||
roo-xls (1.1.0)
|
||||
nokogiri
|
||||
roo (>= 2.0.0beta1, < 3)
|
||||
spreadsheet (> 0.9.0)
|
||||
rspec (3.9.0)
|
||||
rspec-core (~> 3.9.0)
|
||||
rspec-expectations (~> 3.9.0)
|
||||
@@ -653,8 +649,6 @@ GEM
|
||||
tilt (>= 1.3, < 3)
|
||||
spinjs-rails (1.4)
|
||||
rails (>= 3.1)
|
||||
spreadsheet (1.1.7)
|
||||
ruby-ole (>= 1.0)
|
||||
spring (1.7.2)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
@@ -791,7 +785,6 @@ DEPENDENCIES
|
||||
redcarpet
|
||||
roadie-rails (~> 1.3.0)
|
||||
roo (~> 2.8.2)
|
||||
roo-xls (~> 1.1.0)
|
||||
rspec-rails (>= 3.5.2)
|
||||
rspec-retry
|
||||
rubocop
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
//= require ../shared/ng-infinite-scroll.min.js
|
||||
//= require ../shared/ng-tags-input.min.js
|
||||
//= require angular-rails-templates
|
||||
//= require lodash.underscore.js
|
||||
|
||||
// spree
|
||||
//= require admin/spree_backend
|
||||
@@ -27,6 +28,7 @@
|
||||
//= require css_browser_selector_dev
|
||||
//= require responsive-tables
|
||||
//= require admin/spree_paypal_express
|
||||
//= require admin/handlebar_extensions
|
||||
|
||||
// OFN specific
|
||||
//= require_tree ../templates/admin
|
||||
|
||||
@@ -8,7 +8,7 @@ angular.module('admin.orderCycles').factory('ExchangeProduct', ($resource) ->
|
||||
|
||||
index: (params={}, callback=null) ->
|
||||
ExchangeProductResource.index params, (data) =>
|
||||
(callback || angular.noop)(data.products, data.pagination.pages, data.pagination.results)
|
||||
(callback || angular.noop)(data.products, data.pagination?.pages, data.pagination?.results)
|
||||
|
||||
countVariants: (params={}, callback=null) ->
|
||||
ExchangeProductResource.variant_count params, (data) =>
|
||||
|
||||
27
app/assets/javascripts/admin/spree/orders/address_states.js
Normal file
27
app/assets/javascripts/admin/spree/orders/address_states.js
Normal file
@@ -0,0 +1,27 @@
|
||||
var update_state = function(region) {
|
||||
var country = $('span#' + region + 'country .select2').select2('val');
|
||||
|
||||
var state_select = $('span#' + region + 'state select.select2');
|
||||
var state_input = $('span#' + region + 'state input.state_name');
|
||||
|
||||
$.get(Spree.routes.states_search + "?country_id=" + country, function(data) {
|
||||
var states = data["states"]
|
||||
if (states.length > 0) {
|
||||
state_select.html('');
|
||||
var states_with_blank = [{name: '', id: ''}].concat(states);
|
||||
$.each(states_with_blank, function(pos,state) {
|
||||
var opt = $(document.createElement('option'))
|
||||
.attr('value', state.id)
|
||||
.html(state.name);
|
||||
state_select.append(opt);
|
||||
});
|
||||
state_select.prop("disabled", false).show();
|
||||
state_select.select2();
|
||||
state_input.hide().prop("disabled", true);
|
||||
|
||||
} else {
|
||||
state_input.prop("disabled", false).show();
|
||||
state_select.select2('destroy').hide();
|
||||
}
|
||||
})
|
||||
};
|
||||
64
app/assets/javascripts/admin/spree/orders/shipments.js.erb
Normal file
64
app/assets/javascripts/admin/spree/orders/shipments.js.erb
Normal file
@@ -0,0 +1,64 @@
|
||||
// Shipments AJAX API
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
handle_ship_click = function(){
|
||||
var link = $(this);
|
||||
var shipment_number = link.data('shipment-number');
|
||||
var url = Spree.url( Spree.routes.orders_api + "/" + order_number + "/shipments/" + shipment_number + "/ship.json");
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
}).error(function( msg ) {
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
// $('[data-hook=admin_order_edit_form] a.ship').click(handle_ship_click);
|
||||
|
||||
//handle shipping method edit click
|
||||
// $('a.edit-method').click(toggleMethodEdit);
|
||||
// $('a.cancel-method').click(toggleMethodEdit);
|
||||
|
||||
handle_shipping_method_save = function(){
|
||||
var link = $(this);
|
||||
var shipment_number = link.data('shipment-number');
|
||||
var selected_shipping_rate_id = link.parents('tbody').find("select#selected_shipping_rate_id[data-shipment-number='" + shipment_number + "']").val();
|
||||
var unlock = link.parents('tbody').find("input[name='open_adjustment'][data-shipment-number='" + shipment_number + "']:checked").val();
|
||||
var url = Spree.url( Spree.routes.orders_api + "/" + order_number + "/shipments/" + shipment_number + ".json");
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url,
|
||||
data: { shipment: { selected_shipping_rate_id: selected_shipping_rate_id, unlock: unlock } }
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
}).error(function( msg ) {
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
// $('[data-hook=admin_order_edit_form] a.save-method').click(handle_shipping_method_save);
|
||||
|
||||
//handle tracking edit click
|
||||
// $('a.edit-tracking').click(toggleTrackingEdit);
|
||||
// $('a.cancel-tracking').click(toggleTrackingEdit);
|
||||
|
||||
handle_tracking_save = function(){
|
||||
var link = $(this);
|
||||
var shipment_number = link.data('shipment-number');
|
||||
var tracking = link.parents('tbody').find('input#tracking').val();
|
||||
var url = Spree.url( Spree.routes.orders_api + "/" + order_number + "/shipments/" + shipment_number + ".json");
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: url,
|
||||
data: { shipment: { tracking: tracking } }
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
}).error(function( msg ) {
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
$('[data-hook=admin_order_edit_form] a.save-tracking').click(handle_tracking_save);
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
// variant autocompletion
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
if ($('#variant_autocomplete_template').length > 0) {
|
||||
window.variantTemplate = Handlebars.compile($('#variant_autocomplete_template').text());
|
||||
window.variantStockTemplate = Handlebars.compile($('#variant_autocomplete_stock_template').text());
|
||||
|
||||
// handle variant selection, show stock level.
|
||||
$('#add_variant_id').change(function(){
|
||||
var variant_id = $(this).val();
|
||||
|
||||
var variant = _.find(window.variants, function(variant){
|
||||
return variant.id == variant_id
|
||||
})
|
||||
$('#stock_details').html(variantStockTemplate({variant: variant}));
|
||||
$('#stock_details').show();
|
||||
|
||||
$('button.add_variant').click(addVariantFromStockLocation);
|
||||
|
||||
// Add some tips
|
||||
$('.with-tip').powerTip({
|
||||
smartPlacement: true,
|
||||
fadeInTime: 50,
|
||||
fadeOutTime: 50,
|
||||
intentPollInterval: 300
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
//handle edit click
|
||||
// $('a.edit-item').click(toggleItemEdit);
|
||||
|
||||
//handle cancel click
|
||||
// $('a.cancel-item').click(toggleItemEdit);
|
||||
|
||||
handle_save_click = function(){
|
||||
var save = $(this);
|
||||
var shipment_number = save.data('shipment-number');
|
||||
var variant_id = save.data('variant-id');
|
||||
|
||||
var quantity = parseInt(save.parents('tr').find('input.line_item_quantity').val());
|
||||
|
||||
toggleItemEdit();
|
||||
|
||||
adjustItems(shipment_number, variant_id, quantity);
|
||||
return false;
|
||||
}
|
||||
// $('a.save-item').click(handle_save_click);
|
||||
|
||||
handle_delete_click = function(){
|
||||
var del = $(this);
|
||||
var shipment_number = del.data('shipment-number');
|
||||
var variant_id = del.data('variant-id');
|
||||
|
||||
toggleItemEdit();
|
||||
|
||||
adjustItems(shipment_number, variant_id, 0);
|
||||
}
|
||||
// $('a.delete-item').click(handle_delete_click);
|
||||
}
|
||||
});
|
||||
|
||||
adjustItems = function(shipment_number, variant_id, quantity){
|
||||
var shipment = _.findWhere(shipments, {number: shipment_number + ''});
|
||||
var inventory_units = _.where(shipment.inventory_units, {variant_id: variant_id});
|
||||
|
||||
var url = Spree.routes.orders_api + "/" + order_number + "/shipments/" + shipment_number;
|
||||
|
||||
var new_quantity = 0;
|
||||
if(inventory_units.length<quantity){
|
||||
url += "/add"
|
||||
new_quantity = (quantity - inventory_units.length);
|
||||
}else if(inventory_units.length>quantity){
|
||||
url += "/remove"
|
||||
new_quantity = (inventory_units.length - quantity);
|
||||
}
|
||||
url += '.json';
|
||||
|
||||
if(new_quantity!=0){
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: Spree.url(url),
|
||||
data: { variant_id: variant_id, quantity: new_quantity }
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleTrackingEdit = function(){
|
||||
var link = $(this);
|
||||
link.parents('tbody').find('tr.edit-tracking').toggle();
|
||||
link.parents('tbody').find('tr.show-tracking').toggle();
|
||||
}
|
||||
|
||||
toggleMethodEdit = function(){
|
||||
var link = $(this);
|
||||
link.parents('tbody').find('tr.edit-method').toggle();
|
||||
link.parents('tbody').find('tr.show-method').toggle();
|
||||
}
|
||||
|
||||
toggleItemEdit = function(){
|
||||
var link = $(this);
|
||||
link.parent().find('a.edit-item').toggle();
|
||||
link.parent().find('a.cancel-item').toggle();
|
||||
link.parent().find('a.save-item').toggle();
|
||||
link.parent().find('a.delete-item').toggle();
|
||||
link.parents('tr').find('td.item-qty-show').toggle();
|
||||
link.parents('tr').find('td.item-qty-edit').toggle();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
addVariantFromStockLocation = function() {
|
||||
$('#stock_details').hide();
|
||||
|
||||
var variant_id = $('input.variant_autocomplete').val();
|
||||
var stock_location_id = $(this).data('stock-location-id');
|
||||
var quantity = $("input.quantity[data-stock-location-id='" + stock_location_id + "']").val();
|
||||
|
||||
var shipment = _.find(shipments, function(shipment){
|
||||
return shipment.stock_location_id == stock_location_id && (shipment.state == 'ready' || shipment.state == 'pending');
|
||||
});
|
||||
|
||||
if(shipment==undefined){
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: Spree.url(Spree.routes.orders_api + "/" + order_number + "/shipments.json"),
|
||||
data: { variant_id: variant_id, quantity: quantity, stock_location_id: stock_location_id }
|
||||
}).done(function( msg ) {
|
||||
window.location.reload();
|
||||
}).error(function( msg ) {
|
||||
console.log(msg);
|
||||
});
|
||||
}else{
|
||||
//add to existing shipment
|
||||
adjustItems(shipment.number, variant_id, quantity);
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
formatVariantResult = function(variant) {
|
||||
if (variant["images"][0] != undefined && variant["images"][0].urls != undefined) {
|
||||
variant.image = variant.images[0].urls.mini
|
||||
}
|
||||
return variantTemplate({ variant: variant })
|
||||
}
|
||||
|
||||
$.fn.variantAutocomplete = function() {
|
||||
this.parent().children(".options_placeholder").attr('id', this.parent().data('index'))
|
||||
this.select2({
|
||||
placeholder: Spree.translations.variant_placeholder,
|
||||
minimumInputLength: 3,
|
||||
ajax: {
|
||||
url: Spree.url(Spree.routes.variants_search),
|
||||
datatype: 'json',
|
||||
data: function(term, page) {
|
||||
return {
|
||||
q: {
|
||||
"product_name_or_sku_cont": term
|
||||
}
|
||||
}
|
||||
},
|
||||
results: function (data, page) {
|
||||
window.variants = data['variants'];
|
||||
|
||||
return { results: data['variants'] }
|
||||
}
|
||||
},
|
||||
formatResult: formatVariantResult,
|
||||
formatSelection: function (variant) {
|
||||
$(this.element).parent().children('.options_placeholder').html(variant.options_text)
|
||||
return variant.name;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.select2-result-label {
|
||||
.variant-autocomplete-item {
|
||||
.variant-details {
|
||||
padding: 0 10px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.variant-image {
|
||||
margin-top: 5px;
|
||||
background-color: white;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
ul.variant-details {
|
||||
li {
|
||||
display: inline-block;
|
||||
|
||||
&:after {
|
||||
content: ' / ';
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
require 'open_food_network/referer_parser'
|
||||
require 'open_food_network/permissions'
|
||||
require 'open_food_network/order_cycle_permissions'
|
||||
|
||||
module Admin
|
||||
class EnterprisesController < ResourceController
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This controller lists products that can be added to an exchange
|
||||
#
|
||||
# Pagination is optional and can be required by using param[:page]
|
||||
module Api
|
||||
class ExchangeProductsController < Api::BaseController
|
||||
DEFAULT_PAGE = 1
|
||||
DEFAULT_PER_PAGE = 100
|
||||
|
||||
skip_authorization_check only: [:index]
|
||||
@@ -29,22 +32,28 @@ module Api
|
||||
|
||||
def render_variant_count
|
||||
render text: {
|
||||
count: Spree::Variant.
|
||||
not_master.
|
||||
where(product_id: products).
|
||||
count
|
||||
count: variants.count
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def variants
|
||||
renderer.exchange_variants(@incoming, @enterprise)
|
||||
end
|
||||
|
||||
def products
|
||||
ExchangeProductsRenderer.
|
||||
new(@order_cycle, spree_current_user).
|
||||
exchange_products(@incoming, @enterprise)
|
||||
renderer.exchange_products(@incoming, @enterprise)
|
||||
end
|
||||
|
||||
def renderer
|
||||
@renderer ||= ExchangeProductsRenderer.
|
||||
new(@order_cycle, spree_current_user)
|
||||
end
|
||||
|
||||
def paginated_products
|
||||
return products unless pagination_required?
|
||||
|
||||
products.
|
||||
page(params[:page] || DEFAULT_PAGE).
|
||||
page(params[:page]).
|
||||
per(params[:per_page] || DEFAULT_PER_PAGE)
|
||||
end
|
||||
|
||||
@@ -74,19 +83,23 @@ module Api
|
||||
order_cycle: @order_cycle
|
||||
)
|
||||
|
||||
render text: {
|
||||
products: serializer,
|
||||
pagination: pagination_data(paginated_products)
|
||||
}.to_json
|
||||
result = { products: serializer }
|
||||
result = result.merge(pagination: pagination_data(paginated_products)) if pagination_required?
|
||||
|
||||
render text: result.to_json
|
||||
end
|
||||
|
||||
def pagination_data(paginated_products)
|
||||
{
|
||||
results: paginated_products.total_count,
|
||||
pages: paginated_products.num_pages,
|
||||
page: (params[:page] || DEFAULT_PAGE).to_i,
|
||||
page: params[:page].to_i,
|
||||
per_page: (params[:per_page] || DEFAULT_PER_PAGE).to_i
|
||||
}
|
||||
end
|
||||
|
||||
def pagination_required?
|
||||
params[:page].present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/order_cycle_permissions'
|
||||
|
||||
class ExchangeProductsRenderer
|
||||
def initialize(order_cycle, user)
|
||||
@order_cycle = order_cycle
|
||||
@@ -12,6 +16,14 @@ class ExchangeProductsRenderer
|
||||
end
|
||||
end
|
||||
|
||||
def exchange_variants(incoming, enterprise)
|
||||
variants_relation = Spree::Variant.
|
||||
not_master.
|
||||
where(product_id: exchange_products(incoming, enterprise).select(&:id))
|
||||
|
||||
filter_visible(variants_relation)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def products_for_incoming_exchange(enterprise)
|
||||
@@ -21,12 +33,16 @@ class ExchangeProductsRenderer
|
||||
def supplied_products(enterprises_query_matcher)
|
||||
products_relation = Spree::Product.where(supplier_id: enterprises_query_matcher)
|
||||
|
||||
filter_visible(products_relation)
|
||||
end
|
||||
|
||||
def filter_visible(relation)
|
||||
if @order_cycle.present? &&
|
||||
@order_cycle.prefers_product_selection_from_coordinator_inventory_only?
|
||||
products_relation = products_relation.visible_for(@order_cycle.coordinator)
|
||||
relation = relation.visible_for(@order_cycle.coordinator)
|
||||
end
|
||||
|
||||
products_relation
|
||||
relation
|
||||
end
|
||||
|
||||
def products_for_outgoing_exchange
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
= button Spree.t('actions.update'), 'icon-refresh'
|
||||
|
||||
- content_for :head do
|
||||
= javascript_include_tag 'admin/address_states.js'
|
||||
= javascript_include_tag 'admin/spree/orders/address_states.js'
|
||||
|
||||
@@ -51,12 +51,27 @@ module Api
|
||||
let(:exchange) { order_cycle.exchanges.outgoing.first }
|
||||
let(:products_relation) { Spree::Product.includes(:variants).where("spree_variants.id": exchange.variants.map(&:id)) }
|
||||
|
||||
it "paginates results" do
|
||||
spree_get :index, exchange_id: exchange.id, page: 1, per_page: 1
|
||||
before do
|
||||
stub_const("Api::ExchangeProductsController::DEFAULT_PER_PAGE", 1)
|
||||
end
|
||||
|
||||
expect(json_response["products"].size).to eq 1
|
||||
expect(json_response["pagination"]["results"]).to eq 2
|
||||
expect(json_response["pagination"]["pages"]).to eq 2
|
||||
describe "when a specific page is requested" do
|
||||
it "returns the requested page with paginated data" do
|
||||
spree_get :index, exchange_id: exchange.id, page: 1
|
||||
|
||||
expect(json_response["products"].size).to eq 1
|
||||
expect(json_response["pagination"]["results"]).to eq 2
|
||||
expect(json_response["pagination"]["pages"]).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "when no specific page is requested" do
|
||||
it "returns all results without paginating" do
|
||||
spree_get :index, exchange_id: exchange.id
|
||||
|
||||
expect(json_response["products"].size).to eq 2
|
||||
expect(json_response["pagination"]).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,4 +26,38 @@ describe ExchangeProductsRenderer do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#exchange_variants" do
|
||||
describe "for an incoming exchange" do
|
||||
it "loads variants" do
|
||||
exchange = order_cycle.exchanges.incoming.first
|
||||
variants = renderer.exchange_variants(true, exchange.sender)
|
||||
|
||||
expect(variants.first.product.supplier.name).to eq exchange.variants.first.product.supplier.name
|
||||
end
|
||||
|
||||
describe "when OC is showing only the coordinators inventory" do
|
||||
let(:exchange_with_visible_variant) { order_cycle.exchanges.incoming.second }
|
||||
let(:exchange_with_hidden_variant) { order_cycle.exchanges.incoming.first }
|
||||
let!(:visible_inventory_item) { create(:inventory_item, enterprise: order_cycle.coordinator, variant: exchange_with_visible_variant.variants.first, visible: true) }
|
||||
let!(:hidden_inventory_item) { create(:inventory_item, enterprise: order_cycle.coordinator, variant: exchange_with_hidden_variant.variants.first, visible: false) }
|
||||
|
||||
before do
|
||||
order_cycle.prefers_product_selection_from_coordinator_inventory_only = true
|
||||
end
|
||||
|
||||
it "renders visible inventory variants" do
|
||||
variants = renderer.exchange_variants(true, exchange_with_visible_variant.sender)
|
||||
|
||||
expect(variants.size).to eq 1
|
||||
end
|
||||
|
||||
it "does not render hidden inventory variants" do
|
||||
variants = renderer.exchange_variants(true, exchange_with_hidden_variant.sender)
|
||||
|
||||
expect(variants.size).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
1920
vendor/assets/javascripts/handlebars.js
vendored
Normal file
1920
vendor/assets/javascripts/handlebars.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user