diff --git a/app/models/adjustment.rb b/app/models/adjustment.rb index 0cafcbb393..931b8da15a 100644 --- a/app/models/adjustment.rb +++ b/app/models/adjustment.rb @@ -34,18 +34,11 @@ def split_difference [increasing_adjustment, decreasing_adjustment] end - def self.csv_export_headers - ["Created", "Organization", "Storage Location", "Comment", "Changes"] - end + def self.generate_csv(adjustments) + return nil if adjustments.empty? - def csv_export_attributes - [ - created_at.strftime("%F"), - organization.name, - storage_location.name, - comment, - line_items.count - ] + Exports::ExportAdjustmentsCSVService + .generate_csv(adjustments, adjustments.first.organization) end private diff --git a/app/services/exports/export_adjustments_csv_service.rb b/app/services/exports/export_adjustments_csv_service.rb new file mode 100644 index 0000000000..8a4224cbf1 --- /dev/null +++ b/app/services/exports/export_adjustments_csv_service.rb @@ -0,0 +1,48 @@ +module Exports + module ExportAdjustmentsCSVService + class << self + def generate_csv(adjustments, organization) + CSV.generate(headers: true) do |csv| + generate_csv_data(adjustments, organization).each { |row| csv << row } + end + end + + def generate_csv_data(adjustments, organization) + item_names = get_item_names(organization) + headers = [ + "Created date", "Storage Area", + "Comment", "# of changes" + ] + item_names + + [headers] + adjustments.map { |adjustment| build_row(adjustment, item_names) } + end + + private + + def get_item_names(organization) + organization.items.order(:name).pluck(:name).uniq + end + + def build_row(adjustment, item_names) + row = [ + adjustment.created_at.strftime("%F"), + adjustment.storage_location.name, + adjustment.comment, + adjustment.line_items.count { |item| !item.quantity.eql?(0) } + ] + + item_quantities = Hash.new(0) + + adjustment.line_items.each do |line_item| + item_quantities[line_item.item.name] += line_item.quantity + end + + item_names.each do |item_name| + row << item_quantities[item_name] + end + + row + end + end + end +end diff --git a/docs/user_guide/bank/exports.md b/docs/user_guide/bank/exports.md index 3a53c94c35..571a033f86 100644 --- a/docs/user_guide/bank/exports.md +++ b/docs/user_guide/bank/exports.md @@ -27,9 +27,10 @@ The exports available include (in alphabetical order): Click "Inventory", then "Inventory Adjustments" in the left-hand menu. Then click "Export Adjustments", ### Contents of adjustment export -Creation date, Organization, Storage Area, Comment, # of changes. - -[! NOTE] We have improving the adjustments export to include the changes made in each adjustment on our todo list. We'll also remove the organization as redundant information. Please reach out if this is a priority for you. +- Creation date +- Storage Area, Comment +- # of changes +- the quantity of each of your organization's Items in the adjustments ## Annual Survey ### Navigating to export annual survey diff --git a/spec/requests/adjustments_requests_spec.rb b/spec/requests/adjustments_requests_spec.rb index a3dd85e91b..83d4ed65ff 100644 --- a/spec/requests/adjustments_requests_spec.rb +++ b/spec/requests/adjustments_requests_spec.rb @@ -72,10 +72,63 @@ context "csv" do let(:response_format) { 'csv' } + let(:storage_location) { create(:storage_location, organization: organization) } + let(:item1) { create(:item, name: "Item One", organization: organization) } + let(:item2) { create(:item, name: "Item Two", organization: organization) } + + let!(:adjustment1) do + adj = create(:adjustment, + organization: organization, + storage_location: storage_location, + comment: "First adjustment", + created_at: 1.day.ago) + adj.line_items << build(:line_item, quantity: 10, item: item1, itemizable: adj) + adj.line_items << build(:line_item, quantity: 5, item: item2, itemizable: adj) + adj + end + + let!(:adjustment2) do + adj = create(:adjustment, + organization: organization, + storage_location: storage_location, + comment: "Second adjustment", + created_at: 5.days.ago) + adj.line_items << build(:line_item, quantity: -5, item: item1, itemizable: adj) + adj + end - before { adjustment } + before { get adjustments_path(format: 'csv') } - it { is_expected.to be_successful } + it "returns a CSV file" do + expect(response).to be_successful + expect(response.header['Content-Type']).to include 'text/csv' + end + + it "includes appropriate headers and data" do + csv = <<~CSV + Created date,Storage Area,Comment,# of changes,#{item1.name},#{item2.name} + 2019-06-30,Smithsonian Conservation Center,First adjustment,2,10,5 + 2019-06-26,Smithsonian Conservation Center,Second adjustment,1,-5,0 + CSV + + expect(response.body).to eq(csv) + end + + context "when filtering by date" do + it "returns adjustments filtered by date range" do + start_date = 3.days.ago.to_fs(:date_picker) + end_date = Time.zone.today.to_fs(:date_picker) + + get adjustments_path, params: { filters: { date_range: "#{start_date} - #{end_date}" }, format: 'csv' } + + csv = <<~CSV + Created date,Storage Area,Comment,# of changes,Item One,Item Two + 2019-06-30,Smithsonian Conservation Center,First adjustment,2,10,5 + CSV + + expect(response.body).to eq(csv) + end + end end end diff --git a/spec/services/exports/export_adjustments_csv_service_spec.rb b/spec/services/exports/export_adjustments_csv_service_spec.rb new file mode 100644 index 0000000000..acf5473c06 --- /dev/null +++ b/spec/services/exports/export_adjustments_csv_service_spec.rb @@ -0,0 +1,93 @@ +RSpec.describe Exports::ExportAdjustmentsCSVService do + # Create organization after items to ensure proper associations + let!(:item1) { create(:item, name: "item1") } + let!(:item2) { create(:item, name: "item2") } + let!(:item3) { create(:item, name: "item3") } + let!(:item4) { create(:item, name: "item4") } + let!(:item5) { create(:item, :inactive, name: "item5") } + + # Now create organization and associate items with it + let(:organization) do + org = create(:organization) + [item1, item2, item3, item4, item5].each do |item| + item.update!(organization_id: org.id) + end + org + end + + let(:sorted_item_names) do + [item1, item2, item3, item4, item5].map(&:name).sort + end + + let(:storage_location) { create(:storage_location, organization: organization) } + let(:user) { create(:user, organization: organization) } + + around do |example| + travel_to Time.zone.local(2024, 12, 25) + example.run + travel_back + end + + describe "#generate_csv_data" do + subject { described_class.generate_csv(adjustments, organization) } + + context "with multiple adjustments and items" do + let(:adjustments) do + [ + # 1st adjustment with 2 items + create(:adjustment, + user_id: user.id, + storage_location: storage_location, + organization: organization, + comment: "adjustment 1", + line_items_attributes: [ + {item_id: item1.id, quantity: 10}, + {item_id: item2.id, quantity: -5} + ]), + + # 2nd adjustment with 1 item + create(:adjustment, + user_id: user.id, + storage_location: storage_location, + organization: organization, + comment: "adjustment 2", + line_items_attributes: [ + {item_id: item3.id, quantity: 3} + ]), + + # 3rd adjustment with the :with_items trait + create(:adjustment, :with_items, + user_id: user.id, + storage_location: storage_location, + organization: organization, + comment: "adjustment 3", + item: item1, + item_quantity: 7) + ] + end + + it "should include the correct adjustment data" do + csv = <<~CSV + Created date,Storage Area,Comment,# of changes,item1,item2,item3,item4,item5 + 2024-12-25,Smithsonian Conservation Center,adjustment 1,2,10,-5,0,0,0 + 2024-12-25,Smithsonian Conservation Center,adjustment 2,1,0,0,3,0,0 + 2024-12-25,Smithsonian Conservation Center,adjustment 3,1,7,0,0,0,0 + CSV + + expect(subject).to eq(csv) + end + end + + context "when there are no adjustments" do + let(:adjustments) { [] } + + it "returns only headers row" do + csv = <<~CSV + Created date,Storage Area,Comment,# of changes,item1,item2,item3,item4,item5 + CSV + + expect(subject).to eq(csv) + end + end + end +end