human-essentials icon indicating copy to clipboard operation
human-essentials copied to clipboard

Exports should show all items - not just the ones that have data in the filtered list

Open cielf opened this issue 8 months ago • 18 comments

Summary

Exports should show all items - not just the ones that have data in the filtered list

Why

Stability of export -- columns shouldn't change depending on the content

Details

We noticed that donations and purchases are not showing all the items, (We think this might be a regression.) Distributions appears to be correct.

Check donations export, purchase export, transfers export, storage location export, requests export

Include inactive items

You can model this on the distributions export.

Criteria for completion

  • [ ] All the item-based exports show all items, not just the ones that are used within the filtered list
  • [ ] automated tests to confirm.

cielf avatar Apr 13 '25 15:04 cielf

Hey @cielf , I can have a look on this but could you please navigate me to reproduce the issue?

piyush-pawar828 avatar Apr 22 '25 07:04 piyush-pawar828

Intermediate-level issues presume a certain amount of familiarity with the system, and this one includes investigating them yourself.

There are export buttons in the main screens for each of the mentioned areas. If you don't have all the items used, in some of the areas, they are not showing all the items.

cielf avatar Apr 22 '25 11:04 cielf

@cielf was looking at this item as a next issue...conceptually it's an inconsistency in the CSV export logic.

In the case of the Distributions export ( the working one) -- we pass the current_org into the call, so it internally can fetch all items the organization has and do a distinct on it.

# app/services/exports/export_distributions_csv_service.rb
def item_headers
      return @item_headers if @item_headers

      @item_headers = @organization.items.select("DISTINCT ON (LOWER(name)) items.name").order("LOWER(name) ASC").map(&:name)
end

In the other two, we're not passing in the current_org at all, so it's just doing something like this, since it doesn't have a way to get all possible unique items for the organization.

# app/services/exports/export_donations_csv_service.rb
def item_headers
      return @item_headers if @item_headers

      item_names = Set.new

      donations.each do |donation|
        donation.line_items.each do |line_item|
          item_names.add(line_item.item.name)
        end
      end

      @item_headers = item_names.sort
end

Aside from some of the "base" headers for these exports, there is going to be a lot of common code between these 3 reports in processing the "items" headers. I'd try to extract to a common "exports" helper or something...not sure what your convention is here, so looking for a little guidance. Move directly to the "helpers" folder? Lib? Exports/"common"?

bsbonus avatar May 05 '25 22:05 bsbonus

Pointing @dorner at this, for answering questions as the keeper of coding standards etc.

cielf avatar May 06 '25 16:05 cielf

@cielf @dorner After looking closer at this...it's kinda a mess between the csvs. They all are invoked & setup differently. I'll probably file a separate PR later to standardize, since so much of this is the same. None of it is evil code, just either doing extra work or unnecessarily departing from patterns the other ones have.

One open question is what to do with the app/services/exports/export_request_service.rb export. It fundamentally treats the columns differently because of the "enable_packs" feature flag.

if Flipper.enabled?(:enable_packs)
            item.request_units.each do |unit|
              item_names << "#{item.name} - #{unit.name.pluralize}"
            end

            # It's possible that the unit is no longer valid, so we'd
            # add that individually
            if item_request.request_unit.present?
              item_names << "#{item.name} - #{item_request.request_unit.pluralize}"
            end
          end

Is this something we'd keep unique to this export, or something we'd want in the other exports? Leave this one alone?

bsbonus avatar May 07 '25 18:05 bsbonus

Ahh -- that's a new feature (hence the flag).

Packs are specific to requests -- we allow custom units for requests, because diaper pack sizes are not standardized, and some partners think in terms of packs, rather than diapers. The banks have to report in terms of diapers, though, so by the tie we get to distributions everythng is back being expressed in the individual items

So, yes, this is something that we would keep unique to this export.

cielf avatar May 07 '25 19:05 cielf

@cielf Question on how you want to handle the Storage Location export.

Today, this is exporting based off the current snapshot of the Inventory levels for the various storage locations, which makes sense.

However, any item that has 0 quantity across all Storage Locations is excluded (inactive items probably fall here too). You can repro this by just creating a new item and not assigning quantity & then doing the Storage Location export. Not the end of the world, but if the goal is consistency for item headers, it seems like a minor bug. Maybe if there is 0 quantity they don't care?

Changing this is a little gross, because the data access pattern is different. This export uses the snapshot as the source of truth and does a lot of stuff in memory vs in the DB. The other ones don't care about inventory levels, so they never need the snapshot.

Conceptually, we fix this by bringing in all items (not just inventoried ones), like the other reports, but use the inventory quantities from the snapshot. This unfortunately is adding another full table scan to get all those items, which means doing this twice -- once for inventory data, once for all item data -- and then keeping all of that in memory.

I don't think the scale of data you have is a problem, but it's not an ideal data access pattern. Do you still want this change or something like this?

See existing code below:

def index
    @inventory = View::Inventory.new(current_organization.id)
    @selected_item_category = filter_params[:containing]
    @items = StorageLocation.items_inventoried(current_organization, @inventory)
    @include_inactive_storage_locations = params[:include_inactive_storage_locations]
    @storage_locations = current_organization.storage_locations.alphabetized

   <omitted for brevity>

    respond_to do |format|
      format.html
      format.csv do
        send_data StorageLocation.generate_csv_from_inventory(@storage_locations, @inventory), filename: "StorageLocations-#{Time.zone.today}.csv"
      end
    end
  end

bsbonus avatar May 12 '25 18:05 bsbonus

Well, the intent of the issue is to have all the exports have the same set of item columns for ease of working with them outside the system.

I would also say that this is not a frequent use export -- so it's probably not going to be a big performance issue. So my take is that yeah, we still want to have it show all the items.

@dorner -- do you have anything you want to say about this?

cielf avatar May 12 '25 23:05 cielf

@bsbonus could you use this method?

def items_for_location(storage_location_id, include_omitted: false)

include_omitted will give you all items, even those with no inventory in the storage location.

dorner avatar May 15 '25 17:05 dorner

Alternatively, we can add that behavior to all_items to ensure it's available where needed.

dorner avatar May 15 '25 17:05 dorner

@cielf let me pick this one up.

@dorner i'll check -- it might be?

In general, this is a pretty big changeset, so I'll push this as separate PRs. Lot of duplicate code between the exports as I called out earlier, but that could be refactored later if you really want. I kinda doubt these change much, so...might not be worth it.

bsbonus avatar May 22 '25 16:05 bsbonus

Donations export PR: https://github.com/rubyforgood/human-essentials/pull/5200

bsbonus avatar May 22 '25 18:05 bsbonus

Purchases Export PR: https://github.com/rubyforgood/human-essentials/pull/5201

More coming soon -- don't want to drown y'all in PR reviews...

bsbonus avatar May 22 '25 19:05 bsbonus

Transfer export was already happy, but added coverage for it, b/c why not. https://github.com/rubyforgood/human-essentials/pull/5202

bsbonus avatar May 22 '25 19:05 bsbonus

Storage export: https://github.com/rubyforgood/human-essentials/pull/5210/files

bsbonus avatar May 29 '25 21:05 bsbonus

This is only partially completed.

cielf avatar Jun 01 '25 12:06 cielf

(Note: PRs have been put in for everything -- but the one marked as closing the issue got merged before the others)

cielf avatar Jun 01 '25 12:06 cielf

We are in-flight on most of the tasks. Here is the statuses:

  • Donations export - PR open, needs CSV fixture
  • Purchase export - PR open, needs CSV fixture
  • Transfers export - DONE! Bonus tests added.
  • Requests export - PR open, needs some work (has some failing tests)
  • Storage location export - No PR, see above comments to see if it is actionable

awwaiid avatar Sep 13 '25 17:09 awwaiid