diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb index d44f022e3d..93247275a8 100644 --- a/app/controllers/requests_controller.rb +++ b/app/controllers/requests_controller.rb @@ -8,7 +8,7 @@ def index .undiscarded .during(helpers.selected_range) .class_filter(filter_params) - + @unfulfilled_requests_count = current_organization.requests.where(status: [:pending, :started]).count @paginated_requests = @requests.page(params[:page]) @calculate_product_totals = RequestsTotalItemsService.new(requests: @requests).calculate @items = current_organization.items.alphabetized @@ -40,6 +40,24 @@ def start redirect_to new_distribution_path(request_id: request.id) end + def print_unfulfilled + requests = current_organization + .requests + .includes(:item_requests, partner: [:profile]) + .where(status: [:pending, :started]) + .order(created_at: :desc) + + respond_to do |format| + format.any do + pdf = PicklistsPdf.new(current_organization, requests) + send_data pdf.compute_and_render, + filename: format("Picklists_%s.pdf", Time.current.to_fs(:long)), + type: "application/pdf", + disposition: "inline" + end + end + end + private def load_items diff --git a/app/pdfs/picklists_pdf.rb b/app/pdfs/picklists_pdf.rb new file mode 100644 index 0000000000..20022c2dc0 --- /dev/null +++ b/app/pdfs/picklists_pdf.rb @@ -0,0 +1,167 @@ +# Configures a Prawn PDF template for generating Distribution manifests +class PicklistsPdf + include Prawn::View + include ItemsHelper + + def initialize(organization, requests) + @requests = requests + @organization = organization + end + + def compute_and_render + font_families["OpenSans"] = PrawnRails.config["font_families"][:OpenSans] + font "OpenSans" + font_size 10 + footer_height = 35 + + @requests.each do |request| + logo_image = if @organization.logo.attached? + StringIO.open(@organization.logo.download) + else + Organization::DIAPER_APP_LOGO + end + + # Bounding box containing non-footer elements + bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do + image logo_image, fit: [250, 85] + + bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do + text @organization.name, align: :right + text @organization.address, align: :right + text @organization.email, align: :right + end + + text "Requested by:", style: :bold + font_size 12 + text request.partner.name + move_up 24 + + text "Partner Primary Contact:", style: :bold, align: :right + font_size 12 + text request.partner.profile.primary_contact_name, align: :right + font_size 10 + text request.partner.profile.primary_contact_email, align: :right + text request.partner.profile.primary_contact_phone, align: :right + move_down 10 + + if request.partner.profile.pick_up_name.present? + move_up 10 + text "Partner Pickup Person:", style: :bold + font_size 12 + text request.partner.profile.pick_up_name + font_size 10 + text request.partner.profile.pick_up_email + text request.partner.profile.pick_up_phone + move_up 24 + + text "Requested on:", style: :bold, align: :right + font_size 12 + text request.created_at.to_fs(:date_picker), align: :right + font_size 10 + move_down 30 + else + text "Requested on:", style: :bold + font_size 12 + text request.created_at.to_fs(:date_picker) + font_size 10 + end + + if @organization.ytd_on_distribution_printout + move_up 22 + text "Items Received Year-to-Date:", style: :bold, align: :right + font_size 12 + text request.partner.quantity_year_to_date.to_s, align: :right + font_size 10 + end + + move_down 10 + text "Comments:", style: :bold + font_size 12 + text request.comments + + move_down 20 + + line_items = request.item_requests + data = has_custom_units?(line_items) ? data_with_units(line_items) : data_no_units(line_items) + + font_size 11 + + # Line item table + table(data, width: bounds.width, column_widths: {1 => 65, -2 => 35}) do + self.header = true + self.cell_style = {padding: [5, 10, 5, 10]} + self.row_colors = %w[dddddd ffffff] + + cells.borders = [] + + # Header row + row(0).borders = [:bottom] + row(0).border_width = 2 + row(0).font_style = :bold + row(0).size = 10 + row(0).column(1..-1).borders = %i[bottom left] + end + end + + start_new_page unless request == @requests.last + end + + repeat :all do + # Page footer + bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do + stroke_bounds + font "OpenSans" + font_size 9 + stroke_horizontal_rule + move_down 5 + + logo_offset = (bounds.width - 190) / 2 + bounding_box([logo_offset, 0], width: 190, height: 33) do + text "Lovingly created with", valign: :center + image Organization::DIAPER_APP_LOGO, width: 75, vposition: :center, position: :right + end + end + end + + number_pages "Page of ", + start_count_at: 1, + at: [bounds.right - 130, 22], + align: :right + + render + end + + def has_custom_units?(line_items) + Flipper.enabled?(:enable_packs) && line_items.any? { |line_item| line_item.request_unit } + end + + def data_with_units(line_items) + data = [["Items Requested", + "Quantity", + "Unit (if applicable)", + "[X]", + "Differences / Comments"]] + + data + line_items.map do |line_item| + [line_item.name, + line_item.quantity, + line_item.request_unit&.capitalize&.pluralize(line_item.quantity), + "[ ]", + ""] + end + end + + def data_no_units(line_items) + data = [["Items Requested", + "Quantity", + "[X]", + "Differences / Comments"]] + + data + line_items.map do |line_item| + [line_item.name, + line_item.quantity, + "[ ]", + ""] + end + end +end diff --git a/app/views/requests/index.html.erb b/app/views/requests/index.html.erb index 5e02fc6b4e..ef44ea0d3f 100644 --- a/app/views/requests/index.html.erb +++ b/app/views/requests/index.html.erb @@ -61,6 +61,12 @@ <%= clear_filter_button %> <%= modal_button_to("#calculateTotals", {text: "Calculate Product Totals", icon: nil, size: "md", type: "success"}) %> + <% if @unfulfilled_requests_count > 0 %> + <%= print_button_to( + print_unfulfilled_requests_path(format: :pdf), + text: "Print Unfulfilled Picklists (#{@unfulfilled_requests_count})", + size: "md") %> + <% end %> <%= download_button_to(requests_path(format: :csv, filters: filter_params.merge(date_range: date_range_params)), {text: "Export Requests", size: "md"}) if @requests.any? %> diff --git a/config/routes.rb b/config/routes.rb index fc1ed88259..7a6c552856 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,6 +242,7 @@ def set_up_flipper member do post :start end + get :print_unfulfilled, on: :collection end resources :requests, except: %i(destroy) do resource :cancelation, only: [:new, :create], controller: 'requests/cancelation' diff --git a/spec/factories/partners.rb b/spec/factories/partners.rb index ba34f27bbe..d73d02be5a 100644 --- a/spec/factories/partners.rb +++ b/spec/factories/partners.rb @@ -40,11 +40,20 @@ status { :awaiting_review } end + transient do + pick_up_person { false } + end + after(:create) do |partner, evaluator| next if evaluator.try(:without_profile) # Create associated records - create(:partner_profile, partner_id: partner.id) + if evaluator.pick_up_person + create(:partner_profile, :with_pickup_person, partner_id: partner.id) + else + create(:partner_profile, partner_id: partner.id) + end + create(:partner_user, email: partner.email, name: partner.name, partner: partner) end end diff --git a/spec/factories/partners/profiles.rb b/spec/factories/partners/profiles.rb index 7b2e3f3500..5d2486a123 100644 --- a/spec/factories/partners/profiles.rb +++ b/spec/factories/partners/profiles.rb @@ -86,5 +86,11 @@ website { "http://some-site.org" } primary_contact_email { Faker::Internet.email } primary_contact_name { Faker::Name.name } + + trait :with_pickup_person do + pick_up_name { "Paul Bunyan" } + pick_up_email { "paul@kenton.com" } + pick_up_phone { "503-123-4567" } + end end end diff --git a/spec/factories/requests.rb b/spec/factories/requests.rb index d8cdc20594..987527a7da 100644 --- a/spec/factories/requests.rb +++ b/spec/factories/requests.rb @@ -61,6 +61,10 @@ def random_request_items status { 'pending' } end + trait :discarded do + status { 'discarded' } + end + trait :with_varied_quantities do request_items { # get 10 unique item ids diff --git a/spec/pdfs/picklists_pdf_spec.rb b/spec/pdfs/picklists_pdf_spec.rb new file mode 100644 index 0000000000..a9f7481343 --- /dev/null +++ b/spec/pdfs/picklists_pdf_spec.rb @@ -0,0 +1,84 @@ +describe PicklistsPdf do + let(:organization) { create(:organization) } + let(:item1) { create(:item, name: "Item 1", organization: organization) } + let(:item2) { create(:item, name: "Item 2", organization: organization) } + + describe "#compute_and_render" do + it "renders multiple requests correctly" do + request1 = create(:request, :pending, organization: organization) + request2 = create(:request, :pending, organization: organization) + create(:item_request, request: request1, item: item1, name: "Item 1") + create(:item_request, request: request2, item: item2, name: "Item 2") + + pdf = described_class.new(organization, [request1, request2]) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + + expect(pdf_test.page(1).text).to include(request1.partner.name) + expect(pdf_test.page(1).text).to include(request1.partner.profile.primary_contact_name) + expect(pdf_test.page(1).text).to include(request1.partner.profile.primary_contact_email) + expect(pdf_test.page(1).text).to include("Requested on:") + expect(pdf_test.page(1).text).to include("Items Received Year-to-Date:") + expect(pdf_test.page(1).text).to include("Comments") + expect(pdf_test.page(1).text).to include("Items Requested") + expect(pdf_test.page(1).text).to include("Item 1") + + expect(pdf_test.page(2).text).to include(request2.partner.name) + expect(pdf_test.page(2).text).to include(request2.partner.profile.primary_contact_name) + expect(pdf_test.page(2).text).to include(request2.partner.profile.primary_contact_email) + expect(pdf_test.page(2).text).to include("Requested on:") + expect(pdf_test.page(2).text).to include("Items Received Year-to-Date:") + expect(pdf_test.page(2).text).to include("Comments") + expect(pdf_test.page(2).text).to include("Items Requested") + expect(pdf_test.page(2).text).to include("Item 2") + end + + context "When partner pickup person is set" do + it "renders pickup person details" do + partner = create(:partner, pick_up_person: true) + request = create(:request, :pending, organization: organization, partner: partner) + pdf = described_class.new(organization, [request]) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + + expect(pdf_test.page(1).text).to include(request.partner.profile.pick_up_name) + expect(pdf_test.page(1).text).to include(request.partner.profile.pick_up_email) + expect(pdf_test.page(1).text).to include(request.partner.profile.pick_up_phone) + end + end + end + + context "When packs are not enabled" do + specify "#data_no_units" do + request = create(:request, :pending, organization: organization) + create(:item_request, request: request, item: item1, name: "Item 1") + create(:item_request, request: request, item: item2, name: "Item 2") + pdf = described_class.new(organization, [request]) + data = pdf.data_no_units(request.item_requests) + + expect(data).to eq([ + ["Items Requested", "Quantity", "[X]", "Differences / Comments"], + ["Item 1", "5", "[ ]", ""], + ["Item 2", "5", "[ ]", ""] + ]) + end + end + + context "When packs are enabled" do + before { Flipper.enable(:enable_packs) } + + specify "#data_with_units" do + item_with_units = create(:item, name: "Item with units", organization: organization) + create(:item_unit, item: item_with_units, name: "Pack") + request = create(:request, :pending, organization: organization) + create(:item_request, request: request, item: item_with_units, name: "Item with units", request_unit: "Pack") + create(:item_request, request: request, item: item2, name: "Item 2") + pdf = described_class.new(organization, [request]) + data = pdf.data_with_units(request.item_requests) + + expect(data).to eq([ + ["Items Requested", "Quantity", "Unit (if applicable)", "[X]", "Differences / Comments"], + ["Item with units", "5", "Packs", "[ ]", ""], + ["Item 2", "5", nil, "[ ]", ""] + ]) + end + end +end diff --git a/spec/requests/requests_requests_spec.rb b/spec/requests/requests_requests_spec.rb index cc891607a6..6ff8095bfc 100644 --- a/spec/requests/requests_requests_spec.rb +++ b/spec/requests/requests_requests_spec.rb @@ -26,6 +26,21 @@ it { is_expected.to be_successful } end + + context "when there are pending or started requests" do + it "shows print unfulfilled picklists button with correct quantity" do + Request.delete_all + + create(:request, :pending) + create(:request, :started) + create(:request, :fulfilled) + create(:request, :discarded) + + get requests_path + + expect(response.body).to include('Print Unfulfilled Picklists (2)') + end + end end describe 'GET #show' do