diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d2ee0e734a..74c027ce0e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -192,3 +192,30 @@ div.low_priority_warning { margin: 5px; text-align: center; } + +.distribution-title { + display: flex; +} + +legend.with-request { + float: none; + width: 80%; + display: inline-block; +} + +div.distribution-request-unit { + display: inline-block; + flex: 1; + text-align: right; + margin-right: 20px; + font-size: 1.5em; +} + +.li-requested { + font-size: 1.5em; + width: 100px; + min-width: 100px; + text-align: right; + margin-right: 20px; + white-space: nowrap; +} diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index e2a10393a9..f7f0e0d62a 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -109,7 +109,11 @@ def create # does not match any known Request @distribution.request = Request.find(request_id) end - @distribution.line_items.build if @distribution.line_items.size.zero? + if @distribution.line_items.size.zero? + @distribution.line_items.build + elsif request_id + @distribution.initialize_request_items + end @items = current_organization.items.alphabetized if Event.read_events?(current_organization) inventory = View::Inventory.new(@distribution.organization_id) @@ -166,6 +170,7 @@ def show def edit @distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id]) + @distribution.initialize_request_items if (!@distribution.complete? && @distribution.future?) || current_user.has_role?(Role::ORG_ADMIN, current_organization) @distribution.line_items.build if @distribution.line_items.size.zero? @@ -202,6 +207,7 @@ def update else flash[:error] = insufficient_error_message(result.error.message) @distribution.line_items.build if @distribution.line_items.size.zero? + @distribution.initialize_request_items @items = current_organization.items.alphabetized @storage_locations = current_organization.storage_locations.active_locations.alphabetized render :edit diff --git a/app/models/distribution.rb b/app/models/distribution.rb index f8d591e25b..bc5a652e9f 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -102,6 +102,31 @@ def copy_from_donation(donation_id, storage_location_id) self.storage_location = StorageLocation.find(storage_location_id) if storage_location_id end + # This is meant for the Edit page - we will be adding any request items that aren't in the + # distribution for whatever reason, with zero quantity. + def initialize_request_items + return if request.nil? + + item_ids = Set.new + line_items.each do |line_item| + item_request = request.item_requests.find { |r| r.item_id == line_item.item_id } + if item_request + item_ids.add(item_request) + line_item.requested_item = item_request + end + end + + request.item_requests.each do |item_request| + next if item_ids.include?(item_request) + + line_items.new( + requested_item: item_request, + quantity: 0, + item_id: item_request.item_id + ) + end + end + def copy_from_request(request_id) request = Request.find(request_id) self.request = request @@ -110,12 +135,12 @@ def copy_from_request(request_id) self.agency_rep = request.partner_user&.formatted_email self.comment = request.comments self.issued_at = Time.zone.today + 1.day - request.request_items.each do |item| + request.item_requests.each do |item_request| line_items.new( - quantity: item["quantity"], - item: Item.eager_load(:base_item).find_by(organization: request.organization, id: item["item_id"]), - itemizable_id: request.id, - itemizable_type: "Distribution" + requested_item: item_request, + # if there is a custom unit, don't prefill with the quantity - they have to enter it + quantity: item_request.request_unit.present? ? nil : item_request.quantity, + item_id: item_request.item_id ) end end diff --git a/app/models/line_item.rb b/app/models/line_item.rb index e93a52e047..8cdcb79b7c 100644 --- a/app/models/line_item.rb +++ b/app/models/line_item.rb @@ -28,6 +28,10 @@ class LineItem < ApplicationRecord delegate :name, to: :item + # Used in a distribution that was initialized from a request. The `item_request` will be + # populated here. + attr_accessor :requested_item + def quantity_must_be_a_number_within_range if quantity && quantity > MAX_INT errors.add(:quantity, "must be less than #{MAX_INT}") diff --git a/app/services/distribution_update_service.rb b/app/services/distribution_update_service.rb index 5889d2a622..da612d0f95 100644 --- a/app/services/distribution_update_service.rb +++ b/app/services/distribution_update_service.rb @@ -9,6 +9,9 @@ def call perform_distribution_service do @old_issued_at = distribution.issued_at @old_delivery_method = distribution.delivery_method + @params[:line_items_attributes]&.delete_if { |_, a| a[:quantity].to_i.zero? } + + # remove line_items with zero quantity ItemizableUpdateService.call( itemizable: distribution, diff --git a/app/views/distributions/_form.html.erb b/app/views/distributions/_form.html.erb index 2948b48d6c..b72329f950 100644 --- a/app/views/distributions/_form.html.erb +++ b/app/views/distributions/_form.html.erb @@ -33,13 +33,18 @@ <%= f.input :comment, label: "Comment" %>
- Items in this distribution +
+ Items in this distribution + <% if distribution.request %> +
Requested
+ <% end %> +
- <%= render 'line_items/line_item_fields', form: f %> + <%= render 'line_items/line_item_fields', form: f, locals: { show_request_items: true } %>
diff --git a/app/views/line_items/_line_item_fields.html.erb b/app/views/line_items/_line_item_fields.html.erb index 50721acfe8..437f278b12 100644 --- a/app/views/line_items/_line_item_fields.html.erb +++ b/app/views/line_items/_line_item_fields.html.erb @@ -1,24 +1,46 @@ <%= form.simple_fields_for :line_items, defined?(object) ? object : nil do |field| %> + <% requested = field.object.requested_item %>
-
- <%= render partial: "barcode_items/barcode_item_lookup", - locals: { index: field&.options[:child_index] || "new_item" } %> -
-
- + <% if requested.blank? %> +
+ <%= render partial: "barcode_items/barcode_item_lookup", + locals: { index: field&.options[:child_index] || "new_item" } %> +
+
+ + <% end %>
- <%= field.input :item_id, collection: @items, prompt: "Choose an item", include_blank: "", label: false, input_html: { class: "my-0 line_item_name", "data-controller": "select2" } %> + <%= field.input :item_id, + disabled: requested.present?, + collection: @items, prompt: "Choose an item", + include_blank: "", + label: false, + input_html: { class: "my-0 line_item_name", "data-controller": "select2" } %> + <% if requested.present? %> + <%= field.input :item_id, as: :hidden %> + <% end %>
<%= field.input :quantity, - as: :string, - placeholder: "Quantity", - label: false, - input_html: { class: "quantity my-0", data: { quantity: "" } } %> + as: :string, + placeholder: "Quantity", + label: false, + input_html: { class: "quantity my-0", data: { quantity: "" } } %>
+ <% if form.object.respond_to?(:request) && form.object.request %> +
+ <% if requested&.request_unit.present? %> + <%= pluralize(requested.quantity, requested.request_unit) %> + <% elsif requested %> + <%= requested.quantity %> + <% else %> + N/A + <% end %> +
+ <% end %>
diff --git a/spec/factories/items.rb b/spec/factories/items.rb index 4104d0872f..919c5e2a27 100644 --- a/spec/factories/items.rb +++ b/spec/factories/items.rb @@ -35,5 +35,14 @@ trait :inactive do active { false } end + + trait :with_unit do + transient do + unit { "pack" } + end + after(:create) do |item, evaluator| + create(:item_unit, name: evaluator.unit, item: item) + end + end end end diff --git a/spec/factories/requests.rb b/spec/factories/requests.rb index b0ddad8e1d..d8cdc20594 100644 --- a/spec/factories/requests.rb +++ b/spec/factories/requests.rb @@ -28,6 +28,7 @@ def random_request_items request_items { random_request_items } comments { "Urgent" } partner_user { ::User.partner_users.first || create(:partner_user) } + item_requests { [] } # For compatibility we can take in a list of request_items and turn it into a # list of item_requests diff --git a/spec/models/distribution_spec.rb b/spec/models/distribution_spec.rb index bd47938136..5cdab500de 100644 --- a/spec/models/distribution_spec.rb +++ b/spec/models/distribution_spec.rb @@ -250,11 +250,9 @@ item2 = create(:item, name: "Item2", organization: organization) request = create(:request, organization: organization, - partner_user: create(:partner_user), - request_items: [ - { item_id: item1.id, quantity: 15 }, - { item_id: item2.id, quantity: 18 } - ]) + partner_user: create(:partner_user)) + create(:item_request, request: request, item_id: item1.id, quantity: 15) + create(:item_request, request: request, item_id: item2.id, quantity: 18) distribution = Distribution.new distribution.copy_from_request(request.id) expect(distribution.line_items.size).to eq 2 diff --git a/spec/requests/distributions_requests_spec.rb b/spec/requests/distributions_requests_spec.rb index b4adc8fb97..a5b6243cfc 100644 --- a/spec/requests/distributions_requests_spec.rb +++ b/spec/requests/distributions_requests_spec.rb @@ -133,7 +133,19 @@ describe "GET #new" do let!(:partner) { create(:partner, organization: organization) } - let(:request) { create(:request, partner: partner, organization: organization) } + let(:request) { create(:request, partner: partner, organization: organization, item_requests: item_requests) } + let(:items) { + [ + create(:item, :with_unit, organization: organization, name: 'Item 1', unit: 'pack'), + create(:item, organization: organization, name: 'Item 2') + ] + } + let(:item_requests) { + [ + create(:item_request, item: items[0], quantity: 50, request_unit: 'pack'), + create(:item_request, item: items[1], quantity: 25) + ] + } let(:storage_location) { create(:storage_location, :with_items, organization: organization) } let(:default_params) { { request_id: request.id } } @@ -166,6 +178,44 @@ expect(page.css(%(#distribution_storage_location_id option[selected][value="#{storage_location.id}"]))).not_to be_empty end end + + context 'with units' do + before(:each) do + Flipper.enable(:enable_packs) + end + + it 'should behave correctly' do + get new_distribution_path(default_params) + expect(response).to be_successful + page = Nokogiri::HTML(response.body) + + # should have a disabled select and a hidden input + expect(page.css('select[disabled][name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + expect(page.css('input[name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + expect(page.css('select[disabled][name="distribution[line_items_attributes][1][item_id]"]')).not_to be_empty + expect(page.css('input[name="distribution[line_items_attributes][1][item_id]"]')).not_to be_empty + + # input with packs should be blank + expect(page.css('#distribution_line_items_attributes_0_quantity').attr('value')).to eq(nil) + + # input with no packs should show quantity + expect(page.css('#distribution_line_items_attributes_1_quantity').attr('value').value).to eq('25') + end + + context 'with no request' do + it 'should have no inputs' do + get new_distribution_path({}) + expect(response).to be_successful + page = Nokogiri::HTML(response.body) + + # blank input shown + expect(page.css('select[name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + expect(page.css('#distribution_line_items_attributes_0_quantity').attr('value')).to eq(nil) + # in the template + expect(page.css('select[name="distribution[line_items_attributes][1][item_id]"]')).not_to be_empty + end + end + end end describe "GET #show" do @@ -414,6 +464,76 @@ expect(response.body).not_to include("You’ve had an audit since this distribution was started.") end + context 'with units' do + let!(:request) { + create(:request, + partner: partner, + organization: organization, + distribution_id: distribution.id, + item_requests: item_requests) + } + let(:items) { + [ + create(:item, :with_unit, organization: organization, name: 'Item 1', unit: 'pack'), + create(:item, organization: organization, name: 'Item 2'), + create(:item, organization: organization, name: 'Item 3') + ] + } + let!(:item_requests) { + [ + create(:item_request, item: items[0], quantity: 50, request_unit: 'pack'), + create(:item_request, item: items[1], quantity: 25) + ] + } + before(:each) do + Flipper.enable(:enable_packs) + create(:line_item, itemizable: distribution, item_id: items[0].id, quantity: 25) + create(:line_item, itemizable: distribution, item_id: items[2].id, quantity: 10) + end + + it 'should behave correctly' do + get edit_distribution_path(id: distribution.id) + expect(response).to be_successful + page = Nokogiri::HTML(response.body) + + # should have a regular select and no hidden input + expect(page.css('select[disabled][name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + expect(page.css('input[name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + + # should have a regular select and no hidden input + expect(page.css('select[name="distribution[line_items_attributes][1][item_id]"]')).not_to be_empty + expect(page.css('select[disabled][name="distribution[line_items_attributes][1][item_id]"]')).to be_empty + expect(page.css('input[name="distribution[line_items_attributes][1][item_id]"]')).to be_empty + + # should have a disabled select and a hidden input + expect(page.css('select[disabled][name="distribution[line_items_attributes][2][item_id]"]')).not_to be_empty + expect(page.css('input[name="distribution[line_items_attributes][2][item_id]"]')).not_to be_empty + + # existing inputs should show numbers + expect(page.css('#distribution_line_items_attributes_0_quantity').attr('value').value).to eq('25') + expect(page.css('#distribution_line_items_attributes_1_quantity').attr('value').value).to eq('10') + + # input from request should show 0 + expect(page.css('#distribution_line_items_attributes_2_quantity').attr('value').value).to eq('0') + end + + context 'with no request' do + it 'should have everything enabled' do + request.destroy + get edit_distribution_path(id: distribution.id) + expect(response).to be_successful + page = Nokogiri::HTML(response.body) + + expect(page.css('select[name="distribution[line_items_attributes][0][item_id]"]')).not_to be_empty + expect(page.css('select[disabled][name="distribution[line_items_attributes][0][item_id]"]')).to be_empty + expect(page.css('input[name="distribution[line_items_attributes][0][item_id]"]')).to be_empty + expect(page.css('select[name="distribution[line_items_attributes][1][item_id]"]')).not_to be_empty + expect(page.css('select[disabled][name="distribution[line_items_attributes][1][item_id]"]')).to be_empty + expect(page.css('input[name="distribution[line_items_attributes][1][item_id]"]')).to be_empty + end + end + end + # Bug fix #4537 context "when distribution sets storage location total inventory to zero" do let(:item1) { create(:item, name: "Item 1", organization: organization) } diff --git a/spec/system/distribution_system_spec.rb b/spec/system/distribution_system_spec.rb index 956a2a3111..05ebb31492 100644 --- a/spec/system/distribution_system_spec.rb +++ b/spec/system/distribution_system_spec.rb @@ -596,6 +596,8 @@ items = storage_location.items.pluck(:id).sample(2) request_items = [{ "item_id" => items[0], "quantity" => 10 }, { "item_id" => items[1], "quantity" => 10 }] @request = create :request, organization: organization, request_items: request_items + create(:item_request, request: @request, item_id: items[0], quantity: 10) + create(:item_request, request: @request, item_id: items[1], quantity: 10) visit request_path(id: @request.id) click_on "Fulfill request" @@ -629,6 +631,8 @@ items = storage_location.items.pluck(:id).sample(2) request_items = [{ "item_id" => items[0], "quantity" => 1000000 }, { "item_id" => items[1], "quantity" => 10 }] @request = create :request, organization: organization, request_items: request_items + create(:item_request, request: @request, item_id: items[0], quantity: 1000000) + create(:item_request, request: @request, item_id: items[1], quantity: 10) visit request_path(id: @request.id) click_on "Fulfill request"