Skip to content

Commit 98ab422

Browse files
authored
Merge pull request #5087 from jp524/4988-transfers-export-lists-each-item
Transfers exports include quantity moved for each item
2 parents 41a20ad + 05a8b33 commit 98ab422

File tree

4 files changed

+186
-15
lines changed

4 files changed

+186
-15
lines changed

app/controllers/transfers_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def index
1515
@to_storage_locations = StorageLocation.with_transfers_to(current_organization)
1616
respond_to do |format|
1717
format.html
18-
format.csv { send_data Transfer.generate_csv(@transfers), filename: "Transfers-#{Time.zone.today}.csv" }
18+
format.csv { send_data Exports::ExportTransfersCSVService.new(transfers: @transfers.includes(line_items: :item), organization: current_organization).generate_csv, filename: "Transfers-#{Time.zone.today}.csv" }
1919
end
2020
end
2121

app/models/transfer.rb

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class Transfer < ApplicationRecord
1919

2020
include Itemizable
2121
include Filterable
22-
include Exportable
2322
# to make it play nice with Itemizable - alias of `from`
2423
belongs_to :storage_location, class_name: "StorageLocation", inverse_of: :transfers_from, foreign_key: :from_id
2524
scope :from_location, ->(location_id) { where(from_id: location_id) }
@@ -31,19 +30,6 @@ class Transfer < ApplicationRecord
3130
validate :from_storage_quantities
3231
validate :line_items_quantity_is_positive
3332

34-
def self.csv_export_headers
35-
["From", "To", "Comment", "Total Moved"]
36-
end
37-
38-
def csv_export_attributes
39-
[
40-
from.name,
41-
to.name,
42-
comment || "none",
43-
line_items.total
44-
]
45-
end
46-
4733
private
4834

4935
def storage_locations_belong_to_organization
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
module Exports
2+
class ExportTransfersCSVService
3+
def initialize(transfers:, organization:)
4+
@transfers = transfers
5+
@organization = organization
6+
end
7+
8+
def generate_csv
9+
csv_data = generate_csv_data
10+
11+
CSV.generate(headers: true) do |csv|
12+
csv_data.each { |row| csv << row }
13+
end
14+
end
15+
16+
def generate_csv_data
17+
csv_data = []
18+
19+
csv_data << headers
20+
@transfers.each do |transfer|
21+
csv_data << build_row_data(transfer)
22+
end
23+
24+
csv_data
25+
end
26+
27+
private
28+
29+
def headers
30+
base_headers + item_headers
31+
end
32+
33+
def base_table
34+
{
35+
"From" => ->(transfer) {
36+
transfer.from.name
37+
},
38+
"To" => ->(transfer) {
39+
transfer.to.name
40+
},
41+
"Date" => ->(transfer) {
42+
transfer.created_at.strftime("%F")
43+
},
44+
"Comment" => ->(transfer) {
45+
transfer.comment || "none"
46+
},
47+
"Total Moved" => ->(transfer) {
48+
transfer.line_items.total
49+
}
50+
}
51+
end
52+
53+
def base_headers
54+
base_table.keys
55+
end
56+
57+
def item_headers
58+
@item_headers ||= @organization.items.select("DISTINCT ON (LOWER(name)) items.name").order("LOWER(name) ASC").map(&:name)
59+
end
60+
61+
def headers_with_indexes
62+
@headers_with_indexes ||= headers.each_with_index.to_h
63+
end
64+
65+
def build_row_data(transfer)
66+
row = base_table.values.map { |closure| closure.call(transfer) }
67+
68+
row += Array.new(item_headers.size, 0)
69+
70+
transfer.line_items.each do |line_item|
71+
item_name = line_item.item.name
72+
item_column_idx = headers_with_indexes[item_name]
73+
next unless item_column_idx
74+
75+
row[item_column_idx] += line_item.quantity
76+
end
77+
78+
row
79+
end
80+
end
81+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
RSpec.describe Exports::ExportTransfersCSVService do
2+
let!(:organization) { create(:organization) }
3+
4+
describe "#generate_csv_data" do
5+
subject { described_class.new(transfers:, organization:).generate_csv_data }
6+
7+
let(:from_location) { create(:storage_location, name: "From Location") }
8+
let(:to_location) { create(:storage_location, name: "To Location") }
9+
let(:duplicate_item) { create(:item, name: "Dupe Item") }
10+
11+
let(:items_lists) do # Used to created four transfers
12+
[
13+
[
14+
[duplicate_item, 5],
15+
[create(:item), 7],
16+
[duplicate_item, 3]
17+
],
18+
19+
*(Array.new(3) do |i|
20+
[[create(:item), i + 1]]
21+
end)
22+
]
23+
end
24+
25+
let(:transfers) do
26+
items_lists.map do |items|
27+
transfer = create(:transfer, from: from_location, to: to_location)
28+
29+
items.each do |(item, quantity)|
30+
transfer.line_items << create(:line_item, quantity:, item:)
31+
end
32+
33+
transfer
34+
end
35+
end
36+
37+
let(:all_org_items) { organization.items.sort_by { |item| item.name.downcase } }
38+
39+
let(:total_item_quantities) do
40+
template = all_org_items.pluck(:name).index_with(0)
41+
42+
items_lists.map do |items_list|
43+
row = template.dup
44+
items_list.each do |(item, quantity)|
45+
row[item.name] += quantity
46+
end
47+
row.values
48+
end
49+
end
50+
51+
let(:non_item_headers) { ["From", "To", "Date", "Comment", "Total Moved"] }
52+
let(:expected_headers) { non_item_headers + all_org_items.pluck(:name) }
53+
54+
it "should match the expected content for the csv" do
55+
expect(subject[0]).to eq(expected_headers)
56+
57+
transfers.zip(total_item_quantities).each_with_index do |(transfer, total_item_quantity), idx|
58+
row = [
59+
transfer.from.name,
60+
transfer.to.name,
61+
transfer.created_at.strftime("%F"),
62+
transfer.comment,
63+
transfer.line_items.total
64+
]
65+
row += total_item_quantity
66+
67+
expect(subject[idx + 1]).to eq(row)
68+
end
69+
end
70+
71+
context "when a new item is added to the organization" do
72+
let!(:new_item) { create(:item, name: "New Item") }
73+
74+
it "should be included as the last column of the csv" do
75+
expect(subject[0]).to eq(expected_headers).and end_with(new_item.name)
76+
end
77+
78+
it "should have a quantity of 0 if this item isn't part of any transfer" do
79+
transfers.zip(total_item_quantities).each_with_index do |(transfer, total_item_quantity), idx|
80+
row = [
81+
transfer.from.name,
82+
transfer.to.name,
83+
transfer.created_at.strftime("%F"),
84+
transfer.comment,
85+
transfer.line_items.total
86+
]
87+
row += total_item_quantity
88+
89+
expect(subject[idx + 1]).to eq(row).and end_with(0)
90+
end
91+
end
92+
end
93+
94+
context "when there are no transfers but the report is requested" do
95+
let(:transfers) { Transfer.none }
96+
97+
it "returns a csv with only headers and no rows" do
98+
expect(subject.size).to eq(1)
99+
header_row = subject[0]
100+
expect(header_row).to eq(expected_headers)
101+
end
102+
end
103+
end
104+
end

0 commit comments

Comments
 (0)