diff --git a/.erb-lint.yml b/.erb_lint.yml similarity index 100% rename from .erb-lint.yml rename to .erb_lint.yml diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 12940bac4f..fc9a7dccba 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -225,7 +225,6 @@ Layout/SpaceInsideHashLiteralBraces: - 'app/models/product_drive_participant.rb' - 'app/models/distribution.rb' - 'app/models/donation.rb' - - 'app/models/inventory_item.rb' - 'app/models/item.rb' - 'app/models/item_category.rb' - 'app/models/kit.rb' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92a9a83624..0fd34f2603 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,20 +53,49 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa
Bank Users 🏦 + Pawnee Diaper Bank + A fully set up bank with items, storage locations, donations, distributions, requests, etc. + The bank has multiple partners associated with it. ``` Organization Admin - Email: org_admin1@example.com + Email: org_admin1@example.com Password: password! User Email: user_1@example.com Password: password! ``` + + Second City Essentials Bank + A fully set up bank with items, storage locations, donations, distributions, requests, etc. + The bank has four items unique to it (named Second City Item #). + ``` + Organization Admin + Email: second_city_admin@example.com + Password: password! + + User + Email: second_city_user@example.com + Password: password! + ``` + + SF Diaper Bank + A bank which has just been accepted and so is not fully set up. It lacks many of the records the other banks have. + ``` + Organization Admin + Email: org_admin2@example.com + Password: password! + + User + Email: user_2@example.com + Password: password! + ```
Partner Users 👥 + Partners in Pawnee Diaper Bank partner groups ``` Verified Partner Email: verified@example.com @@ -87,10 +116,17 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa Waiting Approval Partner Email: waiting@example.com Password: password! - - Another approved partner (with all groups): + + Another verified partner (in second partner group): Email: approved_2@example.com - Pasword: password! + Password: password! + ``` + + Partners in Second City Essentials Bank partner group + ``` + Verified partner + Email: second_city_senior_center@example.com + Password: password! ```
@@ -221,7 +257,7 @@ Before submitting a pull request, run all tests and lints. Fix any broken tests - Once your first PR has been merged, all commits pushed to an open PR will also run these workflows. #### Local testing -- Run all lints with `bin/lint`. +- Run all lints with `bin/lint`. (You can lint a single file/folder with `bin/lint {path_to_folder_or_file}`.) - Run all tests with `bundle exec rspec` - You can run a single test with `bundle exec rspec {path_to_test_name}_spec.rb` or on a specific line by appending `:LineNumber` - If you need to skip a failing test, place `pending("Reason you are skipping the test")` into the `it` block rather than skipping with `xit`. This will allow rspec to deliver the error message without causing the test suite to fail. diff --git a/Gemfile b/Gemfile index 86f1f20f46..1b9716089a 100644 --- a/Gemfile +++ b/Gemfile @@ -100,6 +100,8 @@ gem "jwt" gem "newrelic_rpm" # Used to manage periodic cron-like jobs gem "clockwork" +# Speed up app boot time by caching expensive operations +gem 'bootsnap', require: false ##### DEPENDENCY PINS ###### # These are gems that aren't used directly, only as dependencies for other gems. diff --git a/Gemfile.lock b/Gemfile.lock index c95149f950..3570545e7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,8 @@ GEM bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) + bootsnap (1.18.4) + msgpack (~> 1.2) bootstrap (5.2.3) autoprefixer-rails (>= 9.1.0) popper_js (>= 2.11.6, < 3) @@ -381,6 +383,7 @@ GEM monetize (~> 1.9) money (~> 6.13) railties (>= 3.0) + msgpack (1.7.5) multi_xml (0.7.1) bigdecimal (~> 3.1) multipart-post (2.4.1) @@ -716,6 +719,7 @@ DEPENDENCIES azure-storage-blob better_errors binding_of_caller + bootsnap bootstrap (~> 5.2) brakeman bugsnag diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss index ad5a229332..83ec2d042c 100644 --- a/app/assets/stylesheets/custom.scss +++ b/app/assets/stylesheets/custom.scss @@ -98,3 +98,13 @@ margin-top: 40px; } } + +.accordion-button.saving::after { + background-image: none; + content: "Saving..."; + font-size: 0.875rem; + color: #005568; + transform: none; + width: 3rem; + cursor: not-allowed; +} diff --git a/app/controllers/admin/base_items_controller.rb b/app/controllers/admin/base_items_controller.rb index 90d37f0a0e..e423c08b49 100644 --- a/app/controllers/admin/base_items_controller.rb +++ b/app/controllers/admin/base_items_controller.rb @@ -1,5 +1,7 @@ # [Super Admin] Manage the BaseItems -- this is the only place in the app where Base Items can be # added / modified. Base Items are both the template and common thread for regular Items +# +# See #4656, BaseItems are pending significant changes/possible deletion class Admin::BaseItemsController < AdminController def edit @base_item = BaseItem.find(params[:id]) @@ -40,7 +42,9 @@ def show def destroy @base_item = BaseItem.includes(:items).find(params[:id]) - if @base_item.items.any? && @base_item.destroy + if @base_item.id == KitCreateService.find_or_create_kit_base_item!.id + redirect_to admin_base_items_path, alert: "You cannot delete the Kits base item. This is reserved for all Kits." + elsif @base_item.items.empty? && @base_item.destroy redirect_to admin_base_items_path, notice: "Base Item deleted!" else redirect_to admin_base_items_path, alert: "Failed to delete Base Item. Are there still items attached?" diff --git a/app/controllers/admin/organizations_controller.rb b/app/controllers/admin/organizations_controller.rb index 11313338e7..33ef830d49 100644 --- a/app/controllers/admin/organizations_controller.rb +++ b/app/controllers/admin/organizations_controller.rb @@ -46,13 +46,14 @@ def new def create @organization = Organization.new(organization_params) + @user = User.new(user_params) if @organization.save Organization.seed_items(@organization) - @user = UserInviteService.invite(name: user_params[:name], - email: user_params[:email], - roles: [Role::ORG_USER, Role::ORG_ADMIN], - resource: @organization) + UserInviteService.invite(name: user_params[:name], + email: user_params[:email], + roles: [Role::ORG_USER, Role::ORG_ADMIN], + resource: @organization) SnapshotEvent.publish(@organization) # need one to start with redirect_to admin_organizations_path, notice: "Organization added!" else diff --git a/app/controllers/audits_controller.rb b/app/controllers/audits_controller.rb index c6d8e2c99d..8d68291cee 100644 --- a/app/controllers/audits_controller.rb +++ b/app/controllers/audits_controller.rb @@ -10,11 +10,7 @@ def index end def show - if Event.read_events?(@audit.organization) - @items = View::Inventory.items_for_location(@audit.storage_location) - else - @inventory_items = @audit.storage_location.inventory_items - end + @items = View::Inventory.items_for_location(@audit.storage_location, include_omitted: true) end def edit @@ -28,24 +24,7 @@ def finalize @audit.adjustment = Adjustment.new(organization_id: @audit.organization_id, storage_location_id: @audit.storage_location_id, user_id: current_user.id, comment: 'Created Automatically through the Auditing Process') @audit.save - inventory_items = @audit.storage_location.inventory_items - - inventory_items.each do |inventory_item| - line_item = @audit.line_items.find_by(item: inventory_item.item) - - next if line_item.nil? - - if line_item.quantity != inventory_item.quantity - @audit.adjustment.line_items.create(item_id: inventory_item.item.id, quantity: line_item.quantity - inventory_item.quantity) - end - end - - increasing_adjustment, decreasing_adjustment = @audit.adjustment.split_difference - ActiveRecord::Base.transaction do - @audit.storage_location.increase_inventory(increasing_adjustment.line_item_values) - @audit.storage_location.decrease_inventory(decreasing_adjustment.line_item_values) - AuditEvent.publish(@audit) - end + AuditEvent.publish(@audit) @audit.finalized! redirect_to audit_path(@audit), notice: "Audit is Finalized." rescue => e @@ -114,7 +93,7 @@ def set_storage_locations end def set_items - @items = current_organization.items.alphabetized + @items = current_organization.items.where(active: true).alphabetized end def save_audit_status_and_redirect(params) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index a56003a716..7439f7a5fb 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -43,30 +43,33 @@ def index @distributions = current_organization .distributions - .includes(:partner, :storage_location, line_items: [:item]) - .order('issued_at DESC') - .apply_filters(filter_params, helpers.selected_range) + .order(issued_at: :desc) + .includes(:partner, :storage_location) + .class_filter(scope_filters) @paginated_distributions = @distributions.page(params[:page]) - @items = current_organization.items.alphabetized - @item_categories = current_organization.item_categories - @storage_locations = current_organization.storage_locations.active_locations.alphabetized - @partners = @distributions.collect(&:partner).uniq.sort_by(&:name) + @items = current_organization.items.alphabetized.select(:id, :name) + @item_categories = current_organization.item_categories.select(:id, :name) + @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select(:id, :name) + @partners = current_organization.partners.active.alphabetized.select(:id, :name) @selected_item = filter_params[:by_item_id].presence - @total_value_all_distributions = total_value(@distributions) - @total_items_all_distributions = total_items(@distributions, @selected_item) - @total_value_paginated_distributions = total_value(@paginated_distributions) - @total_items_paginated_distributions = total_items(@paginated_distributions, @selected_item) + @distribution_totals = DistributionTotalsService.new(current_organization.distributions, scope_filters) + @total_value_all_distributions = @distribution_totals.total_value + @total_items_all_distributions = @distribution_totals.total_quantity + paginated_ids = @paginated_distributions.ids + @total_value_paginated_distributions = @distribution_totals.total_value(paginated_ids) + @total_items_paginated_distributions = @distribution_totals.total_quantity(paginated_ids) @selected_item_category = filter_params[:by_item_category_id] @selected_partner = filter_params[:by_partner] @selected_status = filter_params[:by_state] @selected_location = filter_params[:by_location] # FIXME: one of these needs to be removed but it's unclear which at this point @statuses = Distribution.states.transform_keys(&:humanize) + @distributions_with_inactive_items = @distributions.joins(:inactive_items).pluck(:id) respond_to do |format| format.html format.csv do - send_data Exports::ExportDistributionsCSVService.new(distributions: @distributions, organization: current_organization, filters: filter_params).generate_csv, filename: "Distributions-#{Time.zone.today}.csv" + send_data Exports::ExportDistributionsCSVService.new(distributions: @distributions, organization: current_organization, filters: scope_filters).generate_csv, filename: "Distributions-#{Time.zone.today}.csv" end end end @@ -114,16 +117,12 @@ def create elsif request_id @distribution.initialize_request_items end - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized - if Event.read_events?(current_organization) - inventory = View::Inventory.new(@distribution.organization_id) - @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| - inventory.quantity_for(storage_location: storage_loc.id).positive? - end - else - @storage_locations = current_organization.storage_locations.active_locations.has_inventory_items.alphabetized + inventory = View::Inventory.new(@distribution.organization_id) + @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| + inventory.quantity_for(storage_location: storage_loc.id).positive? end flash_error = insufficient_error_message(result.error.message) @@ -148,16 +147,12 @@ def new @distribution.line_items.build @distribution.copy_from_donation(params[:donation_id], params[:storage_location_id]) end - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized - if Event.read_events?(current_organization) - inventory = View::Inventory.new(current_organization.id) - @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| - inventory.quantity_for(storage_location: storage_loc.id).positive? - end - else - @storage_locations = current_organization.storage_locations.active_locations.has_inventory_items.alphabetized + inventory = View::Inventory.new(current_organization.id) + @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| + inventory.quantity_for(storage_location: storage_loc.id).positive? end end @@ -178,18 +173,14 @@ def edit if (!@distribution.complete? && @distribution.future?) || current_user.has_role?(Role::ORG_ADMIN, current_organization) @distribution.line_items.build if @distribution.line_items.size.zero? - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.alphabetized @audit_warning = current_organization.audits .where(storage_location_id: @distribution.storage_location_id) .where("updated_at > ?", @distribution.created_at).any? - if Event.read_events?(current_organization) - inventory = View::Inventory.new(@distribution.organization_id) - @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| - !inventory.quantity_for(storage_location: storage_loc.id).negative? - end - else - @storage_locations = current_organization.storage_locations.active_locations.has_inventory_items.alphabetized + inventory = View::Inventory.new(@distribution.organization_id) + @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select do |storage_loc| + !inventory.quantity_for(storage_location: storage_loc.id).negative? end else redirect_to distributions_path, error: 'To edit a distribution, @@ -213,7 +204,7 @@ def update 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 + @items = current_organization.items.active.alphabetized @storage_locations = current_organization.storage_locations.active_locations.alphabetized render :edit end @@ -297,16 +288,6 @@ def request_id params.dig(:distribution, :request_attributes, :id) end - def total_items(distributions, item) - query = LineItem.where(itemizable_type: "Distribution", itemizable_id: distributions.pluck(:id)) - query = query.where(item_id: item.to_i) if item - query.sum('quantity') - end - - def total_value(distributions) - distributions.sum(&:value_per_itemizable) - end - def daily_items(pick_ups) item_groups = LineItem.where(itemizable_type: "Distribution", itemizable_id: pick_ups.pluck(:id)).group_by(&:item_id) item_groups.map do |_id, items| @@ -318,21 +299,26 @@ def daily_items(pick_ups) end end + def scope_filters + filter_params + .except(:date_range) + .merge(during: helpers.selected_range) + end + helper_method \ def filter_params return {} unless params.key?(:filters) - params.require(:filters).permit(:by_item_id, :by_item_category_id, :by_partner, :by_state, :by_location) + params + .require(:filters) + .permit(:by_item_id, :by_item_category_id, :by_partner, :by_state, :by_location, :date_range) end def perform_inventory_check inventory_check_result = InventoryCheckService.new(@distribution).call - if inventory_check_result.error.present? - flash[:error] = inventory_check_result.error - end - if inventory_check_result.alert.present? - flash[:alert] = inventory_check_result.alert - end + alerts = [inventory_check_result.minimum_alert, inventory_check_result.recommended_alert] + merged_alert = alerts.compact.join("\n") + flash[:alert] = merged_alert if merged_alert.present? end end diff --git a/app/controllers/donations_controller.rb b/app/controllers/donations_controller.rb index a925ab6c41..f858a34b38 100644 --- a/app/controllers/donations_controller.rb +++ b/app/controllers/donations_controller.rb @@ -93,7 +93,6 @@ def update @original_source = @donation.source ItemizableUpdateService.call(itemizable: @donation, params: donation_params, - type: :increase, event_class: DonationEvent) flash.clear flash[:notice] = "Donation updated!" @@ -169,7 +168,7 @@ def strip_unnecessary_params # If line_items have submitted with empty rows, clear those out first. def compact_line_items - return params unless params[:donation].key?(:line_item_attributes) + return params unless params[:donation].key?(:line_items_attributes) params[:donation][:line_items_attributes].delete_if { |_row, data| data["quantity"].blank? && data["item_id"].blank? } params diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb deleted file mode 100644 index e1a0fc6892..0000000000 --- a/app/controllers/errors_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -class ErrorsController < ApplicationController - def not_found - render status: :not_found - end - - def internal_server_error - render status: :internal_server_error - end -end \ No newline at end of file diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index cd066bac84..e1d7e20dce 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -11,7 +11,7 @@ def index @items = @items.active unless params[:include_inactive_items] @item_categories = current_organization.item_categories.includes(:items).order('name ASC') - @kits = current_organization.kits.includes(line_items: :item, inventory_items: :storage_location) + @kits = current_organization.kits.includes(line_items: :item) @storages = current_organization.storage_locations.active_locations.order(id: :asc) @include_inactive_items = params[:include_inactive_items] @@ -19,20 +19,14 @@ def index @paginated_items = @items.page(params[:page]) - if Event.read_events?(current_organization) - @inventory = View::Inventory.new(current_organization.id) - end + @inventory = View::Inventory.new(current_organization.id) @items_by_storage_collection_and_quantity = ItemsByStorageCollectionAndQuantityQuery.call(organization: current_organization, inventory: @inventory, filter_params: filter_params) respond_to do |format| format.html - if Event.read_events?(current_organization) - format.csv { send_data Item.generate_csv_from_inventory(@items, @inventory), filename: "Items-#{Time.zone.today}.csv" } - else - format.csv { send_data Item.generate_csv(@items), filename: "Items-#{Time.zone.today}.csv" } - end + format.csv { send_data Item.generate_csv_from_inventory(@items, @inventory), filename: "Items-#{Time.zone.today}.csv" } end end @@ -71,13 +65,9 @@ def edit def show @item = current_organization.items.find(params[:id]) - if Event.read_events?(current_organization) - @inventory = View::Inventory.new(current_organization.id) - storage_location_ids = @inventory.storage_locations_for_item(@item.id) - @storage_locations_containing = StorageLocation.find(storage_location_ids) - else - @storage_locations_containing = current_organization.items.storage_locations_containing(@item) - end + @inventory = View::Inventory.new(current_organization.id) + storage_location_ids = @inventory.storage_locations_for_item(@item.id) + @storage_locations_containing = StorageLocation.find(storage_location_ids) @barcodes_for = current_organization.items.barcodes_for(@item) end diff --git a/app/controllers/kits_controller.rb b/app/controllers/kits_controller.rb index 6487f74dba..7ed275ca47 100644 --- a/app/controllers/kits_controller.rb +++ b/app/controllers/kits_controller.rb @@ -1,9 +1,7 @@ class KitsController < ApplicationController def index - @kits = current_organization.kits.includes(line_items: :item, inventory_items: :storage_location).class_filter(filter_params) - if Event.read_events?(current_organization) - @inventory = View::Inventory.new(current_organization.id) - end + @kits = current_organization.kits.includes(line_items: :item).class_filter(filter_params) + @inventory = View::Inventory.new(current_organization.id) unless params[:include_inactive_items] @kits = @kits.active end @@ -56,11 +54,7 @@ def reactivate def allocations @kit = Kit.find(params[:id]) @storage_locations = current_organization.storage_locations.active_locations - if Event.read_events?(current_organization) - @inventory = View::Inventory.new(current_organization.id) - else - @item_inventories = @kit.item.inventory_items - end + @inventory = View::Inventory.new(current_organization.id) load_form_collections end @@ -69,21 +63,14 @@ def allocate @kit = Kit.find(params[:id]) @storage_location = current_organization.storage_locations.active_locations.find(kit_adjustment_params[:storage_location_id]) @change_by = kit_adjustment_params[:change_by].to_i - - if @change_by.positive? - service = AllocateKitInventoryService.new(kit: @kit, storage_location: @storage_location, increase_by: @change_by) - service.allocate - flash[:error] = service.error if service.error - elsif @change_by.negative? - service = DeallocateKitInventoryService.new(kit: @kit, storage_location: @storage_location, decrease_by: @change_by.abs) - service.deallocate - flash[:error] = service.error if service.error - end - - if service.error - flash[:error] = service.error - else - flash[:notice] = "#{@kit.name} at #{@storage_location.name} quantity has changed by #{@change_by}" + begin + if @change_by.positive? + KitAllocateEvent.publish(@kit, @storage_location.id, @change_by) + else + KitDeallocateEvent.publish(@kit, @storage_location.id, -@change_by) + end + rescue => e + flash[:error] = e.message end redirect_to allocations_kit_path(id: @kit.id) diff --git a/app/controllers/partners/dashboards_controller.rb b/app/controllers/partners/dashboards_controller.rb index 0483ac0f6b..bc623be0e5 100644 --- a/app/controllers/partners/dashboards_controller.rb +++ b/app/controllers/partners/dashboards_controller.rb @@ -24,9 +24,7 @@ def show @families = @partner.families @children = @partner.children - if Event.read_events?(@partner.organization) - @inventory = View::Inventory.new(@partner.organization_id) - end + @inventory = View::Inventory.new(@partner.organization_id) @broadcast_announcements = BroadcastAnnouncement.filter_announcements(@parent_org) end diff --git a/app/controllers/partners/profiles_controller.rb b/app/controllers/partners/profiles_controller.rb index d6508f7fa4..e92085c037 100644 --- a/app/controllers/partners/profiles_controller.rb +++ b/app/controllers/partners/profiles_controller.rb @@ -5,6 +5,13 @@ def show; end def edit @counties = County.in_category_name_order @client_share_total = current_partner.profile.client_share_total + + if Flipper.enabled?("partner_step_form") + @sections_with_errors = [] + render "partners/profiles/step/edit" + else + render "edit" + end end def update @@ -12,10 +19,24 @@ def update result = PartnerProfileUpdateService.new(current_partner, partner_params, profile_params).call if result.success? flash[:success] = "Details were successfully updated." - redirect_to partners_profile_path + if Flipper.enabled?("partner_step_form") + if params[:save_review] + redirect_to partners_profile_path + else + redirect_to edit_partners_profile_path + end + else + redirect_to partners_profile_path + end else - flash[:error] = "There is a problem. Try again: %s" % result.error - render :edit + flash.now[:error] = "There is a problem. Try again: %s" % result.error + if Flipper.enabled?("partner_step_form") + error_keys = current_partner.profile.errors.attribute_names + @sections_with_errors = Partners::SectionErrorService.sections_with_errors(error_keys) + render "partners/profiles/step/edit" + else + render :edit + end end end diff --git a/app/controllers/partners/requests_controller.rb b/app/controllers/partners/requests_controller.rb index 2c01b28991..48b7516cd3 100644 --- a/app/controllers/partners/requests_controller.rb +++ b/app/controllers/partners/requests_controller.rb @@ -20,6 +20,7 @@ def show def create create_service = Partners::RequestCreateService.new( + request_type: "quantity", partner_user_id: current_user.id, comments: partner_request_params[:comments], item_requests_attributes: partner_request_params[:item_requests_attributes]&.values || [] @@ -43,6 +44,7 @@ def create def validate @partner_request = Partners::RequestCreateService.new( + request_type: "quantity", partner_user_id: current_user.id, comments: partner_request_params[:comments], item_requests_attributes: partner_request_params[:item_requests_attributes]&.values || [] diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 7e57e24708..4f424f2a3a 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,9 +1,15 @@ class ProfilesController < ApplicationController def edit @partner = current_organization.partners.find(params[:id]) - @counties = County.in_category_name_order @client_share_total = @partner.profile.client_share_total + + if Flipper.enabled?("partner_step_form") + @sections_with_errors = [] + render "profiles/step/edit" + else + render "edit" + end end def update @@ -11,10 +17,25 @@ def update @partner = current_organization.partners.find(params[:id]) result = PartnerProfileUpdateService.new(@partner, edit_partner_params, edit_profile_params).call if result.success? - redirect_to partner_path(@partner) + "#partner-information", notice: "#{@partner.name} updated!" + flash[:success] = "Details were successfully updated." + if Flipper.enabled?("partner_step_form") + if params[:save_review] + redirect_to partner_path(@partner) + "#partner-information" + else + redirect_to edit_profile_path + end + else + redirect_to partner_path(@partner) + "#partner-information" + end else - flash[:error] = "Something didn't work quite right -- try again? %s " % result.error - render action: :edit + flash.now[:error] = "There is a problem. Try again: %s " % result.error + if Flipper.enabled?("partner_step_form") + error_keys = @partner.profile.errors.attribute_names + @sections_with_errors = Partners::SectionErrorService.sections_with_errors(error_keys) + render "profiles/step/edit" + else + render :edit + end end end diff --git a/app/controllers/purchases_controller.rb b/app/controllers/purchases_controller.rb index 32e288c6b6..760c06c1b4 100644 --- a/app/controllers/purchases_controller.rb +++ b/app/controllers/purchases_controller.rb @@ -68,7 +68,6 @@ def update @purchase = current_organization.purchases.find(params[:id]) ItemizableUpdateService.call(itemizable: @purchase, params: purchase_params, - type: :increase, event_class: PurchaseEvent) redirect_to purchases_path rescue => e @@ -112,7 +111,7 @@ def filter_params # If line_items have submitted with empty rows, clear those out first. def compact_line_items - return params unless params[:purchase].key?(:line_item_attributes) + return params unless params[:purchase].key?(:line_items_attributes) params[:purchase][:line_items_attributes].delete_if { |_row, data| data["quantity"].blank? && data["item_id"].blank? } params diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index cbd87cb409..9a86ae5659 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -12,8 +12,7 @@ def manufacturer_donations_summary end def purchases_summary - @purchases = current_organization.purchases.during(helpers.selected_range) - @recent_purchases = @purchases.recent.includes(:vendor) + @summary_struct = Purchase.organization_summary_by_dates(current_organization, helpers.selected_range) end def product_drives_summary diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb index 93247275a8..f1d5a10e13 100644 --- a/app/controllers/requests_controller.rb +++ b/app/controllers/requests_controller.rb @@ -15,6 +15,8 @@ def index @partners = current_organization.partners.order(:name) @statuses = Request.statuses.transform_keys(&:humanize) @partner_users = User.where(id: @paginated_requests.pluck(:partner_user_id)) + @request_types = Request.request_types.transform_keys(&:humanize) + @selected_request_type = filter_params[:by_request_type] @selected_request_item = filter_params[:by_request_item_id] @selected_partner = filter_params[:by_partner] @selected_status = filter_params[:by_status] @@ -63,11 +65,7 @@ def print_unfulfilled def load_items return unless @request.request_items - inventory = nil - if Event.read_events?(@request.organization) - inventory = View::Inventory.new(@request.organization_id) - end - + inventory = View::Inventory.new(@request.organization_id) @request.request_items.map { |json| RequestItem.from_json(json, @request, inventory) } end @@ -75,6 +73,6 @@ def load_items def filter_params return {} unless params.key?(:filters) - params.require(:filters).permit(:by_request_item_id, :by_partner, :by_status) + params.require(:filters).permit(:by_request_item_id, :by_partner, :by_status, :by_request_type) end end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 2a679bea92..2fb3a6cc64 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -9,15 +9,12 @@ def quantity end def index - if Event.read_events?(current_organization) - @inventory = View::Inventory.new(current_organization.id) - end - + @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].present? @storage_locations = current_organization.storage_locations.alphabetized - if @inventory && filter_params[:containing].present? + if filter_params[:containing].present? containing_ids = @inventory.storage_locations.keys.select do |sl| @inventory.quantity_for(item_id: filter_params[:containing], storage_location: sl).positive? end @@ -33,21 +30,7 @@ def index respond_to do |format| format.html format.csv do - if Event.read_events?(current_organization) - send_data StorageLocation.generate_csv_from_inventory(@storage_locations, @inventory), filename: "StorageLocations-#{Time.zone.today}.csv" - else - active_inventory_item_names = [] - @storage_locations.each do |storage_location| - active_inventory_item_names << - storage_location - .active_inventory_items - .joins(:item) - .select('distinct items.name') - .pluck(:name) - end - active_inventory_item_names = active_inventory_item_names.flatten.uniq.sort - send_data StorageLocation.generate_csv(@storage_locations, active_inventory_item_names), filename: "StorageLocations-#{Time.zone.today}.csv" - end + send_data StorageLocation.generate_csv_from_inventory(@storage_locations, @inventory), filename: "StorageLocations-#{Time.zone.today}.csv" end end end @@ -78,16 +61,14 @@ def show @items_out_total = ItemsOutTotalQuery.new(organization: current_organization, storage_location: @storage_location).call @items_in = ItemsInQuery.new(organization: current_organization, storage_location: @storage_location).call @items_in_total = ItemsInTotalQuery.new(organization: current_organization, storage_location: @storage_location).call - if Event.read_events?(current_organization) - if View::Inventory.within_snapshot?(current_organization.id, params[:version_date]) - @inventory = View::Inventory.new(current_organization.id, event_time: params[:version_date]) - else - @legacy_inventory = View::Inventory.legacy_inventory_for_storage_location( - current_organization.id, - @storage_location.id, - params[:version_date] - ) - end + if View::Inventory.within_snapshot?(current_organization.id, params[:version_date]) + @inventory = View::Inventory.new(current_organization.id, event_time: params[:version_date]) + else + @legacy_inventory = View::Inventory.legacy_inventory_for_storage_location( + current_organization.id, + @storage_location.id, + params[:version_date] + ) end respond_to do |format| @@ -154,22 +135,10 @@ def destroy end def inventory - if Event.read_events?(current_organization) - @items = View::Inventory.items_for_location(StorageLocation.find(params[:id]), - include_omitted: params[:include_omitted_items] == "true") - respond_to do |format| - format.json { render :event_inventory } - end - else - @inventory_items = current_organization.storage_locations - .includes(inventory_items: :item) - .find(params[:id]) - .inventory_items - .active - - @inventory_items += include_omitted_items(@inventory_items.collect(&:item_id)) if params[:include_omitted_items] == "true" - @inventory_items.to_a.sort_by! { |inventory_item| inventory_item.item.name.downcase } - respond_to :json + @items = View::Inventory.items_for_location(StorageLocation.find(params[:id]), + include_omitted: params[:include_omitted_items] == "true") + respond_to do |format| + format.json { render :event_inventory } end end diff --git a/app/controllers/transfers_controller.rb b/app/controllers/transfers_controller.rb index e227e16990..e94949a272 100644 --- a/app/controllers/transfers_controller.rb +++ b/app/controllers/transfers_controller.rb @@ -11,8 +11,8 @@ def index .during(helpers.selected_range) @selected_from = filter_params[:from_location] @selected_to = filter_params[:to_location] - @from_storage_locations = Transfer.storage_locations_transferred_from_in(current_organization) - @to_storage_locations = Transfer.storage_locations_transferred_to_in(current_organization) + @from_storage_locations = StorageLocation.with_transfers_from(current_organization) + @to_storage_locations = StorageLocation.with_transfers_to(current_organization) respond_to do |format| format.html format.csv { send_data Transfer.generate_csv(@transfers), filename: "Transfers-#{Time.zone.today}.csv" } diff --git a/app/events/event_types/event_line_item.rb b/app/events/event_types/event_line_item.rb index 9dc1f8769c..e1d947c195 100644 --- a/app/events/event_types/event_line_item.rb +++ b/app/events/event_types/event_line_item.rb @@ -19,6 +19,17 @@ def same_item?(line_item) end end + # @return [EventTypes::EventLineItem] + def negative + self.class.new( + quantity: -quantity, + item_id: item_id, + item_value_in_cents: item_value_in_cents, + from_storage_location: from_storage_location, + to_storage_location: to_storage_location + ) + end + # @param line_item [LineItem] # @param from [Integer] # @param to [Integer] diff --git a/app/events/kit_deallocate_event.rb b/app/events/kit_deallocate_event.rb index 38100bb86a..dd0724f0b6 100644 --- a/app/events/kit_deallocate_event.rb +++ b/app/events/kit_deallocate_event.rb @@ -5,7 +5,7 @@ def self.event_line_items(kit, storage_location, quantity) quantity: item.quantity * quantity, item_id: item.item_id, item_value_in_cents: item.item.value_in_cents, - to_storage_location: storage_location.id, + to_storage_location: storage_location, from_storage_location: nil ) end @@ -13,7 +13,7 @@ def self.event_line_items(kit, storage_location, quantity) quantity: quantity, item_id: kit.item.id, item_value_in_cents: kit.item.value_in_cents, - from_storage_location: storage_location.id, + from_storage_location: storage_location, to_storage_location: nil )) items diff --git a/app/events/snapshot_event.rb b/app/events/snapshot_event.rb index 8c758719cf..ae277bea5e 100644 --- a/app/events/snapshot_event.rb +++ b/app/events/snapshot_event.rb @@ -2,25 +2,7 @@ class SnapshotEvent < Event serialize :data, coder: EventTypes::StructCoder.new(EventTypes::Inventory) # @param organization [Organization] - # @return [Hash] - def self.storage_locations(organization) - organization.storage_locations.to_h do |loc| - [loc.id, - EventTypes::EventStorageLocation.new( - id: loc.id, - items: loc.inventory_items.to_h do |inv_item| - [inv_item.item_id, EventTypes::EventItem.new( - quantity: inv_item.quantity, - item_id: inv_item.item_id, - storage_location_id: loc.id - )] - end - )] - end - end - - # @param organization [Organization] - def self.publish_from_events(organization) + def self.publish(organization) inventory = InventoryAggregate.inventory_for(organization.id) create( eventable: organization, @@ -29,18 +11,4 @@ def self.publish_from_events(organization) data: inventory ) end - - # @param organization [Organization] - def self.publish(organization) - create( - eventable: organization, - group_id: "snapshot-#{SecureRandom.hex}", - organization_id: organization.id, - event_time: Time.zone.now, - data: EventTypes::Inventory.new( - organization_id: organization.id, - storage_locations: storage_locations(organization) - ) - ) - end end diff --git a/app/events/update_existing_event.rb b/app/events/update_existing_event.rb index cdeb1430c3..90133e2e39 100644 --- a/app/events/update_existing_event.rb +++ b/app/events/update_existing_event.rb @@ -38,12 +38,17 @@ def direction(itemizable) end # @param itemizable [Itemizable] - def publish(itemizable, previous_line_items) + # @param previous_line_items [Array] + # @param original_storage_location [StorageLocation] + def publish(itemizable, previous_line_items, original_storage_location) dir = direction(itemizable) - previous_items = item_quantities(previous_line_items, itemizable.storage_location, dir) + previous_items = item_quantities(previous_line_items, original_storage_location, dir) current_items = item_quantities(itemizable.line_items, itemizable.storage_location, dir) - diff_items = diff(previous_items, current_items) - + diff_items = if original_storage_location.id == itemizable.storage_location.id + diff(previous_items, current_items) + else + previous_items.values.map(&:negative) + current_items.values # remove from the old + end create( eventable: itemizable, group_id: "existing-#{itemizable.id}-#{SecureRandom.hex}", diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6f7075f680..9c16d713ad 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -124,4 +124,8 @@ def storage_location_for_source(source_object) end current_organization.default_storage_location end + + def default_location(source_object) + current_organization.default_storage_location || source_object.storage_location_id.presence || current_organization.intake_location + end end diff --git a/app/helpers/date_range_helper.rb b/app/helpers/date_range_helper.rb index 3f00f35db9..cc73af80d8 100644 --- a/app/helpers/date_range_helper.rb +++ b/app/helpers/date_range_helper.rb @@ -1,7 +1,7 @@ # Encapsulates methods used on the Dashboard that need some business logic module DateRangeHelper def date_range_params - params.dig(:filters, :date_range).presence || this_year + params.dig(:filters, :date_range).presence || default_date end def date_range_label @@ -18,13 +18,19 @@ def date_range_label "this month" when "last month" "last month" + when "last 12 months" + "last 12 months" + when "prior year" + "prior year" else selected_range_described end end - def this_year - "January 1, #{Time.zone.today.year} - December 31, #{Time.zone.today.year}" + def default_date + start_date = 2.months.ago.to_date + end_date = 1.month.from_now.to_date + "#{start_date.strftime("%B %d, %Y")} - #{end_date.strftime("%B %d, %Y")}" end def selected_interval diff --git a/app/helpers/distribution_helper.rb b/app/helpers/distribution_helper.rb index c1fc027ee0..5ee6f2d879 100644 --- a/app/helpers/distribution_helper.rb +++ b/app/helpers/distribution_helper.rb @@ -19,21 +19,6 @@ def hashed_calendar_path calendar_distributions_url(hash: crypt.encrypt_and_sign(current_organization.id)) end - def quantity_by_item_id(distribution, item_id) - item_id = Integer(item_id) - quantities = distribution.line_items.quantities_by_name - - single_item = quantities.values.find { |li| item_id == li[:item_id] } || {} - single_item[:quantity] - end - - def quantity_by_item_category_id(distribution, item_category_id) - item_category_id = Integer(item_category_id) - quantities = distribution.line_items.quantities_by_category - - quantities[item_category_id] - end - def distribution_shipping_cost(shipping_cost) (shipping_cost && shipping_cost != 0) ? number_to_currency(shipping_cost) : "" end diff --git a/app/helpers/historical_trends_helper.rb b/app/helpers/historical_trends_helper.rb index 29091194c5..e7ab4533f7 100644 --- a/app/helpers/historical_trends_helper.rb +++ b/app/helpers/historical_trends_helper.rb @@ -16,6 +16,18 @@ module HistoricalTrendsHelper def last_12_months current_month = Time.zone.now.month - MONTHS.rotate(current_month) + current_year = Time.zone.now.year + last_year = current_year - 1 + return_array = MONTHS.rotate(current_month) + return_array.each_with_index do |month, index| + # Last current_month entries are in the current year, earlier entries are + # in the previous year. + return_array[index] = if index >= (MONTHS.length - current_month) + "#{month} #{current_year}" + else + "#{month} #{last_year}" + end + end + return_array end end diff --git a/app/helpers/partners_helper.rb b/app/helpers/partners_helper.rb index fcf9f498c9..74974649f3 100644 --- a/app/helpers/partners_helper.rb +++ b/app/helpers/partners_helper.rb @@ -27,6 +27,20 @@ def humanize_boolean_3state(boolean) end end + # In step-wise editing of the partner profile, the partial name is used as the section header by default. + # This helper allows overriding the header with a custom display name if needed. + def partial_display_name(partial) + custom_names = { + 'attached_documents' => 'Additional Documents' + } + + custom_names[partial] || partial.humanize + end + + def section_with_errors?(section, sections_with_errors = []) + sections_with_errors.include?(section) + end + def partner_status_badge(partner) if partner.status == "approved" tag.span partner.display_status, class: %w(badge badge-pill badge-primary bg-primary float-right) diff --git a/app/helpers/purchases_helper.rb b/app/helpers/purchases_helper.rb index ea6a5b4ffc..bfa08f91fb 100644 --- a/app/helpers/purchases_helper.rb +++ b/app/helpers/purchases_helper.rb @@ -3,8 +3,4 @@ module PurchasesHelper def purchased_from(purchase) purchase.purchased_from.nil? ? "" : "(#{purchase.purchased_from})" end - - def new_purchase_default_location(purchase) - purchase.storage_location_id.presence || current_organization.intake_location - end end diff --git a/app/javascript/application.js b/app/javascript/application.js index f519974bb0..bb8df687e1 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -90,6 +90,7 @@ $(document).ready(function(){ format: "MMMM D, YYYY", ranges: { customRanges: { + 'Default': [today.minus({'months': 2}).toJSDate(), today.plus({'months': 1}).toJSDate()], 'All Time': [today.minus({ 'years': 100 }).toJSDate(), today.plus({ 'years': 1 }).toJSDate()], 'Today': [today.toJSDate(), today.toJSDate()], 'Yesterday': [today.minus({'days': 1}).toJSDate(), today.minus({'days': 1}).toJSDate()], @@ -98,7 +99,9 @@ $(document).ready(function(){ 'This Month': [today.startOf('month').toJSDate(), today.endOf('month').toJSDate()], 'Last Month': [today.minus({'months': 1}).startOf('month').toJSDate(), today.minus({'month': 1}).endOf('month').toJSDate()], - 'This Year': [today.startOf('year').toJSDate(), today.endOf('year').toJSDate()] + 'Last 12 Months': [today.minus({'months': 12}).plus({'days': 1}).toJSDate(), today.toJSDate()], + 'Prior Year': [today.startOf('year').minus({'years': 1}).toJSDate(), today.minus({'year': 1}).endOf('year').toJSDate()], + 'This Year': [today.startOf('year').toJSDate(), today.endOf('year').toJSDate()], } } }); diff --git a/app/javascript/controllers/accordion_controller.js b/app/javascript/controllers/accordion_controller.js new file mode 100644 index 0000000000..def5ebeaa5 --- /dev/null +++ b/app/javascript/controllers/accordion_controller.js @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus"; + +// Connects to data-controller="accordion" +// Intercepts form submission and disables the open/close section buttons. +export default class extends Controller { + static targets = [ "form" ] + + disableOpenClose(event) { + event.preventDefault(); + + const buttons = this.element.querySelectorAll(".accordion-button"); + buttons.forEach(button => { + button.disabled = true; + button.classList.add("saving"); + }); + + this.formTarget.requestSubmit(); + } +} diff --git a/app/jobs/backup_db_rds.rb b/app/jobs/backup_db_rds.rb new file mode 100644 index 0000000000..d5eb004586 --- /dev/null +++ b/app/jobs/backup_db_rds.rb @@ -0,0 +1,24 @@ +# to be called from Clock +module BackupDbRds + def self.run + logger = Logger.new($stdout) + logger.info("Performing dump of the database.") + + current_time = Time.current.strftime("%Y%m%d%H%M%S") + + logger.info("Copying the database...") + backup_filename = "#{current_time}.rds.dump" + system("PGPASSWORD='#{ENV["DIAPER_DB_PASSWORD"]}' pg_dump -Fc -v --host=#{ENV["DIAPER_DB_HOST"]} --username=#{ENV["DIAPER_DB_USERNAME"]} --dbname=#{ENV["DIAPER_DB_DATABASE"]} -f #{backup_filename}") + + account_name = ENV["AZURE_STORAGE_ACCOUNT_NAME"] + account_key = ENV["AZURE_STORAGE_ACCESS_KEY"] + + blob_client = Azure::Storage::Blob::BlobService.create( + storage_account_name: account_name, + storage_access_key: account_key + ) + + logger.info("Uploading #{backup_filename}") + blob_client.create_block_blob("backups", backup_filename, File.read(backup_filename)) + end +end diff --git a/app/jobs/reminder_deadline_job.rb b/app/jobs/reminder_deadline_job.rb index 32cd255586..2006d2931c 100644 --- a/app/jobs/reminder_deadline_job.rb +++ b/app/jobs/reminder_deadline_job.rb @@ -7,6 +7,7 @@ class ReminderDeadlineJob < ApplicationJob def perform remind_these_partners = Partners::FetchPartnersToRemindNowService.new.fetch + Rails.logger.info("Partners to remind: #{remind_these_partners.map(&:id)}") remind_these_partners.each do |partner| ReminderDeadlineMailer.notify_deadline(partner).deliver_later diff --git a/app/models/audit.rb b/app/models/audit.rb index 3c8a613cf2..7f490e0675 100644 --- a/app/models/audit.rb +++ b/app/models/audit.rb @@ -28,7 +28,6 @@ class Audit < ApplicationRecord enum status: { in_progress: 0, confirmed: 1, finalized: 2 } validates :storage_location, :organization, presence: true - validate :line_items_exist_in_inventory validate :line_items_quantity_is_not_negative validate :line_items_unique_by_item_id validate :user_is_organization_admin_of_the_organization diff --git a/app/models/base_item.rb b/app/models/base_item.rb index d910a46f25..2623528887 100644 --- a/app/models/base_item.rb +++ b/app/models/base_item.rb @@ -29,4 +29,3 @@ def to_h { partner_key: partner_key, name: name } end end - diff --git a/app/models/concerns/itemizable.rb b/app/models/concerns/itemizable.rb index 9248238f8b..004f0d3128 100644 --- a/app/models/concerns/itemizable.rb +++ b/app/models/concerns/itemizable.rb @@ -1,5 +1,5 @@ # Creates a veritable powerhouse. -# This module provides Duck Typed behaviors for anything that shuttle Items +# This module provides Duck Typed behaviors for anything that shuttles LINE ITEMS (not items) # throughout the system. e.g. things that `has_many :line_items` -- this provides # all the logic about how those kinds of things behave. module Itemizable @@ -18,7 +18,7 @@ def has_inactive_item? inactive_items.any? end - # @return [Array] + # @return [Array] or [Item::ActiveRecord_Relation] def inactive_items line_items.map(&:item).select { |i| !i.active? } end @@ -94,6 +94,8 @@ def total_value end has_many :items, through: :line_items + has_many :inactive_items, -> { inactive }, through: :line_items, source: :item + accepts_nested_attributes_for :line_items, allow_destroy: true, reject_if: proc { |l| l[:item_id].blank? || l[:quantity].blank? } @@ -138,20 +140,4 @@ def line_items_quantity_is_at_least(threshold) "needs to be at least #{threshold}") end end - - def line_items_exist_in_inventory - return if storage_location.nil? - return if Event.read_events?(storage_location.organization) - - line_items.each do |line_item| - next unless line_item.item - - inventory_item = storage_location.inventory_items.find_by(item: line_item.item) - next unless inventory_item.nil? - - errors.add(:inventory, - "#{line_item.item.name} is not available " \ - "at this storage location") - end - end end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 751d979006..1f7d10c857 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -39,7 +39,6 @@ class Distribution < ApplicationRecord accepts_nested_attributes_for :request validates :storage_location, :partner, :organization, :delivery_method, presence: true - validate :line_items_exist_in_inventory validate :line_items_quantity_is_positive validates :shipping_cost, numericality: { greater_than_or_equal_to: 0 }, allow_blank: true, if: :shipped? @@ -48,10 +47,12 @@ class Distribution < ApplicationRecord enum :state, { scheduled: 5, complete: 10 } enum :delivery_method, { pick_up: 0, delivery: 1, shipped: 2 } scope :active, -> { joins(:line_items).joins(:items).where(items: { active: true }) } + scope :with_diapers, -> { joins(line_items: :item).merge(Item.disposable.or(Item.cloth_diapers)) } + scope :with_period_supplies, -> { joins(line_items: :item).merge(Item.period_supplies) } # add item_id scope to allow filtering distributions by item - scope :by_item_id, ->(item_id) { joins(:items).where(items: { id: item_id }) } + scope :by_item_id, ->(item_id) { includes(:items).where(items: { id: item_id }) } # partner scope to allow filtering by partner - scope :by_item_category_id, ->(item_category_id) { joins(:items).where(items: { item_category_id: item_category_id }) } + scope :by_item_category_id, ->(item_category_id) { includes(:items).where(items: { item_category_id: item_category_id }) } scope :by_partner, ->(partner_id) { where(partner_id: partner_id) } # location scope to allow filtering distributions by location scope :by_location, ->(storage_location_id) { where(storage_location_id: storage_location_id) } @@ -66,14 +67,16 @@ class Distribution < ApplicationRecord .apply_filters(filters, date_range) } scope :apply_filters, ->(filters, date_range) { - includes(:partner, :storage_location, :line_items, :items) - .order(issued_at: :desc) - .class_filter(filters.merge(during: date_range)) + class_filter(filters.merge(during: date_range)) } scope :this_week, -> do where("issued_at > :start_date AND issued_at <= :end_date", start_date: Time.zone.today.beginning_of_week.beginning_of_day, end_date: Time.zone.today.end_of_week.end_of_day) end + scope :in_last_12_months, -> do + where("issued_at > :start_date AND issued_at <= :end_date", + start_date: 12.months.ago.beginning_of_day, end_date: Time.zone.today.end_of_day) + end delegate :name, to: :partner, prefix: true diff --git a/app/models/event.rb b/app/models/event.rb index 4de3eebc62..a67f972666 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -74,13 +74,7 @@ def self.most_recent_snapshot(organization_id) SnapshotEvent.find_by_sql(query, [organization_id]).first end - def self.read_events?(organization) - Flipper.enabled?(:read_events, organization) - end - def validate_inventory - return unless Event.read_events?(organization) - InventoryAggregate.inventory_for(organization_id, validate: true) rescue InventoryError => e item = Item.find_by(id: e.item_id)&.name || "Item ID #{e.item_id}" diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb index f038656ad6..1c46a2bda2 100644 --- a/app/models/inventory_item.rb +++ b/app/models/inventory_item.rb @@ -22,23 +22,14 @@ class InventoryItem < ApplicationRecord validates :quantity, presence: true validates :storage_location_id, presence: true validates :item_id, presence: true - validates :quantity, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: MAX_INT } scope :by_partner_key, ->(partner_key) { joins(:item).merge(Item.by_partner_key(partner_key)) } - scope :active, -> { joins(:item).where(items: { active: true }) } - scope :inactive, -> { joins(:item).where(items: { active: false }) } + scope :active, -> { joins(:item).where(items: {active: true}) } + scope :inactive, -> { joins(:item).where(items: {active: false}) } delegate :name, to: :item, prefix: true def to_h - { item_id: item_id, quantity: quantity, item_name: item.name }.stringify_keys - end - - def lower_than_on_hand_minimum_quantity? - quantity < item.on_hand_minimum_quantity - end - - def lower_than_on_hand_recommended_quantity? - item.on_hand_recommended_quantity.present? && quantity < item.on_hand_recommended_quantity + {item_id: item_id, quantity: quantity, item_name: item.name}.stringify_keys end end diff --git a/app/models/item.rb b/app/models/item.rb index 0fe387a6f1..688bd34512 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -44,16 +44,16 @@ class Item < ApplicationRecord has_many :line_items, dependent: :destroy has_many :inventory_items, dependent: :destroy has_many :barcode_items, as: :barcodeable, dependent: :destroy - has_many :storage_locations, through: :inventory_items has_many :donations, through: :line_items, source: :itemizable, source_type: "::Donation" has_many :distributions, through: :line_items, source: :itemizable, source_type: "::Distribution" has_many :request_units, class_name: "ItemUnit", dependent: :destroy scope :active, -> { where(active: true) } - # Add spec for these - scope :kits, -> { where.not(kit_id: nil) } + # :housing_a_kit are items which house a kit, NOT items is_in_kit + scope :housing_a_kit, -> { where.not(kit_id: nil) } scope :loose, -> { where(kit_id: nil) } + scope :inactive, -> { where.not(active: true) } scope :visible, -> { where(visible_to_partners: true) } scope :alphabetized, -> { order(:name) } @@ -114,10 +114,6 @@ def self.barcoded_items joins(:barcode_items).order(:name).group(:id) end - def self.storage_locations_containing(item) - StorageLocation.joins(:inventory_items).where("inventory_items.item_id = ?", item.id) - end - def self.barcodes_for(item) BarcodeItem.where("barcodeable_id = ?", item.id) end @@ -128,11 +124,7 @@ def self.reactivate(item_ids) end def has_inventory?(inventory = nil) - if inventory - inventory.quantity_for(item_id: id).positive? - else - inventory_items.where("quantity > 0").any? - end + inventory&.quantity_for(item_id: id)&.positive? end def in_request? @@ -151,14 +143,12 @@ def is_in_kit?(kits = nil) end def can_delete?(inventory = nil, kits = nil) - can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? + can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? && kit.blank? end # @return [Boolean] def can_deactivate_or_delete?(inventory = nil, kits = nil) - if inventory.nil? && Event.read_events?(organization) - inventory = View::Inventory.new(organization_id) - end + inventory ||= View::Inventory.new(organization_id) # Cannot deactivate if it's currently in inventory in a storage location. It doesn't make sense # to have physical inventory of something we're now saying isn't valid. # If an active kit includes this item, then changing kit allocations would change inventory @@ -211,16 +201,6 @@ def self.csv_export_headers ["Name", "Barcodes", "Base Item", "Quantity"] end - # TODO remove this method once read_events? is true everywhere - def csv_export_attributes - [ - name, - barcode_count, - base_item.name, - inventory_items.sum(&:quantity) - ] - end - # @param items [Array] # @param inventory [View::Inventory] # @return [String] @@ -239,10 +219,6 @@ def default_quantity distribution_quantity || 50 end - def inventory_item_at(storage_location_id) - inventory_items.find_by(storage_location_id: storage_location_id) - end - def sync_request_units!(unit_ids) request_units.clear organization.request_units.where(id: unit_ids).pluck(:name).each do |name| diff --git a/app/models/kit.rb b/app/models/kit.rb index 759ae7451c..363407266f 100644 --- a/app/models/kit.rb +++ b/app/models/kit.rb @@ -19,7 +19,6 @@ class Kit < ApplicationRecord belongs_to :organization has_one :item, dependent: :restrict_with_exception - has_many :inventory_items, through: :item scope :active, -> { where(active: true) } scope :alphabetized, -> { order(:name) } @@ -34,12 +33,9 @@ class Kit < ApplicationRecord # @param inventory [View::Inventory] # @return [Boolean] - def can_deactivate?(inventory) - if inventory - inventory.quantity_for(item_id: item.id).zero? - else - inventory_items.where('quantity > 0').none? - end + def can_deactivate?(inventory = nil) + inventory ||= View::Inventory.new(organization_id) + inventory.quantity_for(item_id: item.id).zero? end def deactivate diff --git a/app/models/organization.rb b/app/models/organization.rb index d8f46221a6..06279d7db2 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -193,11 +193,7 @@ def address_inline end def total_inventory - if Event.read_events?(self) - View::Inventory.total_inventory(id) - else - inventory_items.sum(:quantity) || 0 - end + View::Inventory.total_inventory(id) end def self.seed_items(organization = Organization.all) diff --git a/app/models/organization_stats.rb b/app/models/organization_stats.rb index 726a988c5d..f314e36201 100644 --- a/app/models/organization_stats.rb +++ b/app/models/organization_stats.rb @@ -26,8 +26,8 @@ def donation_sites_added def locations_with_inventory return [] unless storage_locations - inventoried_storage_location_ids = InventoryItem.where(storage_location: storage_locations).pluck(:storage_location_id) - storage_locations.select { |location| inventoried_storage_location_ids.include? location.id } + inventory = View::Inventory.new(current_organization.id) + storage_locations.select { |loc| inventory.quantity_for(storage_location: loc.id).positive? } end private diff --git a/app/models/partner.rb b/app/models/partner.rb index 301c192208..d75dd0ccc1 100644 --- a/app/models/partner.rb +++ b/app/models/partner.rb @@ -185,7 +185,10 @@ def self.csv_export_headers "Contact Name", "Contact Phone", "Contact Email", - "Notes" + "Notes", + "Counties Served", + "Providing Diapers", + "Providing Period Supplies" ] end @@ -202,10 +205,21 @@ def csv_export_attributes contact_person[:name], contact_person[:phone], contact_person[:email], - notes + notes, + profile.county_list_by_region, + providing_diapers, + providing_period_supplies ] end + def providing_diapers + distributions.in_last_12_months.with_diapers.any? ? "Y" : "N" + end + + def providing_period_supplies + distributions.in_last_12_months.with_period_supplies.any? ? "Y" : "N" + end + def contact_person return @contact_person if @contact_person diff --git a/app/models/partners/item_request.rb b/app/models/partners/item_request.rb index 4d38ccb2de..d10fc2e920 100644 --- a/app/models/partners/item_request.rb +++ b/app/models/partners/item_request.rb @@ -35,5 +35,15 @@ def request_unit_is_supported errors.add(:request_unit, "is not supported") end end + + def name_with_unit(quantity_override = nil) + if item + if Flipper.enabled?(:enable_packs) && request_unit.present? + "#{name} - #{request_unit.pluralize(quantity_override || quantity.to_i)}" + else + name + end + end + end end end diff --git a/app/models/partners/profile.rb b/app/models/partners/profile.rb index b2806db5bf..271ec146b5 100644 --- a/app/models/partners/profile.rb +++ b/app/models/partners/profile.rb @@ -91,6 +91,7 @@ class Profile < Base has_many :served_areas, foreign_key: "partner_profile_id", class_name: "Partners::ServedArea", dependent: :destroy, inverse_of: :partner_profile + has_many :counties, through: :served_areas accepts_nested_attributes_for :served_areas, allow_destroy: true has_many_attached :documents @@ -125,6 +126,11 @@ def split_pick_up_emails pick_up_email.split(/,|\s+/).compact_blank end + def county_list_by_region + # provides a county list in case insensitive alpha order, by region, then county name + counties.order(%w(lower(region) lower(name))).pluck(:name).join("; ") + end + private def check_social_media @@ -142,13 +148,23 @@ def client_share_is_0_or_100 # their allocation actually is total = client_share_total if total != 0 && total != 100 - errors.add(:base, "Total client share must be 0 or 100") + if Flipper.enabled?("partner_step_form") + # need to set errors on specific fields within the form so that it can be mapped to a section + errors.add(:client_share, "Total client share must be 0 or 100") + else + errors.add(:base, "Total client share must be 0 or 100") + end end end def has_at_least_one_request_setting if !(enable_child_based_requests || enable_individual_requests || enable_quantity_based_requests) - errors.add(:base, "At least one request type must be set") + if Flipper.enabled?("partner_step_form") + # need to set errors on specific fields within the form so that it can be mapped to a section + errors.add(:enable_child_based_requests, "At least one request type must be set") + else + errors.add(:base, "At least one request type must be set") + end end end diff --git a/app/models/partners/served_area.rb b/app/models/partners/served_area.rb index 5fa2ea92a5..00bb758c26 100644 --- a/app/models/partners/served_area.rb +++ b/app/models/partners/served_area.rb @@ -16,6 +16,6 @@ class ServedArea < ApplicationRecord belongs_to :partner_profile, class_name: "Partners::Profile" belongs_to :county validates :client_share, numericality: {only_integer: true} - validates :client_share, inclusion: {in: 1..100} + validates :client_share, inclusion: {in: 1..100, message: "Client share must be between 1 and 100 inclusive"} end end diff --git a/app/models/purchase.rb b/app/models/purchase.rb index 4cfa2bb0fd..734d36fff7 100644 --- a/app/models/purchase.rb +++ b/app/models/purchase.rb @@ -81,6 +81,20 @@ def remove(item) line_item&.destroy end + def self.organization_summary_by_dates(organization, date_range) + purchases = where(organization: organization).during(date_range) + + OpenStruct.new( + amount_spent: purchases.sum(:amount_spent_in_cents), + recent_purchases: purchases.recent.includes(:vendor), + period_supplies: purchases.sum(:amount_spent_on_period_supplies_cents), + diapers: purchases.sum(:amount_spent_on_diapers_cents), + adult_incontinence: purchases.sum(:amount_spent_on_adult_incontinence_cents), + other: purchases.sum(:amount_spent_on_other_cents), + total_items: purchases.joins(:line_items).sum(:quantity) + ) + end + private def combine_duplicates diff --git a/app/models/request.rb b/app/models/request.rb index 7509ab6c26..7e78fb0133 100644 --- a/app/models/request.rb +++ b/app/models/request.rb @@ -7,6 +7,7 @@ # discard_reason :text # discarded_at :datetime # request_items :jsonb +# request_type :string # status :integer default("pending") # created_at :datetime not null # updated_at :datetime not null @@ -31,6 +32,7 @@ class Request < ApplicationRecord has_many :child_item_requests, through: :item_requests enum :status, { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, prefix: true + enum :request_type, %w[quantity individual child].map { |v| [v, v] }.to_h validates :distribution_id, uniqueness: true, allow_nil: true validate :item_requests_uniqueness_by_item_id @@ -45,6 +47,7 @@ class Request < ApplicationRecord scope :by_partner, ->(partner_id) { where(partner_id: partner_id) } # status scope to allow filtering by status scope :by_status, ->(status) { where(status: status) } + scope :by_request_type, ->(request_type) { where(request_type: request_type) } scope :during, ->(range) { where(created_at: range) } scope :for_csv_export, ->(organization, *) { where(organization: organization) @@ -60,6 +63,10 @@ def user_email partner_user_id ? User.find_by(id: partner_user_id).email : Partner.find_by(id: partner_id).email end + def request_type_label + request_type&.first&.capitalize + end + private def item_requests_uniqueness_by_item_id diff --git a/app/models/request_item.rb b/app/models/request_item.rb index a22feb5f0d..e4f180d01f 100644 --- a/app/models/request_item.rb +++ b/app/models/request_item.rb @@ -13,9 +13,6 @@ def self.from_json(json, request, inventory = nil) if inventory on_hand = inventory.quantity_for(item_id: item.id) on_hand_for_location = inventory.quantity_for(storage_location: location&.id, item_id: item.id) - else - on_hand = request.organization.inventory_items.where(item_id: item.id).sum(:quantity) - on_hand_for_location = location&.inventory_items&.where(item_id: item.id)&.sum(:quantity) end new(item, quantity, unit, on_hand, on_hand_for_location&.positive? ? on_hand_for_location : 'N/A') end diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 3c7f7a2f5f..94f2a0a23e 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -32,14 +32,13 @@ class StorageLocation < ApplicationRecord dependent: :destroy has_many :donations, dependent: :destroy has_many :distributions, dependent: :destroy - has_many :items, through: :inventory_items has_many :transfers_from, class_name: "Transfer", inverse_of: :from, - foreign_key: :id, + foreign_key: :from_id, dependent: :destroy has_many :transfers_to, class_name: "Transfer", inverse_of: :to, - foreign_key: :id, + foreign_key: :to_id, dependent: :destroy has_many :kit_allocations, dependent: :destroy @@ -53,54 +52,49 @@ class StorageLocation < ApplicationRecord include Filterable include Exportable - scope :containing, ->(item_id) { - joins(:inventory_items).where("inventory_items.item_id = ?", item_id) - } - scope :has_inventory_items, -> { - includes(:inventory_items).where.not(inventory_items: { id: nil }) - } scope :alphabetized, -> { order(:name) } scope :for_csv_export, ->(organization, *) { where(organization: organization) } scope :active_locations, -> { where(discarded_at: nil) } + scope :with_transfers_to, ->(organization) { + joins(:transfers_to).where(organization_id: organization.id).distinct.order(:name) + } + scope :with_transfers_from, ->(organization) { + joins(:transfers_from).where(organization_id: organization.id).distinct.order(:name) + } # @param organization [Organization] # @param inventory [View::Inventory] def self.items_inventoried(organization, inventory = nil) - if inventory - inventory - .all_items - .uniq(&:item_id) - .sort_by(&:name) - .map { |i| OpenStruct.new(name: i.name, id: i.item_id) } - else - organization.items.joins(:storage_locations).select(:id, :name).group(:id, :name).order(name: :asc) - end + inventory ||= View::Inventory.new(organization.id) + inventory + .all_items + .uniq(&:item_id) + .sort_by(&:name) + .map { |i| OpenStruct.new(name: i.name, id: i.item_id) } end - def item_total(item_id) - inventory_items.where(item_id: item_id).pick(:quantity) || 0 + # @return [Array] + def items + View::Inventory.items_for_location(self).map(&:db_item) end + # @return [Integer] def size - inventory_items.sum(:quantity) + View::Inventory.items_for_location(self).map(&:quantity).sum end - def total_active_inventory_count - active_inventory_items - .select('items.quantity') - .sum(:quantity) + # @param item_id [Integer] + # @return [Integer] + def item_total(item_id) + View::Inventory.new(organization_id) + .quantity_for(storage_location: id, item_id: item_id) end + # @param inventory [View::Inventory] + # @return [Integer] def inventory_total_value_in_dollars(inventory = nil) - if inventory - inventory.total_value_in_dollars(storage_location: id) - else - inventory_total_value = inventory_items.joins(:item).map do |inventory_item| - value_in_cents = inventory_item.item.try(:value_in_cents) - value_in_cents * inventory_item.quantity - end.reduce(:+) - inventory_total_value.present? ? (inventory_total_value.to_f / 100) : 0 - end + inventory ||= View::Inventory.new(organization_id) + inventory&.total_value_in_dollars(storage_location: id) end def to_csv @@ -148,77 +142,6 @@ def self.import_inventory(filename, org, loc) AdjustmentCreateService.new(adjustment).call end - # FIXME: After this is stable, revisit how we do logging - def increase_inventory(itemizable_array) - # This is, at least for now, how we log changes to the inventory made in this call - log = {} - # Iterate through each of the line-items in the moving box - itemizable_array.each do |item_hash| - # Locate the storage box for the item, or create a new storage box for it - inventory_item = inventory_items.find_or_create_by!(item_id: item_hash[:item_id]) - # Increase the quantity-on-record for that item - new_quantity = inventory_item.quantity + item_hash[:quantity].to_i - inventory_item.update!(quantity: new_quantity) - # Record in the log that this has occurred - log[item_hash[:item_id]] = "+#{item_hash[:quantity]}" - end - # log could be pulled from dirty AR stuff? - # Save the final changes -- does this need to occur here? - save - # return log - log - end - - # TODO: re-evaluate this for optimization - def decrease_inventory(itemizable_array) - # This is, at least for now, how we log changes to the inventory made in this call - log = {} - # This tracks items that have insufficient inventory counts to be reduced as much - insufficient_items = [] - # Iterate through each of the line-items in the moving box - itemizable_array.each do |item_hash| - # Locate the storage box for the item, or create an empty storage box - inventory_item = inventory_items.find_by(item_id: item_hash[:item_id]) || inventory_items.build - # If we've got sufficient inventory in the storage box to fill the moving box, then continue - next unless inventory_item.quantity < item_hash[:quantity] - - # Otherwise, we need to record that there was insufficient inventory on-hand - insufficient_items << { - item_id: item_hash[:item_id], - item_name: item_hash[:name], - quantity_on_hand: inventory_item.quantity, - quantity_requested: item_hash[:quantity] - } - end - # NOTE: Could this be handled by a validation instead? - # If we found any insufficiencies - if insufficient_items.any? && !Event.read_events?(organization) - # Raise this custom error with information about each of the items that showed insufficient - # This bails out of the method! - raise Errors::InsufficientAllotment.new( - "Requested items exceed the available inventory.", - insufficient_items - ) - end - - # Re-run through the items in the moving box again - itemizable_array.each do |item_hash| - # Look for the moving box for this item -- we know there is sufficient quantity this time - # Raise AR:RNF if it fails to find it -- though that seems moot since it would have been - # captured by the previous block. - inventory_item = inventory_items.find_by(item_id: item_hash[:item_id]) - # Reduce the inventory box quantity - new_quantity = inventory_item.quantity - item_hash[:quantity] - inventory_item.update(quantity: new_quantity) - # Record in the log that this has occurred - log[item_hash[:item_id]] = "-#{item_hash[:quantity]}" - end - # log could be pulled from dirty AR stuff - save! - # return log - log - end - def validate_empty_inventory unless empty_inventory? errors.add(:base, "Cannot delete storage location containing inventory items with non-zero quantities") @@ -230,13 +153,6 @@ def self.csv_export_headers ["Name", "Address", "Square Footage", "Warehouse Type", "Total Inventory"] end - # TODO remove this method once read_events? is true everywhere - def csv_export_attributes - attributes = [name, address, square_footage, warehouse_type, total_active_inventory_count] - active_inventory_items.sort_by { |inv_item| inv_item.item.name }.each { |item| attributes << item.quantity } - attributes - end - # @param storage_locations [Array] # @param inventory [View::Inventory] # @return [String] @@ -257,17 +173,7 @@ def self.generate_csv_from_inventory(storage_locations, inventory) end def empty_inventory? - if Event.read_events?(organization) - inventory = View::Inventory.new(organization_id) - inventory.quantity_for(storage_location: id).zero? - else - inventory_items.map(&:quantity).all?(&:zero?) - end - end - - def active_inventory_items - inventory_items - .includes(:item) - .where(items: { active: true }) + inventory = View::Inventory.new(organization_id) + inventory.quantity_for(storage_location: id).zero? end end diff --git a/app/models/transfer.rb b/app/models/transfer.rb index cec1e3f634..49b2248f2a 100644 --- a/app/models/transfer.rb +++ b/app/models/transfer.rb @@ -30,16 +30,7 @@ class Transfer < ApplicationRecord } scope :during, ->(range) { where(created_at: range) } - def self.storage_locations_transferred_to_in(organization) - includes(:to).where(organization_id: organization.id).distinct(:to_id).collect(&:to).uniq.sort_by(&:name) - end - - def self.storage_locations_transferred_from_in(organization) - includes(:from).where(organization_id: organization.id).distinct(:from_id).collect(&:from).uniq.sort_by(&:name) - end - validates :from, :to, :organization, presence: true - validate :line_items_exist_in_inventory validate :storage_locations_belong_to_organization validate :storage_locations_must_be_different validate :from_storage_quantities @@ -90,11 +81,7 @@ def from_storage_quantities end def insufficient_items - if Event.read_events?(organization) - inventory = View::Inventory.new(organization_id) - line_items.select { |i| i.quantity > inventory.quantity_for(item_id: i.item_id) } - else - line_items.select { |i| i.quantity > from.item_total(i.item_id) } - end + inventory = View::Inventory.new(organization_id) + line_items.select { |i| i.quantity > inventory.quantity_for(item_id: i.item_id) } end end diff --git a/app/pdfs/distribution_pdf.rb b/app/pdfs/distribution_pdf.rb index d9f6affcd6..5942348593 100644 --- a/app/pdfs/distribution_pdf.rb +++ b/app/pdfs/distribution_pdf.rb @@ -164,9 +164,7 @@ def request_data "Packages"]] inventory = nil - if Event.read_events?(@distribution.organization) - inventory = View::Inventory.new(@distribution.organization_id) - end + inventory = View::Inventory.new(@distribution.organization_id) request_items = @distribution.request.request_items.map do |request_item| RequestItem.from_json(request_item, @distribution.request, inventory) end diff --git a/app/pdfs/donation_pdf.rb b/app/pdfs/donation_pdf.rb index 773e1b9095..bbf2a19eb0 100644 --- a/app/pdfs/donation_pdf.rb +++ b/app/pdfs/donation_pdf.rb @@ -20,9 +20,13 @@ def initialize(donation) @address = nil @email = nil when Donation::SOURCES[:product_drive] - @name = donation.product_drive_participant.business_name - @address = donation.product_drive_participant.address - @email = donation.product_drive_participant.email + if donation.product_drive_participant + @name = donation.product_drive_participant.business_name + @address = donation.product_drive_participant.address + @email = donation.product_drive_participant.email + else + @name = "Product Drive -- #{donation.product_drive.name}" + end when Donation::SOURCES[:misc] @name = "Misc. Donation" @address = nil diff --git a/app/queries/items_by_storage_collection_and_quantity_query.rb b/app/queries/items_by_storage_collection_and_quantity_query.rb index cf252dbd3c..664c690d4b 100644 --- a/app/queries/items_by_storage_collection_and_quantity_query.rb +++ b/app/queries/items_by_storage_collection_and_quantity_query.rb @@ -2,59 +2,29 @@ # We're using query objects for some of these more complicated queries to get # the raw SQL out of the models and encapsulate it. class ItemsByStorageCollectionAndQuantityQuery - def self.call(organization:, filter_params:, inventory: nil) - if inventory - items = organization.items.active.order(name: :asc).class_filter(filter_params) - return items.to_h do |item| - locations = inventory.storage_locations_for_item(item.id).map do |sl| - { - id: sl, - name: inventory.storage_location_name(sl), - quantity: inventory.quantity_for(storage_location: sl, item_id: item.id) - } - end - [ - item.id, - { - item_id: item.id, - item_name: item.name, - item_on_hand_minimum_quantity: item.on_hand_minimum_quantity, - item_on_hand_recommended_quantity: item.on_hand_recommended_quantity, - item_value: item.value_in_cents, - item_barcode_count: item.barcode_count, - locations: locations, - quantity: inventory.quantity_for(item_id: item.id) - } - ] - end - end - - items_by_storage_collection = ItemsByStorageCollectionQuery.new(organization: organization, filter_params: filter_params).call - items_by_storage_collection_and_quantity = Hash.new - items_by_storage_collection.each do |row| - unless items_by_storage_collection_and_quantity.key?(row.id) - items_by_storage_collection_and_quantity[row.id] = { - item_id: row.id, - item_name: row.name, - item_on_hand_minimum_quantity: row.on_hand_minimum_quantity, - item_on_hand_recommended_quantity: row.on_hand_recommended_quantity, - item_value: row.value_in_cents, - item_barcode_count: row.barcode_count, - locations: [], - quantity: 0 + def self.call(organization:, filter_params:, inventory:) + items = organization.items.active.order(name: :asc).class_filter(filter_params) + items.to_h do |item| + locations = inventory.storage_locations_for_item(item.id).map do |sl| + { + id: sl, + name: inventory.storage_location_name(sl), + quantity: inventory.quantity_for(storage_location: sl, item_id: item.id) } end - - if row.storage_id - items_by_storage_collection_and_quantity[row.id][:locations] << { - id: row.storage_id, - name: row.storage_name, - quantity: row.quantity + [ + item.id, + { + item_id: item.id, + item_name: item.name, + item_on_hand_minimum_quantity: item.on_hand_minimum_quantity, + item_on_hand_recommended_quantity: item.on_hand_recommended_quantity, + item_value: item.value_in_cents, + item_barcode_count: item.barcode_count, + locations: locations, + quantity: inventory.quantity_for(item_id: item.id) } - end - items_by_storage_collection_and_quantity[row.id][:quantity] += row.quantity || 0 + ] end - - items_by_storage_collection_and_quantity end end diff --git a/app/queries/items_by_storage_collection_query.rb b/app/queries/items_by_storage_collection_query.rb deleted file mode 100644 index 589c586f33..0000000000 --- a/app/queries/items_by_storage_collection_query.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Creates a query object for retrieving the items, grouped by storage location -# We're using query objects for some of these more complicated queries to get -# the raw SQL out of the models and encapsulate it. -class ItemsByStorageCollectionQuery - attr_reader :organization - attr_reader :filter_params - - def initialize(organization:, filter_params:) - @organization = organization - @filter_params = filter_params - end - - # rubocop:disable Naming/MemoizedInstanceVariableName - def call - @items ||= organization - .items - .active - .joins(' LEFT OUTER JOIN "inventory_items" ON "inventory_items"."item_id" = "items"."id"') - .joins(' LEFT OUTER JOIN "storage_locations" ON "storage_locations"."id" = "inventory_items"."storage_location_id"') - .select(' - items.id, - items.name, - items.barcode_count, - items.partner_key, - items.value_in_cents, - items.on_hand_minimum_quantity, - items.on_hand_recommended_quantity, - storage_locations.name as storage_name, - storage_locations.id as storage_id, - sum(inventory_items.quantity) as quantity - ') - .group("storage_locations.name, storage_locations.id, items.id, items.name") - .order(name: :asc).class_filter(filter_params) - end - # rubocop:enable Naming/MemoizedInstanceVariableName -end diff --git a/app/queries/low_inventory_query.rb b/app/queries/low_inventory_query.rb index 500fa53a38..5deee91d7a 100644 --- a/app/queries/low_inventory_query.rb +++ b/app/queries/low_inventory_query.rb @@ -1,45 +1,22 @@ class LowInventoryQuery def self.call(organization) - if Event.read_events?(organization) - inventory = View::Inventory.new(organization.id) - items = inventory.all_items + inventory = View::Inventory.new(organization.id) + items = inventory.all_items.uniq(&:item_id) - low_inventory_items = [] - items.each do |item| - quantity = inventory.quantity_for(item_id: item.id) - if quantity < item.on_hand_minimum_quantity.to_i || quantity < item.on_hand_recommended_quantity.to_i - low_inventory_items.push(OpenStruct.new( - id: item.id, - name: item.name, - on_hand_minimum_quantity: item.on_hand_minimum_quantity, - on_hand_recommended_quantity: item.on_hand_recommended_quantity, - total_quantity: quantity - )) - end + low_inventory_items = [] + items.each do |item| + quantity = inventory.quantity_for(item_id: item.id) + if quantity < item.on_hand_minimum_quantity.to_i || quantity < item.on_hand_recommended_quantity.to_i + low_inventory_items.push(OpenStruct.new( + id: item.id, + name: item.name, + on_hand_minimum_quantity: item.on_hand_minimum_quantity, + on_hand_recommended_quantity: item.on_hand_recommended_quantity, + total_quantity: quantity + )) end - - low_inventory_items.sort_by { |item| item[:name] } - - else - sql_query = <<-SQL - SELECT - items.id, - items.name, - items.on_hand_minimum_quantity, - items.on_hand_recommended_quantity, - sum(inventory_items.quantity) as total_quantity - FROM inventory_items - JOIN items ON items.id = inventory_items.item_id - JOIN storage_locations ON storage_locations.id = inventory_items.storage_location_id - WHERE storage_locations.organization_id = ? - GROUP BY items.id, items.name, items.on_hand_minimum_quantity, items.on_hand_recommended_quantity - HAVING sum(inventory_items.quantity) < items.on_hand_minimum_quantity - OR sum(inventory_items.quantity) < items.on_hand_recommended_quantity - ORDER BY items.name - SQL - - sanitized_sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql_query, organization.id]) - ActiveRecord::Base.connection.execute(sanitized_sql).to_a end + + low_inventory_items.sort_by { |item| item[:name] } end end diff --git a/app/services/adjustment_create_service.rb b/app/services/adjustment_create_service.rb index a068e71fda..1b16f3c8d6 100644 --- a/app/services/adjustment_create_service.rb +++ b/app/services/adjustment_create_service.rb @@ -22,11 +22,6 @@ def call # Make the necessary changes in the db @adjustment.save AdjustmentEvent.publish(adjustment) - # Split into positive and negative portions. - # N.B. -- THIS CHANGES THE ORIGINAL LINE ITEMS ON @adjustment DO **NOT** RESAVE AS THAT WILL CHANGE ANY NEGATIVE LINE ITEMS ON THE ADJUSTMENT TO POSITIVES - increasing_adjustment, decreasing_adjustment = @adjustment.split_difference - @adjustment.storage_location.increase_inventory(increasing_adjustment.line_item_values) - @adjustment.storage_location.decrease_inventory(decreasing_adjustment.line_item_values) rescue Errors::InsufficientAllotment, InventoryError => e @adjustment.errors.add(:base, e.message) raise ActiveRecord::Rollback @@ -44,13 +39,6 @@ def enough_inventory_for_decreases? return false if @adjustment.storage_location.nil? @adjustment.line_items.each do |line_item| next unless line_item.quantity.negative? - - inventory_item = @adjustment.storage_location.inventory_items.find_by(item: line_item.item) - if inventory_item.nil? - @adjustment.errors.add(:inventory, "#{line_item.item.name} is not available to be removed from this storage location") - elsif inventory_item.quantity < line_item.quantity * -1 - @adjustment.errors.add(:inventory, "The requested reduction of #{line_item.quantity * -1} #{line_item.item.name} items exceed the available inventory") - end end @adjustment.errors.none? end diff --git a/app/services/allocate_kit_inventory_service.rb b/app/services/allocate_kit_inventory_service.rb deleted file mode 100644 index 9451010d28..0000000000 --- a/app/services/allocate_kit_inventory_service.rb +++ /dev/null @@ -1,98 +0,0 @@ -class AllocateKitInventoryService - attr_reader :kit, :storage_location, :increase_by, :error - - def initialize(kit:, storage_location:, increase_by:) - @kit = kit - @storage_location = storage_location - @increase_by = increase_by - end - - def allocate - validate_storage_location - if error.nil? - ApplicationRecord.transaction do - allocate_inventory_items_and_increase_kit_quantity - KitAllocateEvent.publish(@kit, @storage_location.id, @increase_by) - end - end - rescue Errors::InsufficientAllotment => e - kit.line_items.assign_insufficiency_errors(e.insufficient_items) - Rails.logger.error "[!] #{self.class.name} failed because of Insufficient Allotment #{kit.organization.short_name}: #{kit.errors.full_messages} [#{e.message}]" - set_error(e) - rescue StandardError => e - Rails.logger.error "[!] #{self.class.name} failed to allocate items for a kit #{kit.name}: #{storage_location.errors.full_messages} [#{e.inspect}]" - set_error(e) - ensure - return self - end - - private - - def validate_storage_location - raise Errors::StorageLocationDoesNotMatch if storage_location.organization != kit.organization - end - - def allocate_inventory_items_and_increase_kit_quantity - ActiveRecord::Base.transaction do - storage_location.decrease_inventory(kit_content) - storage_location.increase_inventory(associated_kit_item) - allocate_inventory_in_and_inventory_out - end - end - - def allocate_inventory_in_and_inventory_out - allocate_inventory_in - allocate_inventory_out - end - - def allocate_inventory_out - kit_allocation = KitAllocation.find_or_create_by!(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_out") - line_items = kit_allocation.line_items - if line_items.present? - kit_content.each_with_index do |line_item, index| - line_item_record = line_items[index] - new_quantity = line_item_record[:quantity] + line_item[:quantity].to_i * -1 - line_item_record.update!(quantity: new_quantity) - end - else - kit_content.each do |line_item| - kit_allocation.line_items.create!(item_id: line_item[:item_id], quantity: line_item[:quantity].to_i * -1) - end - end - end - - def allocate_inventory_in - kit_allocation = KitAllocation.find_or_create_by!(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_in") - line_items = kit_allocation.line_items - if line_items.present? - kit_item = line_items.first - new_quantity = kit_item[:quantity] + increase_by - kit_item.update!(quantity: new_quantity) - else - kit_allocation.line_items.create!(associated_kit_item) - end - end - - def set_error(error) - @error = error.message - end - - def kit_content - kit.line_item_values.map do |item| - item.merge({ - quantity: item[:quantity] * increase_by - }) - end - end - - def associated_kit_item - [ - { - item_id: kit.item.id, - quantity: increase_by - } - ] - end -end diff --git a/app/services/deallocate_kit_inventory_service.rb b/app/services/deallocate_kit_inventory_service.rb deleted file mode 100644 index a394556a09..0000000000 --- a/app/services/deallocate_kit_inventory_service.rb +++ /dev/null @@ -1,106 +0,0 @@ -class DeallocateKitInventoryService - attr_reader :error - - def initialize(kit:, storage_location:, decrease_by:) - @kit = kit - @storage_location = storage_location - @decrease_by = decrease_by - end - - def deallocate - validate_storage_location - if error.nil? - ApplicationRecord.transaction do - deallocate_inventory_items - KitDeallocateEvent.publish(@kit, @storage_location, @decrease_by) - end - end - rescue StandardError => e - Rails.logger.error "[!] #{self.class.name} failed to allocate items for a kit #{kit.name}: #{storage_location.errors.full_messages} [#{e.inspect}]" - set_error(e) - ensure - return self - end - - private - - attr_reader :kit, :storage_location, :decrease_by - - def validate_storage_location - raise Errors::StorageLocationDoesNotMatch if storage_location.organization != kit.organization - end - - def deallocate_inventory_items - ActiveRecord::Base.transaction do - storage_location.increase_inventory(kit_content) - storage_location.decrease_inventory(associated_kit_item) - deallocate_inventory_in_and_inventory_out - end - end - - def deallocate_inventory_in_and_inventory_out - deallocate_inventory_in - deallocate_inventory_out - end - - def deallocate_inventory_out - kit_allocation = KitAllocation.find_by(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_out") - if kit_allocation.present? - line_items = kit_allocation.line_items - kit_content.each_with_index do |line_item, index| - line_item_record = line_items[index] - new_quantity = line_item_record[:quantity] + line_item[:quantity].to_i - if new_quantity.to_i == 0 - kit_allocation.destroy! - break - elsif new_quantity.to_i > 0 - raise StandardError.new("Inconsistent inventory out") - else - line_item_record.update!(quantity: new_quantity) - end - end - else - raise Errors::KitAllocationNotExists - end - end - - def deallocate_inventory_in - kit_allocation = KitAllocation.find_by(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_in") - if kit_allocation.present? - kit_item = kit_allocation.line_items.first - new_quantity = kit_item[:quantity].to_i - decrease_by - if new_quantity.to_i == 0 - kit_allocation.destroy! - elsif new_quantity.to_i < 0 - raise StandardError.new("Inconsistent inventory in") - else - kit_item.update!(quantity: new_quantity) - end - else - raise Errors::KitAllocationNotExists - end - end - - def set_error(error) - @error = error.message - end - - def kit_content - kit.line_item_values.map do |item| - item.merge({ - quantity: item[:quantity] * decrease_by - }) - end - end - - def associated_kit_item - [ - { - item_id: kit.item.id, - quantity: decrease_by - } - ] - end -end diff --git a/app/services/distribution_create_service.rb b/app/services/distribution_create_service.rb index d98a848a0a..7e7dae2f5e 100644 --- a/app/services/distribution_create_service.rb +++ b/app/services/distribution_create_service.rb @@ -14,7 +14,6 @@ def call DistributionEvent.publish(distribution) - distribution.storage_location.decrease_inventory(distribution.line_item_values) distribution.reload @request&.update!(distribution_id: distribution.id, status: 'fulfilled') send_notification if distribution.partner&.send_reminders diff --git a/app/services/distribution_destroy_service.rb b/app/services/distribution_destroy_service.rb index f788bd4bdb..ca12dd073b 100644 --- a/app/services/distribution_destroy_service.rb +++ b/app/services/distribution_destroy_service.rb @@ -7,7 +7,6 @@ def call perform_distribution_service do DistributionDestroyEvent.publish(distribution) distribution.destroy! - distribution.storage_location.increase_inventory(distribution.line_item_values) end end end diff --git a/app/services/distribution_itemized_breakdown_service.rb b/app/services/distribution_itemized_breakdown_service.rb index ecd8a03a97..44c4caadf7 100644 --- a/app/services/distribution_itemized_breakdown_service.rb +++ b/app/services/distribution_itemized_breakdown_service.rb @@ -18,10 +18,7 @@ def initialize(organization:, distribution_ids:) # # @return [Array] def fetch - inventory = nil - if Event.read_events?(@organization) - inventory = View::Inventory.new(@organization.id) - end + inventory = View::Inventory.new(@organization.id) current_onhand = current_onhand_quantities(inventory) current_min_onhand = current_onhand_minimums(inventory) items_distributed = fetch_items_distributed @@ -62,19 +59,11 @@ def distributions end def current_onhand_quantities(inventory) - if inventory - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } - else - organization.inventory_items.group("items.name").sum(:quantity) - end + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } end def current_onhand_minimums(inventory) - if inventory - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).max] } - else - organization.inventory_items.group("items.name").maximum("items.on_hand_minimum_quantity") - end + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).max] } end def fetch_items_distributed diff --git a/app/services/distribution_totals_service.rb b/app/services/distribution_totals_service.rb new file mode 100644 index 0000000000..28ad15d58e --- /dev/null +++ b/app/services/distribution_totals_service.rb @@ -0,0 +1,65 @@ +class DistributionTotalsService + def initialize(distributions, filter_params) + @filter_params = filter_params + @distribution_quantities = calculate_quantities(distributions) + @distribution_values = calculate_values(distributions) + end + + def total_quantity(filter_ids = []) + totals = filter_ids.present? ? @distribution_quantities.slice(*filter_ids) : @distribution_quantities + totals.sum { |_, quantity| quantity } + end + + def total_value(filter_ids = []) + totals = filter_ids.present? ? @distribution_values.slice(*filter_ids) : @distribution_values + totals.sum { |_, value| value } + end + + def fetch_value(id) + @distribution_values.fetch(id) + end + + def fetch_quantity(id) + @distribution_quantities.fetch(id) + end + + private + + attr_reader :filter_params + + # Returns hash of total quantity of items per distribution + # Quantity of items after item filtering (id/category) + # + # @return [Hash] + def calculate_quantities(distributions) + distributions + .class_filter(filter_params) + .left_joins(line_items: [:item]) + .group("distributions.id, line_items.id, items.id") + .pluck( + Arel.sql( + "distributions.id, + COALESCE(SUM(line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS quantity" + ) + ) + .to_h + end + + # Returns hash of total value of items per distribution WIHOUT item id/category filter + # Value of entire distribution (not reduced by filtered items) + # + # @return [Hash] + def calculate_values(distributions) + Distribution + .where(id: distributions.class_filter(filter_params)) + .left_joins(line_items: [:item]) + .group("distributions.id, line_items.id, items.id") + .pluck( + Arel.sql( + "distributions.id, + COALESCE(SUM(COALESCE(items.value_in_cents, 0) * line_items.quantity) OVER (PARTITION BY distributions.id), 0) AS value" + ) + ) + .to_h + end +end diff --git a/app/services/distribution_update_service.rb b/app/services/distribution_update_service.rb index da612d0f95..03b8049da7 100644 --- a/app/services/distribution_update_service.rb +++ b/app/services/distribution_update_service.rb @@ -16,7 +16,6 @@ def call ItemizableUpdateService.call( itemizable: distribution, params: @params, - type: :decrease, event_class: DistributionEvent ) diff --git a/app/services/donation_create_service.rb b/app/services/donation_create_service.rb index fb782ef7d8..c349fc330b 100644 --- a/app/services/donation_create_service.rb +++ b/app/services/donation_create_service.rb @@ -5,7 +5,6 @@ def call(donation) unless donation.save raise donation.errors.full_messages.join("\n") end - donation.storage_location.increase_inventory(donation.line_item_values) DonationEvent.publish(donation) end end diff --git a/app/services/donation_destroy_service.rb b/app/services/donation_destroy_service.rb index b95c01bae9..47cb88fa58 100644 --- a/app/services/donation_destroy_service.rb +++ b/app/services/donation_destroy_service.rb @@ -10,7 +10,6 @@ def call ActiveRecord::Base.transaction do organization = Organization.find(organization_id) donation = organization.donations.find(donation_id) - donation.storage_location.decrease_inventory(donation.line_item_values) DonationDestroyEvent.publish(donation) donation.destroy! end diff --git a/app/services/donation_itemized_breakdown_service.rb b/app/services/donation_itemized_breakdown_service.rb index e909d3b0ae..9a38e245ce 100644 --- a/app/services/donation_itemized_breakdown_service.rb +++ b/app/services/donation_itemized_breakdown_service.rb @@ -13,10 +13,7 @@ def initialize(organization:, donation_ids:) end def fetch - inventory = nil - if Event.read_events?(@organization) - inventory = View::Inventory.new(@organization.id) - end + inventory = View::Inventory.new(@organization.id) items_donated = fetch_items_donated current_onhand = current_onhand_quantities(inventory) @@ -41,11 +38,7 @@ def donations end def current_onhand_quantities(inventory) - if inventory - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } - else - organization.inventory_items.group("items.name").sum(:quantity) - end + inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } end def fetch_items_donated diff --git a/app/services/exports/export_request_service.rb b/app/services/exports/export_request_service.rb index e7bfba43af..ae7a224eaa 100644 --- a/app/services/exports/export_request_service.rb +++ b/app/services/exports/export_request_service.rb @@ -3,7 +3,7 @@ class ExportRequestService DELETED_ITEMS_COLUMN_HEADER = ''.freeze def initialize(requests) - @requests = requests.includes(:partner) + @requests = requests.includes(:partner, {item_requests: :item}) end def generate_csv @@ -46,6 +46,9 @@ def base_table "Requestor" => ->(request) { request.partner.name }, + "Type" => ->(request) { + request.request_type&.humanize + }, "Status" => ->(request) { request.status.humanize } @@ -61,7 +64,25 @@ def item_headers end def compute_item_headers - item_names = items.pluck(:name) + # This reaches into the item, handling invalid deleted items + item_names = Set.new + all_item_requests.each do |item_request| + if item_request.item + item = item_request.item + item_names << item.name + 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 + end + end # Adding this to handle cases in which a requested item # has been deleted. Normally this wouldn't be neccessary, @@ -75,38 +96,20 @@ def build_row_data(request) row += Array.new(item_headers.size, 0) - request.request_items.each do |request_item| - item_name = fetch_item_name(request_item['item_id']) || DELETED_ITEMS_COLUMN_HEADER + request.item_requests.each do |item_request| + item_name = item_request.name_with_unit(0) || DELETED_ITEMS_COLUMN_HEADER item_column_idx = headers_with_indexes[item_name] - - if item_name == DELETED_ITEMS_COLUMN_HEADER - # Add to the deleted column for every item that - # does not match any existing Item. - row[item_column_idx] ||= 0 - end - row[item_column_idx] += request_item['quantity'] + row[item_column_idx] ||= 0 + row[item_column_idx] += item_request.quantity.to_i end row end - def fetch_item_name(item_id) - @item_name_to_id_map ||= items.inject({}) do |acc, item| - acc[item.id] = item.name - acc - end - - @item_name_to_id_map[item_id] - end - - def items - return @items if @items - - item_ids = requests.flat_map do |request| - request.request_items.map { |item| item['item_id'] } - end - - @items ||= Item.where(id: item_ids) + def all_item_requests + return @all_item_requests if @all_item_requests + @all_item_requests ||= Partners::ItemRequest.where(request: requests).includes(item: :request_units) + @all_item_requests end end end diff --git a/app/services/historical_trend_service.rb b/app/services/historical_trend_service.rb index f8615a625b..5f0560804c 100644 --- a/app/services/historical_trend_service.rb +++ b/app/services/historical_trend_service.rb @@ -4,26 +4,36 @@ def initialize(organization_id, type) @type = type end + # Returns: [{:name=>"Adult Briefs (XXL)", :data=>[0, 0, 0, 0, 0, 0, 0, 0, 0, 416, 0, 0], :visible=>false}] + # :data contains quantity from 11 months ago to current month def series - # Preload line_items with a single query to avoid N+1 queries. - items_with_line_items = @organization.items.active - .includes(:line_items) - .where(line_items: {itemizable_type: @type, created_at: 1.year.ago.beginning_of_month..Time.current}) - .order(:name) + type_symbol = @type.tableize.to_sym # :distributions, :donations, :purchases + records_for_type = @organization.send(type_symbol) + .includes(items: :line_items) + .where(issued_at: 1.year.ago.beginning_of_month..Time.current) - month_offset = [*1..12].rotate(Time.zone.today.month) - default_dates = (1..12).index_with { |i| 0 } + array_of_items = [] - items_with_line_items.each_with_object([]) do |item, array_of_items| - dates = default_dates.deep_dup + records_for_type.each do |record| + index = record.issued_at.month - Date.current.month - 1 - item.line_items.each do |line_item| - month = line_item.created_at.month - index = month_offset.index(month) + 1 - dates[index] = dates[index] + line_item.quantity - end + record.line_items.each do |line_item| + name = line_item.item.name + quantity = line_item.quantity + next if quantity.zero? - array_of_items << {name: item.name, data: dates.values, visible: false} unless dates.values.sum.zero? + existing_item = array_of_items.find { |item| item[:name] == name } + if existing_item + quantity_per_month = existing_item[:data] + quantity_per_month[index] += quantity + else + quantity_per_month = Array.new(12, 0) + quantity_per_month[index] += quantity + array_of_items << {name:, data: quantity_per_month, visible: false} + end + end end + + array_of_items.sort_by { |item| item[:name] } end end diff --git a/app/services/inventory_check_service.rb b/app/services/inventory_check_service.rb index 9a149df76a..72886dcdb4 100644 --- a/app/services/inventory_check_service.rb +++ b/app/services/inventory_check_service.rb @@ -1,49 +1,41 @@ class InventoryCheckService - attr_reader :error, :alert + attr_reader :minimum_alert, :recommended_alert def initialize(distribution) @distribution = distribution - @alert = nil - @error = nil + @minimum_alert = nil + @recommended_alert = nil end def call - @inventory = nil - if Event.read_events?(@distribution.organization) - @inventory = View::Inventory.new(@distribution.organization_id) - end + @inventory = View::Inventory.new(@distribution.organization_id) unless items_below_minimum_quantity.empty? - set_error + set_minimum_alert end unless deduplicate_items_below_recommended_quantity.empty? - set_alert + set_recommended_alert end self end - def set_error - @error = "The following items have fallen below the minimum " \ - "on hand quantity: #{items_below_minimum_quantity.map(&:name).sort.join(", ")}" + def set_minimum_alert + @minimum_alert = "The following items have fallen below the minimum " \ + "on hand quantity, bank-wide: #{items_below_minimum_quantity.map(&:name).sort.join(", ")}" end - def set_alert - @alert = "The following items have fallen below the recommended " \ - "on hand quantity: #{deduplicate_items_below_recommended_quantity.map(&:name).sort.join(", ")}" + def set_recommended_alert + @recommended_alert = "The following items have fallen below the recommended " \ + "on hand quantity, bank-wide: #{deduplicate_items_below_recommended_quantity.map(&:name).sort.join(", ")}" end def items_below_minimum_quantity # Done this way to prevent N+1 query on items unless @items_below_minimum_quantity item_ids = @distribution.line_items.select do |line_item| - if @inventory - quantity = @inventory.quantity_for(storage_location: @distribution.storage_location_id, item_id: line_item.item_id) - quantity < (line_item.item.on_hand_minimum_quantity || 0) - else - inventory_item = line_item.item.inventory_item_at(@distribution.storage_location.id) - inventory_item.lower_than_on_hand_minimum_quantity? - end + quantity = @inventory.quantity_for(item_id: line_item.item_id) + quantity < (line_item.item.on_hand_minimum_quantity || 0) end.map(&:item_id) @items_below_minimum_quantity = Item.find(item_ids) @@ -56,13 +48,8 @@ def items_below_recommended_quantity # Done this way to prevent N+1 query on items unless @items_below_recommended_quantity item_ids = @distribution.line_items.select do |line_item| - if @inventory - quantity = @inventory.quantity_for(storage_location: @distribution.storage_location_id, item_id: line_item.item_id) - quantity < (line_item.item.on_hand_recommended_quantity || 0) - else - inventory_item = line_item.item.inventory_item_at(@distribution.storage_location.id) - inventory_item.lower_than_on_hand_recommended_quantity? - end + quantity = @inventory.quantity_for(item_id: line_item.item_id) + quantity < (line_item.item.on_hand_recommended_quantity || 0) end.map(&:item_id) @items_below_recommended_quantity = Item.find(item_ids) diff --git a/app/services/item_create_service.rb b/app/services/item_create_service.rb index 62240b3ea8..60126ad803 100644 --- a/app/services/item_create_service.rb +++ b/app/services/item_create_service.rb @@ -7,19 +7,9 @@ def initialize(organization_id:, item_params:, request_unit_ids: []) def call new_item = organization.items.new(item_params) - organization.transaction do - new_item.save! - if Flipper.enabled?(:enable_packs) - new_item.sync_request_units!(@request_unit_ids) - end - - organization.storage_locations.each do |sl| - InventoryItem.create!( - storage_location_id: sl.id, - item_id: new_item.id, - quantity: 0 - ) - end + new_item.save! + if Flipper.enabled?(:enable_packs) + new_item.sync_request_units!(@request_unit_ids) end OpenStruct.new(success?: true, item: new_item) diff --git a/app/services/itemizable_update_service.rb b/app/services/itemizable_update_service.rb index 45dc7133da..039afabc76 100644 --- a/app/services/itemizable_update_service.rb +++ b/app/services/itemizable_update_service.rb @@ -1,10 +1,9 @@ module ItemizableUpdateService # @param itemizable [Itemizable] - # @param type [Symbol] :increase or :decrease - if the original line items added quantities (purchases or - # donations), use :increase. If the original line_items reduced quantities (distributions) use :decrease. # @param params [Hash] Parameters passed from the controller. Should include `line_item_attributes`. # @param event_class [Class] the event class to publish the itemizable to. - def self.call(itemizable:, type: :increase, params: {}, event_class: nil) + def self.call(itemizable:, params: {}, event_class: nil) + original_storage_location = itemizable.storage_location StorageLocation.transaction do item_ids = params[:line_items_attributes]&.values&.map { |i| i[:item_id].to_i } || [] inactive_item_names = Item.where(id: item_ids, active: false).pluck(:name) @@ -17,9 +16,6 @@ def self.call(itemizable:, type: :increase, params: {}, event_class: nil) verify_intervening_audit_on_storage_location_items(itemizable: itemizable, from_location_id: from_location.id, to_location_id: to_location.id) - apply_change_method = (type == :increase) ? :increase_inventory : :decrease_inventory - undo_change_method = (type == :increase) ? :decrease_inventory : :increase_inventory - previous = nil # TODO once event sourcing has been out for long enough, we can safely remove this if Event.where(eventable: itemizable).none? || UpdateExistingEvent.where(eventable: itemizable).any? @@ -29,14 +25,9 @@ def self.call(itemizable:, type: :increase, params: {}, event_class: nil) line_item_attrs = Array.wrap(params[:line_items_attributes]&.values) line_item_attrs.each { |attr| attr.delete(:id) } - update_storage_location(itemizable: itemizable, - apply_change_method: apply_change_method, - undo_change_method: undo_change_method, - params: params, - from_location: from_location, - to_location: to_location) + update_storage_location(itemizable: itemizable, params: params) if previous - UpdateExistingEvent.publish(itemizable, previous) + UpdateExistingEvent.publish(itemizable, previous, original_storage_location) else event_class&.publish(itemizable) end @@ -44,21 +35,13 @@ def self.call(itemizable:, type: :increase, params: {}, event_class: nil) end # @param itemizable [Itemizable] - # @param apply_change_method [Symbol] - # @param undo_change_method [Symbol] # @param params [Hash] Parameters passed from the controller. Should include `line_item_attributes`. - # @param from_location [StorageLocation] - # @param to_location [StorageLocation] - def self.update_storage_location(itemizable:, apply_change_method:, undo_change_method:, - params:, from_location:, to_location:) - from_location.public_send(undo_change_method, itemizable.line_item_values) + def self.update_storage_location(itemizable:, params:) # Delete the line items -- they'll be replaced later itemizable.line_items.delete_all # Update the current model with the new parameters itemizable.update!(params) itemizable.reload - # Apply the new changes to the storage location inventory - to_location.public_send(apply_change_method, itemizable.line_item_values) end # @param itemizable [Itemizable] diff --git a/app/services/kit_create_service.rb b/app/services/kit_create_service.rb index 80e976d2ee..7884d5d61c 100644 --- a/app/services/kit_create_service.rb +++ b/app/services/kit_create_service.rb @@ -1,8 +1,18 @@ class KitCreateService include ServiceObjectErrorsMixin + KIT_BASE_ITEM_ATTRS = { + name: 'Kit', + category: 'kit', + partner_key: 'kit' + } + attr_reader :kit + def self.find_or_create_kit_base_item! + BaseItem.find_or_create_by!(KIT_BASE_ITEM_ATTRS) + end + def initialize(organization_id:, kit_params:) @organization_id = organization_id @kit_params = kit_params @@ -16,16 +26,15 @@ def call @kit = Kit.new(kit_params_with_organization) @kit.save! - # Create a BaseItem that houses each - # kit item created. - kit_base_item = fetch_or_create_kit_base_item + # Find or create the BaseItem for all items housing kits + item_housing_a_kit_base_item = KitCreateService.find_or_create_kit_base_item! - # Create the Item. + # Create the item item_creation = ItemCreateService.new( organization_id: organization.id, item_params: { name: kit.name, - partner_key: kit_base_item.partner_key, + partner_key: item_housing_a_kit_base_item.partner_key, kit_id: kit.id } ) @@ -45,7 +54,6 @@ def call private attr_reader :organization_id, :kit_params - def organization @organization ||= Organization.find_by(id: organization_id) end @@ -56,18 +64,6 @@ def kit_params_with_organization }) end - def fetch_or_create_kit_base_item - BaseItem.find_or_create_by!({ - name: 'Kit', - category: 'kit', - partner_key: 'kit' - }) - end - - def partner_key_for_kits - 'kit' - end - def valid? if organization.blank? errors.add(:organization_id, 'does not match any Organization') @@ -89,15 +85,4 @@ def kit_validation_errors @kit_validation_errors = kit.errors end - - def associated_item_params - { - kit: kit.name - } - end - - def partner_key_for(name) - "kit_#{name.underscore.gsub(/\s+/, '_')}" - end end - diff --git a/app/services/partners/family_request_create_service.rb b/app/services/partners/family_request_create_service.rb index e585da9a13..8e018530db 100644 --- a/app/services/partners/family_request_create_service.rb +++ b/app/services/partners/family_request_create_service.rb @@ -21,7 +21,7 @@ def call request_create_svc = Partners::RequestCreateService.new( partner_user_id: partner_user_id, comments: comments, - for_families: @for_families, + request_type: request_type, item_requests_attributes: item_requests_attributes ) @@ -43,7 +43,7 @@ def initialize_only Partners::RequestCreateService.new( partner_user_id: partner_user_id, comments: comments, - for_families: @for_families, + request_type: request_type, item_requests_attributes: item_requests_attributes ).initialize_only end @@ -81,5 +81,9 @@ def convert_person_count_to_item_quantity(item_id:, person_count:) def included_items_by_id @included_items_by_id ||= Item.where(id: family_requests_attributes.pluck(:item_id)).index_by(&:id) end + + def request_type + @for_families ? "child" : "individual" + end end end diff --git a/app/services/partners/request_create_service.rb b/app/services/partners/request_create_service.rb index a9875fbf2c..b80321d1f7 100644 --- a/app/services/partners/request_create_service.rb +++ b/app/services/partners/request_create_service.rb @@ -4,19 +4,22 @@ class RequestCreateService attr_reader :partner_request - def initialize(partner_user_id:, comments: nil, for_families: false, item_requests_attributes: [], additional_attrs: {}) + def initialize(request_type:, partner_user_id:, comments: nil, item_requests_attributes: [], additional_attrs: {}) @partner_user_id = partner_user_id @comments = comments - @for_families = for_families + @request_type = request_type @item_requests_attributes = item_requests_attributes @additional_attrs = additional_attrs end def call - @partner_request = ::Request.new(partner_id: partner.id, + @partner_request = ::Request.new( + partner_id: partner.id, organization_id: organization_id, comments: comments, - partner_user_id: partner_user_id) + request_type: request_type, + partner_user_id: partner_user_id + ) @partner_request = populate_item_request(@partner_request) @partner_request.assign_attributes(additional_attrs) @@ -44,6 +47,7 @@ def initialize_only partner_request = ::Request.new(partner_id: partner.id, organization_id: organization_id, comments: comments, + request_type: request_type, partner_user_id: partner_user_id) partner_request = populate_item_request(partner_request) partner_request.assign_attributes(additional_attrs) @@ -52,7 +56,7 @@ def initialize_only private - attr_reader :partner_user_id, :comments, :item_requests_attributes, :additional_attrs + attr_reader :partner_user_id, :comments, :item_requests_attributes, :additional_attrs, :request_type def populate_item_request(partner_request) # Exclude any line item that is completely empty diff --git a/app/services/partners/section_error_service.rb b/app/services/partners/section_error_service.rb new file mode 100644 index 0000000000..5a6ff006b2 --- /dev/null +++ b/app/services/partners/section_error_service.rb @@ -0,0 +1,30 @@ +module Partners + # SectionErrorService identifies which sections of the Partner Profile step-wise form + # should expand when validation errors occur. This helps users easily locate and fix + # fields with errors in specific sections. + # + # Usage: + # error_keys = [:website, :pick_up_name, :enable_quantity_based_requests] + # sections_with_errors = Partners::SectionErrorService.sections_with_errors(error_keys) + # # => ["media_information", "pick_up_person", "partner_settings"] + # + class SectionErrorService + # Maps form sections to the associated fields (error keys) that belong to them. + SECTION_FIELD_MAPPING = { + media_information: %i[no_social_media_presence website twitter facebook instagram], + partner_settings: %i[enable_child_based_requests enable_individual_requests enable_quantity_based_requests], + pick_up_person: %i[pick_up_email pick_up_name pick_up_phone], + area_served: %i[client_share county_id] + } + + # Returns a list of unique sections that contain errors based on the given error keys. + # + # @param error_keys [Array] Array of attribute keys representing the fields with errors. + # @return [Array] An array of section names containing errors. + def self.sections_with_errors(error_keys) + error_keys.flat_map do |key| + SECTION_FIELD_MAPPING.find { |_section, fields| fields.include?(key) }&.first + end.compact.uniq.map(&:to_s) + end + end +end diff --git a/app/services/purchase_create_service.rb b/app/services/purchase_create_service.rb index 16c1718bb5..969d832bc3 100644 --- a/app/services/purchase_create_service.rb +++ b/app/services/purchase_create_service.rb @@ -5,7 +5,6 @@ def call(purchase) unless purchase.save raise purchase.errors.full_messages.join("\n") end - purchase.storage_location.increase_inventory(purchase.line_item_values) PurchaseEvent.publish(purchase) end end diff --git a/app/services/purchase_destroy_service.rb b/app/services/purchase_destroy_service.rb index c32ca02921..6e84e90307 100644 --- a/app/services/purchase_destroy_service.rb +++ b/app/services/purchase_destroy_service.rb @@ -2,7 +2,6 @@ class PurchaseDestroyService class << self def call(purchase) ActiveRecord::Base.transaction do - purchase.storage_location.decrease_inventory(purchase.line_item_values) PurchaseDestroyEvent.publish(purchase) purchase.destroy! end diff --git a/app/services/reports/children_served_report_service.rb b/app/services/reports/children_served_report_service.rb index 1242ba29a6..70886d5e00 100644 --- a/app/services/reports/children_served_report_service.rb +++ b/app/services/reports/children_served_report_service.rb @@ -31,47 +31,8 @@ def average_children_monthly total_children_served / 12.0 end - def disposable_diapers_from_kits_total - organization_id = @organization.id - year = @year - - sql_query = <<-SQL - SELECT SUM(line_items.quantity * kit_line_items.quantity) - FROM distributions - INNER JOIN line_items ON line_items.itemizable_type = 'Distribution' AND line_items.itemizable_id = distributions.id - INNER JOIN items ON items.id = line_items.item_id - INNER JOIN kits ON kits.id = items.kit_id - INNER JOIN line_items AS kit_line_items ON kits.id = kit_line_items.itemizable_id - INNER JOIN items AS kit_items ON kit_items.id = kit_line_items.item_id - INNER JOIN base_items ON base_items.partner_key = kit_items.partner_key - WHERE distributions.organization_id = ? - AND EXTRACT(year FROM issued_at) = ? - AND LOWER(base_items.category) LIKE '%diaper%' - AND NOT (LOWER(base_items.category) LIKE '%cloth%' OR LOWER(base_items.name) LIKE '%cloth%') - AND NOT (LOWER(base_items.category) LIKE '%adult%') - SQL - - sanitized_sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql_query, organization_id, year]) - - result = ActiveRecord::Base.connection.execute(sanitized_sql) - result.first['sum'].to_i - end - private - def total_disposable_diapers_distributed - loose_disposable_distribution_total + disposable_diapers_from_kits_total - end - - def loose_disposable_distribution_total - organization - .distributions - .for_year(year) - .joins(line_items: :item) - .merge(Item.disposable) - .sum("line_items.quantity") - end - def total_children_served_with_loose_disposables organization .distributions diff --git a/app/services/requests_total_items_service.rb b/app/services/requests_total_items_service.rb index dd5e673cb5..1b7ea3c7f6 100644 --- a/app/services/requests_total_items_service.rb +++ b/app/services/requests_total_items_service.rb @@ -1,44 +1,24 @@ class RequestsTotalItemsService def initialize(requests:) - @requests = requests + @requests = requests.includes(item_requests: {item: :request_units}) end def calculate return unless requests - request_items_array = [] - - request_items.each do |items| - items.each do |json| - request_items_array << [item_name(json['item_id']), json['quantity']] - end + totals = Hash.new(0) + item_requests.each do |item_request| + totals[item_request.name_with_unit] += item_request.quantity.to_i end - request_items_array.inject({}) do |item, (quantity, total)| - item[quantity] ||= 0 - item[quantity] += total.to_i - item - end + totals end private attr_accessor :requests - def request_items - @request_items ||= requests.pluck(:request_items) - end - - def request_items_ids - request_items.flat_map { |jitem| jitem.map { |item| item["item_id"] } } - end - - def items_names - @items_names ||= Item.where(id: request_items_ids).as_json(only: [:id, :name]) - end - - def item_name(id) - item_found = items_names.find { |item| item["id"] == id } - item_found&.fetch('name') || '*Unknown Item*' + def item_requests + @item_requests ||= requests.flat_map(&:item_requests) end end diff --git a/app/services/storage_location_deactivate_service.rb b/app/services/storage_location_deactivate_service.rb index 60fb00b87c..5dd8615106 100644 --- a/app/services/storage_location_deactivate_service.rb +++ b/app/services/storage_location_deactivate_service.rb @@ -16,11 +16,7 @@ def call private def valid? - if Event.read_events?(@storage_location.organization) - inventory = View::Inventory.new(@storage_location.organization_id) - inventory.quantity_for(storage_location: @storage_location.id) <= 0 - else - @storage_location.size <= 0 - end + inventory = View::Inventory.new(@storage_location.organization_id) + inventory.quantity_for(storage_location: @storage_location.id) <= 0 end end diff --git a/app/services/transfer_create_service.rb b/app/services/transfer_create_service.rb index fd81089aed..266a095f72 100644 --- a/app/services/transfer_create_service.rb +++ b/app/services/transfer_create_service.rb @@ -4,8 +4,6 @@ def call(transfer) if transfer.valid? ActiveRecord::Base.transaction do transfer.save - transfer.from.decrease_inventory(transfer.line_item_values) - transfer.to.increase_inventory(transfer.line_item_values) TransferEvent.publish(transfer) end else diff --git a/app/services/transfer_destroy_service.rb b/app/services/transfer_destroy_service.rb index 70e562eb71..cbeaaa4def 100644 --- a/app/services/transfer_destroy_service.rb +++ b/app/services/transfer_destroy_service.rb @@ -9,7 +9,6 @@ def call end transfer.transaction do - revert_inventory_transfer! TransferDestroyEvent.publish(transfer) transfer.destroy! end @@ -26,9 +25,4 @@ def call def transfer @transfer ||= Transfer.find(transfer_id) end - - def revert_inventory_transfer! - transfer.to.decrease_inventory(transfer.line_item_values) - transfer.from.increase_inventory(transfer.line_item_values) - end end diff --git a/app/views/audits/_form.html.erb b/app/views/audits/_form.html.erb index 40e88add8d..8cb798a588 100644 --- a/app/views/audits/_form.html.erb +++ b/app/views/audits/_form.html.erb @@ -14,7 +14,7 @@
<%= simple_form_for @audit, data: { controller: "form-input" }, html: {class: "storage-location-required"} do |f| %> - <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?" } %> + <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?", include_omitted_items: true } %>
Items in this audit
@@ -22,7 +22,7 @@
diff --git a/app/views/errors/insufficient.html.erb b/app/views/errors/insufficient.html.erb deleted file mode 100644 index 85f844732c..0000000000 --- a/app/views/errors/insufficient.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

Insufficient Supply

- -

You don't have enough of those to do that :(

diff --git a/app/views/errors/internal_server_error.html.erb b/app/views/errors/internal_server_error.html.erb deleted file mode 100644 index bfedf09242..0000000000 --- a/app/views/errors/internal_server_error.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -
-
-
-
-

500 Error Page

-
-
- -
-
-
-
- -
-
-

500

-
-
-

Oops! Something went wrong.

-

- We will work on fixing that right away. -

-
-
-
diff --git a/app/views/errors/not_found.html.erb b/app/views/errors/not_found.html.erb deleted file mode 100644 index 12c3302ee4..0000000000 --- a/app/views/errors/not_found.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -
-
-
-
-

404 Error Page

-
-
- -
-
-
-
- - -
-
-

404

-
-
-

Oops! Page not found.

- -

- We could not find the page you were looking for. -

- -
-
-
diff --git a/app/views/kits/_form.html.erb b/app/views/kits/_form.html.erb index b91b8887ac..3c1acb8320 100644 --- a/app/views/kits/_form.html.erb +++ b/app/views/kits/_form.html.erb @@ -25,11 +25,9 @@ <%= render 'line_items/line_item_fields', form: f %>
diff --git a/app/views/kits/_table.html.erb b/app/views/kits/_table.html.erb index 7d8bc053c3..61c4b1115c 100644 --- a/app/views/kits/_table.html.erb +++ b/app/views/kits/_table.html.erb @@ -26,21 +26,11 @@ Quantity - <% if @inventory %> - <% @inventory.all_items.select { |i| i.item_id == kit.item.id}.each do |item| %> - - <%= @inventory.storage_location_name(item.storage_location_id) %> - <%= item.quantity %> - - <% end %> - <% else %> - <% kit.inventory_items.map do |inventory_item| %> - <% next if inventory_item.storage_location.discarded_at %> - - <%= inventory_item.storage_location.name %> - <%= inventory_item.quantity %> - - <% end %> + <% @inventory.all_items.select { |i| i.item_id == kit.item.id}.each do |item| %> + + <%= @inventory.storage_location_name(item.storage_location_id) %> + <%= item.quantity %> + <% end %>
diff --git a/app/views/kits/new.html.erb b/app/views/kits/new.html.erb index 2158d68236..d13a03006d 100644 --- a/app/views/kits/new.html.erb +++ b/app/views/kits/new.html.erb @@ -21,3 +21,4 @@ <%= render 'form' %> +<%= render partial: "barcode_items/barcode_modal" %> diff --git a/app/views/layouts/_lte_sidebar.html.erb b/app/views/layouts/_lte_sidebar.html.erb index 7c1c4a0af5..fe4ab55639 100644 --- a/app/views/layouts/_lte_sidebar.html.erb +++ b/app/views/layouts/_lte_sidebar.html.erb @@ -184,13 +184,11 @@ Annual Survey <% end %> - <% if Event.read_events?(current_user.organization) %> - - <% end %> +
+<%= render 'actions', partner: current_partner %> +
@@ -42,37 +44,4 @@
-
-
-
- -
- -
- -
- -
- -
- -
-
+<%= render 'actions', partner: current_partner %> diff --git a/app/views/partners/profiles/step/_accordion_section.html.erb b/app/views/partners/profiles/step/_accordion_section.html.erb new file mode 100644 index 0000000000..382af16521 --- /dev/null +++ b/app/views/partners/profiles/step/_accordion_section.html.erb @@ -0,0 +1,24 @@ +<%# locals: (f:, partner:, section_id:, section_title:, icon_class:, partial_name:, sections_with_errors:) %> + +
+

+ +

+
+
+ <%= render "partners/profiles/step/#{partial_name}_form" , f: f, partner: partner, profile: partner.profile %> +
+
+
diff --git a/app/views/partners/profiles/step/_agency_distribution_information_form.html.erb b/app/views/partners/profiles/step/_agency_distribution_information_form.html.erb new file mode 100644 index 0000000000..8a221dad05 --- /dev/null +++ b/app/views/partners/profiles/step/_agency_distribution_information_form.html.erb @@ -0,0 +1,15 @@ +<%= f.fields_for :profile, profile do |pf| %> +

Agency Distribution Information

+ +
+ <%= pf.input :distribution_times, label: "Distribution Times", class: "form-control" %> +
+ +
+ <%= pf.input :new_client_times, label: "New Client Times", class: "form-control" %> +
+ +
+ <%= pf.input :more_docs_required, label: "More Docs Required", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_agency_information_form.html.erb b/app/views/partners/profiles/step/_agency_information_form.html.erb new file mode 100644 index 0000000000..0a65397daf --- /dev/null +++ b/app/views/partners/profiles/step/_agency_information_form.html.erb @@ -0,0 +1,51 @@ +
+ <%= f.input :name, label: "Agency Name", required: true, class: "form-control" %> +
+ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :agency_type, collection: Partner::AGENCY_TYPES.values, label: "Agency Type", class: "form-control", wrapper: :input_group %> +
+ +
+ <%= pf.input :other_agency_type, label: "Other Agency Type", class: "form-control" %> +
+ +
+ + <% if profile.proof_of_partner_status.attached? %> +
+ Attached file: <%= link_to profile.proof_of_partner_status.blob['filename'], rails_blob_path(profile.proof_of_partner_status), class: "font-weight-bold" %> + <%= pf.file_field :proof_of_partner_status, class: "form-control-file" %> +
+ <% else %> +
+ <%= pf.file_field :proof_of_partner_status, class: "form-control-file" %> +
+ <% end %> +
+ +
+ <%= pf.input :agency_mission, as: :text, label: "Agency Mission", class: "form-control" %> +
+ +
+ <%= pf.input :address1, label: "Address (line 1)", class: "form-control" %> +
+ +
+ <%= pf.input :address2, label: "Address (line 2)", class: "form-control" %> +
+ +
+ <%= pf.input :city, label: "City", class: "form-control" %> +
+ +
+ <%= pf.input :state, label: "State", class: "form-control" %> +
+ +
+ <%= pf.input :zip_code, label: "Zip Code", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_agency_stability_form.html.erb b/app/views/partners/profiles/step/_agency_stability_form.html.erb new file mode 100644 index 0000000000..5368b49bfc --- /dev/null +++ b/app/views/partners/profiles/step/_agency_stability_form.html.erb @@ -0,0 +1,52 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :founded, label: "Year Founded", class: "form-control" %> +
+ +
+ <%= pf.input :form_990, label: "Form 990 Filed", as: :radio_buttons, class: "form-control" %> +
+ + <% if profile.proof_of_form_990.attached? %> +
+ + <%= link_to profile.proof_of_form_990.blob['filename'], rails_blob_path(profile.proof_of_form_990), class: "font-weight-bold" %> +
+ <% end %> + +
+ <%= pf.file_field :proof_of_form_990, class: "form-control-file" %> +
+ +
+ <%= pf.input :program_name, label: "Program Name(s)", class: "form-control" %> +
+ +
+ <%= pf.input :program_description, label: "Program Description(s)", class: "form-control" %> +
+ +
+ <%= pf.input :program_age, label: "Agency Age", class: "form-control" %> +
+ +
+ <%= pf.input :evidence_based, label: "Evidence Based?", as: :radio_buttons, class: "form-control" %> +
+ +
+ <%= pf.input :case_management, label: "Case Management", as: :radio_buttons, class: "form-control" %> +
+ +
+ <%= pf.input :essentials_use, label: "How Are Essentials (e.g. diapers, period supplies) Used In Your Program?", as: :text, class: "form-control" %> +
+ +
+ <%= pf.input :receives_essentials_from_other, label: "Do You Receive Essentials From Other Sources?", class: "form-control" %> +
+ +
+ <%= pf.input :currently_provide_diapers, label: "Currently Providing Diapers?", as: :radio_buttons, class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_area_served_form.html.erb b/app/views/partners/profiles/step/_area_served_form.html.erb new file mode 100644 index 0000000000..05147673a1 --- /dev/null +++ b/app/views/partners/profiles/step/_area_served_form.html.erb @@ -0,0 +1,26 @@ +<%= f.simple_fields_for :profile, profile do |pf| %> +
+
+ +
+ <%= render 'served_areas/served_area_fields', form: pf %> +
+ +
+
Total is:
+ + <%= @client_share_total %> % + +
+ The total client share must be either 0 or 100 %. +
+
+ + +
+
+<% end %> diff --git a/app/views/partners/profiles/step/_attached_documents_form.html.erb b/app/views/partners/profiles/step/_attached_documents_form.html.erb new file mode 100644 index 0000000000..9164809733 --- /dev/null +++ b/app/views/partners/profiles/step/_attached_documents_form.html.erb @@ -0,0 +1,15 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <% if profile.documents.attached? %> + Attached files: +
    + <% profile.documents.each do |doc| %> +
  • <%= link_to doc.blob['filename'], rails_blob_path(doc), class: "font-weight-bold" %>
  • + <% end %> +
+ <%= pf.file_field :documents, multiple: true, class: "form-control-file" %> + <% else %> + <%= pf.file_field :documents, multiple: true, class: "form-control-file" %> + <% end %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_executive_director_form.html.erb b/app/views/partners/profiles/step/_executive_director_form.html.erb new file mode 100644 index 0000000000..2cb5e963dd --- /dev/null +++ b/app/views/partners/profiles/step/_executive_director_form.html.erb @@ -0,0 +1,26 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :executive_director_name, label: "Executive Director Name", class: "form-control" %> +
+
+ <%= pf.input :executive_director_phone, label: "Executive Director Phone", class: "form-control" %> +
+
+ <%= pf.input :executive_director_email, label: "Executive Director Email", class: "form-control" %> +
+ +

Primary Contact

+ +
+ <%= pf.input :primary_contact_name, label: "Primary Contact Name", class: "form-control" %> +
+
+ <%= pf.input :primary_contact_phone, label: "Primary Contact Phone", class: "form-control" %> +
+
+ <%= pf.input :primary_contact_mobile, label: "Primary Contact Cell", class: "form-control" %> +
+
+ <%= pf.input :primary_contact_email, label: "Primary Contact Email", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_form_actions.html.erb b/app/views/partners/profiles/step/_form_actions.html.erb new file mode 100644 index 0000000000..a98860c120 --- /dev/null +++ b/app/views/partners/profiles/step/_form_actions.html.erb @@ -0,0 +1,6 @@ +<%# locals: (f:, partner:) %> + +
+ <%= f.submit "Save Progress", class: 'btn btn-primary mr-2', data: { action: "click->accordion#disableOpenClose" } %> + <%= f.submit "Save and Review", name: "save_review", class: 'btn btn-success' %> +
diff --git a/app/views/partners/profiles/step/_media_information_form.html.erb b/app/views/partners/profiles/step/_media_information_form.html.erb new file mode 100644 index 0000000000..4104d39ec1 --- /dev/null +++ b/app/views/partners/profiles/step/_media_information_form.html.erb @@ -0,0 +1,22 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :website, label: "Website", class: "form-control" %> +
+ +
+ <%= pf.input :facebook, label: "Facebook", class: "form-control" %> +
+ +
+ <%= pf.input :twitter, label: "Twitter", class: "form-control" %> +
+ +
+ <%= pf.input :instagram, label: "Instagram", class: "form-control" %> +
+ +
+ <%= pf.check_box :no_social_media_presence, as: :boolean %>  + <%= pf.label :no_social_media_presence, "No Social Media Presence" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_organizational_capacity_form.html.erb b/app/views/partners/profiles/step/_organizational_capacity_form.html.erb new file mode 100644 index 0000000000..8321820890 --- /dev/null +++ b/app/views/partners/profiles/step/_organizational_capacity_form.html.erb @@ -0,0 +1,13 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :client_capacity, label: "Client Capacity", class: "form-control" %> +
+ +
+ <%= pf.input :storage_space, label: "Storage Space", as: :radio_buttons, class: "form-control" %> +
+ +
+ <%= pf.input :describe_storage_space, label: "Storage Space Description", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_partner_settings_form.html.erb b/app/views/partners/profiles/step/_partner_settings_form.html.erb new file mode 100644 index 0000000000..7ffa36a1d5 --- /dev/null +++ b/app/views/partners/profiles/step/_partner_settings_form.html.erb @@ -0,0 +1,22 @@ +<%= f.fields_for :profile, profile do |pf| %> + <% if pf.object.organization.enable_quantity_based_requests? %> +
+ <%= pf.check_box :enable_quantity_based_requests, as: :boolean %>  + <%= pf.label :enable_quantity_based_requests, "Enable Quantity-based Requests" %> +
+ <% end %> + + <% if pf.object.organization.enable_child_based_requests? %> +
+ <%= pf.check_box :enable_child_based_requests, as: :boolean %>  + <%= pf.label :enable_child_based_requests, "Enable Child-based Requests (unclick if you only do bulk requests)" %> +
+ <% end %> + + <% if pf.object.organization.enable_individual_requests? %> +
+ <%= pf.check_box :enable_individual_requests, as: :boolean %>  + <%= pf.label :enable_individual_requests, "Enable Requests for Individuals" %> +
+ <% end %> +<% end %> diff --git a/app/views/partners/profiles/step/_pick_up_person_form.html.erb b/app/views/partners/profiles/step/_pick_up_person_form.html.erb new file mode 100644 index 0000000000..192a94466f --- /dev/null +++ b/app/views/partners/profiles/step/_pick_up_person_form.html.erb @@ -0,0 +1,11 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :pick_up_name, label: "Pick Up Person Name", class: "form-control" %> +
+
+ <%= pf.input :pick_up_phone, label: "Pick Up Person's Phone #", class: "form-control" %> +
+
+ <%= pf.input :pick_up_email, label: "Pick Up Person's Email", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_population_served_form.html.erb b/app/views/partners/profiles/step/_population_served_form.html.erb new file mode 100644 index 0000000000..e42e1298fd --- /dev/null +++ b/app/views/partners/profiles/step/_population_served_form.html.erb @@ -0,0 +1,58 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :income_requirement_desc, label: "Clients Have An Income Requirement to Work With You?", + as: :radio_buttons, class: "form-control", wrapper: :input_group, + wrapper_html: {class: "form-yesno"}, input_html: {class: "radio-yesno"} %> +
+ +
+ <%= pf.input :income_verification, label: "Do You Verify The Income Of Your Clients?", + as: :radio_buttons, class: "form-control", wrapper: :input_group, + wrapper_html: {class: "form-yesno"}, input_html: {class: "radio-yesno"} %> +
+ +

Race/Ethnicity of Client Base

+ +
+ <%= pf.input :population_black, label: "% African American", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_white, label: "% Caucasian", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_hispanic, label: "% Hispanic", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_asian, label: "% Asian", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_american_indian, label: "% American Indian", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_island, label: "% Pacific Island", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_multi_racial, label: "% Multi-racial", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :population_other, label: "% Other", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :zips_served, label: "Zip Codes Served", class: "form-control", wrapper: :input_group %> +
+ +

Poverty Information of Those Served

+ +
+ <%= pf.input :at_fpl_or_below, label: "% At FPL or Below", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :above_1_2_times_fpl, label: "% Above 1-2 times FPL", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :greater_2_times_fpl, label: "% Greater than 2 times FPL", as: :integer, class: "form-control", wrapper: :input_group %> +
+
+ <%= pf.input :poverty_unknown, label: "% Poverty Unknown", as: :integer, class: "form-control", wrapper: :input_group %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_program_delivery_address_form.html.erb b/app/views/partners/profiles/step/_program_delivery_address_form.html.erb new file mode 100644 index 0000000000..f47f9fa272 --- /dev/null +++ b/app/views/partners/profiles/step/_program_delivery_address_form.html.erb @@ -0,0 +1,21 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :program_address1, label: "Address (line 1)", class: "form-control" %> +
+ +
+ <%= pf.input :program_address2, label: "Address (line 2)", class: "form-control" %> +
+ +
+ <%= pf.input :program_city, label: "City", class: "form-control" %> +
+ +
+ <%= pf.input :program_state, label: "State", class: "form-control" %> +
+ +
+ <%= pf.input :program_zip_code, label: "Zip Code", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/_sources_of_funding_form.html.erb b/app/views/partners/profiles/step/_sources_of_funding_form.html.erb new file mode 100644 index 0000000000..aeacdfba47 --- /dev/null +++ b/app/views/partners/profiles/step/_sources_of_funding_form.html.erb @@ -0,0 +1,17 @@ +<%= f.fields_for :profile, profile do |pf| %> +
+ <%= pf.input :sources_of_funding, label: "Sources Of Funding", class: "form-control" %> +
+ +
+ <%= pf.input :sources_of_diapers, label: "How do you currently obtain diapers?", class: "form-control" %> +
+ +
+ <%= pf.input :essentials_budget, label: "Essentials Budget", class: "form-control" %> +
+ +
+ <%= pf.input :essentials_funding_source, label: "Essentials Funding Source", class: "form-control" %> +
+<% end %> diff --git a/app/views/partners/profiles/step/edit.html.erb b/app/views/partners/profiles/step/edit.html.erb new file mode 100644 index 0000000000..513a53b3fe --- /dev/null +++ b/app/views/partners/profiles/step/edit.html.erb @@ -0,0 +1,46 @@ +
+
+
+
+ <% content_for :title, "Step Editing - #{current_partner.name}" %> +

  Edit My Organization    + <%= partner_status_badge(current_partner) %> + for <%= current_partner.name %> +

+
+ +
+
+
+ + + +
+ <%= simple_form_for current_partner, + data: { controller: "form-input", accordion_target: "form" }, + url: partners_profile_path, + html: { multipart: true } do |f| %> + + <%= render 'partners/profiles/step/form_actions', f: f, partner: current_partner %> + +
+ <%= render 'partners/profiles/step/accordion_section', f: f, partner: current_partner, section_id: 'agency_information', section_title: 'Agency Information', icon_class: 'fa-edit', partial_name: 'agency_information', sections_with_errors: @sections_with_errors %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: current_partner, section_id: 'program_delivery_address', section_title: 'Program / Delivery Address', icon_class: 'fa-map', partial_name: 'program_delivery_address', sections_with_errors: @sections_with_errors %> + <% current_partner.partials_to_show.each do |partial| %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: current_partner, section_id: partial, section_title: partial_display_name(partial), icon_class: 'fa-cogs', partial_name: partial, sections_with_errors: @sections_with_errors %> + <% end %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: current_partner, section_id: 'partner_settings', section_title: 'Settings', icon_class: 'fa-cog', partial_name: 'partner_settings', sections_with_errors: @sections_with_errors %> +
+ + <%= render 'partners/profiles/step/form_actions', f: f, partner: current_partner %> + <% end %> +
diff --git a/app/views/profiles/step/edit.html.erb b/app/views/profiles/step/edit.html.erb new file mode 100644 index 0000000000..fe706c9e59 --- /dev/null +++ b/app/views/profiles/step/edit.html.erb @@ -0,0 +1,36 @@ +
+
+
+
+ <% content_for :title, "Editing - #{@partner.name}" %> +

  Editing Partner - <%= @partner.name %> +

+
+
+
+
+ + + +
+ <%= simple_form_for @partner, + url: profile_path, + data: { controller: "form-input", accordion_target: "form" }, + html: {role: 'form', class: 'form-horizontal'} do |f| %> + + <%= render 'partners/profiles/step/form_actions', f: f, partner: @partner %> + +
+ <%= render 'partners/profiles/step/accordion_section', f: f, partner: @partner, section_id: 'agency_information', section_title: 'Agency Information', icon_class: 'fa-edit', partial_name: 'agency_information', sections_with_errors: @sections_with_errors %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: @partner, section_id: 'program_delivery_address', section_title: 'Program / Delivery Address', icon_class: 'fa-map', partial_name: 'program_delivery_address', sections_with_errors: @sections_with_errors %> + <% @partner.partials_to_show.each do |partial| %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: @partner, section_id: partial, section_title: partial_display_name(partial), icon_class: 'fa-cogs', partial_name: partial, sections_with_errors: @sections_with_errors %> + <% end %> + <%= render 'partners/profiles/step/accordion_section', f: f, partner: @partner, section_id: 'partner_settings', section_title: 'Settings', icon_class: 'fa-cog', partial_name: 'partner_settings', sections_with_errors: @sections_with_errors %> +
+ + <%= render 'partners/profiles/step/form_actions', f: f, partner: @partner %> + <% end %> +
diff --git a/app/views/purchases/_purchase_form.html.erb b/app/views/purchases/_purchase_form.html.erb index f0765e4dd2..d1dbbb035e 100644 --- a/app/views/purchases/_purchase_form.html.erb +++ b/app/views/purchases/_purchase_form.html.erb @@ -18,7 +18,7 @@ collection: @storage_locations, label: "Storage Location", error: "Where is it being stored?", - selected: new_purchase_default_location(@purchase), + selected: default_location(@purchase), wrapper: :input_group %>
diff --git a/app/views/purchases/index.html.erb b/app/views/purchases/index.html.erb index cdbda040b8..42f2d482f6 100644 --- a/app/views/purchases/index.html.erb +++ b/app/views/purchases/index.html.erb @@ -89,7 +89,7 @@ - <%= render partial: "purchase_row", collection: @purchases %> + <%= render partial: "purchase_row", collection: @paginated_purchases %> diff --git a/app/views/reports/purchases_summary.html.erb b/app/views/reports/purchases_summary.html.erb index ec1b5e88e7..0f58e6066d 100644 --- a/app/views/reports/purchases_summary.html.erb +++ b/app/views/reports/purchases_summary.html.erb @@ -9,12 +9,21 @@ filter_url: reports_purchases_summary_path ) do %> <%= new_button_to new_purchase_path, {text: "New Purchase"} %> -

<%= dollar_presentation(@purchases.sum(&:amount_spent_in_cents)) %> +

<%= dollar_presentation(@summary_struct.amount_spent) %> spent <%= @selected_date_range_label %>

+
+

Total spent on diapers: <%= dollar_presentation @summary_struct.diapers %>

+

Total spent on period supplies: <%= dollar_presentation @summary_struct.period_supplies %>

+

Total spent on adult incontinence: <%= dollar_presentation @summary_struct.adult_incontinence %>

+

Total spent on other: <%= dollar_presentation @summary_struct.other %>

+
+
+

Total items: <%= @summary_struct.total_items %>

+

Recent purchases

- <%= render partial: "purchase", collection: @recent_purchases, as: :purchase %> + <%= render partial: "purchase", collection: @summary_struct.recent_purchases, as: :purchase %>
<% end %> diff --git a/app/views/requests/_calculate_product_totals.html.erb b/app/views/requests/_calculate_product_totals.html.erb index 5bc8dcd389..5410923fb9 100644 --- a/app/views/requests/_calculate_product_totals.html.erb +++ b/app/views/requests/_calculate_product_totals.html.erb @@ -16,7 +16,7 @@ - <% @calculate_product_totals.sort_by { |name, quantity| -quantity}.each do |name, quantity| %> + <% @calculate_product_totals.sort_by { |name, quantity| name.downcase }.each do |name, quantity| %> <%= name %> <%= quantity %> diff --git a/app/views/requests/_request_row.html.erb b/app/views/requests/_request_row.html.erb index dbdbceb467..86523947ed 100644 --- a/app/views/requests/_request_row.html.erb +++ b/app/views/requests/_request_row.html.erb @@ -12,6 +12,11 @@ <%= request_row.comments %> + + + <%= request_row.request_type_label %> + + <%= render partial: "status", locals: {status: request_row.status} %> diff --git a/app/views/requests/index.html.erb b/app/views/requests/index.html.erb index ef44ea0d3f..0fa0872137 100644 --- a/app/views/requests/index.html.erb +++ b/app/views/requests/index.html.erb @@ -47,6 +47,9 @@ <%= filter_select(scope: :by_partner, collection: @partners, selected: @selected_partner) %> <% end %> +
+ <%= filter_select(scope: :by_request_type, collection: @request_types, key: :last, value: :first, selected: @selected_request_type) %> +
<%= filter_select(scope: :by_status, collection: @statuses, key: :last, value: :first, selected: @selected_status) %>
@@ -98,6 +101,7 @@ Request Sender # of Items (Request Limit) Comments + Type Status Actions diff --git a/app/views/requests/show.html.erb b/app/views/requests/show.html.erb index 89478380fa..acb2a0635f 100644 --- a/app/views/requests/show.html.erb +++ b/app/views/requests/show.html.erb @@ -37,6 +37,7 @@ Request was sent by: Request Sender: + Request Type: Request Status: Comments: @@ -45,6 +46,7 @@ <%= @request.partner.name %> <%= @request.partner_user&.formatted_email %> + <%= @request.request_type&.humanize %> <%= render partial: "status", locals: {status: @request.status} %> <%= @request.comments || 'None' %> diff --git a/app/views/served_areas/_served_area_fields.html.erb b/app/views/served_areas/_served_area_fields.html.erb index 2d5866fdb5..124261ecc1 100644 --- a/app/views/served_areas/_served_area_fields.html.erb +++ b/app/views/served_areas/_served_area_fields.html.erb @@ -1,14 +1,14 @@ -<%= form.fields_for :served_areas, defined?(object) ? object : nil do |f| %> +<%= form.fields_for :served_areas, defined?(object) ? object : nil do |fsa| %>
- <%= f.input :county_id, collection: @counties, prompt: "County or equivalent", include_blank: "", + <%= fsa.input :county_id, collection: @counties, prompt: "County or equivalent", include_blank: "", label: false, input_html: { class: "my-0 pc-county-select", "data-controller": "select2" } %>
- <%= f.input :client_share, collection: 1..100, placeholder: "Client Share", label: false, input_html: { + <%= fsa.input :client_share, collection: 1..100, placeholder: "Client Share", label: false, input_html: { class: "pc-client-share percentage-selector", "data-area-served-target": "share", "data-served-area-target": "share", "data-action": "change->area-served#calculateClientShareTotal keyup->area-served#calculateClientShareTotal" } %> diff --git a/app/views/storage_locations/_inventory_item_row.html.erb b/app/views/storage_locations/_inventory_item_row.html.erb deleted file mode 100644 index f73220d95d..0000000000 --- a/app/views/storage_locations/_inventory_item_row.html.erb +++ /dev/null @@ -1,11 +0,0 @@ - - <%= link_to inventory_item_row.item.name, item_path(inventory_item_row.item) %> - - <% if version_date.present? %> - <% version = inventory_item_row.paper_trail.version_at(version_date) %> - <%= version ? number_with_delimiter(version.quantity) : "N/A" %> - <% else %> - <%= number_with_delimiter(inventory_item_row.quantity) %> - <% end %> - - diff --git a/app/views/storage_locations/_storage_location_row.html.erb b/app/views/storage_locations/_storage_location_row.html.erb index 2e8e645852..70721d2c28 100644 --- a/app/views/storage_locations/_storage_location_row.html.erb +++ b/app/views/storage_locations/_storage_location_row.html.erb @@ -3,7 +3,7 @@ <%= storage_location.address %> <%= storage_location.square_footage %> <%= storage_location.warehouse_type %> - <%= @inventory ? @inventory.quantity_for(storage_location: storage_location.id) : storage_location.size %> + <%= @inventory.quantity_for(storage_location: storage_location.id) %> <%= number_to_currency(storage_location.inventory_total_value_in_dollars(@inventory)) %> <%= view_button_to storage_location %> @@ -11,7 +11,8 @@ <% if storage_location.discarded? %> <%= reactivate_button_to storage_location_reactivate_path(storage_location), { confirm: confirm_reactivate_msg(storage_location.name) } %> <% else %> - <%= deactivate_button_to storage_location_deactivate_path(storage_location), { confirm: confirm_deactivate_msg(storage_location.name) } %> + <%= deactivate_button_to storage_location_deactivate_path(storage_location), + { confirm: confirm_deactivate_msg(storage_location.name), enabled: @inventory.quantity_for(storage_location: storage_location.id).zero? } %> <% end %> diff --git a/app/views/storage_locations/inventory.json.jbuilder b/app/views/storage_locations/inventory.json.jbuilder deleted file mode 100644 index 002020da57..0000000000 --- a/app/views/storage_locations/inventory.json.jbuilder +++ /dev/null @@ -1,5 +0,0 @@ -json.array! @inventory_items do |inventory| - json.item_id inventory.item.id - json.item_name inventory.item.name - json.quantity inventory.quantity -end diff --git a/app/views/storage_locations/show.html.erb b/app/views/storage_locations/show.html.erb index 4baf8b3d35..2b2827d2e1 100644 --- a/app/views/storage_locations/show.html.erb +++ b/app/views/storage_locations/show.html.erb @@ -110,9 +110,6 @@ <% end %> <% else %> - <%= render partial: "inventory_item_row", - collection: @storage_location.inventory_items.joins(:item).where(items: { active: true }), - locals: { version_date: params[:version_date] } %> <% end %> diff --git a/app/views/users/mailer/invitation_instructions.html.erb b/app/views/users/mailer/invitation_instructions.html.erb index ca9501789c..87c7f64cc8 100644 --- a/app/views/users/mailer/invitation_instructions.html.erb +++ b/app/views/users/mailer/invitation_instructions.html.erb @@ -346,6 +346,7 @@

Hello <%= @resource.email %>

<% if @resource.partner.present? && is_primary_partner %>

You've been invited to become a partner with <%= organization.name %>!

+

<%= organization.invitation_text %>

Please click the link below to accept your invitation and create an account and you'll be able to begin requesting distributions.

Please contact <%= organization.email %> if you are encountering any issues.

<% elsif @resource.partner.present? && !is_primary_partner %> @@ -373,7 +374,6 @@ <% if @resource.invitation_due_at %>

<%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>

<% end %> -

For security reasons these invitations expire. This invitation will expire in 8 hours or if a new password reset is triggered.

If your invitation has an expired message, go <%= link_to "here", new_user_password_url %> and enter your email address to reset your password.

Feel free to ignore this email if you are not interested or if you feel it was sent by mistake.

diff --git a/app/views/users/mailer/reset_password_instructions.html.erb b/app/views/users/mailer/reset_password_instructions.html.erb index 6656a60751..1352deb8fa 100644 --- a/app/views/users/mailer/reset_password_instructions.html.erb +++ b/app/views/users/mailer/reset_password_instructions.html.erb @@ -6,7 +6,7 @@

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

-

For security reasons these invitations expire. This invitation will expire in 8 hours or if a new password reset is triggered.

+

For security reasons these invitations expire. This invitation will expire in 6 hours or if a new password reset is triggered.

If your invitation has an expired message, go <%= link_to "here", new_user_password_url %> and enter your email address to reset your password.

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/bin/lint b/bin/lint index fa2178887a..daed34265e 100755 --- a/bin/lint +++ b/bin/lint @@ -1,4 +1,16 @@ #!/bin/bash -bundle exec rubocop -A -bundle exec erblint --lint-all -a +# Note: Any arguments passed in will be passed along to rubocop or erb_lint +# +# Example to only lint Rails models: +# $ bin/lint app/models +# +echo "Running rubocop" +bundle exec rubocop -A $@ + +echo "Running erb_lint" +if [ $# -eq 0 ]; then # If no arguments are supplied check all files + bundle exec erb_lint --lint-all -a +else # otherwise pass those arguments along (likely a specific file or folder) + bundle exec erb_lint $@ -a +fi diff --git a/clock.rb b/clock.rb index 5fad7b561a..b19137dd1c 100644 --- a/clock.rb +++ b/clock.rb @@ -31,9 +31,10 @@ module Clockwork end every(4.hours, "Backup prod DB to Azure blob storage", if: lambda { |_| Rails.env.production? }) do - rake = Rake.application - rake.init - rake.load_rakefile - rake["backup_db_rds"].invoke + BackupDbRds.run + end + + every(1.day, "Send reminder emails", at: "12:00", if: lambda { |_| Rails.env.production? }) do + ReminderDeadlineJob.perform_later end end diff --git a/config/application.rb b/config/application.rb index 783b8c2f02..bd3480a205 100644 --- a/config/application.rb +++ b/config/application.rb @@ -19,7 +19,6 @@ class Application < Rails::Application config.load_defaults 7.0 config.legacy_connection_handling = false config.action_dispatch.return_only_media_type_on_content_type = false - config.exceptions_app = routes config.active_job.queue_adapter = :delayed_job diff --git a/config/boot.rb b/config/boot.rb index d2ebcbca6b..988a5ddc46 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. -# require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/brakeman.ignore b/config/brakeman.ignore index ac55cf67be..225ff60f53 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -1,5 +1,28 @@ { "ignored_warnings": [ + { + "warning_type": "Command Injection", + "warning_code": 14, + "fingerprint": "5dd8c58cca239b1a2527f25255c49c8800a31e5ec8cb8e31e14003fc435dd677", + "check_name": "Execute", + "message": "Possible command injection", + "file": "app/jobs/backup_db_rds.rb", + "line": 11, + "link": "https://brakemanscanner.org/docs/warning_types/command_injection/", + "code": "system(\"PGPASSWORD='#{ENV[\"DIAPER_DB_PASSWORD\"]}' pg_dump -Fc -v --host=#{ENV[\"DIAPER_DB_HOST\"]} --username=#{ENV[\"DIAPER_DB_USERNAME\"]} --dbname=#{ENV[\"DIAPER_DB_DATABASE\"]} -f #{\"#{Time.current.strftime(\"%Y%m%d%H%M%S\")}.rds.dump\"}\")", + "render_path": null, + "location": { + "type": "method", + "class": "BackupDbRds", + "method": "s(:self).run" + }, + "user_input": "ENV[\"DIAPER_DB_PASSWORD\"]", + "confidence": "Medium", + "cwe_id": [ + 77 + ], + "note": "" + }, { "warning_type": "Dynamic Render Path", "warning_code": 15, @@ -7,7 +30,7 @@ "check_name": "Render", "message": "Render path contains parameter value", "file": "app/controllers/static_controller.rb", - "line": 25, + "line": 20, "link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(template => \"static/#{params[:name]}\", {})", "render_path": null, @@ -18,29 +41,12 @@ }, "user_input": "params[:name]", "confidence": "Medium", - "note": "" - }, - { - "warning_type": "SQL Injection", - "warning_code": 0, - "fingerprint": "db8503246322c7079274c6aa7e68675a336b4d8dd4fb9c2bb6c566545b139c8a", - "check_name": "SQL", - "message": "Possible SQL injection", - "file": "config/initializers/postgres.rb", - "line": 9, - "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", - "code": "connection.select_all(\"select pg_terminate_backend(pg_stat_activity.pid) from pg_stat_activity where datname='#{configuration_hash[:database]}' AND state='idle';\")", - "render_path": null, - "location": { - "type": "method", - "class": "PostgreSQLDatabaseTasks", - "method": "drop" - }, - "user_input": "configuration_hash[:database]", - "confidence": "Medium", + "cwe_id": [ + 22 + ], "note": "" } ], - "updated": "2021-04-24 20:03:05 -0700", - "brakeman_version": "4.10.1" + "updated": "2024-11-24 10:40:00 -0500", + "brakeman_version": "6.2.1" } diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 3951cd3f08..4772210d83 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -12,7 +12,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = '"Human Essentials" ' + config.mailer_sender = '"[DO NOT REPLY] Human Essentials" ' # Configure the class responsible to send e-mails. config.mailer = "CustomDeviseMailer" @@ -120,7 +120,7 @@ # The period the generated invitation token is valid, after # this period, the invited resource won't be able to accept the invitation. # When invite_for is 0 (the default), the invitation won't expire. - # config.invite_for = 2.weeks + config.invite_for = 2.weeks # Number of invitations users can send. # - If invitation_limit is nil, there is no limit for invitations, users can diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml index 7d5fc7c09c..ba47068d82 100644 --- a/config/locales/devise_invitable.en.yml +++ b/config/locales/devise_invitable.en.yml @@ -21,7 +21,7 @@ en: hello: "Hello %{email}" someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below." accept: "Accept invitation" - accept_until: "This invitation will be due in %{due_date}." + accept_until: "This invitation will expire at %{due_date} GMT or if a new password reset is triggered." ignore: "If you don't want to accept the invitation, please ignore this email.
\nYour account won't be created until you access the link above and set your password." time: formats: diff --git a/config/routes.rb b/config/routes.rb index 7a6c552856..161941480f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,9 +86,6 @@ def set_up_flipper end end - match "/404", to: "errors#not_found", via: :all - match "/500", to: "errors#internal_server_error", via: :all - resources :users do get :switch_to_role, on: :collection post :partner_user_reset_password, on: :collection diff --git a/db/migrate/20240315190152_add_type_to_requests.rb b/db/migrate/20240315190152_add_type_to_requests.rb new file mode 100644 index 0000000000..335f3908bc --- /dev/null +++ b/db/migrate/20240315190152_add_type_to_requests.rb @@ -0,0 +1,5 @@ +class AddTypeToRequests < ActiveRecord::Migration[7.0] + def change + add_column :requests, :request_type, :string + end +end diff --git a/db/migrate/20241106184508_remove_excess_kit_village_diaper_bank.rb b/db/migrate/20241106184508_remove_excess_kit_village_diaper_bank.rb new file mode 100644 index 0000000000..55c0badcab --- /dev/null +++ b/db/migrate/20241106184508_remove_excess_kit_village_diaper_bank.rb @@ -0,0 +1,6 @@ +class RemoveExcessKitVillageDiaperBank < ActiveRecord::Migration[7.1] + def change + return unless Rails.env.production? + Kit.find(204).destroy + end +end diff --git a/db/migrate/20241112184800_fix_invalid_distribution_event20241112.rb b/db/migrate/20241112184800_fix_invalid_distribution_event20241112.rb new file mode 100644 index 0000000000..e2a8f13943 --- /dev/null +++ b/db/migrate/20241112184800_fix_invalid_distribution_event20241112.rb @@ -0,0 +1,12 @@ +class FixInvalidDistributionEvent20241112 < ActiveRecord::Migration[7.1] + def change + return unless Rails.env.production? + + # We are not sure why yet, but this org was able to create a distribution + # that put them at a negative inventory. Later playback of the events with + # validation turned on then raised it as an error. For now we are deleting + # the distribution and event directly. + Event.where(id: 42075, eventable_type: 'Distribution', eventable_id: 78924).first.destroy + Distribution.find(78924).destroy + end +end diff --git a/db/migrate/20241204111437_cleanup_invalid_partner_profiles.rb b/db/migrate/20241204111437_cleanup_invalid_partner_profiles.rb new file mode 100644 index 0000000000..1b21236aeb --- /dev/null +++ b/db/migrate/20241204111437_cleanup_invalid_partner_profiles.rb @@ -0,0 +1,61 @@ +class CleanupInvalidPartnerProfiles < ActiveRecord::Migration[7.1] + def up + # ActiveRecord::Base.logger = Logger.new(STDOUT) + invalid_profiles = Partners::Profile.all.reject(&:valid?) + + return if !invalid_profiles.present? + + invalid_profiles.each do |profile| + # address invalid social media section + + unless (profile.website.present? || + profile.twitter.present? || + profile.facebook.present? || + profile.instagram.present? || + profile.no_social_media_presence || + profile.partner.partials_to_show.exclude?("media_information")) + profile.no_social_media_presence = true + end + + # address no request types set + + unless(profile.enable_quantity_based_requests || profile.enable_individual_requests || profile.enable_child_based_requests) + profile.enable_quantity_based_requests = profile.partner.organization.enable_quantity_based_requests + profile.enable_individual_requests = profile.partner.organization.enable_individual_requests + profile.enable_child_based_requests = profile.partner.organization.enable_child_based_requests + end + + + + # address bad pickup email + + unless profile.valid? + # if profile is not valid at this point, it is a bad pickup email + + pick_up = profile.pick_up_email + pick_up.downcase! + pick_up.strip! + if pick_up == "none" or pick_up == "na" or pick_up == "n/a" or pick_up == "see above" + profile.pick_up_email = "" + else + profile.pick_up_email.gsub!("/",",") + profile.pick_up_email.gsub!(";",",") + profile.pick_up_email.gsub!(" or ", ", ") + profile.pick_up_email.gsub!(" and ", ", ") + profile.pick_up_email.gsub!(" & ", ", ") + end + + if(!profile.valid?) ## If we can't fix the email, append it to the name so we don't lose the information aspect + profile.pick_up_name += ", email: " + profile.pick_up_email + profile.pick_up_email = "" + end + end + + profile.save! + + end + end + def down + # irreversible + end +end diff --git a/db/migrate/20241204133041_remove_index_on_name_from_counties_table.rb b/db/migrate/20241204133041_remove_index_on_name_from_counties_table.rb new file mode 100644 index 0000000000..d4fbc1e3c6 --- /dev/null +++ b/db/migrate/20241204133041_remove_index_on_name_from_counties_table.rb @@ -0,0 +1,7 @@ +class RemoveIndexOnNameFromCountiesTable < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + remove_index :counties, :name, algorithm: :concurrently + end +end diff --git a/db/migrate/20241204133506_remove_index_on_organization_id_from_events_table.rb b/db/migrate/20241204133506_remove_index_on_organization_id_from_events_table.rb new file mode 100644 index 0000000000..94e4b37745 --- /dev/null +++ b/db/migrate/20241204133506_remove_index_on_organization_id_from_events_table.rb @@ -0,0 +1,7 @@ +class RemoveIndexOnOrganizationIdFromEventsTable < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + remove_index :events, :organization_id, algorithm: :concurrently + end +end diff --git a/db/migrate/20241204133715_remove_index_on_user_id_from_users_roles_table.rb b/db/migrate/20241204133715_remove_index_on_user_id_from_users_roles_table.rb new file mode 100644 index 0000000000..ef7cd0f0a1 --- /dev/null +++ b/db/migrate/20241204133715_remove_index_on_user_id_from_users_roles_table.rb @@ -0,0 +1,7 @@ +class RemoveIndexOnUserIdFromUsersRolesTable < ActiveRecord::Migration[7.1] + disable_ddl_transaction! + + def change + remove_index :users_roles, :user_id, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index a185dea300..69dade3cbe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_10_02_205346) do +ActiveRecord::Schema[7.1].define(version: 2024_12_04_133715) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -202,7 +202,6 @@ t.datetime "updated_at", null: false t.enum "category", default: "US_County", null: false, enum_type: "category" t.index ["name", "region"], name: "index_counties_on_name_and_region", unique: true - t.index ["name"], name: "index_counties_on_name" t.index ["region"], name: "index_counties_on_region" end @@ -298,7 +297,6 @@ t.bigint "user_id" t.string "group_id" t.index ["organization_id", "event_time"], name: "index_events_on_organization_id_and_event_time" - t.index ["organization_id"], name: "index_events_on_organization_id" t.index ["user_id"], name: "index_events_on_user_id" end @@ -712,6 +710,7 @@ t.datetime "discarded_at", precision: nil t.text "discard_reason" t.integer "partner_user_id" + t.string "request_type" t.index ["discarded_at"], name: "index_requests_on_discarded_at" t.index ["distribution_id"], name: "index_requests_on_distribution_id", unique: true t.index ["organization_id"], name: "index_requests_on_organization_id" @@ -812,7 +811,6 @@ t.bigint "role_id" t.index ["role_id"], name: "index_users_roles_on_role_id" t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" - t.index ["user_id"], name: "index_users_roles_on_user_id" end create_table "vendors", force: :cascade do |t| diff --git a/db/seeds.rb b/db/seeds.rb index 55c5a3d308..27b35ce2e8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -19,26 +19,12 @@ def random_record_for_org(org, klass) # Script-Global Variables # ---------------------------------------------------------------------------- -# Initial starting qty for our test organizations -base_items = File.read(Rails.root.join("db", "base_items.json")) -items_by_category = JSON.parse(base_items) - # ---------------------------------------------------------------------------- # Base Items # ---------------------------------------------------------------------------- -items_by_category.each do |category, entries| - entries.each do |entry| - BaseItem.find_or_create_by!(name: entry["name"], category: category, partner_key: entry["key"]) - end -end - -# Create global 'Kit' base item -BaseItem.find_or_create_by!( - name: 'Kit', - category: 'kit', - partner_key: 'kit' -) +require 'seeds' +Seeds.seed_base_items # ---------------------------------------------------------------------------- # NDBN Members @@ -51,32 +37,62 @@ def random_record_for_org(org, klass) # ---------------------------------------------------------------------------- pdx_org = Organization.find_or_create_by!(short_name: "diaper_bank") do |organization| - organization.name = "Pawnee Diaper Bank" - organization.street = "P.O. Box 22613" - organization.city = "Pawnee" - organization.state = "Indiana" + organization.name = "Pawnee Diaper Bank" + organization.street = "P.O. Box 22613" + organization.city = "Pawnee" + organization.state = "Indiana" organization.zipcode = "12345" - organization.email = "info@pawneediaper.org" + organization.email = "info@pawneediaper.org" end Organization.seed_items(pdx_org) sf_org = Organization.find_or_create_by!(short_name: "sf_bank") do |organization| - organization.name = "SF Diaper Bank" - organization.street = "P.O. Box 12345" - organization.city = "San Francisco" - organization.state = "CA" + organization.name = "SF Diaper Bank" + organization.street = "P.O. Box 12345" + organization.city = "San Francisco" + organization.state = "CA" organization.zipcode = "90210" - organization.email = "info@sfdiaperbank.org" + organization.email = "info@sfdiaperbank.org" end Organization.seed_items(sf_org) +sc_org = Organization.find_or_create_by!(short_name: "sc_bank") do |organization| + organization.name = "Second City Essentials Bank" + organization.street = Faker::Address.street_address + organization.city = Faker::Address.city + organization.state = Faker::Address.state_abbr + organization.zipcode = Faker::Address.zip_code + organization.email = "info@scdiaperbank.org" +end +Organization.seed_items(sc_org) + +# The list of organizations that will have donations, purchases, requests, distributions, +# and the records those rely on generated. +complete_orgs = [pdx_org, sc_org] + # At least one of the items is marked as inactive -Organization.all.each do |org| +Organization.all.find_each do |org| org.items.order(created_at: :desc).last.update(active: false) end +def seed_random_item_with_name(organization, name) + base_items = BaseItem.all.map(&:to_h) + base_item = Array.wrap(base_items).sample + base_item[:name] = name + organization.seed_items(base_item) +end + +# Add a couple unique items based on random base items named after the sc_bank +# so it will be clear if they are showing up where they aren't supposed to be +4.times do |index| + seed_random_item_with_name(sc_org, "Second City Item ##{index + 1}") +end + +# Keep a list of these unique items so its easy to use them for later records +sc_org_unique_items = sc_org.items.where("name ilike ?", "%Second City Item #%") + # Assign a value to some organization items to verify totals are working -Organization.all.each do |org| +Organization.all.find_each do |org| org.items.where(value_in_cents: 0).limit(10).each do |item| item.update(value_in_cents: 100) end @@ -86,18 +102,20 @@ def random_record_for_org(org, klass) # Request Units # ---------------------------------------------------------------------------- -%w(pack box flat).each do |name| - Unit.create!(organization: pdx_org, name: name) -end +complete_orgs.each do |org| + %w[pack box flat].each do |name| + Unit.create!(organization: org, name: name) + end -pdx_org.items.each_with_index do |item, i| - if item.name == 'Pads' - %w(box pack).each { |name| item.request_units.create!(name: name) } - elsif item.name == 'Wipes (Baby)' - item.request_units.create!(name: 'pack') - elsif item.name == 'Kids Pull-Ups (5T-6T)' - %w(pack flat).each do |name| - item.request_units.create!(name: name) + org.items.each_with_index do |item, i| + if item.name == "Pads" + %w[box pack].each { |name| item.request_units.create!(name: name) } + elsif item.name == "Wipes (Baby)" + item.request_units.create!(name: "pack") + elsif item.name == "Kids Pull-Ups (5T-6T)" + %w[pack flat].each do |name| + item.request_units.create!(name: name) + end end end end @@ -106,8 +124,8 @@ def random_record_for_org(org, klass) # Item Categories # ---------------------------------------------------------------------------- -Organization.all.each do |org| - ['Diapers', 'Period Supplies', 'Adult Incontinence'].each do |letter| +Organization.all.find_each do |org| + ["Diapers", "Period Supplies", "Adult Incontinence"].each do |letter| FactoryBot.create(:item_category, organization: org, name: "Category #{letter}") end end @@ -116,19 +134,19 @@ def random_record_for_org(org, klass) # Item < - > ItemCategory # ---------------------------------------------------------------------------- -Organization.all.each do |org| +Organization.all.find_each do |org| # Added `nil` to randomly choose to not categorize items sometimes via sample item_category_ids = org.item_categories.map(&:id) + [nil] org.items.each do |item| - item.update_column(:item_category_id, item_category_ids.sample) + item.update(item_category_id: item_category_ids.sample) end end # ---------------------------------------------------------------------------- # Partner Group & Item Categories # ---------------------------------------------------------------------------- -Organization.all.each do |org| +Organization.all.find_each do |org| # Setup the Partner Group & their item categories partner_group_one = FactoryBot.create(:partner_group, organization: org) @@ -150,18 +168,20 @@ def random_record_for_org(org, klass) # ---------------------------------------------------------------------------- [ - { email: 'superadmin@example.com', organization_admin: false, super_admin: true }, - { email: 'org_admin1@example.com', organization_admin: true, organization: pdx_org }, - { email: 'org_admin2@example.com', organization_admin: true, organization: sf_org }, - { email: 'user_1@example.com', organization_admin: false, organization: pdx_org }, - { email: 'user_2@example.com', organization_admin: false, organization: sf_org }, - { email: 'test@example.com', organization_admin: false, organization: pdx_org, super_admin: true }, - { email: 'test2@example.com', organization_admin: true, organization: pdx_org } + {email: "superadmin@example.com", organization_admin: false, super_admin: true}, + {email: "org_admin1@example.com", organization_admin: true, organization: pdx_org}, + {email: "org_admin2@example.com", organization_admin: true, organization: sf_org}, + {email: "second_city_admin@example.com", organization_admin: true, organization: sc_org}, + {email: "user_1@example.com", organization_admin: false, organization: pdx_org}, + {email: "user_2@example.com", organization_admin: false, organization: sf_org}, + {email: "second_city_user@example.com", organization_admin: false, organization: sc_org}, + {email: "test@example.com", organization_admin: false, organization: pdx_org, super_admin: true}, + {email: "test2@example.com", organization_admin: true, organization: pdx_org} ].each do |user_data| user = User.create( email: user_data[:email], - password: 'password!', - password_confirmation: 'password!' + password: "password!", + password_confirmation: "password!" ) if user_data[:organization] @@ -181,15 +201,17 @@ def random_record_for_org(org, klass) # Donation Sites # ---------------------------------------------------------------------------- -[ - { name: "Pawnee Hardware", address: "1234 SE Some Ave., Pawnee, OR 12345" }, - { name: "Parks Department", address: "2345 NE Some St., Pawnee, OR 12345" }, - { name: "Waffle House", address: "3456 Some Bay., Pawnee, OR 12345" }, - { name: "Eagleton Country Club", address: "4567 Some Blvd., Eagleton, OR 12345" } -].each do |donation_option| - DonationSite.find_or_create_by!(name: donation_option[:name]) do |donation| - donation.address = donation_option[:address] - donation.organization = pdx_org +complete_orgs.each do |org| + [ + {name: "#{org.city} Hardware", address: "1234 SE Some Ave., #{org.city}, #{org.state} 12345"}, + {name: "#{org.city} Parks Department", address: "2345 NE Some St., #{org.city}, #{org.state} 12345"}, + {name: "Waffle House", address: "3456 Some Bay., #{org.city}, #{org.state} 12345"}, + {name: "Eagleton Country Club", address: "4567 Some Blvd., Eagleton, #{org.state} 12345"} + ].each do |donation_option| + DonationSite.find_or_create_by!(address: donation_option[:address]) do |donation| + donation.name = donation_option[:name] + donation.organization = org + end end end @@ -238,39 +260,51 @@ def random_record_for_org(org, klass) status: :approved, email: "approved_2@example.com", notes: note.sample + }, + { + name: "Second City Senior Center", + email: "second_city_senior_center@example.com", + status: :approved, + quota: 500, + notes: note.sample, + organization: sc_org } ].each do |partner_option| p = Partner.find_or_create_by!(partner_option) do |partner| - partner.organization = pdx_org + partner.organization = if partner_option.key?(:organization) + partner_option[:organization] + else + pdx_org + end - if partner_option[:name] == "Second Street Community Outreach" - partner.partner_group = pdx_org.partner_groups.find_by(name: 'Group 2') + partner.partner_group = if partner_option[:name] == "Second Street Community Outreach" + pdx_org.partner_groups.find_by(name: "Group 2") else - partner.partner_group = pdx_org.partner_groups.first + partner.organization.partner_groups.first end end - profile = Partners::Profile.create!({ - essentials_bank_id: p.organization_id, - partner_id: p.id, - address1: Faker::Address.street_address, - address2: "", - city: Faker::Address.city, - state: Faker::Address.state_abbr, - zip_code: Faker::Address.zip, - website: Faker::Internet.domain_name, - zips_served: Faker::Address.zip, - executive_director_name: Faker::Name.name, - executive_director_email: p.email, - executive_director_phone: Faker::PhoneNumber.phone_number, - primary_contact_name: Faker::Name.name, - primary_contact_email: Faker::Internet.email, - primary_contact_phone: Faker::PhoneNumber.phone_number, - primary_contact_mobile: Faker::PhoneNumber.phone_number, - pick_up_name: Faker::Name.name, - pick_up_email: Faker::Internet.email, - pick_up_phone: Faker::PhoneNumber.phone_number - }) + Partners::Profile.create!({ + essentials_bank_id: p.organization_id, + partner_id: p.id, + address1: Faker::Address.street_address, + address2: "", + city: Faker::Address.city, + state: Faker::Address.state_abbr, + zip_code: Faker::Address.zip, + website: Faker::Internet.domain_name, + zips_served: Faker::Address.zip, + executive_director_name: Faker::Name.name, + executive_director_email: p.email, + executive_director_phone: Faker::PhoneNumber.phone_number, + primary_contact_name: Faker::Name.name, + primary_contact_email: Faker::Internet.email, + primary_contact_phone: Faker::PhoneNumber.phone_number, + primary_contact_mobile: Faker::PhoneNumber.phone_number, + pick_up_name: Faker::Name.name, + pick_up_email: Faker::Internet.email, + pick_up_phone: Faker::PhoneNumber.phone_number + }) user = ::User.create!( name: Faker::Name.name, @@ -298,7 +332,7 @@ def random_record_for_org(org, klass) # Skip creating records that they would have created after # they've accepted the invitation # - next if p.status == 'uninvited' + next if p.status == "uninvited" families = (1..Faker::Number.within(range: 4..13)).to_a.map do Partners::Family.create!( @@ -373,7 +407,7 @@ def random_record_for_org(org, klass) dates_generator = DispersedPastDatesGenerator.new - Faker::Number.within(range: 32..56).times do + Faker::Number.within(range: 32..56).times do |index| date = dates_generator.next partner_request = ::Request.new( @@ -385,7 +419,7 @@ def random_record_for_org(org, klass) updated_at: date ) - pads = p.organization.items.find_by(name: 'Pads') + pads = p.organization.items.find_by(name: "Pads") new_item_request = Partners::ItemRequest.new( item_id: pads.id, quantity: Faker::Number.within(range: 10..30), @@ -394,7 +428,7 @@ def random_record_for_org(org, klass) partner_key: pads.partner_key, created_at: date, updated_at: date, - request_unit: 'pack' + request_unit: "pack" ) partner_request.item_requests << new_item_request @@ -419,6 +453,24 @@ def random_record_for_org(org, klass) } end + # Guarantee that there is a request for the items unique to the Second City Bank + if (p.organization == sc_org) && (index < 4) + unique_item = sc_org_unique_items[index] + # Make sure we don't violate item request uniqueness if the unique_item was + # randomly selected already + if !partner_request.item_requests.any? { |item_request| item_request.item_id == unique_item.id } + partner_request.item_requests << Partners::ItemRequest.new( + item_id: unique_item.id, + quantity: Faker::Number.within(range: 10..30), + children: [], + name: unique_item.name, + partner_key: unique_item.partner_key, + created_at: date, + updated_at: date + ) + end + end + partner_request.save! end end @@ -439,6 +491,18 @@ def random_record_for_org(org, klass) inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[1] inventory.square_footage = 20_000 end +StorageLocation.find_or_create_by!(name: "Second City Bulk Storage") do |inventory| + inventory.address = "#{Faker::Address.street_address}, #{sc_org.city}, #{sc_org.state} #{sc_org.zipcode}" + inventory.organization = sc_org + inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[0] + inventory.square_footage = 10_000 +end +StorageLocation.find_or_create_by!(name: "Second City Main Bank (Office)") do |inventory| + inventory.address = "#{Faker::Address.street_address}, #{sc_org.city}, #{sc_org.state} #{sc_org.zipcode}" + inventory.organization = sc_org + inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[1] + inventory.square_footage = 20_000 +end inactive_storage = StorageLocation.find_or_create_by!(name: "Inactive Storage Location") do |inventory| inventory.address = "Unknown" @@ -461,79 +525,81 @@ def random_record_for_org(org, klass) ) end end -Organization.all.each { |org| SnapshotEvent.publish(org) } - -# Set minimum and recomended inventory levels for items at the Pawnee Diaper Bank Organization -half_items_count = (pdx_org.items.count/2).to_i -low_items = pdx_org.items.left_joins(:inventory_items) - .select('items.*, SUM(inventory_items.quantity) AS total_quantity') - .group('items.id') - .order('total_quantity') - .limit(half_items_count) - -min_qty = low_items.first.total_quantity -max_qty = low_items.last.total_quantity +Organization.all.find_each { |org| SnapshotEvent.publish(org) } + +# Set minimum and recomended inventory levels for the complete organizations +# Only set inventory levels for the half of each org's items with the lowest stock +complete_orgs.each do |org| + half_items_count = (org.items.count / 2).to_i + low_items = org.items.left_joins(:inventory_items) + .select("items.*, SUM(inventory_items.quantity) AS total_quantity") + .group("items.id") + .order("total_quantity") + .limit(half_items_count).to_a + + min_qty = low_items.first.total_quantity + max_qty = low_items.last.total_quantity + + # Ensure at least one of the items unqiue to the Second City Bank has minimum + # and recommended quantities set + if (org == sc_org) && !(low_items & sc_org_unique_items).any? + low_items << sc_org_unique_items.last + end -low_items.each do |item| - min_value = rand((min_qty / 10).floor..(max_qty/10).ceil) * 10 - recomended_value = rand((min_value/10).ceil..1000) * 10 - item.update(on_hand_minimum_quantity: min_value, on_hand_recommended_quantity: recomended_value) + low_items.each do |item| + min_value = rand((min_qty / 10).floor..(max_qty / 10).ceil) * 10 + recomended_value = rand((min_value / 10).ceil..1000) * 10 + item.update(on_hand_minimum_quantity: min_value, on_hand_recommended_quantity: recomended_value) + end end -# ---------------------------------------------------------------------------- -# Product Drives -# ---------------------------------------------------------------------------- - -[ - { - name: 'Pamper the Poopsies', - start_date: Time.current, - organization: pdx_org - } -].each { |drive| ProductDrive.create! drive } - -# ---------------------------------------------------------------------------- -# Product Drive Participants -# ---------------------------------------------------------------------------- - -[ - { business_name: "A Good Place to Collect Diapers", - contact_name: "fred", - email: "good@place.is", - organization: pdx_org }, - { business_name: "A Mediocre Place to Collect Diapers", - contact_name: "wilma", - email: "ok@place.is", - organization: pdx_org } -].each { |participant| ProductDriveParticipant.create! participant } - -# ---------------------------------------------------------------------------- -# Product Drives -# ---------------------------------------------------------------------------- - -[ - { name: "First Product Drive", - start_date: 3.years.ago, - end_date: 3.years.ago, - organization: sf_org }, - { name: "Best Product Drive", - start_date: 3.weeks.ago, - end_date: 2.weeks.ago, - organization: sf_org }, - { name: "Second Best Product Drive", - start_date: 2.weeks.ago, - end_date: 1.week.ago, - organization: pdx_org } -].each { |product_drive| ProductDrive.find_or_create_by! product_drive } - -# ---------------------------------------------------------------------------- -# Manufacturers -# ---------------------------------------------------------------------------- - -[ - { name: "Manufacturer 1", organization: pdx_org }, - { name: "Manufacturer 2", organization: pdx_org } -].each { |manu| Manufacturer.find_or_create_by! manu } +# Reload, since some of the items in sc_org_unique_items will have been altered +sc_org_unique_items.reload + +complete_orgs.each do |org| + # ---------------------------------------------------------------------------- + # Product Drives + # ---------------------------------------------------------------------------- + + [ + {name: "First Product Drive", + start_date: 3.years.ago, + end_date: 3.years.ago, + organization: org}, + {name: "Best Product Drive", + start_date: 3.weeks.ago, + end_date: 2.weeks.ago, + organization: org}, + {name: "Second Best Product Drive", + start_date: 2.weeks.ago, + end_date: 1.week.ago, + organization: org} + ].each { |product_drive| ProductDrive.find_or_create_by! product_drive } + + # ---------------------------------------------------------------------------- + # Product Drive Participants + # ---------------------------------------------------------------------------- + + [ + {business_name: "A Good Place to Collect Diapers", + contact_name: "fred", + email: "good@place.is", + organization: org}, + {business_name: "A Mediocre Place to Collect Diapers", + contact_name: "wilma", + email: "ok@place.is", + organization: org} + ].each { |participant| ProductDriveParticipant.create! participant } + + # ---------------------------------------------------------------------------- + # Manufacturers + # ---------------------------------------------------------------------------- + + [ + {name: "Manufacturer 1", organization: org}, + {name: "Manufacturer 2", organization: org} + ].each { |manu| Manufacturer.find_or_create_by! manu } +end # ---------------------------------------------------------------------------- # Line Items @@ -553,10 +619,10 @@ def seed_quantity(item_name, organization, storage_location, quantity) AdjustmentCreateService.new(adjustment).call end -items_by_category.each do |_category, entries| +JSON.parse(File.read(Rails.root.join("db", "base_items.json"))).each do |_category, entries| entries.each do |entry| - seed_quantity(entry['name'], pdx_org, inv_arbor, entry['qty']['arbor']) - seed_quantity(entry['name'], pdx_org, inv_pdxdb, entry['qty']['pdxdb']) + seed_quantity(entry["name"], pdx_org, inv_arbor, entry["qty"]["arbor"]) + seed_quantity(entry["name"], pdx_org, inv_pdxdb, entry["qty"]["pdxdb"]) end end @@ -565,19 +631,19 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- [ - { value: "10037867880046", name: "Kids (Size 5)", quantity: 108 }, - { value: "10037867880053", name: "Kids (Size 6)", quantity: 92 }, - { value: "10037867880039", name: "Kids (Size 4)", quantity: 124 }, - { value: "803516626364", name: "Kids (Size 1)", quantity: 40 }, - { value: "036000406535", name: "Kids (Size 1)", quantity: 44 }, - { value: "037000863427", name: "Kids (Size 1)", quantity: 35 }, - { value: "041260379000", name: "Kids (Size 3)", quantity: 160 }, - { value: "074887711700", name: "Wipes (Baby)", quantity: 8 }, - { value: "036000451306", name: "Kids Pull-Ups (4T-5T)", quantity: 56 }, - { value: "037000862246", name: "Kids (Size 4)", quantity: 92 }, - { value: "041260370236", name: "Kids (Size 4)", quantity: 68 }, - { value: "036000407679", name: "Kids (Size 4)", quantity: 24 }, - { value: "311917152226", name: "Kids (Size 4)", quantity: 82 }, + {value: "10037867880046", name: "Kids (Size 5)", quantity: 108}, + {value: "10037867880053", name: "Kids (Size 6)", quantity: 92}, + {value: "10037867880039", name: "Kids (Size 4)", quantity: 124}, + {value: "803516626364", name: "Kids (Size 1)", quantity: 40}, + {value: "036000406535", name: "Kids (Size 1)", quantity: 44}, + {value: "037000863427", name: "Kids (Size 1)", quantity: 35}, + {value: "041260379000", name: "Kids (Size 3)", quantity: 160}, + {value: "074887711700", name: "Wipes (Baby)", quantity: 8}, + {value: "036000451306", name: "Kids Pull-Ups (4T-5T)", quantity: 56}, + {value: "037000862246", name: "Kids (Size 4)", quantity: 92}, + {value: "041260370236", name: "Kids (Size 4)", quantity: 68}, + {value: "036000407679", name: "Kids (Size 4)", quantity: 24}, + {value: "311917152226", name: "Kids (Size 4)", quantity: 82} ].each do |item| BarcodeItem.find_or_create_by!(value: item[:value]) do |barcode| barcode.item = pdx_org.items.find_by(name: item[:name]) @@ -586,68 +652,90 @@ def seed_quantity(item_name, organization, storage_location, quantity) end end -# ---------------------------------------------------------------------------- -# Donations -# ---------------------------------------------------------------------------- - dates_generator = DispersedPastDatesGenerator.new -# Make some donations of all sorts -20.times.each do - source = Donation::SOURCES.values.sample - # Depending on which source it uses, additional data may need to be provided. - donation = Donation.new(source: source, - storage_location: StorageLocation.active_locations.sample, - organization: pdx_org, - issued_at: dates_generator.next) - case source - when Donation::SOURCES[:product_drive] - donation.product_drive = ProductDrive.first - donation.product_drive_participant = random_record_for_org(pdx_org, ProductDriveParticipant) - when Donation::SOURCES[:donation_site] - donation.donation_site = random_record_for_org(pdx_org, DonationSite) - when Donation::SOURCES[:manufacturer] - donation.manufacturer = random_record_for_org(pdx_org, Manufacturer) - end +complete_orgs.each do |org| + # ---------------------------------------------------------------------------- + # Donations + # ---------------------------------------------------------------------------- + + # Make some donations of all sorts + 20.times.each do |index| + source = Donation::SOURCES.values.sample + # Depending on which source it uses, additional data may need to be provided. + donation = Donation.new( + source: source, + storage_location: org.storage_locations.active_locations.sample, + organization: org, + issued_at: dates_generator.next + ) + case source + when Donation::SOURCES[:product_drive] + donation.product_drive = org.product_drives.find_by(name: "Best Product Drive") + donation.product_drive_participant = random_record_for_org(org, ProductDriveParticipant) + when Donation::SOURCES[:donation_site] + donation.donation_site = random_record_for_org(org, DonationSite) + when Donation::SOURCES[:manufacturer] + donation.manufacturer = random_record_for_org(org, Manufacturer) + end - rand(1..5).times.each do - donation.line_items.push(LineItem.new(quantity: rand(250..500), item: random_record_for_org(pdx_org, Item))) - end - DonationCreateService.call(donation) -end + rand(1..5).times.each do + donation.line_items.push(LineItem.new(quantity: rand(250..500), item: random_record_for_org(org, Item))) + end -# ---------------------------------------------------------------------------- -# Distributions -# ---------------------------------------------------------------------------- -dates_generator = DispersedPastDatesGenerator.new + # Guarantee that there are at least a few donations for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + donation.line_items.push(LineItem.new(quantity: rand(250..500), item: sc_org_unique_items[index])) + end -inventory = InventoryAggregate.inventory_for(pdx_org.id) -# Make some distributions, but don't use up all the inventory -20.times.each do - issued_at = dates_generator.next + DonationCreateService.call(donation) + end - storage_location = StorageLocation.active_locations.sample - stored_inventory_items_sample = inventory.storage_locations[storage_location.id].items.values.sample(20) - delivery_method = Distribution.delivery_methods.keys.sample - shipping_cost = delivery_method == "shipped" ? (rand(20.0..100.0)).round(2).to_s : nil - distribution = Distribution.new( - storage_location: storage_location, - partner: random_record_for_org(pdx_org, Partner), - organization: pdx_org, - issued_at: issued_at, - created_at: 3.days.ago(issued_at), - delivery_method: delivery_method, - shipping_cost: shipping_cost, - comment: 'Urgent' - ) + # ---------------------------------------------------------------------------- + # Distributions + # ---------------------------------------------------------------------------- + + inventory = InventoryAggregate.inventory_for(org.id) + # Make some distributions, but don't use up all the inventory + 20.times.each do |index| + issued_at = dates_generator.next + + storage_location = org.storage_locations.active_locations.sample + stored_inventory_items_sample = inventory.storage_locations[storage_location.id].items.values.sample(20) + delivery_method = Distribution.delivery_methods.keys.sample + shipping_cost = (delivery_method == "shipped") ? rand(20.0..100.0).round(2).to_s : nil + distribution = Distribution.new( + storage_location: storage_location, + partner: random_record_for_org(org, Partner), + organization: org, + issued_at: issued_at, + created_at: 3.days.ago(issued_at), + delivery_method: delivery_method, + shipping_cost: shipping_cost, + comment: "Urgent" + ) + + stored_inventory_items_sample.each do |stored_inventory_item| + distribution_qty = rand(stored_inventory_item.quantity / 2) + if distribution_qty >= 1 + distribution.line_items.push(LineItem.new(quantity: distribution_qty, + item_id: stored_inventory_item.item_id)) + end + end - stored_inventory_items_sample.each do |stored_inventory_item| - distribution_qty = rand(stored_inventory_item.quantity / 2) - if distribution_qty >= 1 - distribution.line_items.push(LineItem.new(quantity: distribution_qty, - item_id: stored_inventory_item.item_id)) + # Guarantee that there are at least a few distributions for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + unique_item_id = sc_org_unique_items[index].id + distribution_qty = rand(storage_location.item_total(unique_item_id) / 2) + distribution.line_items.push( + LineItem.new( + quantity: distribution_qty, + item_id: unique_item_id + ) + ) end + + DistributionCreateService.new(distribution).call end - DistributionCreateService.new(distribution).call end # ---------------------------------------------------------------------------- @@ -655,18 +743,18 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- BroadcastAnnouncement.create( - user: User.find_by(email: 'superadmin@example.com'), + user: User.find_by(email: "superadmin@example.com"), message: "This is the staging /demo server. There may be new features here! Stay tuned!", link: "https://example.com", - expiry: Date.today + 7.days, + expiry: Time.zone.today + 7.days, organization: nil ) BroadcastAnnouncement.create( - user: User.find_by(email: 'org_admin1@example.com'), + user: User.find_by(email: "org_admin1@example.com"), message: "This is the staging /demo server. There may be new features here! Stay tuned!", link: "https://example.com", - expiry: Date.today + 10.days, + expiry: Time.zone.today + 10.days, organization: pdx_org ) @@ -675,20 +763,22 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- # Create some Vendors so Purchases can have vendor_ids -Vendor.create( - contact_name: Faker::FunnyName.two_word_name, - email: Faker::Internet.email, - phone: Faker::PhoneNumber.cell_phone, - comment: Faker::Lorem.paragraph(sentence_count: 2), - organization_id: pdx_org.id, - address: "#{Faker::Address.street_address} #{Faker::Address.city}, #{Faker::Address.state_abbr} #{Faker::Address.zip_code}", - business_name: Faker::Company.name, - latitude: rand(-90.000000000...90.000000000), - longitude: rand(-180.000000000...180.000000000), - created_at: (Time.zone.today - rand(15).days), - updated_at: (Time.zone.today - rand(15).days), -) -4.times do +complete_orgs.each do |org| + Vendor.create( + contact_name: Faker::FunnyName.two_word_name, + email: Faker::Internet.email, + phone: Faker::PhoneNumber.cell_phone, + comment: Faker::Lorem.paragraph(sentence_count: 2), + organization_id: org.id, + address: "#{Faker::Address.street_address} #{Faker::Address.city}, #{Faker::Address.state_abbr} #{Faker::Address.zip_code}", + business_name: Faker::Company.name, + latitude: rand(-90.000000000...90.000000000), + longitude: rand(-180.000000000...180.000000000), + created_at: (Time.zone.today - rand(15).days), + updated_at: (Time.zone.today - rand(15).days) + ) +end +3.times do Vendor.create( contact_name: Faker::FunnyName.two_word_name, email: Faker::Internet.email, @@ -700,7 +790,7 @@ def seed_quantity(item_name, organization, storage_location, quantity) latitude: rand(-90.000000000...90.000000000), longitude: rand(-180.000000000...180.000000000), created_at: (Time.zone.today - rand(15).days), - updated_at: (Time.zone.today - rand(15).days), + updated_at: (Time.zone.today - rand(15).days) ) end @@ -708,7 +798,8 @@ def seed_quantity(item_name, organization, storage_location, quantity) # Purchases # ---------------------------------------------------------------------------- -suppliers = %w(Target Wegmans Walmart Walgreens) +suppliers = %w[Target Wegmans Walmart Walgreens] +amount_items = %w[period_supplies diapers adult_incontinence other] comments = [ "Maecenas ante lectus, vestibulum pellentesque arcu sed, eleifend lacinia elit. Cras accumsan varius nisl, a commodo ligula consequat nec. Aliquam tincidunt diam id placerat rutrum.", "Integer a molestie tortor. Duis pretium urna eget congue porta. Fusce aliquet dolor quis viverra volutpat.", @@ -717,27 +808,47 @@ def seed_quantity(item_name, organization, storage_location, quantity) dates_generator = DispersedPastDatesGenerator.new -25.times do - purchase_date = dates_generator.next - storage_location = StorageLocation.active_locations.sample - vendor = random_record_for_org(pdx_org, Vendor) - purchase = Purchase.new( - purchased_from: suppliers.sample, - comment: comments.sample, - organization_id: pdx_org.id, - storage_location_id: storage_location.id, - amount_spent_in_cents: rand(200..10_000), - issued_at: purchase_date, - created_at: purchase_date, - updated_at: purchase_date, - vendor_id: vendor.id - ) +complete_orgs.each do |org| + 25.times do |index| + purchase_date = dates_generator.next + storage_location = org.storage_locations.active_locations.sample + vendor = random_record_for_org(org, Vendor) + purchase = Purchase.new( + purchased_from: suppliers.sample, + comment: comments.sample, + organization_id: org.id, + storage_location_id: storage_location.id, + issued_at: purchase_date, + created_at: purchase_date, + updated_at: purchase_date, + vendor_id: vendor.id, + amount_spent_on_period_supplies_cents: rand(0..5_000), + amount_spent_on_diapers_cents: rand(0..5_000), + amount_spent_on_adult_incontinence_cents: rand(0..5_000), + amount_spent_on_other_cents: rand(0..5_000) + ) + + purchase.amount_spent_in_cents = amount_items.map { |i| purchase.send("amount_spent_on_#{i}_cents") }.sum - rand(1..5).times do - purchase.line_items.push(LineItem.new(quantity: rand(1..1000), - item_id: pdx_org.item_ids.sample)) + rand(1..5).times do + purchase.line_items.push( + LineItem.new(quantity: rand(1..1000), + item_id: org.item_ids.sample) + ) + end + + # Guarantee that there are at least a few purchases for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + purchase.line_items.push( + LineItem.new( + quantity: rand(1..1000), + item_id: sc_org_unique_items[index].id + ) + ) + end + + PurchaseCreateService.call(purchase) end - PurchaseCreateService.call(purchase) end # ---------------------------------------------------------------------------- @@ -747,18 +858,19 @@ def seed_quantity(item_name, organization, storage_location, quantity) Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "new_logo") Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "read_events") Flipper.enable(:read_events) - +Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "partner_step_form") +Flipper.enable(:partner_step_form) # ---------------------------------------------------------------------------- # Account Requests # ---------------------------------------------------------------------------- # Add some Account Requests to fill up the account requests admin page -[{ organization_name: "Telluride Diaper Bank", website: "TDB.com", confirmed_at: nil }, - { organization_name: "Ouray Diaper Bank", website: "ODB.com", confirmed_at: nil }, - { organization_name: "Canon City Diaper Bank", website: "CCDB.com", confirmed_at: nil }, - { organization_name: "Golden Diaper Bank", website: "GDB.com", confirmed_at: (Time.zone.today - rand(15).days) }, - { organization_name: "Westminster Diaper Bank", website: "WDB.com", confirmed_at: (Time.zone.today - rand(15).days) }, - { organization_name: "Lakewood Diaper Bank", website: "LDB.com", confirmed_at: (Time.zone.today - rand(15).days) }].each do |account_request| +[{organization_name: "Telluride Diaper Bank", website: "TDB.com", confirmed_at: nil}, + {organization_name: "Ouray Diaper Bank", website: "ODB.com", confirmed_at: nil}, + {organization_name: "Canon City Diaper Bank", website: "CCDB.com", confirmed_at: nil}, + {organization_name: "Golden Diaper Bank", website: "GDB.com", confirmed_at: (Time.zone.today - rand(15).days)}, + {organization_name: "Westminster Diaper Bank", website: "WDB.com", confirmed_at: (Time.zone.today - rand(15).days)}, + {organization_name: "Lakewood Diaper Bank", website: "LDB.com", confirmed_at: (Time.zone.today - rand(15).days)}].each do |account_request| AccountRequest.create( name: Faker::Name.unique.name, email: Faker::Internet.unique.email, @@ -813,7 +925,7 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- # Counties # ---------------------------------------------------------------------------- -Rake::Task['db:load_us_counties'].invoke +Rake::Task["db:load_us_counties"].invoke # ---------------------------------------------------------------------------- # Partner Counties @@ -830,16 +942,15 @@ def seed_quantity(item_name, organization, storage_location, quantity) profile = partner.profile num_counties_for_partner = Faker::Number.within(range: 1..10) remaining_percentage = 100 - share_ceiling = 100/num_counties_for_partner #arbitrary, so I can do the math easily + share_ceiling = 100 / num_counties_for_partner # arbitrary, so I can do the math easily county_index = 0 county_ids_for_this_partner = county_ids.sample(num_counties_for_partner) county_ids_for_this_partner.each do |county_id| - client_share = 0 - if county_index == num_counties_for_partner - 1 - client_share = remaining_percentage + client_share = if county_index == num_counties_for_partner - 1 + remaining_percentage else - client_share = Faker::Number.within(range:1..share_ceiling) + Faker::Number.within(range: 1..share_ceiling) end Partners::ServedArea.create( @@ -848,21 +959,25 @@ def seed_quantity(item_name, organization, storage_location, quantity) client_share: client_share ) county_index += 1 - remaining_percentage = remaining_percentage - client_share + remaining_percentage -= client_share end end # ---------------------------------------------------------------------------- # Transfers # ---------------------------------------------------------------------------- +from_id, to_id = pdx_org.storage_locations.active_locations.limit(2).pluck(:id) +quantity = 5 +inventory = View::Inventory.new(pdx_org.id) +# Ensure storage location has enough of item for transfer to succeed +item = inventory.items_for_location(from_id).find { _1.quantity > quantity }.db_item + transfer = Transfer.new( comment: Faker::Lorem.sentence, organization_id: pdx_org.id, - from_id: pdx_org.id, - to_id: sf_org.id, - line_items: [ - LineItem.new(quantity: 5, item: pdx_org.items.first) - ] + from_id: from_id, + to_id: to_id, + line_items: [ LineItem.new(quantity: quantity, item: item) ] ) TransferCreateService.call(transfer) @@ -870,12 +985,33 @@ def seed_quantity(item_name, organization, storage_location, quantity) # Users invitation status # ---------------------------------------------------------------------------- # Mark users `invitation_status` as `accepted` -# +# # Addresses and resolves issue #4689, which can be found in: # https://github.com/rubyforgood/human-essentials/issues/4689 -User.where(invitation_token: nil).each do |user| +User.where(invitation_token: nil).find_each do |user| user.update!( invitation_sent_at: Time.current, invitation_accepted_at: Time.current ) -end \ No newline at end of file +end + +# Guarantee that at least one of the items unique to the Second City Bank has an +# inventory less than the recommended quantity. +item_to_make_scarce = sc_org_unique_items.where("on_hand_recommended_quantity > ?", 0).first +sc_org.storage_locations.each do |location| + num_on_hand = location.item_total(item_to_make_scarce.id) + if num_on_hand > item_to_make_scarce.on_hand_recommended_quantity + num_to_remove = item_to_make_scarce.on_hand_recommended_quantity - 1 - num_on_hand + adjustment = sc_org.adjustments.create!( + comment: "Ensuring example of item below recommended inventory", + storage_location: location, + user: User.with_role(:org_admin, sc_org).first + ) + adjustment.line_items = [LineItem.new( + quantity: num_to_remove, + item: item_to_make_scarce, + itemizable: adjustment + )] + AdjustmentCreateService.new(adjustment).call + end +end diff --git a/docs/user_guide/bank/account_management.md b/docs/user_guide/bank/account_management.md index 4ea7fa1792..d1db436693 100644 --- a/docs/user_guide/bank/account_management.md +++ b/docs/user_guide/bank/account_management.md @@ -1,26 +1,29 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Account Management ## Logging out You can log out be clicking your name in the top right corner, then clicking 'Log out'. -[TODO: Navigational screenshot] - +![Navigating to log out](images/account_management/account_management_logout.png) ## Account settings You can change your name, email, and password in account settings. Any of these changes will need your current password. Click your name in the top right corner, then "Account Settings" -[TODO: Navigational screenshot] +![Navigating to account settings](images/account_management/account_management_account_settings_navigation.png) Provide your updated information, including your current password, then click save. If you want to change your password, you can do it here -- click the Change Password section to show the new password and confirmation fields. -[TODO: screen shot] -[TODO: Actually test this out. *I've* never used it.] +![Account settings screen](images/account_management/account_management_account_settings.png) -Then click save. +hen click save. ## My organization -If you are an org admin, you can also manage your organization and users through the top-right menu. This goes to the same screen as clicking on "My Organization" in the left-hand menu. For details, see [Customization](getting_started_customization.md) and [User Management](user_management.md) -[TODO: Add switching to partner ] +If you are an org admin, you can also manage your organization and users through the top-right menu, by clicking on your name, then "My Organization" +![Navigating to My Organization](images/account_management/account_management_my_organization.png) +This goes to the same screen as clicking on "My Organization" in the left-hand menu. For details, see [Customization](getting_started_customization.md#basic-information) and [User Management](user_management.md) + +## Switching to another role +You may have an id that works with multiple banks, or, more commonly, with a bank and a Partner. To switch between these, click on your name in the top right corner, then the name of the organizaation (bank or Partner) that you want to switch to. + diff --git a/docs/user_guide/bank/asking_for_changes.md b/docs/user_guide/bank/asking_for_changes.md index 808d004570..3d6e70f7eb 100644 --- a/docs/user_guide/bank/asking_for_changes.md +++ b/docs/user_guide/bank/asking_for_changes.md @@ -1,3 +1,4 @@ +READY FOR REVIEW # Asking for changes We welcome ideas for how Human Essentials can support your work better. diff --git a/docs/user_guide/bank/community_donation_sites.md b/docs/user_guide/bank/community_donation_sites.md index 9071f62690..8b6b71bad7 100644 --- a/docs/user_guide/bank/community_donation_sites.md +++ b/docs/user_guide/bank/community_donation_sites.md @@ -1,23 +1,24 @@ +READY FOR REVIEW # Donation Sites -Donation sites are places where people drop off donations. +Donation Sites are places where people drop off Donations. -## The donation site list +## The Donation Site list You can manage the sites' information on the "Donation Sites" page under the "Community" section. ![Donation Sites](images/community/donation_sites/donation_sites.jpg) -Previously recorded information about donation sites appears on this page including the name of the donation site name, address, contact name, e-mail and phone number. +Previously recorded information about Donation Sites appears on this page including the name of the Donation Site name, address, contact name, e-mail and phone number. ### Adding a Donation Site -Create a new site by populating the donation site, address, contact name, e-mail and phone fields and clicking the "Create" button. +Create a new site by populating the Donation Site, address, contact name, e-mail and phone fields and clicking the "Create" button. ![Create Donation_Site](images/community/donation_sites/create_donation_site.jpg) -Note that the donation site and address fields are mandatory while the contact name, email and phone are optional. +Note that the Donation Site and address fields are mandatory while the contact name, email and phone are optional. -You can also use the "+ New Donation Site" button which renders a form for you to fill in details of a new donation site. +You can also use the "+ New Donation Site" button which renders a form for you to fill in details of a new Donation Site. ![Add Donation_Site](images/community/donation_sites/add_new_donation_site.jpg) @@ -26,19 +27,19 @@ After saving the site's details there will be a new row on the Donation Sites pa ## Viewing Donation Site information -Clicking on the "view" button beside a donation site will show detailed information for that site, including the donation site name, address, contact name, e-mail, phone number, storage location. It also shows a list of the donations for that site including the quantity of items and variety of items. You can drill down to see the full details of each donation by clicking "View donation details". +Clicking on the "view" button beside a Donation Site will show detailed information for that site, including the Donation Site name, address, contact name, e-mail, phone number, storage location. It also shows a list of the Donations for that site including the quantity of items and variety of items. You can drill down to see the full details of each Donation by clicking "View Donation details". ![Donation Sites Details](images/community/donation_sites/donation_sites_details.jpg) ## Editing Donation Site information -Clicking on the "Edit" button beside a donation site in the donation site list lets you edit the name, address, contact name, email and phone number. +Clicking on the "Edit" button beside a Donation Site in the Donation Site list lets you edit the name, address, contact name, email and phone number. ![Edit Donation Site Details](images/community/donation_sites/edit_donation_site.jpg) ## Deactivating a Donation Site -Use the "Deactivate" button to delete information about a donation site that is no longer active. +Use the "Deactivate" button to hide a Donation Site that is no longer active. ##### (NB) at time of writing there is no way for you to undo this. @@ -46,7 +47,7 @@ Use the "Deactivate" button to delete information about a donation site that is ## Exporting Donation Sites -You can export the active donation sites by clicking on the "Export Donation Sites" button. This will provide a .csv file containing the name, address, and contact information for each active donation site. +You can export the active Donation Sites by clicking on the "Export Donation Sites" button. This will provide a .csv file containing the name, address, and contact information for each active Donation Site. ![Export Donation Sites](images/community/donation_sites/export_donation_sites.jpg) diff --git a/docs/user_guide/bank/community_manufacturers.md b/docs/user_guide/bank/community_manufacturers.md index 61c3011dfc..9e036d9005 100644 --- a/docs/user_guide/bank/community_manufacturers.md +++ b/docs/user_guide/bank/community_manufacturers.md @@ -1,25 +1,26 @@ +READY FOR REVIEW # Manufacturers -The Manufacturers page under the "Community" section lets you track donations from manufacturers. +The Manufacturers page under the "Community" section lets you manage your Manufacturers and track donations from them. ![Manufacturers](images/community/manufacturers/manufacturers_page.jpg) -It shows the name and total items donated by a manufacturer. +It shows the name and total Items donated by a Manufacturer. ### Adding a manufacturer -Click on the "+ New Manufacturer" button, to add the manufacturer's name. +Click on the "+ New Manufacturer" button, to add the Manufacturer's name. ![New Manufacturer](images/community/manufacturers/new_manufacturer.jpg) -## Viewing manufacturer information +## Viewing Manufacturer information -Click on "View" for more details about the manufacturer which shows the date of each donation, volume (total items in the donation), and lets you view the full details of each donation. +Click on "View" for more details about the Manufacturer which shows the date of each donation, volume (total items in the donation), and lets you view the full details of each donation. ![Manufacturer Details](images/community/manufacturers/manufacturer_details.jpg) ## Editing manufacturer information -Click the "Edit" button to edit the manufacturer's name. +Click the "Edit" button to edit the Manufacturer's name. [Prior: Vendors](community_vendors.md)[Next: Exports](exports.md) \ No newline at end of file diff --git a/docs/user_guide/bank/community_product_drive_participants.md b/docs/user_guide/bank/community_product_drive_participants.md index 48e00f18a7..a4f2b39051 100644 --- a/docs/user_guide/bank/community_product_drive_participants.md +++ b/docs/user_guide/bank/community_product_drive_participants.md @@ -1,12 +1,13 @@ +READY FOR REVIEW # Product Drive Participants -If you conduct product drives, the product drive participants list lets you manage the participant's contact information and see their donation history. +If you conduct Product Drives, the Product Drive Participants list lets you manage the participant's contact information and see their Donation history. -To manage product drive participants, click on "Product Drive Participants" under the "Community" section. +To manage Product Drive Participants, click on "Product Drive Participants" under the "Community" section. ![Product Drive Participants](images/community/product_drive_participants/product_drive_page.jpg) -This page lists some basic details from all previously entered product drive participants -- business, contact, phone email, and total items, letting you drill down to more information, or to edit each participant's information. +This page lists some basic details from all previously entered Product Drive Participants -- business, contact, phone email, and total items, letting you drill down to more information, or to edit each participant's information. ## Adding participants @@ -16,13 +17,12 @@ To add a new participant, click on the "+ New Product Drive Participant" button, After saving the participant's details there will be a new row in the Product Drive Participants page. -You can also add new participants "on the fly" as you enter the donations, through the [New Donation](essentials_donations.md) page. +You can also add new participants "on the fly" as you enter the donations, through the [New Donation](essentials_donations.md#new-donations) page. -[TODO: link to new donations ] ## Viewing participant information -Click on "View" for more details about the product drive participant which also shows the date of each donation, volume, and donations details. +Click on "View" for more details about the Product Drive Participant which also shows the date of each Donation, volume, and Donation details. ![Participant Details](images/community/product_drive_participants/participant_details.jpg) @@ -34,7 +34,7 @@ Click the "Edit" button to edit the participant's details. ## Exporting participants -You can export all the participants by clicking on the "Export Product Drive Paticipants" button. +You can export all the participants by clicking on the "Export Product Drive Participants" button. Currently we are not providing all the participants' details in the export. ![Export Drive Participants](images/community/product_drive_participants/export_participants.jpg) diff --git a/docs/user_guide/bank/community_product_drives.md b/docs/user_guide/bank/community_product_drives.md index 25d4eaf939..2b22258a70 100644 --- a/docs/user_guide/bank/community_product_drives.md +++ b/docs/user_guide/bank/community_product_drives.md @@ -1,65 +1,57 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Product Drives -Many banks hold seasonal or one-off events where they are seeking donations. In Human Essentials, Product drives help manage those events by providing a way to link specific donations to the events. +Many banks hold seasonal or one-off events where they are seeking Donations. In Human Essentials, Product Drives help manage those events by providing a way to link specific Donations to the events. -[TODO: For consistency, I would remove 'New Product Drive' from the lhs, and change "All Product Drives" to "Product Drives] +## Product Drives list +To see all your Product Drives, click on "Community", then "All Product Drives" in the left-hand menu. -## Product drives list -To see all your product drives, click on "Community", then "All Product Drives" in the left-hand menu. +![Product Drive navigation](images/community/product_drives/community_product_drives_navigation.png) -This presents a filterable list of all your product drives, including the following fields: + +This presents a filterable list of all your Product Drives, including the following fields: - Product Drive Name - Start Date - End Date (this is optional) - Held Virtually? - Quantity of Items - Variety of Items -- In-Kind Value (TODO: Fix spelling in header) +- In-Kind Value - Actions (only view at the moment) -The list initially shows all the product drives starting in the current year. - -[TODO: NAvigation screenshot] - - +The list initially shows all the Product Drives that are at least partly in our default period of 60 days past to 30 days in the future. -[TODO: Is that right, or is it all product drives that are at least partly in the current year? It probably should be the latter] - -### Filtering you product drives list -The product drives list is filterable by any of the following. -- Product drive name -- Item category +### Filtering your Product Drives list +The Product Drives list is filterable by any of the following. +- Product Drive name +- Item Category - Date range (by starting date) -[TODO: confirm how the date range works.] - Provide the information you want to filter by, then click "Filter". Clicking "Clear Filters" will return the list to its defaults. -## Adding a product drive -To add a product drive, either: +## Adding a Product Drive +To add a Product Drive, either: a) click "Community", then "New Product Drive" in the left hand menu, or b) click "+New Product Drive" in the Product Drives list -[TODO: Navigation screenshot] +![Navigation to New Product Drive](images/community/product_drives/community_product_drives_add_navigation.png) -Either will bring up this screen, which contains all the fields you can specify for a new product drive: +Either will bring up this screen, which contains all the fields you can specify for a new Product Drive: - Name - Start Date - End Date (this is optional) - Product Drive is Virtual? (check this if it's a virtual drive) -[TODO: Screenshot] -[TODO: Styling of "Create product drive" -- button should be white on green] +![+New Product Drives page](images/community/product_drives/community_product_drives_add.png) -## Viewing your product drive -To view a specific product drive, click the "View" button beside it in the product drives list. -[TODO: Navigational Screenshot] -[TODO: Screenshot] -This will bring up the product drive view, which includes informtation about both the donations for the product drive, and the participants in the drive, in addition to the basic information about the product drive itself. +## Viewing a Product Drive +To view a specific Product Drive, click the "View" button beside it in the Product Drives list. +![Navigation to view a Product Drive](images/community/product_drives/community_product_drives_view_navigation.png) + +This will bring up the Product Drive view, which includes information about both the Donations for the Product Drive, and the participants in the drive, in addition to the basic information about the drive itself. +![Product Drive view page](images/community/product_drives/community_product_drives_view.png) Basic info: - Name - Start Date - End Date -[TODO: Move the "Make a correction" button up to below the basic information, not below the donations] Information about each donation: - Donation ID -- this is a number the system applies to each donation - Product Drive Participant @@ -72,25 +64,27 @@ For each product drive participant associated with this product drive: - Phone - Email - Address -- Items for this Drive (number of items for this drive) +- Items for this drive (number of Items for this drive) - Total Items (for all drives) - Comment -You can also make a correction to your product drive or delete it from this page. +You can also make a correction to your Product Drive or delete it from this page. + +[!CAUTION] DO NOT DELETE your Product Drive if it has Donations in it. It will not go well. We have an outstanding issue to block you doing that. -#### NB -- DO NOT DELETE your product drive if it has donations in it. It will not go well. +## Modifying your Product Drive +If you want to edit the base information in a Product Drive, you can do that by clicking "Community", then "All Product Drives" in the left-hand menu, then clicking "View" beside the product drive you wish to modify, and then clicking "Make a correction". +![Navigation to update Product Drive](images/community/product_drives/community_product_drives_modify_navigation.png) -## Modifying your product drive -If you want to edit the base information in a product drive, you can do that by clicking "Community", then "All Product Drives" in the left-hand menu, then clicking "View" beside the product drive you wish to modify, and then clicking "Make a correction". -This will bring up the basic information for your product drive, You can rename it, change the dates, or change whether it is a virtual drive. -Then click "Update Product drive" to save your changes. +This will bring up the basic information for your Product Drive, You can rename it, change the dates, or change whether it is a virtual drive. +Then click "Update Product Drive" to save your changes. -[TODO: Screenshot] +![Modify Product Drive page](images/community/product_drives/community_product_drives_modify.png) -## Exporting product drives -To export your currently filtered product drives, click "Export product drives" on the Product Drives List +## Exporting Product Drives +To export your currently filtered Product Drives, click "Export Product Drives" on the Product Drives List +![Navigation to Product Drive export](images/community/product_drives/community_product_drives_export_navigation.png) -[TODO: Navigation Screenshot] [Prior: Transfers](inventory_transfers.md) [Next: Product Drive Participants](community_product_drive_participants.md) \ No newline at end of file diff --git a/docs/user_guide/bank/community_vendors.md b/docs/user_guide/bank/community_vendors.md index 35d18a5988..8d7fb96253 100644 --- a/docs/user_guide/bank/community_vendors.md +++ b/docs/user_guide/bank/community_vendors.md @@ -1,43 +1,40 @@ +READY FOR REVIEW # Vendors -The vendors list lets you manage vendor contact information and view your purchases from each. To access it, click on "Vendors" under the "Community" section. +The Vendors list lets you manage Vendor contact information and view your Purchases from each Vendor. To access it, click on "Vendors" under the "Community" section. ![Vendors](images/community/vendors/vendors_page.jpg) -This page shows all previously entered vendors -- business name, contact name, phone, email, and total items -- and lets you view or edit them. +This page shows all previously entered Vendors -- business name, contact name, phone, email, and total items -- and lets you view or edit them. -## Adding vendors +## Adding Vendors -To add a new vendor, click on the "+ New Vendor" button, add their details including the business name, contact name, phone, email, and address. +To add a new Vendor, click on the "+ New Vendor" button, add their details including the business name (mandatory), contact name, phone, email, and address. -![New Vendor](images/community/vendors/new_vendor.jpg) +![New Vendor Navigation](images/community/vendors/new_vendor.jpg) -You must enter at least one of business name or contact name. We recommend that you enter the business name, as this is used when selecting the vendor on entering purchases. +![Vendor informtiaon to be entered](images/community/vendors/add_vendor.jpg) -![Add Vendor](images/community/vendors/add_vendor.jpg) +After saving the Vendor's details there will be a new row in the Vendors page. -After saving the vendor's details there will be a new row in the Vendors page. +You can also add a new Vendor "on the fly" as you enter the Purchases, through the [New Purchase](essentials_purchases.md#entering-a-new-purchase) page. -You can also add a new vendor "on the fly" as you enter the purchases, through the [New Purchase](essentials_purchases.md) page. +## Viewing Vendor information -[TODO: link to New Purchase section of essentials purchases] - -## Viewing vendor information - -Click on "View" for more details about the vendor which also shows the date of each purchase, volume (total items in the purchase), and lets you view the full details of each purchase. +Click on "View" for more details about the Vendor which also shows the date of each Purchase, volume (total items in the Purchase), and lets you view the full details of each Purchase. ![Vendor Details](images/community/vendors/vendor_details.jpg) ## Editing vendors -Click the "Edit" button to edit the vendor's contact information. +Click the "Edit" button to edit the Vendor's contact information. ![Edit Vendor Details](images/community/vendors/edit_vendors.jpg) ## Exporting vendors -You can export all the vendors by clicking on the "Export Vendors" button. -Currently we are only providing the vendors' contact information in the export. +You can export all the Vendors by clicking on the "Export Vendors" button. +Currently we are only providing the Vendors' contact information in the export. ![Export Vendors](images/community/vendors/export_vendors.jpg) diff --git a/docs/user_guide/bank/essentials_dashboard.md b/docs/user_guide/bank/essentials_dashboard.md index 02fb21ab7d..c21988c405 100644 --- a/docs/user_guide/bank/essentials_dashboard.md +++ b/docs/user_guide/bank/essentials_dashboard.md @@ -1,29 +1,28 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Your Dashboard When you log in, your initial screen will be a dashboard with information that is useful on a daily basis for most banks. If you haven't finished setting up your bank in the system, the "getting started" steps (as described in an earlier section) will appear. -Otherwise, there are 4 sections to the dashboard: announcements, outstanding requests, partner approvals, and bank-wide low inventory. +Otherwise, there are 4 sections to the dashboard: Announcements, Outstanding Requests, Partner Approvals, and Bank-wide Low Inventory. ![top of dashboard page](images/essentials/dashboard/essentials_dashboard_1.png) #### Announcements -This section is announcements from the human resources team. This is where we tell you what changes there have been in the latest release, and let everyone know about any significant issues with the system that affect everybody (with workarounds, if we can.) +This section contains announcements from the Human Essentials team. This is where we tell you what changes there have been in the latest release, and let everyone know about any significant issues with the system that affect everybody (with workarounds, if we can.) Look for new info here every Monday - as we put out new releases most Sunday mornings. #### Outstanding Requests This is pretty much what it sounds like - a list of the requests from your partners that have not yet been fulfilled. You can bring up the details of each request by clicking on the date. - ![bottom of dashboard page](images/essentials/dashboard/essentials_dashboard_2.png) #### Partner Approvals -This lists the partner profiles that have been submitted for approval. Partners can not submit requests until they have been approved. To review the application, click on the "Review Applicant's Profile" button beside the partner in the Action column. For more details on that, see [Approving a partner](pm_approving_a_partner.md) +This lists the Partner Profiles that have been submitted for approval. Partners can not submit requests until they have been approved. To review the application, click on the "Review Applicant's Profile" button beside the partner in the Action column. For more details on that, see [Approving a partner](pm_approving_a_partner.md) #### Bank-wide Low Inventory -This lists items whose *bank-wide* inventory has fallen below the recommended or minimum quantity levels you have set on the items. If the item's level in inventory across the bank has fallen below the minimum quantity, it will appear in red. +This lists Items whose *bank-wide* inventory has fallen below the recommended or minimum quantity levels you have set on the items. If the item's level in inventory across the bank has fallen below the minimum quantity, it will appear in red. -For help on setting those levels, see [Inventory Items](inventory_items.md). If you haven't set those levels, the items will not appear on this list, even if you have no inventory. +For help on setting those levels, see [Inventory Items](inventory_items.md). If you haven't set those levels, the Items will not appear on this list, even if you have no inventory. [Prior: User management](getting_started_user_management.md) [Next: Donations](essentials_donations.md) \ No newline at end of file diff --git a/docs/user_guide/bank/essentials_distributions.md b/docs/user_guide/bank/essentials_distributions.md index b992451cf7..1d05dd47e3 100644 --- a/docs/user_guide/bank/essentials_distributions.md +++ b/docs/user_guide/bank/essentials_distributions.md @@ -1,61 +1,68 @@ -DRAFT USER GUIDE +READY FOR REVIEW + # Distributions Distributions are where you record what you allocate to your partner agencies. Some things to know: -* Once you save a distribution, those items are allocated to the partner, and are no longer part of your inventory in the system. -* If you are accepting requests from partners, you initiate the distribution by "fulfilling" the partner request. (see [Requests](essentials_requests.md)) - -## Seeing a list of your distributions -To view a list of your distributions, click on 'Distributions' in the left hand menu. This brings up a list of all your distributions for the current year. You can change what distributions are displayed using the distribution filters at the top of the list. +* Once you save a Distribution, those items are allocated to the Partner, and are no longer part of your inventory in the system. +* If you are accepting Requests from partners, you initiate the Distribution by "fulfilling" the Partner's Request. (see [Requests](essentials_requests.md#fulfilling-a-request)) +## Seeing a list of your Distributions +To view a list of your Distributions, click on 'Distributions' in the left hand menu. This brings up a list of all your Distributions for the default period of 60 days in the past, 30 days in the future. You can change what Distributions are displayed using the filters at the top of the list. +![Navigation to distributions](images/essentials/distributions/essentials_distributions_navigation.png) -### Filtering the distribution list +### Filtering the Distribution list -[TODO: Insert mini screenshot of just the filtration section here] +Your Distribution list may grow to the point that you really need to be able to narrow things down to find a particular Distribution. -When you have been using human essentials for a few months, your distribution list may grow to the point that you really need to be able to narrow things down to find a particular distribution. +To help with that, you can filter the Distribution list by several aspects: Item, Item Category, Partner, source inventory (i.e. Storage Location), status, and date range. +If you pick several things, you will get only the Distributions that match all of them. -To help with that, you can filter the distribution list by several aspects: item, item category, partner, source inventory (i.e. storage location) and distribution status, and date range. -If you pick several things, you will get only the distributions that match all of them. +![Distributions filtering](images/essentials/distributions/essentials_distributions_filter.png) -Except for date range, all the filters are specified by picking from a drop-down list as follows -Item: all active items (TODO: Confirm -- is it just active or are the inactives there too?). This will filter to only the distributions that contained that item. -Item category: Item categories (as specified in [Items & Inventory | Item Categories](inventory_items.md)) [TODO: point right to categories section]. This will filter the list to the distributions that contain items that are in the chosen item category. -Partner: This will filter the lists to just the distributions to the chosen partner. -Source Inventory: This will limit the list to the distributions from the chosen storage location. -Status: Distributions can be Scheduled or Complete. This will limit the list to those with the given status. -Date range: This is based on the "Distribution date and time" field, ignoring the time. Date range is selected using a little calendar gizmo with several presets, or by typing the date range into the field. We highly recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing mismatches there every month. +Except for date range, all the filters are specified by picking from a drop-down list as follows: +- Item: all of your bank's Items. This will filter to only the distributions that contain that Item. +- Item Category: Item Categories (as specified in [Items & Inventory | Item Categories](inventory_items.md)). This will filter the list to the distributions that contain items that are in the chosen item category. +- Partner: This will filter the lists to just the distributions to the chosen partner. +- Source Inventory: This will limit the list to the distributions from the chosen storage location. +- Status: Distributions can be Scheduled or Complete. This will limit the list to those with the given status. +- Date range: This is based on the "Distribution date and time" field, ignoring the time. Date range is selected using a little calendar gizmo with several presets, or by typing the date range into the field. We highly recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing mismatches there every month. -When you have have selected your filters, press "Filter" to do the filtering. If you still have too many distributions showing, you can add another filter to narrow it down further. +When you have have selected your filters, press "Filter" to do the filtering. If you still have too many Distributions showing, you can add another filter to narrow it down further. -Clicking "Clear filters" will blank out the filters that are drop-down selection, and revert the date range to the current year. +Clicking "Clear filters" will blank out the filters that are drop-down selection, and revert the date range to the default period. ## New Distribution -To enter a new distribution, click on "New Distribution" in the Distributions list. +To enter a new Distribution, click on "New Distribution" in the Distributions list. -Here, you will enter some information about the whole distribution, then add the all the items that make it up. +Here, you will enter some information about the whole Distribution, then add the all the items that make it up. The fields include: - Partner (mandatory) - Distribution date and time -- this is defaulted to midnight of the current day. If you want to change it (if, for example, you have a specific time you are scheduling the pickup for), we recommend you use the little calendar gizmo at the right of the field. - [question: does send email reminder the day before appear based on an organization flag?] + - Send email reminder the day before --> causes an email to be sent the day before - [question: what happens if we check this and it's today?] -- Agency representative - for information only [TODO: is this defaulted from the chosen partner? It could be.. but do we?] + +- Agency representative - for information only. This is defaulted to the email of the person who sent the request. - Delivery method -- we default this to pickup because it's the most common across banks. -- From storage location: The storage location the distribution is coming from. Obviously mandatory. +- From storage location: The storage location the distribution is coming from. Mandatory. - Comment - All the items: -- For each item: - - If you have set up barcodes for items, you can just boop the item in. Otherwise, select the item from the list, and enter the quantity - - [TODO: Totally rewrite this bit for packs] -## Exporting Distributions -To export your distributions, click "Export Distributions" on the distributions view. This will include all the top-level information, and a column [or more, if you use custom units] for each item in the distribution, in alphabetic order. It will include all the distributions within the filter you have already applied. -[TODO: add navigational screenshot and sample csv] + - For each item: + - If you have set up Barcodes for Items, you can just 'boo'p' the item in. Otherwise, select the Item from the list, and enter the quantity + ## Exporting Distributions +To export your Distributions, click "Export Distributions" on the distributions view. + +![navigation to export Distributions](images/essentials/distributions/essentials_distributions_export_navigation.png) + +This will include all the top-level information, and a column [or more, if you use custom units] for each Item in the distribution, in alphabetic order. It will include all the Distributions within the filter you have already applied. + +![sample Distributions csv snapshot](images/essentials/distributions/essentials_distributions_export_sample.png) ## Viewing a Distribution -To view a distribution, click "view" beside it in the distributions view. +To view a Distribution, click "view" beside it in the distributions view. +![Navigation for distribution view](images/essentials/distributions/essentials_distributions_view_navigation.png) + This includes the following fields: - distribution id (for our reference for support) , - Source location (the storage location the inventory came from), @@ -63,26 +70,43 @@ This includes the following fields: - Delivery method (pickup delivery or shipped), - Shipping cost (if shipped), - Comments, and -- the current status. - [TODO: add screenhot of view] - [TODO: check -- do we use "state" throughout -- I feel like we probably use "status"] +- the current status, as well as +- a breakdown of the items in the view including + - Item name + - Value per Item + - Total value + - Quantity + - Package count (only if you have defined a package size for the Item) + +![Navigation for Distribution view](images/essentials/distributions/essentials_distributions_view.png) + +You can mark the Distribution "complete", or make a correction (which brings up the edit page, below) from this page. + ## Editing a Distribution -To edit a distribution, click on "Edit" beside the distribution in the list, or on "Make a Correction" in the view. +To edit a Distribution, click on "Edit" beside the Distribution in the list, or on "Make a Correction" in the view. +![Edit Distribution navigation](images/essentials/distributions/essentials_distributions_edit_navigation.png) + +[!NOTE] When you edit a Distribution -- if is was in the past, you will see a warning to that effect -- because we assumed that you wouldn't normally need to change the Distribution once it had been put together! Some banks will add things when the Partners come to pick up the distributions, based on additional needs and/or supply opportunities. +However, We will give you a stern warning if there has been an Audit since the Distribution was entered, and you may be prevented from changing some Distribution information (such as the storage location), because we just don't know how to handle some of those cases. -If the distribution is in the past, you will see a warning to that effect -- because we assumed that you wouldn't normally need to change the distribution once it had gone out the door! -We will give you a stern warning if there has been an audit since the distribution was entered, and you may be prevented from changing some distribution information (such as the storage location), because we just don't know how to handle some of those cases. -[TODO: More writing about the PACKS version] -[TODO: screenshot] +![Distributions edit](images/essentials/distributions/essentials_distributions_edit.png) ## Printing a Distribution -Printing a distribution produces an invoice-like page that can be used as a packing slip. +Printing a Distribution produces an invoice-like page that can be used as a packing slip. + +![Distribution print navigation](images/essentials/distributions/essentials_distributions_print_navigation.png) +![Distribution printout](images/essentials/distributions/essentials_distributions_printout.png) -It is somewhat configurable -- there are options on your [Organization](getting_started_customization.md) page to allow you to: a) add a place for a signature, or b) hide certain columns in the printout. -[TODO: point that at the exact location in the document] +This is the same pdf that is emailed to the Partner when a new Distribution is saved. +[!NOTE] The partner does not get a new email when you edit a Distribution, so you may want to print the Distribution to send them in that case. -Please note that your logo (also configurable on the organization, above) is included on this printout -- we strongly advice keeping it fairly small, as a large logo will just be resized anyway, and will potentially break this function. +The printout is somewhat configurable -- there are options on your [Organization](getting_started_customization.md#customizing-the-distribution-printout) page to allow you to: a) add a place for a signature, or b) hide certain columns in the printout. + +Please note that your logo (also configurable on the [Organization](getting_started_customization.md#logo) ) is included on this printout -- we strongly advice keeping it fairly small, as a large logo will just be resized anyway, and will potentially break this function. ## Reclaiming a Distribution -What do you do if, for some reason, the distribution that was entered was not picked up? You can reclaim it, adding the items back into your inventory. -To do this, click "Reclaim" beside the distribution in question. -NOTE: You can not reverse a reclaim. If you do it by accident, you will have to re-enter the distribution. +What do you do if, for some reason, the Distribution that was entered was not picked up? You can reclaim it, adding the items back into your inventory. +To do this, click "Reclaim" beside the Distribution in question, and cliock "Ok" in the confirmation screen that appears. + +[!WARN] You can not reverse a reclaim. If you reclaim by accident, you will have to re-enter the Distribution. If that happens, you should be able to grab the Item quantities from the [History Report](reports_history.md), but we don't retain the rest of the information. + [Prior: Requests](essentials_requests.md)[Next: Pick Ups and Deliveries](essentials_pick_ups.md) \ No newline at end of file diff --git a/docs/user_guide/bank/essentials_donations.md b/docs/user_guide/bank/essentials_donations.md index d1feb1f0f2..0737967741 100644 --- a/docs/user_guide/bank/essentials_donations.md +++ b/docs/user_guide/bank/essentials_donations.md @@ -1,24 +1,25 @@ -DRAFT USER GUIDE +READY FOR REVIEW + # Donations -Donations are obviously one of the most important elements of an essential bank's operations, being the primary way most banks get the goods to distribute to their partners to help folk. +Donations are obviously one of the most important elements of an Essential Bank's operations, being the primary way most banks get the goods to distribute to their partners to help folk. -In Human Essentials you enter in-kind donations by specifying where they came from, where they are being stored, and how many of each item are included. You can also record monetary or mixed donations. +In Human Essentials you enter in-kind donations by specifying where they came from, where they are being stored, and how many of each item are included. You can also record monetary or mixed Donations. -## Seeing all your donations +## Seeing all your Donations -To view a list of all your donations, click on 'Donations', then "All Donations" in the left-hand menu, +To view a list of all your Donations, click on 'Donations', then "All Donations" in the left-hand menu, This defaults to a list of the donations made in the default period of 60 days in the past to 30 days in the future. ![Navigating to Donations](images/essentials/donations/essentials_donations_1.png) -This screen includes a filter so you can narrow down your search to a particular donation, and some basic information on each donation: -- Source -- Whether this comes from a [Product Drive](product_drives.md), Manufacturer, Donation Site, or Miscellaneous Donation -- Date -- The date of the donation -- Details -- this depends on the source -- it is the name of the product drive, or manufacturer, or donation site -- Storage location -- where you are stored the goods from this donation. -- Quantity of items -- total quantity of the items in the donation. +This screen includes a filter so you can narrow down your search for a Donation, and some basic information on each Donation: +- Source -- Whether this comes from a [Product Drive](product_drives.md), [Manufacturer](community_manufacturers.md), [Donation Site](community_donation_sites.md), or Miscellaneous Donation +- Date -- The date of the Donation +- Details -- this depends on the source -- it is the name of the Product drive, or Manufacturer, or Donation site +- Storage location -- where the goods from this Donation are stored. +- Quantity of items -- total quantity of the Items in the Donation. - Money raised - in dollars. -- In Kind value -- this is calculated by multiplying the number of each item by the current fair market value of that item (entered in [Inventory | Items](inventory_items.md) ) +- In-Kind value -- this is calculated by multiplying the quantity of each Item by the current fair market value of that Item (entered in [Inventory | Items](inventory_items.md) ) - Comments - Actions - you can view or print donations from this screen. Other actions are available on the view screen. @@ -27,82 +28,84 @@ This screen includes a filter so you can narrow down your search to a particular ![Donations Filter Section](images/essentials/donations/essentials_donations_2.png) -When you have been using human essentials for a few months, your donation list may grow to the point that you really need to be able to narrow things down to find a particular donation. +When you have been using Human Essentials for a few months, your Donation list may grow to the point that you want to narrow things down to find a particular donation quickly. -To help with that, you can filter the donation list by several aspects: storage location, source, product drive, product drive participant, manufacturer, donation site, and date range. +To help with that, you can filter the Donation list by several aspects: Storage Location, source, Product Drive, Product Drive Participant, Manufacturer, Donation Site, and date range. Except for date range, all the filters are specified by picking from a drop-down list. -Date range is selected using a little calendar gizmo with several presets. We highly recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing mismatches there every month. +Date range is selected using a little calendar gizmo with several presets. We recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing mismatches there every month. -When you have have selected your filters, press "Filter" to do the filtering. If you still have too many donations showing, you can add another filter to narrow it down further. +When you have have selected your filters, press "Filter" to do the filtering. If you still have too many Donations showing, you can add another filter to narrow it down further. -Clicking "Clear filters" will blank out the filters that are drop-down selection, and revert the date range to the current year. +Clicking "Clear filters" will blank out the filters that are drop-down selection, and revert the date range to the default period. ## New Donations -To enter a new donation, click "Donations", then "New Donation" on the left hand menu, or click the +New Donation button on the Donations list. +To enter a new Donation, click "Donations", then "New Donation" on the left hand menu, or click the +New Donation button on the Donations list. ![Navigation to New Donations](images/essentials/donations/essentials_donations_3.png) -Here you can enter all the information for your donation: +Here you can enter all the information for your Donation: - Source - - What kind of donation this is. You have a choice of product drive, donation site, manufacturer or misc. donation. If you pick anything but misc. donation, you will be prompted for more details. + - What kind of Donation this is. You have a choice of Product Drive, Donation Site, Manufacturer or Misc. Donation. If you pick anything but Misc. Donation, you will be prompted for more details. - Storage Location - Money raised in dollars (optional) - Comment (optional) - Issued On - - This is the date that the donation was made. Note that inventory changes happen as of when you enter the information. They are not back-dated. -- Items in this donation - - If you have set up [barcodes ](inventory_barcodes.md) for the items you are receiving, you can use your barcode reader to enter your items. Otherwise, pick the item from "Choose an item", and enter the number of that item in "Quantity". - - Click "add another item" as needed to get room for more items. + - This is the date that the Donation was made. Note that inventory changes happen as of when you enter the information. They are not back-dated. +- Items in this Donation + - If you have set up [barcodes ](inventory_barcodes.md) for theIitems you are receiving, you can use your barcode reader to enter your Items. Otherwise, pick the Item from "Choose an item", and enter the number of that Item in "Quantity". + - Click "Add Another Item" as needed to get room for more Items. When you are finished entering your information, click 'Save'. ### Information entered in Donations #### Source -Here you specify what kind of source the donation originated from. +Here you specify what kind of source the Donation originated from. For Product Drive, Manufacturer, or Donation Site, you will further specify the specific source. ##### *Product Drive -If you specify Product drive, you'll also need to specify the particular drive and participant for the donation -- but you can enter them "on the fly", here. You can view summaries for the product drives in [Product Drives](product_drives.md), and manage the contact info for a product drive participant under [Community | Product Drive Participants](community_product_drive_participants.md). +If you specify Product drive, you'll also need to specify the particular drive and participant for the Donation -- but you can enter them "on the fly", here. You can view summaries for the Product Drives in [Product Drives](product_drives.md), and manage the contact info for a Product Drive Participant under [Community | Product Drive Participants](community_product_drive_participants.md). ##### *Donation Site Donation Site is meant to capture the concept of any place you have a more-or-less permanent place people can drop off donations at, such as your main office, or community locations such as fire halls, etc. -You can see the donations for each donation site and manage their contact information under [Community|Donation Sites](community_donation_sites.md). Unlike product drives, you can't create a new donation site 'on the fly' through the fields here. - +You can see the Donations for each Donation Site and manage their contact information under [Community|Donation Sites](community_donation_sites.md). Unlike Product Drives, you can't create a new Donation Site 'on the fly' through the fields here ##### *Manufacturer -This is for the donations that come straight from the manufacturer. You can view the total donations and a donation breakdown for each manufacturer under [Community | Manufacturers](community_manufacturers.md) +This is for the donations that come straight from a Manufacturer. You can view a breakdown of the Donations for each Manufacturer under [Community | Manufacturers](community_manufacturers.md) ##### *Misc. Donation -Misc. Donation is a catch-all for any donation without an identified source. +Misc. Donation is a catch-all for any Donation without an identified source. #### Storage Location -Pick the storage location the donation is going to. If it will be split among multiple locations, you can either enter separate donations, or enter the donation using one location, then use the [Inventory | Transfers] function to move the appropriate inventory to other locations. +Pick the Storage Location the Donation is going to. If it will be split among multiple locations, you can either enter separate Donations, or enter the Donation using one location, then use the [Inventory | Transfers] function to move the appropriate inventory to other locations. #### Money raised in dollars -Self explanatory. This number is used in the Annual Survey, and the Donations - Summary report as well as being shown in the main Donations page. +This number is used in the Annual Survey, and the Donations - Summary report as well as being shown in the main Donations page. #### Issued On -This is the date we use for any and all date filtering that you might do on donations. It is meant to be the date that you received the donation. It defaults to the current date. +This is the date we use for any and all date filtering that you might do on Donations. It is meant to be the date that you received the Donation. It defaults to the current date. #### Items in this donation -There are a couple of ways to get items into the donation quickly: -(1)You can "bloop" a barcode in to get your items into the system -- that requires some initial setup, as detailed in [Inventory | Barcodes] or (2) You can pick the item from the drop-down of all *active* items in your system, and enter the quantity of that item. +There are a couple of ways to get Items into the donation quickly: +(1) You can "bloop" a barcode -- that requires some initial setup, as detailed in [Inventory | Barcodes], or +(2) You can pick the item from the drop-down of all *active* items in your system, and enter the quantity of that item. + In either case, you can click "Add Another Item" (3) to open up another item for entry, or "Remove" (4) if you've added too many! -The quantity here is meant to be individual items (e.g. diapers), rather than packs. The reason behind this is that, ultimately, your reporting will be based on the number of individual items, and package size is inconsistent across manufacturers. -Note: If you make two entries with the same item, they will be added together when you view them later. +The quantity here is meant to be individual items (e.g. diapers), rather than packs. The reason behind this is that, ultimately, your reporting will be based on the number of individual items, and package size is inconsistent across brands. + +Note: If you make two entries with the same Item, they will be added together when you view them later. -When you are done entering your items, click "Save". Barring any errors, this will return you to the "All Donations" page +When you are done entering your Items, click "Save". Barring any errors, this will return you to the "All Donations" page -## Viewing the details of a donation -To view the details of a donation, click on the "View" button beside the donation on the All Donations page. +## Viewing the details of a Donation +To view the details of a Donation, click on the "View" button beside the donation on the All Donations page. ![Navigation to New Donations](images/essentials/donations/essentials_donations_4.png) -Here you'll see the donation, including: +Here you'll see the Donation, including: - Date - Source - Donation Site @@ -115,26 +118,26 @@ Here you'll see the donation, including: - Total in-kind value - Comment -You can make a correction, to delete the donation, to print it. You can also start a new distribution from this page. (This is a convenience for banks that distribute the bulk of their donations immediately, and don't fulfill partner requests.) +You can make a correction, or delete the Donation, or print it. You can also start a new Distribution from this page. (This is a convenience for banks that distribute the bulk of their Donations immediately, and don't fulfill Partner Requests.) ![Single Donation View](images/essentials/donations/essentials_donations_5.png) -## Editing a donation -Donations shouldn't need to be updated very often -- you usually have all the information you need when you enter them the first time. Differences in the physical count, or new information may occasion a need for a correction, though. If you need to correct a donation, you first "View" it (see above), then "Make a Correction" +## Editing a Donation +Donations shouldn't need to be updated very often -- you usually have all the information you need when you enter them the first time. Differences in the physical count, or new information may occasion a need for a correction, though. If you need to correct a Donation, you first "View" it (see above), then "Make a Correction" ![Navigation to Edit a Donation](images/essentials/donations/essentials_donations_6.png) -Here, you can change all the information on the donation, including adding and removing items. +Here, you can change all the information on the Donation, including adding and removing items. -##### Note that changes you make to the levels of items will take effect as of the date you made the changes. They will not be back-dated to the "issued on" date +##### Note that changes you make to the levels of Items will take effect as of the date you made the changes. They will not be back-dated to the "issued on" date -##### Note also that donations do not currently work well with kits. We have an open issue for this. +##### Note also that Donations do not currently work well with Kits. We have an open issue for this. -## Deleting a donation -Hopefully you won't need to delete a donation - but it's certainly possible that you might have entered a duplicate. Should you need to delete the donation, "View" it from the All Donations page, then click 'Delete'. You'll be asked to confirm your decision. Use this with extreme caution - because you won't be able to undelete it! +## Deleting a Donation +Hopefully you won't need to delete a Donation - but it's certainly possible that you might have entered a duplicate. Should you need to delete the donation, "View" it from the All Donations page, then click 'Delete'. You'll be asked to confirm your decision. Use this with extreme caution - because you won't be able to undelete it! -## Printing a donation -You can print a single donation by either viewing it, then clicking print, or just clicking "Print" beside it on the All Donations page. The printout is meant to be useful for parallel record keeping, or as the basis for a tax receipt. +## Printing a Donation +You can print a single Donation by either viewing it, then clicking print, or just clicking "Print" beside it on the All Donations page. The printout is meant to be useful for parallel record keeping, or as the basis for a tax receipt. [Prior: Dashboard](essentials_dashboard.md)[Next: Purchases](essentials_purchases.md) \ No newline at end of file diff --git a/docs/user_guide/bank/essentials_pick_ups.md b/docs/user_guide/bank/essentials_pick_ups.md index c1a9e7da9f..b5cc6a81c4 100644 --- a/docs/user_guide/bank/essentials_pick_ups.md +++ b/docs/user_guide/bank/essentials_pick_ups.md @@ -1,3 +1,4 @@ +READY FOR REVIEW # Pick Ups & Deliveries Calendar Pick Ups & Deliveries shows scheduled and completed distributions in a calendar format on a monthly basis. @@ -6,18 +7,18 @@ Click on "Pick Ups & Deliveries" in the left-hand menu to view the calendar. ![PickUps & Delivery Calendar](images/essentials/pick_ups/pickup&delivery.jpg) -Click on any scheduled distribution to view details on all the distributions for that day. -The Distribution Schedule page shows details including the Partner,the time of distribution, source inventory, the total number of items, and the status of the distribution. +Click on any scheduled Distribution to view details on all the Distributions for that day. +The Distribution Schedule page shows details including the Partner,the time of Distribution, Storage Location, the total number of items, and the status of the Distribution. ![Specific Day Distribution](images/essentials/pick_ups/specific_day_distribution_schedule.jpg) Once the partner has the distributed goods, clicking "Distribution Complete" changes the status of the distribution from "Scheduled" to "Complete", so you can track what is still in your hands and what is with your partners in the community. It does not, at this time, remove it from the calendar. -Click "View" for details on the distribution's source location, agency representative, delivery method, shipping cost, comments, and state. This also shows a list of items included in the distribution. +Click "View" for details on the Distribution's source Storage Location, agency representative, delivery method, shipping cost, comments, and state. This also shows a list of items included in the Distribution. ![Distribution from Source Inventory to Partner](images/essentials/pick_ups/distribution_from_source_to_partner.jpg) -If you want to print the details of the distribution to use as a contents list or receipt, click on "Print". +If you want to print the details of the Distribution to use as a contents list or receipt, click on "Print". ## Sync Pick Ups & Deliveries Calendar with Google Calendar @@ -32,7 +33,7 @@ Then, open your Google Calendar. On Google Calendar, in the "Other calendars" s Select "From URL"and paste the URL you copied in the "URL of calendar" section and click on "Add Calendar" -![Add Calendar](images/essentials/pickups/add_calendar.jpg) +![Add Calendar](images/essentials/pick_ups/add_calendar.jpg) Events from the Human Essentials Pick Ups & Deliveries Calendar should be accessible to you on Google Calendar when the sync is complete. diff --git a/docs/user_guide/bank/essentials_purchases.md b/docs/user_guide/bank/essentials_purchases.md index 618b209133..26002188c0 100644 --- a/docs/user_guide/bank/essentials_purchases.md +++ b/docs/user_guide/bank/essentials_purchases.md @@ -1,67 +1,105 @@ - -DRAFT USER GUIDE +READY FOR REVIEW # Purchases -The other major way we add to inventory in human essentials is through purchases. +The other major way we add to inventory in human essentials is through Purchases. -In Human Essentials you enter in-kind donations by specifying the vendor, where they are being stored, and how many of each item are included. +In Human Essentials you enter Purchases by specifying the Vendor, where they are being stored, and how many of each Item are included. -## Seeing all your purchases +## Seeing all your Purchases -To view a list of all your purchases, click on 'Purchases', then "All Purchases" in the left-hand menu, +To view a list of all your Purchases, click on 'Purchases', then "All Purchases" in the left-hand menu, -[TODO: Insert navigational screenshot here.] +![Navigaton to view all Purchases](images/essentials/purchases/essentials_purchases_1.png) -This screen includes filters so you can narrow down your search to a particular purchase, and some basic information on each purchase: -- Purchases from - the vendor you purchased the goods from -- Storage location -- where you are stored the goods from this purchase. +This screen includes filters so you can narrow down your search to a particular Purchase, and some basic information on each Purchase: +- Purchases from - the Vendor you purchased the goods from +- Storage location -- where you are stored the goods from this Purchase. - Comments -- Quantity of items -- the total number of items in the purchase -- Variety of items -- the number of different items in the purchase +- Quantity of items -- the total number of Items in the Purchase +- Variety of items -- the number of different Items in the Purchase - Amount spent (in dollars) -- FMV -- this is the Fair Market Value of the purchase using to the current fair market value of the items in it. Fair Market Value can be entered on the item in [Inventory | Items](inventory_items.md) [TODO: Make that point to the appropriate section witin inventory items once we have it written] -- Actions - you can view more details on each purchase from this screen. +- FMV -- this is the Fair Market Value of the Purchase using the current fair market value of the Items in it. Fair Market Value can be entered on the item in [Inventory | Items](inventory_items.md) +- Actions - you can view more details on each Purchase from this screen. ### Filters -You can filter your purchases by single storage location , by single vendor, or by purchase date range. +You can filter your Purchases by single Storage Location , by Single vendor, or by date range. + +![filter section](images/essentials/purchases/essentials_purchases_2.png) +The Vendors and Storage Locations are selected using drop-down lists of all your bank's Storage Locations / Vendors. +Date range is selected using a little calendar gizmo with several presets. We highly recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing problems there every month. +Once you have selected your values, click Filter to make the list conform to your selection. To reset the selection, just click "Clear Filters". This will set the list to all the Purchases from the current calendar year. -[TODO: mini screenshot of filter section] -The vendors and storage locations are selected using drop-down lists of all the storage locations / vendors. -Date range is selected using a little calendar gizmo with several presets. We highly recommend using the calendar gizmo instead of typing in the field, as the text field is very particular as to the format - we have a few people experiencing mismatches there every month. -Once you have selected your values, click Filter to make the list conform to your selection. To reset the selection, just click "Clear Filters". This will set the list to all the purchases from the current calendar year. +## Entering a new Purchase +To enter a new Purchase, you can either click "Purchases | New Purchase" on the left hand menu, or click the +New Purchase button on the Purchases list +![Navigation to new Purchase](images/essentials/purchases/essentials_purchases_3.png) -## Entering a new purchase -To enter a new purchase, you can either click "Purchases | New Purchase" on the left hand menu, or click the +New Purchase button on the Purchases list -[TODO: Navigational screenhot] -[TODO: screenshot of new purchase screen] +![new Purchase screen](images/essentials/purchases/essential_purchases_4.png) Enter the following information (starred items are mandatory): ### Vendor * -Select the vendor from a drop-down list of all your vendors, but if you choose "Not Listed", you can enter a new vendor on the fly. You have to enter at least one of the business name and contact name, but we recommend both (there is a current issue where only the business name shows up on dropdowns) -[TODO: screenshot of NewVendor form] +Select the Vendor from a drop-down list of all your vendors. If you choose "Not Listed", you can enter a new vendor on the fly. The Business Name is mandatory. ### Storage Location * Select the storage location from a drop-down list of all your active storage locations. -### Purchase Total *, and broken down purchase totals -The purchase total has to be greater than 0, and it should equal the sum of the 4 fields that break down the purchase into categories. These are used in the Annual Survey report. +### Purchase Total *, and broken down Purchase totals +The Purchase total has to be greater than 0, and it should equal the sum of the 4 fields that break down the Purchase into categories: +- Amount spent on diapers, +- Amount spent on adult incontinence, +- Amount spent on period supplies, and +- Amount spent on other. + +These are used in the Annual Survey report. ### Comment -Self explanatory, we hope? This shows up on the all purchases list [TODO: update this if we have included it into the purchase details] +This is optional. ### Purchase date -This should be the date the purchase was made - it is defaulted to today's date. This is used for filtering, but also for what year the purchase is included in for the annual survey. -### Items in this purchase -There are a couple of ways to get items into the purchase quickly: +This should be the date the Purchase was made - it is defaulted to today's date. This is used for filtering, but also for what year the Purchase is included in for the annual survey. +### Items in this Purchase +There are a couple of ways to get items into the Purchase quickly: (1)You can "bloop" a barcode in to get your items into the system -- that requires some initial setup, as detailed in [Inventory | Barcodes] or (2) You can pick the item from the drop-down of all *active* items in your system, and enter the quantity of that item. In either case, you can click "Add Another Item" (3) to open up another item for entry, or "Remove" (4) if you've added too many! -The quantity here is meant to be individual items (e.g. diapers), rather than packs. The reason behind this is that, ultimately, your reporting will be based on the number of individual items, and package size is inconsistent across manufacturers. +The quantity here is meant to be individual items (e.g. diapers), rather than packs. The reason behind this is that, ultimately, your reporting will be based on the number of individual items, and package size is inconsistent across brands. Note: If you make two entries with the same item, they will be added together when you view them later. -When you are done entering your items, click "Save". Barring any errors, this will return you to the "All Purchases" page +When you are done entering your Items, click "Save". Barring any errors, this will return you to the "All Purchases" page + +## Viewing a Purchase +To view Purchase details, click the "View" button beside the Purchase on the All Purchases list. +[ Navigational screenshot for view Purchase](images/essentials/purchases/essential_purchases_4.png) +This brings up a page with the Purchase's details, including all the information you entered and the entry date. From here you can make a correction to the Purchase or delete it. + + +## Editing a Purchase +Editing a Purchase should be relatively rare, but it happens. To edit a Purchase, view it (see above), then click "Make a correction". This brings up the same screen as you used to enter the info -- make your changes and click save. + +![navigational screenshot for editing Purchases](images/essentials/purchases/essentials_purchases_edit_navigation.png) + +Note that any quantity changes will be reflected in inventory as of the day you make the changes, not the Purchase date. + +## Deleting a Purchase +If, somehow, you have entered a Purchase in error, you can delete it by viewing it, then clicking Delete, and then "OK". This is a permanent deletion -- you can't undo it. + + +![navigational screenshot for deleting purchases](images/essentials/purchases/essentials_purchases_delete_navigation.png) + +You'll be asked to confirm that you want to *permanently* remove the Purchase. Click OK to confirm. + +## Exporting your Purchases +You can export your Purchases from the "All Purchases" screen, above, by clicking "Export Purchases". +![navigation for export Purchases](images/essentials/purchases/essentials_purchases_export_navigation.png) +This creates a .csv file containing all the information for each of the Purchases in your filtered list. +It includes: +- "Purchases from" - the vendor +- Storage Location +- Purchased Date +- Quantity of Items (total number of items bought) +- Variety of Items (how many different kinds of items were bought) +- Amount Spent +- Spent on Diapers +- Spent on Adult Incontinence +- Spent on Period Supplies +- Spent on Other +- Comment +- For each Item in your bank (whether it was purchased or not), in alphabetical order + - the quantity of that Item that was purchased -## Viewing a purchase -To view a single purchase, click the "View" button beside it on the All Purchases list. -## Editing a purchase -Editing a purchase should be relatively rare, but it happens. To Edit a purchase, view it (see above), then click "Make a correction" -## Deleting a purchase -If, somehow, you have entered a purchase in error, you can delete it by viewing it, then clicking Delete, and then "OK". This is a permanent deletion -- you can't undo it. -## Exporting your purchases -You can export your purchases from the "All Purchases" screen, above. This creates a .csv file containing all the information for each of the purchases in your filtered list. + [Prior: Donations](essentials_donations.md)[Next: Requests](essentials_requests.md) \ No newline at end of file diff --git a/docs/user_guide/bank/essentials_requests.md b/docs/user_guide/bank/essentials_requests.md index 83f4b2d75e..faeaeb1b7c 100644 --- a/docs/user_guide/bank/essentials_requests.md +++ b/docs/user_guide/bank/essentials_requests.md @@ -1,4 +1,4 @@ -DRAFT USER GUIDE +READY FOR REVIEW #Requests Requests are how you get the information on what items the partners need. (You may think of them as orders.) @@ -6,35 +6,32 @@ The unfulfilled ones appear in your dashboard, but you can also manage them in t For a more fulsome description of how the whole shebang works, see [Partner Management -- Request Distribution Cycle](pm_request_distribution_cycle.md). -## Seeing your unfulfilled requests -We show the unfulfilled requests in two places -- on the [dashboard](essentials_dashboard.md), and as part of the requests list, which you access by clicking "Requests" in the left hand menu. -[TODO: insert navigational/request list screenshot] +## Seeing your unfulfilled Requests +We show the unfulfilled Requests in two places -- on the [dashboard](essentials_dashboard.md), and as part of the Requests list, which you access by clicking "Requests" in the left hand menu. +![navigation to Request list](images/essentials/requests/essentials_requests_navigation.png) -On the request list, the requests are in order by the status (with pending first, then started, then fulfilled), then reverse chronological by date -You can view or cancel any request by usin gthe buttons under "Actions" -To see a list of requests, click on "Requests" in the left hand menu +On the Request list, the Requests are in order by the status (with pending first, then started, then fulfilled), then reverse chronological by date +You can view or cancel any Request by using the buttons under "Actions" +To see a list of Requests, click on "Requests" in the left hand menu -This list is defaulted to a date range of the current year, all items, all partners, and all statuses, ordered by -status, then reverse date (i.e. newest first). +This list is defaulted to a date range of the 60 days in the past to 30 days in the future (though there will be no future-dated Requests), all items, all partners, and all statuses, ordered by +status, then reverse chronological (i.e. newest first). The list contains: -- Date -- the date the request was entered by the partner -- Request was sent by -- the name of the partner that sent the request -- Request sender -- the user that sent the request [TODO: Double check that we aren't just using the partner email here] -- #of items (request limit) -- the number of items in the request, and, if you have entered it, the quota for the partner (see [Partners](getting_started_partners.md)[TODO: Point right to the quota section] -[TODO: What is the impact of packs on this?] -- Comments -- the comments the partner entered on the request +- Date -- the date the Request was entered by the partner +- Request was sent by -- the name of the partner that sent the Request +- Request sender -- the user that sent the Request +- #of Items (Request limit) -- the number of items in the Request, and, if you have entered it, the quota for the partner (see [Partners](getting_started_partners.md) +- Comments -- the comments the partner entered on the Request - Status - pending -- haven't started fulfilling it yet - started -- have started fulfilling, but haven't saved the resulting distribution - - fulfilled -- have created the distribution for this request - - discarded -- have cancelled the request -- and the actions you can take on that request -[TODO: Is it also sorted further by partner name? or something else?] -[TODO: Update when we get the default date change in.] - -### Filtering your requests -You can filter the request list by: + - fulfilled -- have created the distribution for this Request + - discarded -- have cancelled the Request +- and the actions you can take on that Request + +### Filtering your Requests +You can filter the Request list by: - Item - Partner - Status @@ -44,70 +41,77 @@ Fill in the fields with the values you want to filter by, then click "filter" To reset to the defaults, click "Clear Filters" ## Product totals -You can find out how much of each product you'll need to fulfill the current filtered open (pending and started) requests by clicking "Calculate Product Totals". -This takes into account the current filters. -[TODO: Navigational screenshot] -[TODO: Screenshot of result] - -# Viewing a request -To view a given request, click "View" beside it in the request list. -[TODO: Navigational screenshot] -[TODO: result screenshot] -This brings up details of the request including: -- partner -- date the request was sent -- who sent the request -- request status -- comments -- and, for each item in the request: +You can find out how much of each product you'll need to fulfill the current filtered open (pending and started) Requests by clicking "Calculate Product Totals". + +![Navigational screenshot, product totals](images/essentials/requests/essentials_requests_product_totals_navigation.png) +This brings up a scrollable list showing the quantity of each Item you need to fulfill the Requests. It takes into account the current filters. Click the "x" in the top right corner of the list to close it. +![Product totals list](images/essentials/requests/essentials_requests_product_totals.png) + +## Viewing a Request +To view a given Request, click "View" beside it in the Request list. +![Navigation to Request view](images/essentials/requests/essentials_requests_view_navigation.png) +This brings up details of the Request including: +- Partner +- Date the Request was sent +- Who sent the Request +- Request status +- Comments +- and, for each Item in the Request: - Item - Quantity - - If you are using custom units, those custom units will appear here. - - Total Inventory (across all storage locations) -At the bottom of the screen are buttons letting you start to fulfill the request, or to cancel it. - -# Fulfilling a request -To fulfill a request, bring up the request list by clicking on "Requests" in the left-hand menu, then click on "view" beside the request, then scroll to the bottom of that screen and click "Fulfill request". -That will bring you into a screen that allows you to specify the details for the distribution based on that request -- you'll see a notice "request started". + - If you are using [custom units](special_custom_units.md), those custom units will appear here. + - Total Inventory (across all Storage Locations) +At the bottom of the screen are buttons letting you start to fulfill the Request, or to cancel it. +![Request view](images/essentials/requests/essentials_requests_view.png) + +## Fulfilling a Request +To fulfill a Request, bring up the Request list by clicking on "Requests" in the left-hand menu, then click on "view" beside the request, then scroll to the bottom of that screen and click "Fulfill request". +That will bring you into a screen that allows you to specify the details for the distribution based on that Request -- you'll see a notice "request started". Fill in the remaining needed information. The fields include: - Partner (It would be rare indeed to change this) -- [TODO: should it be unchangeable?] - Distribution date and time (the scheduled pickup delivery or shipment date) - Send email reminder the day before? -- Agency representative (defaulted to the user who sent the request) +- Agency representative (defaulted to the user who sent the Request) - Delivery method - Pick up, - Delivery, or - Shipped - Shipping cost (if the delivery method is shipped) -- From storage location (if you have chosen a default location for the partner, or for your organization, this will be filled in) +- From Storage Location (if you have chosen a default location for the Partner, or for your organization, this will be filled in) - Comment -- For each item in the request - - The item - - When you have chosen the storage location, the quantity of this item that is available at that storage location will appear here in parentheses. +- For each Item in the Request + - The Item + - When you have chosen the Storage Location, the quantity of this Item that is available at that Storage Location will appear here in parentheses. - A field for the quantity to be distributed - the requested amount. - - For any item that has custom units (See custom units in [Getting Started -- Customization](getting_started_customization.md) [TODO: Point right at the custom units section] - - If you are using custom units, the units the partner chose will appear here as well. -You can remove any item by clicking the associated "remove" button, and you can add more items, by clicking the "Add Another Item" button. + - For any Item that has [custom units](special_custom_units.md), this will show both the amount and the unit +You can remove any Item by clicking the associated "remove" button, and you can add more iIems, by clicking the "Add Another Item" button. -When you have finished filling in the information, save the distribution by clicking the "save" button -The partner will be sent an email letting them know that their request has been fulfilled. -[TODO: include sample email] -# Cancelling a request +When you have finished filling in the information, save the Distribution by clicking the "save" button.You'll see a page showing the details of the Distribution. + +If you have set the [Partner](getting_started_partners.md) to receive emails for distributions and reminders from the system +the partner will be sent an email letting them know that their Request has been fulfilled. This will contain the text you have [customized](getting_started_customization.md), with an attachment showing the details of the distribution. +![Example distribution printout](images/essentials/distributions/essentials_distributions_printout.png) + +## Cancelling a Request +To cancel a Request from the Requests list, click the "cancel" button beside it. +![navigation to cancel Request](images/essentials/requests/essentials_requests_cancel_navigation.png) +You can also cancel a Request from the single Request view by clicking the "cancel" button at the bottom of that page. -To cancel a request from the requests list, click the "cancel" button beside it. You can also cancel a request from the single request view by clicking the "cancel" button at the bottom of that page. -[TODO: NAvigational screenshots] In either case, -You will be prompted to provide a reason for the cancellation, which will be sent in an email to the partner. -[TODO: Screenshot] -[TODO: Is it just sent to the user who sent the request, or to the partner email and the user who sent the request?] - -[TODO: Sample email] -# Exporting requests -To export the requests from the request list, click "Export requests" -[TODO: Navigational email] -This will create a .csv file with the following information for each filtered request: +You will be prompted to provide a reason for the cancellation. +![cancel Request confirmation](images/essentials/requests/essentials_requests_cancel_confirm.png) + +The Partner will receive an email notifying them of the cancellation. +![cancel Request email](images/essentials/requests/essentials_requests_cancel_email.png) + +NOTE: This email goes to the Partner, rather than to the User who sent the request. + + +## Exporting Requests +To export the Requests from the Request list, click "Export Requests" +![Export Requests navigation](images/essentials/requests/essentials_requests_export_navigation.png) +This will create a .csv file with the following information for each filtered Request: - Date - Requestor (partner) - Status @@ -115,6 +119,16 @@ This will create a .csv file with the following information for each filtered re - the quantity requested Note: If you use custom units, there will be a column for each item/unit that is available to be requested. +## Printing unfulfilled Request picklists + +Finally, you can also print "picklists" for your unfulfilled Requests. +This function produces a printable pdf file showing all the items requested for each of your unfulfilled Requests + +Click "Print Unfulfilled Picklists" on your Requests list. +![Print Picklist](images/essentials/requests/essentials_requests_print_picklists_navigation.png) +Here is a sample picklist: + +![Picklist](images/essentials/requests/essentials_requests_picklist.png) [Prior: Purchases](essentials_purchases.md) [Next: Distributions](essentials_distributions.md) diff --git a/docs/user_guide/bank/exports.md b/docs/user_guide/bank/exports.md index 33a870947f..0b6c529d63 100644 --- a/docs/user_guide/bank/exports.md +++ b/docs/user_guide/bank/exports.md @@ -1,44 +1,38 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Exports -[TODO: Screenshots throughout, if we need them] There are several exports available to allow you to use the information from the application in other apps. These all export files in .csv format, which is importable by all common spreadsheet programs. Many of these can be filtered down to the information you might need for a specific communication need. The exports available include (in alphabetical order): -- adjustments -- annual survey -- barcode items -- distributions -- donations -- donation sites -- items -- partners -- product drives -- product drive participants -- purchases -- requests -- storage locations -- transfers -- vendors - -## Adjustments -[TODO: The adjustments export needs to be better to be actually useful] +- Adjustments +- Annual Survey +- Barcode Items +- Distributions +- Donations +- Donation Sites +- Items +- Partners +- Product Drives +- Product Drive Participants +- Purchases +- Requests +- Storage Locations +- Transfers +- Vendors + +## Adjustments + ### Navigating to export adjustments 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. -### Notes: -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. - +[! 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. ## Annual Survey -[TODO: Raise question -- shouldn't this be an export *across* years?] ### Navigating to export annual survey Click "Reports", then "Annual Survey" in the left-hand menu. Then click the year of the report you want to export. Then click "Export Report." ### Contents of annual report export @@ -48,24 +42,24 @@ For more information on these, please see the [Annual Survey Report](reports_ann - Disposable diapers distributed, - Cloth diapers distributed, - Average monthly disposable diapers distributed, -- Total product drives, +- Total Product Drives, - Disposable diapers collected from drives, - Cloth diapers collected from drives, -- Money raised from product drives, -- Total product drives (virtual), -- Money raised from product drives (virtual), +- Money raised from Product Drives, +- Total Product Drives (virtual), +- Money raised from Product Drives (virtual), - Disposable diapers collected from drives (virtual), - Cloth diapers collected from drives (virtual), - Disposable diapers donated, - % disposable diapers donated, - % cloth diapers donated, -- Disposable diapers purchased, -- % disposable diapers purchased, -- % cloth diapers purchased, +- Disposable diapers Purchased, +- % disposable diapers Purchased, +- % cloth diapers Purchased, - Money spent purchasing diapers, - Purchased from, -- Vendors diapers purchased through, -- Total storage locations, +- Vendors diapers Purchased through, +- Total Storage Locations, - Total square footage, - Largest storage site type, - Adult incontinence supplies distributed, @@ -92,13 +86,13 @@ For more information on these, please see the [Annual Survey Report](reports_ann - Total children served, - Repackages diapers?, - Monthly diaper distributions?, -- % difference in yearly donations, +- % difference in yearly Donations, - % difference in total money donated, -- % difference in disposable diaper donations +- % difference in disposable diaper Donations ## Barcode Items -### Navigating to export barcode items +### Navigating to export barcode Items Click "Inventory" then "Barcode Items" in the left-hand menu. Then click "Export Barcode Items." -### Contents of barcode items export +### Contents of barcode Items export For each Barcode Item: - Item Type, - Quantity in the Box, @@ -107,13 +101,13 @@ For each Barcode Item: ### Navigating to export distributions Click "Distributions" in the left hand menu. Click the "Export Distributions" button. ### Filtering the distributions export -The distributions export shows the same distributions as are in the main distributions page, and the filtering works the same way. +The Distributions export shows the same Distributions as are in the main Distributions page, and the filtering works the same way. Before clicking the export button, you can filter by any of: - date range (we recommend you use the calendar-style selection rather than typing it in, as the format is a bit fussy.) -- item -- item category -- partner -- source inventory (i.e. storage location) +- Item +- Item Category +- Partner +- source inventory (i.e. Storage Location) - status (i.e. scheduled or complete) Specify the filtration you want, then click "filter". @@ -131,18 +125,16 @@ For each of the distributions in the filtered list: - State, - Agency Representative, - Comments, -- and the quantity in the distribution for each of your bank's items. +- and the quantity in the distribution for each of your bank's Items. -Note: This includes inactive items as well as active ones. -[TODO: confirm that that statement is accurate] +[!NOTE] This includes inactive Items as well as active ones. ## Donations - -### Navigating to export donations +### Navigating to export Donations Click "Donations", then "All Donations" in the left hand menu, then click 'Export Donations'. -### Filtering the donations export -The donations export shows the same donations as are in the main donations page, and the filtering works the same way. +### Filtering the Donations export +The Donations export shows the same Donations as are in the main Donations page, and the filtering works the same way. You can filter by any of: - Storage Location - Source (i.e. Manufacturer, Product Drive, or Misc. Donation) @@ -153,41 +145,54 @@ You can filter by any of: When you have selected your filters, click "Filter", then "Export Donations" -### Contents of the donations export -For each of the donations in the filtered list: +### Contents of the Donations export +For each of the Donations in the filtered list: - Source - Date (this is the date you enter in the donation, rather than the date it was put into the system) -- Details (this is the manufacturer name or the product drive) +- Details (this is the manufacturer name or the Product Drive) - Storage Location, -- Quantity of Items (the total quantity of items) -- Variety of Items (the number of different items) +- Quantity of Items (the total quantity of Items) +- Variety of Items (the number of different Items) - In-Kind Value, - Comments, -- and the quantity of each of your organization's items in the donations. +- and the quantity of each of your organization's Items in the Donations. ## Donation Sites -The donation sites export is not yet implemented. If this is a priority for you, please reach out. +### Navigating to export Donation Sites +Click "Community", then "Donation Sites" in the left hand menu. Then click "Export Donation Sites" +### Contents of the Donation Sites export +For each active Donation Site: +- site name +- address +- contact name +- e-mail +- phone + + + + + + ## Items -### Navigating to export items +### Navigating to export Items Click "Inventory", then "Items & Inventory" in the left hand menu. Then click "Export Items" -### Filtering the item export -By default, the export will contain all active items. -You can filter it differently by only including the items for a specific base item, -or by also including inactive items. +### Filtering the Item export +By default, the export will contain all active Items. +You can filter it differently by only including the Items for a specific base Item, +or by also including inactive Items. Select what you wish to filter by,than click "Filter". -### Contents of the item export -For each filtered item, the export includes: +### Contents of the Item export +For each filtered Item, the export includes: - Name, -- Barcodes (each barcode associated with that item), +- Barcodes (each barcode associated with that Item), - Base Item, - Quantity (across your entire bank) ## Partners The partners export contains high level information about the partner. It does not contain the information in the partner profile. -[TODO: Do we neeed a partner profile export?] ### Navigating to export partners Click "Partner Agencies", then "All Partners" in the left-hand menu. Then click "Export Partner Agencies" ### Filtering the partner export @@ -198,11 +203,9 @@ You can export different groups of partners by clicking the partner filters whic - Awaiting review - Approved - Error -- - (TODO: Check if error is a current thing, or perhaps a relic of the two db system?) - Recertification required - Decactivated - Active -[TODO: add contents] ### Partner Export contents For each partner in the filtered list: @@ -220,19 +223,19 @@ For each partner in the filtered list: - Notes ## Product Drives -### Navigating to export product drives +### Navigating to export Product Drives Click 'Community', then 'All Product Drives' in the left hand menu, then click "Export Product Drives" -### Filtering the product drives -The product drives can be filtered by +### Filtering the Product Drives +The Product Drives can be filtered by - name, -- item category, and +- Item category, and - date range. -It is defaulted to all drives, this year. [TODO: This will need updating when we change the default timespan] +It is defaulted to all drives that overlap the default time period of 60 days back, 30 days forward. To filter the export, make your selections, then click "Filter" before clicking "Export Product Drives". ### Product Drive Export content -For each filtered product drive, the export will contain: +For each filtered Product Drive, the export will contain: - Product Drive Name, - Start Date, - End Date, @@ -240,10 +243,10 @@ For each filtered product drive, the export will contain: - Quantity of Items, - Variety of Items, - In Kind Value, and -- the quantity donated for each item in alphabetical order. +- the quantity donated for each Item in alphabetical order. - ## Product Drive Participants -### Navigating to export product drive participants +### Navigating to export Product Drive participants Click 'Community', then 'Product Drive Participants' in the left hand menu, then click "Export Product Drive Participants" ### Product Drive Participant Export content - Business Name, @@ -251,24 +254,22 @@ Click 'Community', then 'Product Drive Participants' in the left hand menu, the - Phone, - Email, - Total Diapers - - The title for this should be Total Items, as that is what is shown. - - [TODO: initialize issue for this discrepency] + - The title for this should be Total Items, as that is what is shown. We have an issue in the queue to fix that ## Purchases -### Navigating to export purchases +### Navigating to export Purchases Click 'Purchases', then 'All Purchases' in the left hand menu. Then click "Export Purchases" -### Filtering exported purchases -You can filter the purchases by: +### Filtering exported Purchases +You can filter the Purchases by: - Storage location - Vendor - Purchase date -The default is all storage locations and vendors, and the current year -[TODO: update this when we change to 60 days prior, 30 days forward] +The default is all Storage Locations and vendors, and the default period of 60 days prior, 30 days forward from today's date. Make your selection and click "Filter" before clicking "Export Purchases" ### Content of Purchases Export -For each purchase in the filtered list: -- Purchases from (the vendor name) (TODO: Shouldn't this be Purchased from, or Vendor?) +For each Purchase in the filtered list: +- Purchases from (the Vendor name) - Storage Location, - Purchased Date, - Quantity of Items, @@ -279,72 +280,69 @@ For each purchase in the filtered list: - Spent on Period Supplies, - Spent on Other, - Comment, and -- the quantity of each item included in the purchase. +- the quantity of each Item included in the Purchase. ## Requests -### Navigating to export requests +### Navigating to export Requests Click 'Requests' in the left-hand menu, then "Export Requests" -### Filtering exported requests -You can filter the exported requests by the following: +### Filtering exported Requests +You can filter the exported Requests by the following: - Item - Partner - Status (Pending, Started, Fulfilled, Discarded) - Date Range Make your selection, then click 'Filter' before clicking 'Export Requests' -The default is all requests in the current year - -(TODO: update the default if we get to that before this is published) +The default is all Requests in the last 60 days -### Contents of requests export -For each filtered request, +### Contents of Requests export +For each filtered Request, - Date, - Requestor (i.e. partner) - Status, and -- the quantity of each item requested. - - Note: If you have packs enabled (upcoming feature), there will be a column for each unit that you have enabled for each item. Otherwise, one column per item. +- the quantity of each Item requested. + - Note: If you have packs enabled (upcoming feature), there will be a column for each unit that you have enabled for each Item. Otherwise, one column per Item. ## Storage Locations -### Navigating to export storage locations +### Navigating to export Storage Locations Click "Inventory", then "Storage Locations" in the left-hand menu. Then click "Export Storage Locations" ### Filtering exported Storage Locations -You can filter the exported list by Item. This will give all storage locations that have ever had that item, *not* those with current inventory. -(Note: including inactive is currently broken) -[TODO: Write up including inactive] -[TODO: Check that by item filter works for export storage location] -The default is all active storage locations. -[TODO: IT looks like the export doesn't include the inactive when selected.] +You can filter the exported list by Item. This will give all Storage Locations that have ever had that Item, *not* just those with current inventory. +(Note: including inactive Storage Locations is not working at time of writing) + +The default is all active Storage Locations. + Make your selections, then click "Filter" before clicking "Export Storage Locations" -### Contents of storage location exports -For each storage location in the filtered list: +### Contents of Storage Location exports +For each Storage Location in the filtered list: - Name, - Address, - Square Footage, - Warehouse Type, - Total Inventory, and -- Quantity for each of the organization's items. +- Quantity for each of the organization's Items. ## Transfers -### Navigating to export transfers +### Navigating to export Transfers Click "Inventory", then "Transfers" in the left-hand menu. Then click "Export Storage Locations" -### Filtering exported transfers -You can filter the transfers by: +### Filtering exported Transfers +You can filter the Transfers by: - From location - To location - date range. - - Note that this is the date the transfer was entered in the system, which may or may not be when it happened. + - Note that this is the date the Transfer was entered in the system, which may or may not be when it happened. -The default is all the transfers for the current year (Note: we are soon changing this to 60 days prior, 30 days forward from today). +The default is all the Transfers for the period of 60 days prior, 30 days forward from today. Make you selection and click 'Filter' before clicking "Export Transfers". -### Contents of transfers exports -For each selected transfer: +### Contents of Transfers export +For each selected Transfer: - From, - To, - Comment, - Total Moved -- (TODO: Really? This should have the total moved for each item. Make an issue to make it so.) +[! NOTE] We have an issue in the queue to provide the total moved for each Item. ## Vendors ### Navigating to Export Vendors diff --git a/docs/user_guide/bank/getting_started_access_levels.md b/docs/user_guide/bank/getting_started_access_levels.md index e4703f9533..2b017c4484 100644 --- a/docs/user_guide/bank/getting_started_access_levels.md +++ b/docs/user_guide/bank/getting_started_access_levels.md @@ -1,4 +1,4 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Levels of Access There are 4 different levels of access in the system: @@ -7,45 +7,43 @@ There are 4 different levels of access in the system: 3. Partner 4. Super Admin -(We are currently discussing adding a user who is restricted to a specific location, but we don't have it yet) +(We are currently discussing adding a User who is restricted to a specific location, but we don't have it yet) ## Organization Admin -Organization Admins are the top-level user at your essentials bank. They can do everything an Organization User (see below) can do, plus: +Organization Admins are the top-level User at your essentials bank. They can do everything an Organization User (see below) can do, plus: -- Administer the users for your bank ( see [User Management](getting_started_user_management.md)) -- Administer the users for your partners ( see [Partner User Management](pm_partner_user_admin.md)) -- Finalize audits (see [Audits](inventory_audits.md)) +- Administer the Users for your bank ( see [User Management](getting_started_user_management.md)) +- Administer the Users for your partners ( see [Partner User Management](pm_partner_user_admin.md)) +- Finalize Audits (see [Audits](inventory_audits.md)) - Customize organization settings( see [Customization](getting_started_customization.md)) ## Organization User This is your basic user for your bank -- they can do all the data entry except for those things reserved for the Organization Admin, above. Organization Users have the right to: -- Enter donations, purchases, distributions -- Fulfil requests -- View pickup and delivery calendars +- Enter Donations, Purchases, Distributions +- Fulfil Requests +- View Pickup and Delivery calendars - Administer Partners (excluding partner user management) - Perform Audit data entry (finalization is reserved for the Organization Admin) -- Manage the Community information +- Manage the community information (Donation Sites, Product Drives, Product Drive Participants, Manufacturers) - View all the bank-level reports ## Partner -Partner users' access is, of course, limited to the scope of the Partner they belong to. +A Partner User's access is, of course, limited to the scope of the Partner they belong to. Within that scope, they can: -- Edit their partner profile -- Submit requests (if the Partner has been approved) -- View their distributions -- Administer families and children (only if they are allowed to make Child Requests (this can be limited by the Org Admin, see [Customization](getting_started_customization.md) )) (note, the other user types cannot see this information) +- Edit their Partner profile +- Submit Requests (if the Partner has been approved) +- View their Distributions +- Administer Families and Children (only if they are allowed to make Child Requests (this can be limited by the Org Admin, see [Customization](getting_started_customization.md) )) (note, the other user types cannot see this information) ## Super Admin This bit is for information only -- A limited number of Human Essentials staff have a superadmin role that allows us to perform the following duties: - Review and approve Account requests - Make system-wide announcements - Administer users (we need it because every once in awhile, the organization admin for a bank will leave without promoting someone else to that role.) -- Administer "Base Items" +- Administer Base Items - Administer the list of NDBN members - Administer some organization settings -[TODO: Raise the question of whether we *should* be administering most of this -- we don't have rights to change any of the more recently added values on the org.] - [Prior: Customization](getting_started_customization.md)[Next: User Management](getting_started_user_management.md) \ No newline at end of file diff --git a/docs/user_guide/bank/getting_started_choices.md b/docs/user_guide/bank/getting_started_choices.md index 673420e169..66f18524d9 100644 --- a/docs/user_guide/bank/getting_started_choices.md +++ b/docs/user_guide/bank/getting_started_choices.md @@ -1,8 +1,6 @@ -DRAFT USER GUIDE +READY FOR REVIEW -This page is a work in progress until we are all done. - -# Some up front things to think about +# Some Things to Think about before Getting Started Human Essentials is designed to help you manage your inventory and your relationship with partners, while also providing at least some information to help with your grant writing. diff --git a/docs/user_guide/bank/getting_started_customization.md b/docs/user_guide/bank/getting_started_customization.md index 9f3a239b6d..28fceedd3f 100644 --- a/docs/user_guide/bank/getting_started_customization.md +++ b/docs/user_guide/bank/getting_started_customization.md @@ -1,46 +1,55 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Organization information and customization -Every essentials bank has its own way of doing things. +Every Essentials Bank has its own way of doing things. With that in mind, there are a number of things you can tweak. This is done through "My Organization". Only organization admins have access to this area. ## Getting to the organization edit -Scroll down to the bottom of the left-hand menu (you may have to collapse areas that you've opened)to the last item. Click on "My Organization". +Scroll down to the bottom of the left-hand menu (you may have to collapse areas that you've opened) to the last option. Click on "My Organization". + +![Navigation 1](images/getting_started/customization/gs_customization_navigation_1.png) This brings up a view of the organization settings. It shows everything we are going to talk about for the rest of this section, as well as the users (more on them in the next section) + Scroll down until you see an Edit button. Click it. -You should now be in a screen that is titled "Editing [Your bank name]" +![Navigation 1](images/getting_started/customization/gs_customization_navigation_1.png) + + +You should now be in a screen that is titled "Editing [Your bank name]". Change the information to suit your organization's needs and then click the "save" button at the bottom + +![Top of edit screen](images/getting_started/customization/gs_customization_top_of_edit.png) Here's all the fields, with a bit about the implications of each one: -## Basic Information -### Name -The name of your essentials bank. This appears in the headings on most screens, and will appear on printouts (such as the distribution printout many banks use as a packing slip), and most reports. +## Basic Information +#### Name +The name of your Essentials Bank. This appears in the headings on most screens, and will appear on printouts (such as the distribution printout many banks use as a packing slip), and most reports. -### Short name +#### Short name You don't change this -- we assigned it when we set it up -- it's here for reference for support calls if we need it. -### NDBN membership -This should be filled in already from your account request,but if it isn't, you can select it from the list. That list is updated on an irregular basis, so if you are an NDBN member, and you aren't on the list, let us know and we'll get a fresh list uploaded. -This is included on the Annual survey report. That's the only effect. +#### NDBN membership +This should be filled in already from your Account Request,but if it isn't, you can select it from the list. That list is updated on an irregular basis, so if you are an NDBN member, and you aren't on the list, let us know and we'll get a fresh list uploaded. +This is included on the Annual Survey report. That's the only effect. -### Url -Your essentials bank's website address. This is mostly used during the account request process, so we can check if you are a good fit before you invest a lot of time and energy into the system. +#### Url +Your Essentials Bank's website address. This is mostly used during the Account Request process, so we can check if you are a good fit before you invest a lot of time and energy into the system. -### Email -Your essential bank's email address. This is shown to the partners on their help page, and is included in reminder emails, so please use an email that is monitored. This email is also included on distribution and donation printouts and the annual survey [TODO: Confirm each of those.] +#### Email +Your Essential Bank's email address. This is shown to the partners on their help page, and is included in reminder emails, so please use an email that is monitored. This email is also included on Distribution and Donation printouts. -### Address -Your essential bank's primary address. This is shown on the distribution and donation printouts, and the annual survey [TODO: Confirm annual survey] +#### Address +Your Essential Bank's primary address. This is shown on the distribution and Donation printouts. +------------ ## Reminder Emails (optional) You can opt, on a partner by partner basis, to have reminder emails sent. -There is also a check-box on the partner that must be checked for the partner to get these emails. +There is also a check-box on the Partner that must be checked for the Partner to get these emails. The text of this email will be: @@ -48,7 +57,7 @@ The text of this email will be: Hello [Partner's name], This is a friendly reminder that [Your bank's name] requires your human essentials requests to be submitted by [the deadline date, including month and year] -if you would like to receive a distribution next month. +if you would like to receive a Distribution next month. Please log into Human Essentials at https://humanessentials.app before this date and submit your request if you are intending to submit an essentials request. @@ -60,20 +69,21 @@ if you have any questions about this! -### Reminder day (Day of month an e-mail reminder is sent to partners to submit requests) - [TODO: This will need updating with the day of week/day of month update]At this point, we send those emails once a month on the day of the month you indicate here. +#### Reminder day (Day of month an e-mail reminder is sent to partners to submit Requests) +At this point, we send those emails once a month on the day of the month you indicate here. If you do not pick a day, no reminder emails are sent. -### Deadline day (Final day of the month to submit requests) +#### Deadline day (Final day of the month to submit Requests) This day will be included in the reminder email message, +---------- -## Default Intake Location +#### Default Intake Location -This is the default storage location for donations and purchases. -If you specify this, it will be pre-populated as the storage location when you are adding new donations or purchases. +This is the default storage location for Donations and Purchases. +If you specify this, it will be pre-populated as the storage location when you are adding new Donations or Purchases. -## Partner Profile Sections +#### Partner Profile Sections The [Partner Profile](pm_partner_profiles.md) is a very large form that includes a lot of information. You might not care about all of it. This field lets you specify which of the sub-sections of that form will be used. The Agency Information subsection is always included. @@ -88,92 +98,120 @@ The sections are: - Agency Distribution Information - Attached Documents -## Default Storage Location +#### Default Storage Location -The bank-wide default storage location for donations and purchases. -You can also specify a different default storage location on any partner, which will override this default. -If you specify a default storage location, it will be pre-populated as the storage location when you are adding new distributions. +The bank-wide default Storage Location for Donations and Purchases. +You can also specify a different default Storage Location on any Partner, which will override this default. +If you specify a default Storage Location, it will be pre-populated as the Storage Location when you are adding new Distributions. -## Custom Partner Invitation Message -[TODO: Ensure that this is working!] +#### Custom Partner Invitation Message +[!NOTE] The Custom Partner Invitation Message is currenty not working as advertised (as of November 13, 2024.). The current behavior is as if you did not enter anything here. We have fixing it on our "to do" list. -When you invite a partner, they get an email. This field lets you specify the message you are sending to them. Just text -- we don't have any personalization capability for this email at this time. +When you invite a Partner, they get an email. This field lets you specify the message you are sending to them. Just text -- we don't have any personalization capability for this email at this time. If you do not specify a message, the invitation will contain: -Hello [partner's email] +Hello [Partner's email] You've been invited to become a partner with Pawnee Diaper Bank! -Please click the link below to accept your invitation and create an account and you'll be able to begin requesting distributions. +[Customer Partner Invitation Message If Present] + +Please click the link below to accept your invitation and create an account and you'll be able to begin requesting Distributions. -Please contact [bank's email] if you are encountering any issues. +Please contact [Bank's email] if you are encountering any issues. + +[Accept Invitation button] -Accept Invitation For security reasons these invitations expire. This invitation will expire in 8 hours or if a new password reset is triggered. If your invitation has an expired message, go here(link to the log in page) and enter your email address to reset your password. Feel free to ignore this email if you are not interested or if you feel it was sent by mistake. +---------- +## Questions for the Annual Survey +These two fields are only here to be reported on the Annual Survey. -## Questions for the annual survey -These fields are only here to be reported on the annual survey. - -### Does your bank repackage essentials? -### Does your bank distribute monthly +#### Does your Bank repackage essentials? +#### Does your Bank distribute monthly -## Custom Units +----------- -NOTE: This is not yet implemented as of Oct 12, 2024. +#### Custom Units -The number of items throughout the bank's view of the system is the number of units (e.g. diapers), but -partners often think in terms of packs of diapers. Because banks were getting a lot of partners requesting the number of packs of diapers, instead of the number of diapers, we have introduced the ability for banks to allow the partners to request other units (e.g. packs) +NOTE: This is not yet implemented as of Oct 12, 2024. We expect it to be implemented before this guide is launched. -This deserves a page of it's own - but in short, you can specify units here, that you can add to your items. The partners then can ask for, say, 'packs' of diapers. You will still have to translate those to the number of items when distributing -Because there is a lot of variety in pack size across brands. +This is a special topic that has its own guide page [here](special_custom_units.md). -[TODO: This is actually a good candidate for a video showing the whole process] +---------- -## Controlling what kind of request a partner can make +## Controlling what kind of Requests a Partner can make -There are three different ways a partner can request essentials -- a "Child based" request, a request by number of individuals, and a straight quantity-based request. Some banks want to limit which requests the partners can make, in order to minimize partner confusion. -These three fields allow you to control which requests the partners can use. -If you allow more than one kind, the partner can also limit their own. -Note that if any partner limits themselves to a single type, you won't be able to remove that type. So, if you think you only want to allow quantity-based requests, doing that up front is a fine idea. +There are three different ways a Partner can request essentials -- a "Child based" Request, a Request by number of individuals, and a quantity-based Request. Some banks want to limit which Requests the partners can make, in order to minimize partner confusion. +These three fields allow you to control which Requests the Partners can use. +If you allow more than one kind, the Partner can also limit their own. +Note that if any Partner limits themselves to a single type, you won't be able to remove that type. So, if you think you only want to allow quantity-based Requests, doing that up front is a fine idea. -### Enable partners to make child-based requests -### Enable partners to make requests for individuals? -### Enable partners to make quantity-based requests? +#### Enable partners to make child-based Requests +#### Enable partners to make Requests for individuals? +#### Enable partners to make quantity-based Requests? -## Customizing the distribution printout -There are four fields that allow you to tweak the appearance of the distribution printout +---------- +## Customizing the Distribution printout +There are four fields that allow you to tweak the appearance of the Distribution printout -### Show Year-to-date values on the distribution printout? +### Show Year-to-date values on the Distribution printout? Some banks don't want to show year-to-date values on the receipt (1, below) because their fiscal year is not the calendar year. -### Include Signature Lines on Distribution Printout -If "yes", this will include a space for someone from the bank and from the partner to sign the distribution printout (2, below) - which can be useful as a receipt acknowledgement. +### Include Signature Lines on Distribution printout +If "yes", this will include a space for someone from the bank and from the Partner to sign the Distribution printout (2, below) - which can be useful as a receipt acknowledgement. ### Hide both value columns -The default is to show the in-kind value of the items on the receipt (3, below). Many banks don't need to show this information on the distribution printout. -Note: Hiding this also hides the corresponding values on the single donation printout. -### Hide the package column on distribution receipts? -This hides the packages column on the distribution printout (4, below). Because different brands of essentials use different size packages, this -column is useful mainly for banks that repackage their essentials into uniform package sizes. If you have a uniform package size, you can specify that on the item (see [Inventory Items](inventory_items.md)) +The default is to show the in-kind value of the Items on the receipt (3, below). Many banks don't need to show this information on the Distribution printout. +Note: Hiding this also hides the corresponding values on the single Donation printout. +### Hide the package column on Distribution receipts? +This hides the packages column on the Distribution printout (4, below). Because different brands of essentials use different size packages, this +column is useful mainly for banks that repackage their essentials into uniform package sizes. If you have a uniform package size, you can specify that on the Item (see [Inventory Items](inventory_items.md)) + +![Distribution printout marked up with customizable sections](images/getting_started/customization/gs_customization_distribution_printout_customizable_sections.png) -![distribution printout marked up with customizable sections](images/getting_started/customization/gs_customization_distribution_printout_customizable_sections.png) +-------- -## Use One Step Invite and Approve partner process? -Partners can't submit requests until they are approved by the bank. +#### Use One Step Invite and Approve partner process? +Partners can't submit Requests until they are approved by the bank. The full partner approval process requires the partner to fill in their profile and submit it for approval. Some banks handle that for their partners, gather the information through other means (such as a phone conversation). Checking this will change the process so that the partners are automatically approved when they are invited. Note that any invited partners that are not yet approved will still need to be approved by the bank. -## Distribution Email Content -Note that there is a checkbox on the partner for them to receive distribution emails. We recommend you do customize this content, as the default text is abrupt. -You can customize this quite a bit! [TODO: expand. Maybe provide a real life example.] +#### Distribution Email Content +Note that there is a checkbox on the partner for them to receive Distribution emails. We recommend you do customize this content, as the default text is abrupt. +You can customize this quite a bit! + +Specifically, you can use the variables %{partner_name}, %{delivery_method}, %{distribution_date}, and %{comment} to include the partner's name, delivery method, distribution date, and comments sent in the email. You can also format the text, and attach files by using the buttons above the field. + +Here's a real-life example (except for the URL) + +------ + +%{partner_name}, + +Your essentials request has been approved and you can find attached to this email a copy of the distribution you will be receiving. + +Your distribution has been set to be %{delivery_method} on %{distribution_date}. + +Friendly reminder: don't forget to keep up with important updates at https://example.com. Subscribe there to get email notifications when updates are posted! + +See you soon! + +%{comment} + +----- + + + + -## Logo +#### Logo -The logo that you upload here will appear several places throughout the system, including on your distribution and donation printouts. Larger logos will impact your performace -- the 763 x 188 size is a good guideline. +The logo that you upload here will appear several places throughout the system, including on your Distribution and Donation printouts. Larger logos will impact your performance -- the 763 x 188 size is a good guideline. [Prior: Inventory](getting_started_inventory.md) [Next: Adding your Staff - levels of access](getting_started_access_levels.md) \ No newline at end of file diff --git a/docs/user_guide/bank/getting_started_donation_sites.md b/docs/user_guide/bank/getting_started_donation_sites.md index c34205f5a5..596de249f5 100644 --- a/docs/user_guide/bank/getting_started_donation_sites.md +++ b/docs/user_guide/bank/getting_started_donation_sites.md @@ -1,15 +1,15 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Donation Sites -A donation site represents a drop-off location for donations. -The information regarding a donation site is mainly for your reference. There are, at time of writing, no automated emails that go to the donation sites' contacts. -Note that you can filter your donation list ( see [Donations](essentials_donations.md)) by donation site. +A Donation Site represents a drop-off location for donations. +The information regarding a Donation Site is mainly for your reference. There are, at time of writing, no automated emails that go to the donation sites' contacts. +Note that you can filter your Donation list ( see [Donations](essentials_donations.md)) by Donation Site. -## Adding a single donation site -If you are following along in the "getting started", you'll have reached stage 3 "Donation Sites". You can add a single donation site by clicking on "Add a Donation Site". -![Navigating to add a donation site from getting started](images/getting_started/donation_sites/getting_started_donation_sites_1.png) -This will take you to the new donation site screen, where you can provide the basic information for your side. Click save when done: -![New donation site](images/getting_started/donation_sites/getting_started_donation_sites_2.png) +## Adding a single Donation Site +If you are following along in the "getting started", you'll have reached stage 3 "Donation Sites". You can add a single Donation Site by clicking on "Add a Donation Site". +![Navigating to add a Donation Site from getting started](images/getting_started/donation_sites/getting_started_donation_sites_1.png) +This will take you to the new Donation Site screen, where you can provide the basic information for your side. Click save when done: +![New Donation Site](images/getting_started/donation_sites/getting_started_donation_sites_2.png) -To add more donation sites, or if you are in a different place in the process, see the full entry on [Donation Sites](community_donation_sites.md). +To add more Donation Sites, or if you are in a different place in the process, see the full entry on [Donation Sites](community_donation_sites.md). [Prior: Partners](getting_started_partners.md)[Next: Inventory](getting_started_inventory.md) \ No newline at end of file diff --git a/docs/user_guide/bank/getting_started_inventory.md b/docs/user_guide/bank/getting_started_inventory.md index 4b31e63c3f..a2c062824c 100644 --- a/docs/user_guide/bank/getting_started_inventory.md +++ b/docs/user_guide/bank/getting_started_inventory.md @@ -1,32 +1,31 @@ -DRAFT USER GUIDE -# Getting started -- inventory -When we set up your organization, we provide a default set of items ranging from adult briefs to kids (newborn) +READY FOR REVIEW +# Getting started -- Inventory +When we set up your organization, we provide a default set of Items ranging from adult briefs to kids (newborn) to tampons. You can modify this list at any time, through the [Inventory Items](inventory_items.md) feature. This section of the guide will take you through a couple of ways to get your initial inventory in. -[TODO: Rewrite once we have audits available as a starting strategy] +## Adding a past Donation +One easy way to get your current inventory set up is to just enter a Donation per Storage Location with all of the current inventory, and put a +comment on it to indicate that this was your starting inventory. The values you enter there will be included in your Donation reports for the year, +so if you don't want that, either backdate the Donation to the previous year or use the Inventory Adjustments method, below. -[TODO: Really -- this should just be Audits front and center -- but that's not how the system works atm] +To enter a past Donation, simply click on the "Add past Donation" button, which takes you to the New Donation screen, which is described in a lot of detail [here](essentials_donations.md) -## Adding a past donation -One easy way to get your current inventory set up is to just enter a donation per storage location with all of the current inventory, and put a -comment on it to indicate that this was your starting inventory. The values you enter there will be included in your donation reports for the year, -so if you don't want that, either backdate the donation to the previous year or use the inventory adjustments method, below. +![navigation to enter_past_Donation](images/getting_started/inventory/gs_inventory_1.png) -To enter a past donation, simply click on the "Add past donation" button, which takes you to the New Donation screen, which is described in a lot of detail [here](essentials_donations.md) - -![navigation to enter_past_donation](images/getting_started/inventory/gs_inventory_1.png) - -## Adding a past purchase +## Adding a past Purchase Similarly, if you've started your bank with a purchase of goods, you might want to enter the details as a purchase. The values you enter here will be included in your purchase reports for the year chosen. -![navigation to enter_past_purchase](images/getting_started/inventory/gs_inventory_2.png) -## Inventory adjustment -Another (some say superior) way to set up your bank is through inventory adjustments. This has the advantage of keeping your initial -inventory, however it was acquired, out of donation and purchase reports. The details on entering an inventory adjustment can be found [here](inventory_adjustments.md) +![navigation to enter_past_Purchase](images/getting_started/inventory/gs_inventory_2.png) +## Inventory Adjustment +Another (some say superior) way to set up your bank is through Inventory Adjustments. This has the advantage of keeping your initial +inventory, however it was acquired, out of Donation and Purchase reports. The details on entering an Inventory Adjustment can be found [here](inventory_adjustments.md) ![navigation to enter new inventory adjustment](images/getting_started/inventory/gs_inventory_3.png) -[Prior: Donation Sites](getting_started_donation_sites.md) [Next: Customization and other organizational-level info](getting_started_customization.md) -[TODO: Will need to update this when we remove Donation Sites from Getting Started ] + + + + +[Prior: Donation Sites](getting_started_donation_sites.md) [Next: Customization and other organizational-level info](getting_started_customization.md) \ No newline at end of file diff --git a/docs/user_guide/bank/getting_started_partners.md b/docs/user_guide/bank/getting_started_partners.md index 07d660f8fb..a279864ad6 100644 --- a/docs/user_guide/bank/getting_started_partners.md +++ b/docs/user_guide/bank/getting_started_partners.md @@ -1,28 +1,34 @@ -DRAFT USER GUIDE -# Getting started -- partners +READY FOR REVIEW +# Getting Started -- Partner Management -## Things you need to know about partners before deciding how you are handling them +## Things You need to know about Partners before deciding how you are handling them -1/ You need to have your partners in the system to be able to record distributions to them. +1/ You need to have your Partners in the system to be able to record distributions to them. -2/ However, if you're not ready to have your partners make requests yet, that's ok -- you can put them in the system without inviting them. You'll still be able to record what you are distributing to them. +2/ However, if you're not ready to have your Partners make Requests yet, that's ok -- you can put them in the system without inviting them. You'll still be able to record what you are distributing to them. -3/ You can import all your partners at once. You can only import partners once, though -- this is a precaution to make sure we don't accidently create duplicates. +3/ You can import all your Partners at once. You can only import Partners once, though -- this is a precaution to make sure we don't accidently create duplicates. -4/ The usual way to handle bringing on partners to be able to make requests is to invite them, then have them fill in their profile before approving them, so that you get the information from them that your bank needs for grants, etc. However, if that's not how you want to work, it is also possible to invite and approve them in one step. +4/ The usual way to handle bringing on Partners to be able to make Requests is to invite them, then have them fill in their profile before approving them, so that you get the information from them that your bank needs for grants, etc. However, if that's not how you want to work, it is also possible to invite and approve them in one step. -5/ A lot of banks set up a partner as a proxy for their direct distribution. This is allowed, and you can switch back and forth between being a bank and a partner with the same login +5/ A lot of Banks set up a Partner as a proxy for their direct distribution to people in need. This is allowed, and you can switch back and forth between being a Bank and a Partner with the same login -6/ You can group partners, and allow those groups to request different sets of items (One thing some banks use this for is handling grants that are geographically constrained.) -## Adding a single partner -Click on the "Add a Partner" button on your "Getting started" screen -(you can also click on "Partner Agencies", then "All Partners", then "Add a Partner") +6/ You can group Partners, and allow those Partber Groups to request different sets of items (One thing some banks use this for is handling grants that are geographically constrained.) -![navigation](images/getting_started/partners/gs_just_starting_step_2.png) -Further details on adding a partner can be found [here](pm_adding_a_partner.md) +## Importing Partners -## Importing partners For details on how to do a bulk import of your partners, please click [here](pm_importing_partners.md) +## Adding a single Partner +For your first partner, you can Click on the "Add a Partner" button in your "Getting Started" portion of you dashboard. + +![navigation](images/getting_started/partners/gs_just_starting_step_2.png) + + +For any subsequent Partners, please click on "Partner Agencies", then "All Partners", then "Add a Partner") + +![add a partner navigation](images/partners/partners_add_1.png) +Further details on adding a partner can be found [here](pm_adding_a_partner.md) + [Prior: Storage Locations](getting_started_storage_locations.md)[Next: Donation sites](getting_started_donation_sites.md) \ No newline at end of file diff --git a/docs/user_guide/bank/getting_started_storage_locations.md b/docs/user_guide/bank/getting_started_storage_locations.md index a00613cbfe..e7639bf861 100644 --- a/docs/user_guide/bank/getting_started_storage_locations.md +++ b/docs/user_guide/bank/getting_started_storage_locations.md @@ -1,4 +1,4 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Getting Started -- Storage Locations A bank can have multiple storage locations -- these range from warehouses down to space in people's houses. You need at least one storage location in the system. diff --git a/docs/user_guide/bank/getting_started_user_management.md b/docs/user_guide/bank/getting_started_user_management.md index 30b04c749a..1655bceba7 100644 --- a/docs/user_guide/bank/getting_started_user_management.md +++ b/docs/user_guide/bank/getting_started_user_management.md @@ -1,33 +1,33 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Essentials Bank User Administration -If you're not the sole worker at your essentials bank, you'll likely want other staff to have some access to Human Essentials. +If you're not the sole worker at your Essentials Bank, you'll likely want other staff to have some access to Human Essentials. -Note that only people with admin status can administer users, and that partners are administered on a partner-by-partner basis (see [Partner User Admin](pm_partner_user_admin.md)). +Note that only people with admin status can administer Users, and that Partners are administered on a partner-by-partner basis (see [Partner User Admin](pm_partner_user_admin.md)). -To manage the rights for your essentials bank users: +To manage the rights for your Essentials Bank's Users: -Click on the My Organisation view, then scroll down to the bottom. There is a user administration section there. +Click on the My Organisation view, then scroll down to the bottom. There is a User administration section there. -You can also manage the users in your partners, see [Administering partner users](pm_partner_user_admin.md) +You can also manage the Users in your partners, see [Administering Partner Users](pm_partner_user_admin.md) +![navigation for Users in organization](images/getting_started/user_admin/gs_user_admin_navigation.png) -![navigation users in organization](images/getting_started/user_admin/gs_user_admin_navigation.png) - -## Inviting new users +## Inviting new Users Clicking "Invite User to this Organization" will open a popup where you will enter the name and email of the user. Once you click "Invite User", they will receive an email with a link to follow to set up their password. -Please note that this link expires. If they don't click on the link in time, the workaround is for them to go to the login screen (http::/human_essentials.app) and click on "reset password". +Please note that this link expires. If they don't click on the link in time, the workaround is for them to go to the login screen (http::/human_essentials.app) and click on "Reset Password". ![invite user pop_up](images/getting_started/user_admin/gs_user_admin_invite_user.png) -## Promote a user to admin -To promote a user to admin, just click on the "actions" button beside their information, and then click on "Promote to Admin" -![promote_user](images/getting_started/user_admin/gs_user_admin_promote_user.png) +## Promote a User to admin +To promote a User to admin, just click on the "actions" button beside their information, and then click on "Promote to Admin" +![promote_User](images/getting_started/user_admin/gs_user_admin_promote_user.png) ## Demote an admin -To remove a admin's admin rights, just click on the "Demote to User" button beside their information. -This is a necessary step if you want to remove their rights entirely. [TODO: Discuss if this really is necessary -- shouldn't we be able to do this as one step?] +To remove an admin's admin rights, just click on the "Demote to User" button beside their information. +This is a necessary step if you want to remove their rights entirely. + Also note that you can't demote yourself. This reduces the risk of leaving the bank without an admin accidentally! ![demote_user](images/getting_started/user_admin/gs_user_admin_demote_admin.png) ## Remove a user diff --git a/docs/user_guide/bank/images/account_management/account_management_account_settings.png b/docs/user_guide/bank/images/account_management/account_management_account_settings.png new file mode 100644 index 0000000000..808dfba651 Binary files /dev/null and b/docs/user_guide/bank/images/account_management/account_management_account_settings.png differ diff --git a/docs/user_guide/bank/images/account_management/account_management_account_settings_navigation.png b/docs/user_guide/bank/images/account_management/account_management_account_settings_navigation.png new file mode 100644 index 0000000000..fe2c47c94b Binary files /dev/null and b/docs/user_guide/bank/images/account_management/account_management_account_settings_navigation.png differ diff --git a/docs/user_guide/bank/images/account_management/account_management_logout.png b/docs/user_guide/bank/images/account_management/account_management_logout.png new file mode 100644 index 0000000000..d220846bf9 Binary files /dev/null and b/docs/user_guide/bank/images/account_management/account_management_logout.png differ diff --git a/docs/user_guide/bank/images/account_management/account_management_my_organization.png b/docs/user_guide/bank/images/account_management/account_management_my_organization.png new file mode 100644 index 0000000000..b1437a1a89 Binary files /dev/null and b/docs/user_guide/bank/images/account_management/account_management_my_organization.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_add.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_add.png new file mode 100644 index 0000000000..ec7b355f9b Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_add.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_add_navigation.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_add_navigation.png new file mode 100644 index 0000000000..2aecd0648d Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_add_navigation.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_export_navigation.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_export_navigation.png new file mode 100644 index 0000000000..ee30090202 Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_export_navigation.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify.png new file mode 100644 index 0000000000..0ae10b59ee Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify_navigation.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify_navigation.png new file mode 100644 index 0000000000..4b544b4933 Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_modify_navigation.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_navigation.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_navigation.png new file mode 100644 index 0000000000..4fab821627 Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_navigation.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_view.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_view.png new file mode 100644 index 0000000000..903639f549 Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_view.png differ diff --git a/docs/user_guide/bank/images/community/product_drives/community_product_drives_view_navigation.png b/docs/user_guide/bank/images/community/product_drives/community_product_drives_view_navigation.png new file mode 100644 index 0000000000..a27b9c93ad Binary files /dev/null and b/docs/user_guide/bank/images/community/product_drives/community_product_drives_view_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distribution_print_navigation.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distribution_print_navigation.png new file mode 100644 index 0000000000..496460f6d8 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distribution_print_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit.png new file mode 100644 index 0000000000..c79115e206 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit_navigation.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit_navigation.png new file mode 100644 index 0000000000..556f54f09d Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_navigation.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_navigation.png new file mode 100644 index 0000000000..f753b1c06b Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_sample.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_sample.png new file mode 100644 index 0000000000..3a9680b20c Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_sample.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_filter.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_filter.png new file mode 100644 index 0000000000..c2e2c433a5 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_filter.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_navigation.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_navigation.png new file mode 100644 index 0000000000..a7285b17d7 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_printout.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_printout.png new file mode 100644 index 0000000000..162aac34e0 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_printout.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view.png new file mode 100644 index 0000000000..ac224d279f Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view.png differ diff --git a/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view_navigation.png b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view_navigation.png new file mode 100644 index 0000000000..2dcd52ad77 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchaces_view.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchaces_view.png new file mode 100644 index 0000000000..e26beb4601 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchaces_view.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_1.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_1.png new file mode 100644 index 0000000000..366d41af33 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_1.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_2.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_2.png new file mode 100644 index 0000000000..ae38483afb Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_2.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_3.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_3.png new file mode 100644 index 0000000000..b8d49247a9 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_3.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_4.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_4.png new file mode 100644 index 0000000000..1ef9d3c69d Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_4.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_delete_navigation.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_delete_navigation.png new file mode 100644 index 0000000000..ee68a23666 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_delete_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_edit_navigation.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_edit_navigation.png new file mode 100644 index 0000000000..0cb7c63f77 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_export_navigation.png b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_export_navigation.png new file mode 100644 index 0000000000..ceb233f3c2 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/purchases/essentials_purchases_export_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_confirm.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_confirm.png new file mode 100644 index 0000000000..2fff9e9b1f Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_confirm.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_email.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_email.png new file mode 100644 index 0000000000..21dcd05e02 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_email.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_navigation.png new file mode 100644 index 0000000000..c88d13ef3d Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_export_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_export_navigation.png new file mode 100644 index 0000000000..dd2573b9de Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_export_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_navigation.png new file mode 100644 index 0000000000..17bfcd5c59 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_picklist.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_picklist.png new file mode 100644 index 0000000000..66f2ef5ce7 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_picklist.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_print_picklists_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_print_picklists_navigation.png new file mode 100644 index 0000000000..70d0b6531e Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_print_picklists_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals.png new file mode 100644 index 0000000000..17b0492843 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals_navigation.png new file mode 100644 index 0000000000..7cc05de46c Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals_navigation.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_view.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_view.png new file mode 100644 index 0000000000..446d4a2252 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_view.png differ diff --git a/docs/user_guide/bank/images/essentials/requests/essentials_requests_view_navigation.png b/docs/user_guide/bank/images/essentials/requests/essentials_requests_view_navigation.png new file mode 100644 index 0000000000..0bccf9ee49 Binary files /dev/null and b/docs/user_guide/bank/images/essentials/requests/essentials_requests_view_navigation.png differ diff --git a/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_1.png b/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_1.png new file mode 100644 index 0000000000..6f76807640 Binary files /dev/null and b/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_1.png differ diff --git a/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_2.png b/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_2.png new file mode 100644 index 0000000000..be763b50ad Binary files /dev/null and b/docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_2.png differ diff --git a/docs/user_guide/bank/images/getting_started/customization/gs_customization_top_of_edit.png b/docs/user_guide/bank/images/getting_started/customization/gs_customization_top_of_edit.png new file mode 100644 index 0000000000..27d071231a Binary files /dev/null and b/docs/user_guide/bank/images/getting_started/customization/gs_customization_top_of_edit.png differ diff --git a/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2.png b/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2.png index 778466ff43..9598d80585 100644 Binary files a/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2.png and b/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2.png differ diff --git a/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2_import.png b/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2_import.png new file mode 100644 index 0000000000..e64d52a79d Binary files /dev/null and b/docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2_import.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_navigation.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_navigation.png new file mode 100644 index 0000000000..06c92d8d04 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_new.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_new.png new file mode 100644 index 0000000000..deb49e6f67 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_new_navigation.png new file mode 100644 index 0000000000..ca213f0f5f Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_result.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_result.png new file mode 100644 index 0000000000..60cfe3654c Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_result.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_view.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_view.png new file mode 100644 index 0000000000..efd05b0452 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_view.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_adjustments_view_navigation.png b/docs/user_guide/bank/images/inventory/inventory_adjustments_view_navigation.png new file mode 100644 index 0000000000..a7a5889ae6 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_adjustments_view_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_confirm.png b/docs/user_guide/bank/images/inventory/inventory_audits_confirm.png new file mode 100644 index 0000000000..19c34c52bc Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_confirm.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_delete.png b/docs/user_guide/bank/images/inventory/inventory_audits_delete.png new file mode 100644 index 0000000000..d207862c06 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_delete.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_finalize.png b/docs/user_guide/bank/images/inventory/inventory_audits_finalize.png new file mode 100644 index 0000000000..b522c2a847 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_finalize.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_new.png b/docs/user_guide/bank/images/inventory/inventory_audits_new.png new file mode 100644 index 0000000000..1fa99625a6 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_audits_new_navigation.png new file mode 100644 index 0000000000..5726e239d2 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_progress.png b/docs/user_guide/bank/images/inventory/inventory_audits_progress.png new file mode 100644 index 0000000000..5326a7357d Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_progress.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_resume.png b/docs/user_guide/bank/images/inventory/inventory_audits_resume.png new file mode 100644 index 0000000000..26adff2cd5 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_resume.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_audits_view_navigation.png b/docs/user_guide/bank/images/inventory/inventory_audits_view_navigation.png new file mode 100644 index 0000000000..13815aef3e Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_audits_view_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_delete.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_delete.png new file mode 100644 index 0000000000..8705dcd980 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_delete.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_edit_navigation.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_edit_navigation.png new file mode 100644 index 0000000000..ede92711f3 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_export.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_export.png new file mode 100644 index 0000000000..0d120f436b Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_export.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_export_navigation.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_export_navigation.png new file mode 100644 index 0000000000..7d142b8883 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_export_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_navigation.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_navigation.png new file mode 100644 index 0000000000..c9c81e3fea Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_new.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_new.png new file mode 100644 index 0000000000..c33b6b10fa Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_barcodes_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_barcodes_new_navigation.png new file mode 100644 index 0000000000..262d0fb458 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_barcodes_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_category_view_navigation.png b/docs/user_guide/bank/images/inventory/inventory_category_view_navigation.png new file mode 100644 index 0000000000..4873969384 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_category_view_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_edit.png b/docs/user_guide/bank/images/inventory/inventory_item_category_edit.png new file mode 100644 index 0000000000..9ff789024f Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_edit.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_edit_navigation.png b/docs/user_guide/bank/images/inventory/inventory_item_category_edit_navigation.png new file mode 100644 index 0000000000..bb0d387814 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_navigation.png b/docs/user_guide/bank/images/inventory/inventory_item_category_navigation.png new file mode 100644 index 0000000000..26cfbc9bbc Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_new.png b/docs/user_guide/bank/images/inventory/inventory_item_category_new.png new file mode 100644 index 0000000000..39003ff592 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_item_category_new_navigation.png new file mode 100644 index 0000000000..e8a2fad3f0 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_category_view.png b/docs/user_guide/bank/images/inventory/inventory_item_category_view.png new file mode 100644 index 0000000000..945675e236 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_category_view.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_item_location_navigation.png b/docs/user_guide/bank/images/inventory/inventory_item_location_navigation.png new file mode 100644 index 0000000000..278396c755 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_item_location_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_delete_vs_deactivate.png b/docs/user_guide/bank/images/inventory/inventory_items_delete_vs_deactivate.png new file mode 100644 index 0000000000..d78691b263 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_delete_vs_deactivate.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_edit.png b/docs/user_guide/bank/images/inventory/inventory_items_edit.png new file mode 100644 index 0000000000..9ebbaac008 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_edit.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_edit_navigation.png b/docs/user_guide/bank/images/inventory/inventory_items_edit_navigation.png new file mode 100644 index 0000000000..5c2b7d5a3a Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_inventory_navigation.png b/docs/user_guide/bank/images/inventory/inventory_items_inventory_navigation.png new file mode 100644 index 0000000000..c3fb4a32c9 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_inventory_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_kits_navigation.png b/docs/user_guide/bank/images/inventory/inventory_items_kits_navigation.png new file mode 100644 index 0000000000..f29b286b07 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_kits_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_navigation.png b/docs/user_guide/bank/images/inventory/inventory_items_navigation.png new file mode 100644 index 0000000000..dc90155680 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_reactivation.png b/docs/user_guide/bank/images/inventory/inventory_items_reactivation.png new file mode 100644 index 0000000000..010790aac7 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_reactivation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_view.png b/docs/user_guide/bank/images/inventory/inventory_items_view.png new file mode 100644 index 0000000000..d1567ac51a Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_view.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_items_view_navigation.png b/docs/user_guide/bank/images/inventory/inventory_items_view_navigation.png new file mode 100644 index 0000000000..d56e463308 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_items_view_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_allocation.png b/docs/user_guide/bank/images/inventory/inventory_kits_allocation.png new file mode 100644 index 0000000000..63cdd3b643 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_allocation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_allocation_post_save.png b/docs/user_guide/bank/images/inventory/inventory_kits_allocation_post_save.png new file mode 100644 index 0000000000..1ccbbde49f Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_allocation_post_save.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_deactivate.png b/docs/user_guide/bank/images/inventory/inventory_kits_deactivate.png new file mode 100644 index 0000000000..c36e44b3b5 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_deactivate.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_modify_allocation_navigation.png b/docs/user_guide/bank/images/inventory/inventory_kits_modify_allocation_navigation.png new file mode 100644 index 0000000000..f55a09dbc0 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_modify_allocation_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_new.png b/docs/user_guide/bank/images/inventory/inventory_kits_new.png new file mode 100644 index 0000000000..a024516285 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_kits_new_navigation.png new file mode 100644 index 0000000000..8fbac6773b Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_kits_reactivate.png b/docs/user_guide/bank/images/inventory/inventory_kits_reactivate.png new file mode 100644 index 0000000000..4a24f518f2 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_kits_reactivate.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_location_reactivation.png b/docs/user_guide/bank/images/inventory/inventory_storage_location_reactivation.png new file mode 100644 index 0000000000..6165c8cae0 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_location_reactivation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_location_view_inventory.png b/docs/user_guide/bank/images/inventory/inventory_storage_location_view_inventory.png new file mode 100644 index 0000000000..7878d14224 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_location_view_inventory.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_locations_add.png b/docs/user_guide/bank/images/inventory/inventory_storage_locations_add.png new file mode 100644 index 0000000000..bd760f4c81 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_locations_add.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_locations_add_navigation.png b/docs/user_guide/bank/images/inventory/inventory_storage_locations_add_navigation.png new file mode 100644 index 0000000000..bdb92ad663 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_locations_add_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_locations_coming_in.png b/docs/user_guide/bank/images/inventory/inventory_storage_locations_coming_in.png new file mode 100644 index 0000000000..95b7abf013 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_locations_coming_in.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_locations_going_out.png b/docs/user_guide/bank/images/inventory/inventory_storage_locations_going_out.png new file mode 100644 index 0000000000..e7f3cf78fe Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_locations_going_out.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_storage_locations_navigation.png b/docs/user_guide/bank/images/inventory/inventory_storage_locations_navigation.png new file mode 100644 index 0000000000..c638a559ad Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_storage_locations_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_delete.png b/docs/user_guide/bank/images/inventory/inventory_transfers_delete.png new file mode 100644 index 0000000000..1569ef9760 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_delete.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_export.png b/docs/user_guide/bank/images/inventory/inventory_transfers_export.png new file mode 100644 index 0000000000..0cdaf2eb68 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_export.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_export_navigation.png b/docs/user_guide/bank/images/inventory/inventory_transfers_export_navigation.png new file mode 100644 index 0000000000..13c59c2543 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_export_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_navigation.png b/docs/user_guide/bank/images/inventory/inventory_transfers_navigation.png new file mode 100644 index 0000000000..0debdd6700 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_new.png b/docs/user_guide/bank/images/inventory/inventory_transfers_new.png new file mode 100644 index 0000000000..a299c286a7 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_new.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_new_navigation.png b/docs/user_guide/bank/images/inventory/inventory_transfers_new_navigation.png new file mode 100644 index 0000000000..7ee84b394f Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_new_navigation.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_view.png b/docs/user_guide/bank/images/inventory/inventory_transfers_view.png new file mode 100644 index 0000000000..6bfdf738df Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_view.png differ diff --git a/docs/user_guide/bank/images/inventory/inventory_transfers_view_navigation.png b/docs/user_guide/bank/images/inventory/inventory_transfers_view_navigation.png new file mode 100644 index 0000000000..3df0e467f7 Binary files /dev/null and b/docs/user_guide/bank/images/inventory/inventory_transfers_view_navigation.png differ diff --git a/docs/user_guide/bank/images/partners/partners_add.png b/docs/user_guide/bank/images/partners/partners_add.png new file mode 100644 index 0000000000..555e2bf576 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_add.png differ diff --git a/docs/user_guide/bank/images/partners/partners_add_navigation.png b/docs/user_guide/bank/images/partners/partners_add_navigation.png new file mode 100644 index 0000000000..0cd2810d54 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_add_navigation.png differ diff --git a/docs/user_guide/bank/images/partners/partners_approving_1.png b/docs/user_guide/bank/images/partners/partners_approving_1.png new file mode 100644 index 0000000000..d4c7656b99 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_approving_1.png differ diff --git a/docs/user_guide/bank/images/partners/partners_approving_2.png b/docs/user_guide/bank/images/partners/partners_approving_2.png new file mode 100644 index 0000000000..3f7364b083 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_approving_2.png differ diff --git a/docs/user_guide/bank/images/partners/partners_deactivate_1.png b/docs/user_guide/bank/images/partners/partners_deactivate_1.png new file mode 100644 index 0000000000..23c3a5cfe8 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_deactivate_1.png differ diff --git a/docs/user_guide/bank/images/partners/partners_deactivate_2.png b/docs/user_guide/bank/images/partners/partners_deactivate_2.png new file mode 100644 index 0000000000..a1b5349744 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_deactivate_2.png differ diff --git a/docs/user_guide/bank/images/partners/partners_edit.png b/docs/user_guide/bank/images/partners/partners_edit.png new file mode 100644 index 0000000000..346c1ad912 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_edit.png differ diff --git a/docs/user_guide/bank/images/partners/partners_edit_navigation.png b/docs/user_guide/bank/images/partners/partners_edit_navigation.png new file mode 100644 index 0000000000..9dfe5cd389 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_edit_navigation.png differ diff --git a/docs/user_guide/bank/images/partners/partners_invitation_email.png b/docs/user_guide/bank/images/partners/partners_invitation_email.png new file mode 100644 index 0000000000..a6cd89553c Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_invitation_email.png differ diff --git a/docs/user_guide/bank/images/partners/partners_inviting.png b/docs/user_guide/bank/images/partners/partners_inviting.png new file mode 100644 index 0000000000..f043c06a72 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_inviting.png differ diff --git a/docs/user_guide/bank/images/partners/partners_inviting_and_approving.png b/docs/user_guide/bank/images/partners/partners_inviting_and_approving.png new file mode 100644 index 0000000000..0b30c3c953 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_inviting_and_approving.png differ diff --git a/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_1.png b/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_1.png new file mode 100644 index 0000000000..119aff6db3 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_1.png differ diff --git a/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_2.png b/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_2.png new file mode 100644 index 0000000000..edd3dd613b Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_profile_edit_navigation_2.png differ diff --git a/docs/user_guide/bank/images/partners/partners_recertification.png b/docs/user_guide/bank/images/partners/partners_recertification.png new file mode 100644 index 0000000000..685d4320e7 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_recertification.png differ diff --git a/docs/user_guide/bank/images/partners/partners_recertification_email.png b/docs/user_guide/bank/images/partners/partners_recertification_email.png new file mode 100644 index 0000000000..c567b5f5b5 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_recertification_email.png differ diff --git a/docs/user_guide/bank/images/partners/partners_review_application_from_dashboard.png b/docs/user_guide/bank/images/partners/partners_review_application_from_dashboard.png new file mode 100644 index 0000000000..15465c5f51 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_review_application_from_dashboard.png differ diff --git a/docs/user_guide/bank/images/partners/partners_review_application_navigation.png b/docs/user_guide/bank/images/partners/partners_review_application_navigation.png new file mode 100644 index 0000000000..b04bfd3732 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_review_application_navigation.png differ diff --git a/docs/user_guide/bank/images/partners/partners_user_management.png b/docs/user_guide/bank/images/partners/partners_user_management.png new file mode 100644 index 0000000000..c7caa08ec9 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_user_management.png differ diff --git a/docs/user_guide/bank/images/partners/partners_user_management_navigation_1.png b/docs/user_guide/bank/images/partners/partners_user_management_navigation_1.png new file mode 100644 index 0000000000..4d2301e081 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_user_management_navigation_1.png differ diff --git a/docs/user_guide/bank/images/partners/partners_user_management_navigation_2.png b/docs/user_guide/bank/images/partners/partners_user_management_navigation_2.png new file mode 100644 index 0000000000..a9c809edeb Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_user_management_navigation_2.png differ diff --git a/docs/user_guide/bank/images/partners/partners_viewing_and_reactivating_deactivated.png b/docs/user_guide/bank/images/partners/partners_viewing_and_reactivating_deactivated.png new file mode 100644 index 0000000000..3379c686a4 Binary files /dev/null and b/docs/user_guide/bank/images/partners/partners_viewing_and_reactivating_deactivated.png differ diff --git a/docs/user_guide/bank/images/reports/reports_summary_distributions.png b/docs/user_guide/bank/images/reports/reports_summary_distributions.png new file mode 100644 index 0000000000..aed7914403 Binary files /dev/null and b/docs/user_guide/bank/images/reports/reports_summary_distributions.png differ diff --git a/docs/user_guide/bank/images/reports/reports_summary_donations.png b/docs/user_guide/bank/images/reports/reports_summary_donations.png new file mode 100644 index 0000000000..72bf7feca0 Binary files /dev/null and b/docs/user_guide/bank/images/reports/reports_summary_donations.png differ diff --git a/docs/user_guide/bank/images/reports/reports_summary_purchases.png b/docs/user_guide/bank/images/reports/reports_summary_purchases.png new file mode 100644 index 0000000000..3bec6bfbf9 Binary files /dev/null and b/docs/user_guide/bank/images/reports/reports_summary_purchases.png differ diff --git a/docs/user_guide/bank/images/user_management/user_access_admin_and_user.png b/docs/user_guide/bank/images/user_management/user_access_admin_and_user.png new file mode 100644 index 0000000000..94d0f6cbf2 Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_access_admin_and_user.png differ diff --git a/docs/user_guide/bank/images/user_management/user_access_partner.png b/docs/user_guide/bank/images/user_management/user_access_partner.png new file mode 100644 index 0000000000..6925b22c3c Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_access_partner.png differ diff --git a/docs/user_guide/bank/images/user_management/user_delete_bank_user.png b/docs/user_guide/bank/images/user_management/user_delete_bank_user.png new file mode 100644 index 0000000000..1d90118273 Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_delete_bank_user.png differ diff --git a/docs/user_guide/bank/images/user_management/user_demote_admin.png b/docs/user_guide/bank/images/user_management/user_demote_admin.png new file mode 100644 index 0000000000..35fd7f804f Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_demote_admin.png differ diff --git a/docs/user_guide/bank/images/user_management/user_invite_new_bank_user.png b/docs/user_guide/bank/images/user_management/user_invite_new_bank_user.png new file mode 100644 index 0000000000..58080d69b0 Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_invite_new_bank_user.png differ diff --git a/docs/user_guide/bank/images/user_management/user_promote_bank_user.png b/docs/user_guide/bank/images/user_management/user_promote_bank_user.png new file mode 100644 index 0000000000..d93e0452e9 Binary files /dev/null and b/docs/user_guide/bank/images/user_management/user_promote_bank_user.png differ diff --git a/docs/user_guide/bank/intro_i.md b/docs/user_guide/bank/intro_i.md index 88f4b34ef4..27819b9ad2 100644 --- a/docs/user_guide/bank/intro_i.md +++ b/docs/user_guide/bank/intro_i.md @@ -1,10 +1,8 @@ -DRAFT USER GUIDE +READY FOR REVIEW -# Is Human Essentials right for you? (what we help with and what we don't) -## Human Essentials is a free system for essentials banks -Human Essentials is for essentials banks -- by which we mean organizations that distribute essentials (diapers, period products, etc) to other organizations that work directly with people in need. - -If you *only* deal directly with people in need, this is probably not the system for you. +# Is Human Essentials Right for You? (What We Help with and What We Don't) +## Human Essentials is a free system for Essentials Banks +Human Essentials is for Essentials Banks -- by which we mean organizations that distribute essentials (diapers, period products, etc) to other organizations that work directly with people in need. ## We are an all-volunteer organization @@ -12,26 +10,27 @@ We can offer this system for free because we are an all-volunteer organization. sponsors who provide funding or in-kind services for the servers that the system runs on, but changes/support can take time. ## What we help with -- managing your partners - - gathering the information about your partners that you need - - your request / distribution cycle (partner agencies can provide requests to you through the system, and you fulfill them) - - monthly reminders to the partners re request deadlines - - calendar that shows when you've scheduled pickups and deliveries +- managing your Partners + - gathering the information about your Partners that you need + - your Request / Distribution cycle (Partners can provide requests to you through the system, and you fulfill them) + - monthly reminders to the Partners re Request deadlines + - calendar that shows when you've scheduled Pickups and Deliveries - inventory - - when you record donations, purchases, and distributions, your inventory record is automatically adjusted as of the date/time you enter the information - - audits and inventory adjustments - - handles multiple storage locations - - making up kits for distribution from existing inventory (this is mostly used by our period supply focused banks) + - when you record Donations, Purchases, and Distributions (to Partners), your Inventory record is automatically adjusted as of the date/time you enter the information + - Audits and Inventory Adjustments + - handles multiple Storage Locations + - making up Kits for distribution from existing Inventory (this is mostly used by our period supply focused banks) - community management - - keeping track of your donation sites, product drives (and participants) + - keeping track of your Donation sites, Product Drives (and Product Drive Participants) - reporting - yearly report with much of the information the NDBN annual survey requires drawn from your activity in the system - itemized distribution breakdown over time - trend reporting + ## Things we don't help with - general business needs (like payroll and office supplies and the like) - volunteer coordination - donor tax receipts -- direct distribution -- there are some things that help with that in the partner side, and some banks run a partner for their direct distribution, but it is not our strength. +- direct distribution -- there are some things that help with that in the Partner side, and some banks run a Partner for their direct distribution practice, but it is not our strength. [Next: Support](intro_ii.md) \ No newline at end of file diff --git a/docs/user_guide/bank/intro_ii.md b/docs/user_guide/bank/intro_ii.md index 0d0ed0d1fb..0f65d18eb5 100644 --- a/docs/user_guide/bank/intro_ii.md +++ b/docs/user_guide/bank/intro_ii.md @@ -1,12 +1,10 @@ -DRAFT USER GUIDE - +READY FOR REVIEW # Support There are a few ways to get support -- some are better than others. -## For banks: +## For Banks: ### Slack -We highly recommend that you become a member of the human essentials Slack. Here's a link to an invite: https://human-essential.slack.com/join/shared_invite/zt-bfa8tymd-d8Ks3Mq000COcRe~nfs~zg#/shared-invite/email - +We highly recommend that you become a member of the Human Essentials Slack. Here's a link to an invite: https://human-essential.slack.com/join/shared_invite/zt-bfa8tymd-d8Ks3Mq000COcRe~nfs~zg#/shared-invite/email It's a good place to ask about how other banks *actually* use the system -- any workarounds for things that don't 100% match how the system is set up. @@ -24,6 +22,13 @@ You can email info@humanessentials.app. We generally look at these about once We hold a stakeholder circle the first Wednesday of every month at 6:00pm Eastern Time. There will be other banks there, plus a couple members of the Human Essentials team. We publish the zoom, and (when we remember) in the Announcements in Human Essentials. + +### Keeping up-to-date with changes +We release changes to the system most weeks on Sundays between 10am and noon Eastern time (the system will be down for a short period when we do). When we do, we put up an announcment in the system, which you will see on your [dashboard](essentials_dashboard.md). We also record them in a newsletter at: https://ruby-for-good.gitbook.io/human-essentials-news + + + + ## For partners: We recommend that the partners seek support through the banks. Each bank has their own particular way of doing things, and the development team doesn't want to mess that up. diff --git a/docs/user_guide/bank/inventory_adjustments.md b/docs/user_guide/bank/inventory_adjustments.md index 0f382dc3a3..fa6fdd35e7 100644 --- a/docs/user_guide/bank/inventory_adjustments.md +++ b/docs/user_guide/bank/inventory_adjustments.md @@ -1,35 +1,41 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Inventory Adjustments -Inventory adjustments are an alternative way to record known differences in the inventory without going through the audit process. -They don't require the two step process that an audit does, but are entered as adjustments, rather than as the actual inventory levels. +Inventory adjustments are an alternative way to record known differences in the inventory without going through the Audit process. +They don't require the two step process that an Audit does, but are entered as adjustments, rather than as the actual inventory levels. -Note that inventory adjustments are permanent -- if you need to reverse them, you have to enter an adjustment in the other direction. +Note that Inventory Adjustments are permanent -- if you need to reverse them, you have to enter an adjustment in the other direction. -Unlike the audit, there is no review step. The changes to inventory are made immediately. +Unlike the Audit, there is no review step. The changes to inventory are made immediately. -You can not edit inventory adjustments. - -[TODO: Who can make these?] +You can not edit Inventory Adjustments. ## The inventory adjustment page -You can manage your inventory adjustments through the inventory adjustment page. Click on "Inventory" in the left-hand menu, then "Imventry Adjustments" +You can manage your Inventory Adjustments through the inventory adjustment page. Click on "Inventory" in the left-hand menu, then "Inventory Adjustments" +![Navigation to Inventory Adjustments](images/inventory/inventory_adjustments_navigation.png) ## Making an inventory adjustment -To enter a new inventory adjustment, click on "New adjustment" -[TODO: Screenshot] -Choose the storage location, add any notes you have in the comments section, and, for each item you are adjusting, enter either a number greater than 0 to increase the inventory, or a number less than zero to decrease it. + +To enter a new inventory adjustment, click on "New Adjustment" +![Navigation to new Inventory Adjustment](images/inventory/inventory_adjustments_new_navigation.png) +This brings up the page for entering Inventory Adjustments: +![New Inventory Adjustment page](images/inventory/inventory_adjustments_new.png) + +Choose the Storage Location, add any notes you have in the comments section, and, for each item you are adjusting, enter either a number greater than 0 to increase the inventory, or a number less than zero to decrease it. Click save to finish entering the adjustment. This will change the inventory levels on those items. -[TODO: Screenshot of result] +In this sample there are both negative adjustments (showing in red, with "Removed") and positive adjustments (showing in green, with "Added) -## Viewing inventory adjustments +![Result of entering adjustment](images/inventory/inventory_adjustments_result.png) -You can see the details of old inventory adjustments by clicking 'View' beside the adjustment +## Viewing Inventory Adjustments +You can see the details of old Inventory Adjustments by clicking 'View' beside the adjustment +![View Inventory Adjustments navigation](images/inventory/inventory_adjustments_view_navigation.png) +![view Inventory Adjustments](images/inventory/inventory_adjustments_view.png) [Prior: Barcode Items](inventory_barcodes.md) [Next: Transfers](inventory_transfers.md) diff --git a/docs/user_guide/bank/inventory_audits.md b/docs/user_guide/bank/inventory_audits.md index c5ab91943e..fff466324d 100644 --- a/docs/user_guide/bank/inventory_audits.md +++ b/docs/user_guide/bank/inventory_audits.md @@ -1,67 +1,81 @@ -Draft User Guide +READY FOR REVIEW # Audits -When you do an inventory count, you'll want to set the inventory in each storage location to the physical count. That's what the Audits function is for. +When you do an inventory count, you'll want to set the inventory in each Storage Location to the physical count. That's what the Audits function is for. -The big thing you need to remember is that Audits have to be 'finalized' by an organization admin before they update the inventory, so don't record any activity after the physical count until the audit is finalized. +The big thing you need to remember is that Audits have to be 'finalized' by an organization admin before they update the inventory, so don't record any activity after the physical count until the Audit is finalized. ## Starting an Audit Click on "Inventory", then "Inventory Audit" in the left hand menu, then "+New Audit". -[TODO: Screenshot] +![Navigaton to new Audit](images/inventory/inventory_audits_new_navigation.png) -This brings up the new audit page. -Specify the storage location, then each item you have counted, along with its quantity. -Note that you can do partial audits. Any item you don't specify during the audit will keep its current quantity in the inventory. +This brings up the new Audit page. -[TODO: Screenshot] +![Empty new Audit page](images/inventory/inventory_audits_new.png) + +Specify the Storage Location, then each Item you have counted, along with its quantity. + +[!NOTE] You can do partial Audits. Any Item you don't specify during the Audit will keep its current quantity in the inventory. -Clicking "Confirm Audit" and clicking "OK" in answer to "Are you sure?" will set the status to "confirmed" - that is ready for the org admin to review and "finalize" - so that should be done when you are finished your counts and have checked that you entered the right numbers. Clicking "Save Progress" saves what you have done, but you can come back and continue entering. -You *must* choose a storage location to save progress. +You *must* choose a Storage Location to save progress. + +Clicking "Confirm Audit" and clicking "OK" in answer to "Are you sure?" will set the status to "confirmed" - that is ready for the org admin to review and "finalize" - so that should be done when you are finished your counts and have checked that you entered the right numbers. -When you save progress, you'll see this page, which shows the progess of the audit. Anything you haven't counted has a red background, and is noted as "Not Audited". +When you save progress, you'll see this page, which shows the progress of the Audit. Anything you haven't counted has a red background, and is noted as "Not Audited". -[TODO: Screenshot] +![Audit post save progress](images/inventory/inventory_audits_progress.png) -You will be able to return to this page from the list of audits (Inventory | Inventory Audit). +You will be able to return to this page from the list of Audits (Inventory | Inventory Audit). The Audit Quantity is the amount that you entered, and the Quantity in Records is the amount that is currently recorded as being in your inventory. You have two options from this page: Resume Audit and Delete Audit, which we treat below. ## Resuming an Audit -When you want to continue entering your counts, you can resume your audit by going to the Audits page (Inventory | Inventory Audit). You'll see the audit you are working on listed with a status of "In Progress". Click "View" to bring up the details on that audit. +When you want to continue entering your counts, you can resume your Audit by going to the Audits page (Inventory | Inventory Audit). You'll see the Audit you are working on listed with a status of "In Progress". Click "View" to bring up the details on that Audit. -[TODO: Screenshot] +![Audit view navigation](images/inventory/inventory_audits_view_navigation.png) Scroll to the bottom of the page, and click "Resume Audit" -[TODO: Screenshot] +![Resume Audit navigation](images/inventory/inventory_audits_resume.png) -This will bring up the new audit form you were working on, and you can continue to enter more items. +This will bring up the new Audit form you were working on, and you can continue to enter more Items. -You can "Save Progress" again, or you can "Confirm Audit", and click "OK" in answer to "Are you sure?" to set the audit for approval (or "Finalizing") by a user with organization admin privileges. +You can "Save Progress" again, or you can "Confirm Audit", and click "OK" in answer to "Are you sure?" to set the Audit for approval (or "Finalizing") by a user with organization admin privileges. + +## Confirming an Audit +When you are entering an Audit, and have completed the counts for that Audit, you confirm it. This indicates that the counts for the Audit are done, and available for final review before being set in stone. + +This step is here so that a bank can have users without admin access enter the counts, but have a final check (the "Finalize" step, below) before the inventory levels are permanently changed. + +On the Audit entry screen, scroll to the bottom and click 'Confirm Audit'. There is a detailed confirmation window with instructions for next steps. + +![Confirming an Audit](images/inventory/inventory_audits_confirm.png) ## Finalizing an Audit -Once an audit had been confirmed, it must be finalized before any inventory changes will take place. Finalizing can only be done by someone with organization admin privileges. +Once an Audit had been confirmed, it must be finalized before any inventory changes will take place. Finalizing can only be done by someone with organization admin privileges. To Finalize an Audit, sign in as an organization admin, Click Inventory, then Inventory Audit. -[TODO: Screenshot] +Select the "View" beside the Audit, review it, and if all is correct, scroll down to the bottom and click "Finalize". Then, if you are sure, click "OK" to confirm. -Select the "View" beside the audit, review it, and if all is correct, scroll down to the bottom and click "Finalize". Then, if you are sure, click "OK" to confirm. +[!NOTE] You will only see "Finalize" if the Audit has been confirmed. -[TODO: Screenshot] +[!WARN] **** THIS PERMANENTLY CHANGES THE LEVELS OF INVENTORY AND CANNOT BE UNDONE. ******* -##### **** N.B. THIS CHANGES THE LEVELS OF INVENTORY AND CANNOT BE UNDONE. ******* +![Finalize Audit](images/inventory/inventory_audits_finalize.png) -Now, when you view the audit, the quantity in records will match the audit quantity -- until you enter more activity. +Now, when you view the Audit, the quantity in records will match the Audit quantity -- until you enter more activity. ## Deleting an Audit -If the audit is in error and has not been finalized, you can delete it. -From the Audits page (Inventory | Inventory Audit), click "View" beside the audit you wish to delete, then scroll to the bottom, click "Delete Audit", and "OK" to confirm. This is not reversible. +If the Audit has not been finalized, you can delete it. +From the Audits page (Inventory | Inventory Audit), click "View" beside the Audit you wish to delete, then scroll to the bottom, click "Delete Audit", and "OK" to confirm. +[!WARN] This cannot be undone + -[TODO: Screenshot] +![Audit delete](images/inventory/inventory_audits_delete.png) [Prior: Storage Locations](inventory_storage_locations.md) [Next: Kits](inventory_kits.md) \ No newline at end of file diff --git a/docs/user_guide/bank/inventory_barcodes.md b/docs/user_guide/bank/inventory_barcodes.md index 4872fe8f50..1d8aba6967 100644 --- a/docs/user_guide/bank/inventory_barcodes.md +++ b/docs/user_guide/bank/inventory_barcodes.md @@ -1,43 +1,47 @@ -Not yet written +DRAFT USER GUIDE # Barcodes ## Introduction to Barcodes -You can set up Human Essentials to enable reading barcodes for input of items when entering donations, purchases, and distributions. +You can set up Human Essentials to enable reading Barcodes for input of Items when entering Donations, Purchases, and Distributions. [TODO: Hoping to get a paragraph or two from Scott on: -- barcode readers -- making your own barcodes +- Barcode readers +- making your own Barcodes ] ## The Barcodes page -You can reach the barcodes page, where you will administer your organizations barcodes, by clicking "Inventory", then "Barcode Items" +You can reach the Barcodes page, where you will administer your organizations Barcodes, by clicking "Inventory", then "Barcode Items" -[TODO: Screenshot] +![Navigation to Barcodes page](images/inventory/inventory_barcodes_navigation.png) -[TODO: Check - is this an admin only thing?] +### Getting a Barcode font for printing your own Barcodes +You can download a font file for printing Barcode labels from the Barcodes page by clicking the button "Download Barcode Font" +This can be used with MS Word on Windows, or with Pages on Apple machines. Consult the help on those for installation. -### Getting a barcode font for printing your own barcodes -You can download a font file for printing barcode labels from the barcodes page by clicking the button "Download Barcode Font" -This can be used with MS Word on Windows, or with Pages on Apple machines[TODO: Verify] +### Making a new Barcode +To add a Barcode to the system, click the "+New Barcode" button on the Barcodes page. -### Making a new barcode -To add a barcode to the system, click the "+New Barcode" button on the barcodes page. +![Navigation to new Barcode](images/inventory/inventory_barcodes_new_navigation.png) +This will bring up a simple form that allows you to specify the quantity of the Item, select the Item and enter the Barcode - either as a number, or by using a Barcode reader. -[TODO: Screenshot] -This will bring up a simple form that allows you to specify the quantity of the item, select the item and enter the barcode - either as a number, or by using a barcode reader. +![new Barcode page](images/inventory/inventory_barcodes_new.png) + +[TODO: Question for Scott -- is this "boopable"? I suspect it is, but I don't want to claim it is if it isn't]. -[TODO: Question -- is this "boopable? I suspect it is, but I don't want to claim it is if it isn't]. Enter those three fields and click "Save" -### Changing a barcode -Should you need to edit this barcode information, you can do so by clicking 'Edit' beside the barcode you need to update. -[TODO: Screenshot] -### Deleting a barcode -To delete a barcode you are no longer using, click "Delete" beside the barcode, and confirm by clicking "OK". -[TODO: Screenshot] -### Exporting barcodes -To export a list of the barcodes, just click "Export barcodes" on the barcodes page. +### Changing a Barcode +Should you need to edit this Barcode information, you can do so by clicking 'Edit' beside the Barcode you need to update. +![Edit Barcodes navigation](images/inventory/inventory_barcodes_edit_navigation.png) +### Deleting a Barcode +To delete a Barcode you are no longer using, click "Delete" beside the Barcode, and confirm by clicking "OK". +![Deleting Barcodes](images/inventory/inventory_barcodes_delete.png) +### Exporting Barcodes +To export a list of the Barcodes, just click "Export Barcodes" on the Barcodes page. +![Export Barcodes navigation](images/inventory/inventory_barcodes_export_navigation.png) +![Export Barcodes sample](images/inventory/inventory_barcodes_export.png) + [Prior: Audits](inventory_audits.md) [Next: Adjustments](inventory_adjustments.md) \ No newline at end of file diff --git a/docs/user_guide/bank/inventory_items.md b/docs/user_guide/bank/inventory_items.md index fb0cd4831e..5bf3918fa9 100644 --- a/docs/user_guide/bank/inventory_items.md +++ b/docs/user_guide/bank/inventory_items.md @@ -1,150 +1,168 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Items ## Introduction -Your bank is initialized with a basic set of items that contains many common product types that essentials banks distribute. These items represent the stock you have for distribution. Here is the current default list: - -[TODO: Bulletize this list.] - -Adult Briefs (Large/X-Large), -Adult Briefs (Medium/Large), -Adult Briefs (Small/Medium), -Adult Briefs (XS/Small), -Adult Briefs (XXL), -Adult Briefs (XXS), -Adult Briefs (XXXL), -Adult Cloth Diapers (Large/XL/XXL), -Adult Cloth Diapers (Small/Medium), -Adult Incontinence Pads, -Bed Pads (Cloth), -Bed Pads (Disposable), -Bibs (Adult & Child), -Cloth Diapers (AIO's/Pocket), -Cloth Diapers (Covers), -Cloth Diapers (Plastic Cover Pants), -Cloth Diapers (Prefolds & Fitted), -Cloth Inserts (For Cloth Diapers), -Cloth Potty Training Pants/Underwear, -Cloth Swimmers (Kids), -Diaper Rash Cream/Powder, -Disposable Inserts, -Kids (Newborn), -Kids (Preemie), -Kids (Size 1), -Kids (Size 2), -Kids (Size 3), -Kids (Size 4), -Kids (Size 5), -Kids (Size 6), -Kids (Size 7), -Kids L/XL (60-125 lbs), -Kids Pull-Ups (2T-3T), -Kids Pull-Ups (3T-4T), -Kids Pull-Ups (4T-5T), -Kids Pull-Ups (5T-6T), -Kids S/M (38-65 lbs), -Liners (Incontinence), -Liners (Menstrual), -Other, -Pads, -Swimmers, -Tampons, -Underpads (Pack), -Wipes (Adult), -Wipes (Baby) - - - - -[TODO: "Kit" is a base item, and it is in the "new" bank on staging. Is it one of the items that is initialized? Shouldn't be, IMO] - - -"Under the hood" each of these basic items belongs to a particular reporting category used for the annual survey, such as disposable diapers, cloth diapers, kits (which are made up of other items), cloth diapers, and other. - -You can add more items, basing them off our base item list, and customize them. The things you can do include: - - hiding them from your partners (useful if, say, you only distribute kits, but get donations of materials that go into the kits) +Your bank is initialized with a basic set of Items that contains many common product types that essentials banks distribute. These Items represent the stock you have for distribution. Here is the current default list: + +- Adult Briefs (Large/X-Large), +- Adult Briefs (Medium/Large), +- Adult Briefs (Small/Medium), +- Adult Briefs (XS/Small), +- Adult Briefs (XXL), +- Adult Briefs (XXS), +- Adult Briefs (XXXL), +- Adult Cloth Diapers (Large/XL/XXL), +- Adult Cloth Diapers (Small/Medium), +- Adult Incontinence Pads, +- Bed Pads (Cloth), +- Bed Pads (Disposable), +- Bibs (Adult & Child), +- Cloth Diapers (AIO's/Pocket), +- Cloth Diapers (Covers), +- Cloth Diapers (Plastic Cover Pants), +- Cloth Diapers (Prefolds & Fitted), +- Cloth Inserts (For Cloth Diapers), +- Cloth Potty Training Pants/Underwear, +- Cloth Swimmers (Kids), +- Diaper Rash Cream/Powder, +- Disposable Inserts, +- Kids (Newborn), +- Kids (Preemie), +- Kids (Size 1), +- Kids (Size 2), +- Kids (Size 3), +- Kids (Size 4), +- Kids (Size 5), +- Kids (Size 6), +- Kids (Size 7), +- Kids L/XL (60-125 lbs), +- Kids Pull-Ups (2T-3T), +- Kids Pull-Ups (3T-4T), +- Kids Pull-Ups (4T-5T), +- Kids Pull-Ups (5T-6T), +- Kids S/M (38-65 lbs), +- Liners (Incontinence), +- Liners (Menstrual), +- Other, +- Pads, +- Swimmers, +- Tampons, +- Underpads (Pack), +- Wipes (Adult), and +- Wipes (Baby) + +"Under the hood" each of these basic Items belongs to a particular reporting category used for the annual survey, such as disposable diapers, cloth diapers, Kits (which are made up of other Items), cloth diapers, and other. + +You can add more Items, basing them off our base Item list, and customize them. The things you can do include: + - hiding them from your Partners (useful if, for example, you only distribute Kits, but get donations of materials that go into the Kits) - grouping them into categories (you can limit which categories groups of partners can access) - adding minimum and recommended bank-wide inventory levels (which enable warnings, and drive the low inventory list in your dashboard) - - creating [kits](inventory_kits.md) that will contain items (the inventory levels will show the items that are yet not in the kits) - - remove the items from your lists on a go-forward basis. - - -[TODO: links pointing to each of these things] + - creating [Kits](inventory_kits.md) that will contain Items (the inventory levels show the Items that are yet not in the Kits) + - remove an Item from your lists on a go-forward basis. ## The Items & Inventory Views - -This is a multi-tabbed view - you have several angles to look at your items and bank-wide inventory (if you want to see everything that's in a particular storage location, that's under [Storage Locations](inventory_storage_locations.md)) +To bring up your Items & Inventory view, click "Inventory", then "Items & Inventory" in the left-hand menu. +![Navigation to Items & Inventory](images/inventory/inventory_items_navigation.png) +This brings up a multi-tabbed view - you have several different ways to look at your Items and bank-wide inventory (if you want to see everything that's in a particular Storage Location, that's under [Storage Locations](inventory_storage_locations.md)) ### Item List -This shows all of your items, and allows you access to view/edit/and delete them. -[ToDo: screenshot] -#### Viewing an item -Clicking "View" will bring up details on the item, including all the things you can change, and a breakdown of the inventory at each location you currently have stock at. -[ToDo: screenshot] +This shows all of your Items, and allows you access to view/edit/and delete them. +#### Viewing an Item +Clicking "View" will bring up details on the Item, including all the things you can change, and a breakdown of the inventory at each location you currently have stock at. + +![Navigation to view an Item](images/inventory/inventory_items_view_navigation.png) +![View Item page](images/inventory/inventory_items_view.png) The fields are: -- Base item -- this is the "base item" for this item -- which determines what section it is in for the Annual Survey. You can also search by base item (at this time) -- Category -- this is a category you define (see Item Categories, below -- -- [TODO: put the link in]) -- -- Value per Item -- this is currently shown in cents (there is a request to change it to dollars in our list). This is used for any "Fair Market Value" values -- including on donation and distribution printouts. -Note: We only have one 'value per item' per item -- so it's always the current fair market value not the historical. If provided, this is used for the value column on the distribution and donation printouts (unless you hide those columns when [customizing your bank](getting_started_customization.md)) -- Quantity per individual -- This is used for two things: 1/ If you have enabled "request by individual" for your partners (and they use it), this is the number of items that will be in their request per individual they request for. (so, if it's 25, and they indicate 3 individuals, you will receive a request for 75 of that item). It is also used in the annual survey for the estimated people served -- we take the total of the item that was distributed, and divide it by this number to get the number of people helped. **NOTE** We use 50 for this if you don't give a value. -- On hand minimum quantity -- This is a bank-wide on-hand minimum quantity of the item -- being below this triggers the item appearing in your low inventory report in red. +- Base Item -- this is the "Base Item" for this Item -- which determines what section it is in for the Annual Survey. You can also search by Base Item (at this time) +- Category -- this is a category you define (see [Item Categories](inventory_items.md#item-categories), below +- Value per Item -- this is currently shown in cents (there is a request to change it to dollars in our list). It is used for any "Fair Market Value" calculations -- including on donation and distribution printouts. +[!NOTE] We only have one 'Value per Item' per Item -- so it's always the current fair market value not the historical. If provided, this is used for the value column on the distribution and donation printouts (unless you hide those columns when [customizing your bank](getting_started_customization.md)) +- Quantity per individual -- This is used for two things: + - If you have enabled "request by individual" for your partners (and they use it), this is the number of Items that will be in their request per individual they request for. (so, if it's 25, and they indicate 3 individuals, you will receive a request for 75 of that Item). + - It is also used in the annual survey for the estimated people served -- we take the total of the Item that was distributed, and divide it by this number to get the number of people helped. + - [!NOTE] We use 50 for this calculation in the annual survey if you don't give a value. +- On hand minimum quantity -- This is a bank-wide on-hand minimum quantity of the Item -- being below this triggers the Item appearing in your low inventory report in red. - On hand recommended quantity -- This is the amount you want to have on hand -- if you don't have this, it will appear in the low inventory list on your dashboard, just not in red. -- Package size -- If you use this, the calculated number of packages for the item will appear on the distribution printout, unless you hide it when [customizing your bank](getting_started_customization.md). -- Item visible to partners -- This is useful if you have items that you do not want the partners directly requesting. Uses include: items you don't get very often, of items you only have because they are going into kits you haven't assembled yet. You can uncheck this to hide those items from all your partners. -#### Editing an item -Clicking "Edit" beside an item on the item list lets you edit the item definition, with the fields as described above. **NOTE*: Value per item is in dollars on this screen. -#### Deleting vs Deactivating an item -The button "delete" will only appear beside an item if there hasn't been any activity on it at all. +- Package size -- If you use this, the calculated number of packages for the Item will appear on the distribution printout, unless you hide it when [customizing your bank](getting_started_customization.md). +- Item visible to partners -- This is useful if you have Items that you do not want the partners directly requesting. Uses include: Items you don't get very often, or Items you only have because they are going into kits you haven't assembled yet. You can uncheck this to hide those Items from all your partners. +#### Filtering your item list +The most common thing you'll when filtering your item list is to include inactive items. If you have deactivated an item, but are going to offer it again, you'll need to check the "Also include inactive Items" box in the filter, and then click "Filter" to show it, so that you can reactivate it. -The button "deactivate" will appear if there has been activity. But it will be greyed out unless your bank-wide level of inventory on that item is 0. -Deactivating an item removes it whenever you are entering a new distribution/donation/purchase/transfer/audit, and removes it from the partner's new requests. -### Item Categories +#### Editing an Item +Clicking "Edit" beside an Item on the Item list lets you edit the Item definition, with the fields as described above. +![Navigation to edit an Item](images/inventory/inventory_items_edit_navigation.png) +![Edit Item page](images/inventory/inventory_items_edit.png) +[!NOTE] Value per Item is in dollars on this screen. -Item categories are largely used for limiting the items specific [Partner Groups](pm_partner_groups.md) can see, though you can also filter distributions by them. This tab shows all the item categories you have, allowing you to view and edit each one, as well as enter new ones. +#### Adding a new Item +To add a new Item, click the "+ New Item" button on this page. It will bring up the same page as "Editing an Item", above (only, of course, with none of the fields completed). -**Note: Each item can only belong to one category ** +#### Deleting or Deactivating an Item +The button "delete" will appear beside an Item if there hasn't been any activity on it at all. (A) Deleting an Item is permanent -[TODO: Question - why do we not filter Donations, Requests, or Purchases by them? ] +The button "deactivate" will appear if there has been activity. But it will be greyed out unless your bank-wide level of inventory on that Item is 0. (B)) +Deactivating an Item removes it whenever you are entering a new distribution/donation/purchase/transfer/audit, and removes it from the partner's new requests. +You can still see deactivated items in most reports, and can include them in your filtered lists. -[TODO: Screenshot] -#### Adding a new item category -To add a new item category, Click on Inventory, then Items & Inventory, then the Item Categories tab, then the "Add Item Category" button. +![Item list showing delete and deactivate buttons](images/inventory/inventory_items_delete_vs_deactivate.png) -Enter a unique Category Name, and a suitable description, then click Save. +#### Reactivating an Item +To reactivate an Item, you'll need to +(1) click "Also include inactive Items" +(2) click "Filter", then +(3) click "Restore" beside the Item you wish to reactivate. -#### Viewing an item category -All the information about an item category is in the list of all of them, but you can also manage the items in the category through the view. To view an item category, you click on Inventory, then Items & Inventory, then the Item Categories tab, then the "View" button. -[TODO: Screenshot] +Click 'Ok' on the confirmation screen that appears. -You can also remove items from the category by clicking "Remove from category" beside the item. This will hide them from any partner groups that have this category. +![Item Reactivation sequence](images/inventory/inventory_items_reactivation.png) -#### Editing an item category -[TODO: "Update Record on this isn't our usual terminology -- should be Edit Item Category -- add that to the inbox] +### Item Categories -To edit the name and description of an item category, To view an item category, you click on Inventory, then Items & Inventory, then the Item Categories tab, then the "Edit" button beside the category you wish to edit. -[TODO: Screenshot] +Item categories are largely used for limiting the Items specific [Partner Groups](pm_partner_groups.md) can see, though you can also filter distributions by them. This tab shows all the Item categories you have, allowing you to view and edit each one, as well as enter new ones. -Update the category name (still needs to be unique) and category description, and click save. This will take you to the item category view [Add pointer to above], which lets you change the items in the category. +[!NOTE] Each Item can only belong to one Item Category -### Items, Quantity and Location tab +To view your Item Categories, click the "Item Categories" tab on the Items & Inventory page. -[TODO: We should highlight the bank-wide ones that are below the minimum. Add that to the inbox ] +![Navigation to view Item Categories](images/inventory/inventory_item_category_navigation.png) +#### Adding a new Item category +To add a new Item category, Click on Inventory, then Items & Inventory, then the Item Categories tab, then the "Add Item Category" button. + +![New Item Category navigation](images/inventory/inventory_item_category_new_navigation.png) +This brings up the new Item Category page +![New Item Category page](images/inventory/inventory_item_category_new.png) + +Enter a unique Category Name, and a suitable description, then click Save. + +#### Viewing an Item Category +All the information about an Item category is in the list, but you can also manage the Items in the category through the view. To view an Item category, you click on Inventory, then Items & Inventory, then the Item Categories tab, then the "View" button. +![View Item Category navigation](images/inventory/inventory_category_view_navigation.png) +![View Item Category page](images/inventory/inventory_item_category_view.png) + +You can also remove Items from the category by clicking "Remove from category" beside the Item. This will hide them from any partner groups that have this category. + +#### Editing an Item category +To edit the name and description of an Item category, To edit an Item category, you click on Inventory, then Items & Inventory, then the Item Categories tab, then the "Edit" button beside the category you wish to edit. +![Edit Item Category navigation](images/inventory/inventory_item_category_edit_navigation.png) +![Edit Item Category page](images/inventory/inventory_item_category_edit.png) + +Update the category name (still needs to be unique) and category description, and click save. This will take you to the Item category view [Add pointer to above], which lets you change the Items in the category. + +### Items, Quantity and Location tab -This tab shows all the item inventory across all the storage locations, along with each item's minimum quantity, recommended quantity, and bank-wide quantity. +This tab shows all the Item inventory across all the storage locations, along with each Item's minimum quantity, recommended quantity, and bank-wide quantity. -[TODO: Screenshot] +![Navigation to Items, Quantity, and Location](images/inventory/inventory_item_location_navigation.png) ### Item Inventory tab -This tab shows the bank-wide inventory for each item. Clicking the + beside the item name will show the breakdown of that inventory by storage area. +This tab shows the bank-wide inventory for each Item. Clicking the + beside the Item name will show the breakdown of that inventory by storage area. -[TODO: Raise the question of whether we really need both the Items, Quantity and Location tab at a Stakeholder's circle.] +![Navigation to Item Inventory tab](images/inventory/inventory_items_inventory_navigation.png) ### Kits tab -This shows the same information as the main [Kits](inventory_kits.md) view. +This shows the same information as the main [Kits](inventory_kits.md) list. +![Navigation to the Kits tab ](images/inventory/inventory_items_kits_navigation.png) -[TODO: Note the filter does not work on this view. Add to inbox.] [Prior: Partner Announcements](pm_announcements.md)[Next: Storage Locations](inventory_storage_locations.md) diff --git a/docs/user_guide/bank/inventory_kits.md b/docs/user_guide/bank/inventory_kits.md index f78c4c35da..a83411d766 100644 --- a/docs/user_guide/bank/inventory_kits.md +++ b/docs/user_guide/bank/inventory_kits.md @@ -1,80 +1,84 @@ -USER GUIDE DRAFT +READY FOR REVIEW # Kits -Many banks distribute kits -- the classic example is a package that contains different types of menstrual supplies (e.g. pads, tampons, wipes). +Many banks distribute Kits -- the classic example is a package that contains different types of menstrual supplies (e.g. pads, tampons, wipes). -What the kits feature lets you do is to manage your inventory of what goes into the kits versus what is in the kits. +What the Kits feature lets you do is to manage your inventory of what goes into the Kits versus what is in the Kits. -Working with kits has two steps -- creating the kit and allocating kits +Working with Kits has two steps -- creating the Kit and allocating Kits -## Creating versus allocating kits +## Creating versus allocating Kits -Creating a kit is defining what is in a kit -- so it's saying that, for example, a period kit will contain 8 tampons and 4 pads. +Creating a Kit is defining what is in a Kit -- so it's saying that, for example, a period Kit will contain 8 tampons and 4 pads. -Allocating kits is akin to assembling kits -- when you create the physical kits from the supplies you have, you then allocate those kits in the system. +Allocating Kits is akin to assembling Kits -- when you create the physical Kits from the supplies you have, you then allocate those Kits in the system. -In our example of a kit with 8 tampons and 4 pads, if you allocate 10 kits, you are going to be reducing your inventory of pads by 40 and your inventory of tampons by 80, but increasing your inventory of period kits by 10. +In our example of a Kit with 8 tampons and 4 pads, if you allocate 10 Kits, you are going to be reducing your inventory of pads by 40 and your inventory of tampons by 80, but increasing your inventory of period Kits by 10. -Then you distribute those period kits. +Then you distribute those period Kits. -## Creating a kit +## Creating a Kit -***NB: You can't edit a kit -- once you've defined it, it's set in stone. So do be careful! *** +[!WARN] You can't edit a Kit -- once you've defined it, it's set in stone. So do be careful! -To create a kit, click on "Inventory", then "Kits" in your left hand menu. This brings up a page that shows all your current kits. +To create a Kit, click on "Inventory", then "Kits" in your left hand menu. This brings up a page that shows all your current Kits. Then click the "+New Kit" button on the right hand side of the page. -[TODO: Screenshot] +![Kit new navigation](images/inventory/inventory_kits_new_navigation.png) This brings up the "New Kit" form, which has the following info: -[TODO: Screenshot] -- Name: This is the item name for the kit -- what it will appear as in the drop-down lists and in any reports -- Item is Visible to Partners? Check this if you allow partners to order the kit. -Note: If you need to control which partners can request a kit, you'll need to put it in a category, through the Item page, after creation, and use partner groups to control which partners can request the item. -[TODO: link to the Edit Item section] -- Value for kit: This is the Fair Market Value for the kit. We don't sum up the items within the kits for FMV calculations. -- Items in this kit: - You can enter multiple items, adding each item with the following - - Barcode Entry -- if you have already entered [Barcode Items](inventory_barcodes.md), you can just "boop" the item into the kit. +![New Kit](images/inventory/inventory_kits_new.png) +- Name: This is the Item name for the Kit -- what it will appear as in the drop-down lists and in any reports +- Item is Visible to Partners? Check this if you allow Partners to order the Kit. +[!INFO] If you need to control which Partners can request a Kit, you'll need to put it in a category once you've defined it, through the Item page, after creation, and use Partner Groups to control which Partners can request the Item. +[(see Editing an Item)](inventory_items.md#editing-an-item), [Item Categories](inventory_items.md#item-categories) and [Partner Groups](pm_partner_groups.md) +- Value for Kit: This is the Fair Market Value for the Kit. We don't sum up the Items within the Kits for FMV calculations. +- Items in this Kit: + You can enter multiple Items, adding each Item with the following + - Barcode Entry -- if you have already entered [Barcode Items](inventory_barcodes.md), you can just "boop" the Item into the Kit. OR - - Choose an item from the list of all [Items](inventory_items.md) you have, and add the quantity of the item that will be in the kit. + - Choose an Item from the list of all [Items](inventory_Items.md) you have, and add the quantity of the Item that will be in the Kit. -To add the rest of your items, click "+Add Another Item." If you need to remove an item, click "Remove" under it. +To add the rest of your Items, click "+Add Another Item." If you need to remove an Item, click "Remove" under it. -Once you are satisfied with the definition of your kit, click "Save". This will return you to the kits screen, where you'll see your new kit. +Once you are satisfied with the definition of your Kit, click "Save". This will return you to the Kits screen, where you'll see your new Kit. -## Allocating kits +## Allocating Kits -Once you have created your kit, you can allocate it. This represents assembling kits from their components, and will reduce the inventory of those items appropriately. +Once you have created your Kit, you can allocate it. This represents assembling Kits from their components, and will reduce the inventory of those Items appropriately. -From the Kits page (Inventory | Kits), click "Modify Allocation" on your kit -[TODO: Screenshot] +From the Kits page (Inventory | Kits), click "Modify Allocation" on your Kit + +![Navigation to Kit alloocation](images/inventory/inventory_kits_modify_allocation_navigation.png) This takes you to the Kit Allocation page -This lists your current on-hand quantity for each storage location you have kits in, and lets you change the allocation. +This lists your current on-hand quantity for each storage location you have Kits in, and lets you change the allocation. + +Pick the Storage Location (A) and the amount you want to increase the Kits by (B). When you put a number in the "Change Kit quantity ", you'll see what effect the allocation will have on your inventory. -Pick the storage location (A) and the amount you want to increase the kits by (B). When you put a number in the "Change kit quantity ", you'll see what effect the allocation will have on your inventory. +![Kit Allocation](images/inventory/inventory_kits_allocation.png) -[TODO: Screenshot] +[!NOTE] you can also 'deallocate' Kits if need be, by putting a negative number in the "Change Kit quantity by" field. When you deallocation Kits, the contents will be returned to the appropriate Items' inventory. -Note: you can also 'deallocate' kits if need be, by putting a negative number in the "Change kit quantity by" field. When you deallocation kits, the contents will be returned to the appropriate items' inventory. +Then click "Save". The system will check if there are enough of each component Item in the storage location. If there isn't, it will give you an error. If there is, it will adjust the inventory appropriately, and return to this screen, which will reflect the new on-hand quantity. -Then click "Save". The system will check if there are enough of each component item in the storage location. If there isn't, it will give you an error. If there is, it will adjust the inventory appropriately, and return to this screen, which will reflect the new on-hand quantity. +![Kit allocation post save](images/inventory/inventory_kits_allocation_post_save.png) -[TODO: Screenshot] +## Deactivating a Kit -## Deactivating a kit +If you are no longer using a Kit, you can deactivate it - which will remove it from the dropdowns, and from the Kits page (you can always see it by clicking the "show inactive" in the filter, then clicking "Filter") -If you are no longer using a kit, you can deactivate it - which will remove it from the dropdowns, and from the Kits page (you can always see it by clicking the "show inactive" in the filter, then clicking "Filter") +You can only deactivate a Kit if it has no allocations. To deactivate a Kit, click the "Deactivate" button beside it in the Kits page, and then click "OK" to confirm. -You can only deactivate a kit if it has no allocations. To deactivate a kit, click the "Deactivate" button beside it in the Kits page, and then click "OK" to confirm. +![Kit deactivation](images/inventory/inventory_kits_deactivate.png) -## Reactivating a kit +## Reactivating a Kit -To reactivate a kit, go to the Kits page (Inventory | Kits). -Then click "Show Inactive" and "Filter". This will show the Inactive kits. -Then click "Reactivate" beside the kit you wish to reactivate and click "OK" to confirm. +To reactivate a Kit, go to the Kits page (Inventory | Kits). +Then click "Show Inactive" and "Filter". This will show the Inactive Kits. +Then click "Reactivate" beside the Kit you wish to reactivate and click "OK" to confirm. +![Kit reactivation](images/inventory/inventory_kits_reactivate.png) [Prior: Audits](inventory_audits.md) [Next: Barcodes](inventory_barcodes.md) diff --git a/docs/user_guide/bank/inventory_storage_locations.md b/docs/user_guide/bank/inventory_storage_locations.md index d4502565de..3e81537029 100644 --- a/docs/user_guide/bank/inventory_storage_locations.md +++ b/docs/user_guide/bank/inventory_storage_locations.md @@ -1,65 +1,60 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Storage Locations -You need at least one storage location. Each donation and purchase increases the inventory in a storage location, and each distribution is made from the inventory in a storage location. +You need at least one Storage Location. Each Donation and Purchase increases the inventory in a Storage Location, and each distribution is made from the inventory in a Storage Location. -## Storage location summary page -To get to the storage location summary page, click "Inventory", then "Storage Locations" -[TODO: Screenshot] -This shows all your storage locations, and allows you to navigate to view, edit, or possibly deactivate each one. +## Storage Location summary page +To get to the Storage Location summary page, click "Inventory", then "Storage Locations" in the left-hand menu +![Navigation to Storage Locations summary](images/inventory/inventory_storage_locations_navigation.png) +This shows all your Storage Locations, and allows you to navigate to view, edit, or possibly deactivate each one. -## Adding a storage location -To add a storage location, click "New Storage Location" on the storage location summary page. -The square footage and storage site type information are used in the annual survey report. -[TODO: Is the address used anywhere? (Distribution printout? maybe? Maybe available for the distribution email?)] -[TODO: Speak intelligently on how the time zone is used] -[TODO: Screenshot] + +## Adding a Storage Location +To add a Storage Location, click "New Storage Location" on the Storage Location summary page. + +![Navigation to add a Storage Location](images/inventory/inventory_storage_locations_add_navigation.png) +This brings up the New Storage Location page +![New Storage Location page](images/inventory/inventory_storage_locations_add.png) +- Name is mandatory, and is used in drop-downs throughout the system to indicate the Storage Location +- Address is also mandatory, but is information only. +- Square Footage is used in the annual survey report +- Warehouse Type is used in the annual survey report +- Time Zone is used with the pickup and deliveries calendar, particularly with allowing you to sync your pickups calendar with Google Calendar ## Storage location view -To view a storage location, click the "View" button beside it in the storage location summary page. +To view a Storage Location, click the "View" button beside it in the Storage Location summary page. The view has three tabs: Inventory, Inventory Coming In, and Inventory Going Out ### Inventory -Here you see the current levels of inventory for the chosen storage location. You can see the inventory levels for past dates by choosing the date in "Show Inventory at Date" and clicking "View" +Here you see the current levels of inventory for the chosen Storage Location. You can see the inventory levels for past dates by choosing the date in "Show Inventory at Date" and clicking "View" That shows the inventory at the beginning of the day. -[TODO: Screenshot] +![Storage Location - Inventory tab](images/inventory/inventory_storage_location_view_inventory.png) ### Inventory Coming In -This shows the sum of all the inventory coming in ([Purchases](essentials_purchases.md), [Donations](essentials_donations.md), [Adjustments](inventory_adjustments.md), [Transfers](inventory_transfers.md), and [positive changes for kit allocations.deallocations](inventory_kits.md)) for the given storage location for all time - -[TODO: Check that that is exactly what it is doing] - -[TODO: Would this be more useful with a date range, and is that feasible?] +This shows the sum of all the inventory coming in ([Purchases](essentials_purchases.md), [Donations](essentials_donations.md), [Adjustments](inventory_adjustments.md), [Transfers](inventory_transfers.md), and [positive changes for kit allocations.deallocations](inventory_kits.md)) for the given Storage Location for all time -[TODO: Screenshot] +![Inventory coming in](images/inventory/inventory_storage_locations_coming_in.png) ### Inventory Going Out -This shows the sum of all the inventory going out ([Distributions](essentials_purchases.md), [Adjustments](inventory_adjustments.md), and [Transfers](inventory_transfers.md), and [positive changes for kit allocations.deallocations](inventory_kits.md)) for the given storage location for all time - -[TODO: Check that that is exactly what it is doing] - -[TODO: Would this be more useful with a date range, and is that feasible?] - -[TODO: Screenshot] - -## Editing a storage location -You can edit your storage location (though it seems a very rare event), by clicking "Edit" beside the storage location in the Storage Location Summary Page. -The same fields are as available as for your new storage locations. Make your updates and click save. +This shows the sum of all the inventory going out ([Distributions](essentials_purchases.md), [Adjustments](inventory_adjustments.md), and [Transfers](inventory_transfers.md), and [positive changes for kit allocations.deallocations](inventory_kits.md)) for the given Storage Location for all time -## Deactivating a storage location -You can only deactivate a storage location if the inventory for all its items is 0. -To deactivate the storage location, click "Deactivate" beside it on the storage location summary screen, then click "OK" to confirm. -[TODO: Screenshot] +![Inventory going out](images/inventory/inventory_storage_locations_going_out.png) +## Editing a Storage Location +You can edit your Storage Location (though it seems a very rare event), by clicking "Edit" beside the Storage Location in the Storage Location Summary Page. +The same fields are as available as for [New Storage Locations](inventory_storage_locations.md). Make your updates and click save. -## Reactivating a storage location -If you need to reactivate a storage location that was deactivated, go to the storage locations summary screen (Inventory | Storage Locations), then click "include inactive storage locations", then Filter. -This will include inactive storage locations in the list. Find the storage location you want to use again and clic "Reactivate" beside it. -[TODO: Screenshot] +## Deactivating a Storage Location +You can only deactivate a Storage Location if the inventory for all its items is 0. +To deactivate the Storage Location, click "Deactivate" beside it on the Storage Location summary screen, then click "OK" to confirm. +## Reactivating a Storage Location +If you need to reactivate a Storage Location that was deactivated, go to the Storage Locations summary screen (Inventory | Storage Locations), then click "include inactive Storage Locations", then Filter. +This will include inactive Storage Locations in the list. Find the Storage Location you want to use again and clic "Reactivate" beside it. +![Storage Location reactivation](images/inventory/inventory_storage_location_reactivation.png) diff --git a/docs/user_guide/bank/inventory_transfers.md b/docs/user_guide/bank/inventory_transfers.md index 477c4bd0f2..f2cb79083c 100644 --- a/docs/user_guide/bank/inventory_transfers.md +++ b/docs/user_guide/bank/inventory_transfers.md @@ -1,54 +1,59 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Transfers -If you have multiple storage locations, sometimes you have to move inventory between them. -[TODO: Throughout - doublecheck what requires org_admin, vs what needs org user] +If you have multiple Storage Locations, sometimes you have to move inventory between them. ## Working with Transfers -To start working with transfers, click "Inventory", then "Transfers" in the left-hand menu. -That will bring up the transfers page, which lists all your past transfers in chronological order. From here you can make a new transfer, add a transfer, view the details of a past transfer or delete it. -You can filter the transfers based on source (From), destination(to), and date. +To start working with Transfers, click "Inventory", then "Transfers" in the left-hand menu. +That will bring up the Transfers page, which lists all your past Transfers in chronological order. +![Navigation to Transfers](images/inventory/inventory_transfers_navigation.png) -[TODO: Screenshot] -## Adding a transfer -To add a transfer, click the "+New Transfer" button on the transfers page. That will bring up this screen. +You can filter the Transfers based on source (From), destination(to), and date. +From here you can make a new Transfer, add a Transfer, view the details of a past Transfer or delete it. -[TODO: Screenshot] +## Adding a Transfer +To add a Transfer, click the "+New Transfer" button on the Transfers page. +![New Transfer navigation](images/inventory/inventory_transfers_new_navigation.png) +That will bring up the New Inventory Transfer page. +![New Transfer](images/inventory/inventory_transfers_new.png) -Specify where the items being moved are coming from, in "From Storage Location," and where they are going to in "To storage location". +Specify where the Items being moved are coming from, in "From Storage Location," and where they are going to in "To Storage Location". -The Comment field is a good place to note the reason for the transfer, but you can leave it blank. +The Comment field is a good place to note the reason for the Transfer, but you can leave it blank. -Then select the item and quantity for each item that is being transferred. If you have [barcodes](inventory_barcodes.md) set up, you can use your barcode reader to "boop" in the materials being transfered. +Then select the Item and quantity for each Item that is being Transferred. If you have [barcodes](inventory_barcodes.md) set up, you can use your barcode reader to "boop" in the materials being Transfered. -When you are done, click "Save". The system will check that you have enough inventory in "From" to cover the transfer. If so, the inventory changes will take place immediately. +When you are done, click "Save". The system will check that you have enough inventory in "From" to cover the Transfer. If so, the inventory changes will take place immediately. -[TODO: Make clear on each item where it might be unclear when the inventory changes happen. Probably should write something in the intro for that too] +## Viewing the details of a Transfer +To view the details of a Transfer, click the "view" button beside it in the Transfers list. -## Viewing the details of a transfer -To view the details of a transfer, click the "view" button beside it in the transfers list. +![view Transfer navigation](images/inventory/inventory_transfers_view_navigation.png) +This lists all the Items in the Transfer, and how much was transferred, as well as your comment. +![view Transfer](images/inventory/inventory_transfers_view.png) +## Deleting a Transfer -[TODO: Screenshot] +This should not happen very often! -This lists all the items in the transfer, and how much was transferred, as well as your comment. +To delete a Transfer, click the "delete" button beside the Transfer. +[Delete Transfer navigation](images/inventory/inventory_transfers_delete.png) -## Deleting a transfer - -To delete a transfer, click the "delete" button beside the transfer, and press "OK" to confirm. -This check that the inventory levels in the two storage locations will allow the change. If they will, it will roll back the inventory changes that were made when you entered the transfer. +Then press "OK" to confirm. +This will check that the inventory levels in the two Storage Locations will allow the change. If they will, it will roll back the inventory changes that were made when you entered the Transfer. ##### ** N.B. This is not undoable *** ---- -Note: If you do delete the wrong transfer, you don't have to panic, but it will be a hassle. You can find a record of any transfer made since September 2024 in the "History" Report to grab the numbers and re-enter it. The inventory changes in that case will be as of the date you re-enter, though. +[!NOTE] If you do delete the wrong Transfer, you can find a record of any Transfer made since September 2024 in the "History" Report. There you can find the amount transferred for each Item, so that you could re-enter them. The inventory changes in that case will be as of the date you re-enter, though. ---- -## Exporting transfers -To export a list of the transfers, click "Export Transfers" on the transfers page -[TODO: Screenshot] -[TODO : screenshot of sample export] +## Exporting Transfers +To export a list of the Transfers, click "Export Transfers" on the Transfers page +![Navigation to Transfer export](images/inventory/inventory_transfers_export_navigation.png) + +The details in the export are lacking -- it doesn't show each Item, but only the total. +![Sample Transfer export](images/inventory/inventory_transfers_export.png) -The details in the export are lacking -- it doesn't show each item, but only the total. -[TODO: Add a better export to the Things to do] +(Expanding it to include the Items transferred is a task on our things to do. Please [reach out](intro_ii.md)) if this is a high priority for you.) [Prior: Adjustments](inventory_adjustments.md) [Next: Product Drives](community_product_drives.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_adding_a_partner.md b/docs/user_guide/bank/pm_adding_a_partner.md index 3444f7b6e0..795bed78cf 100644 --- a/docs/user_guide/bank/pm_adding_a_partner.md +++ b/docs/user_guide/bank/pm_adding_a_partner.md @@ -1,27 +1,31 @@ -# Adding a single partner -To add a single partner, you can either Click on the "Add a Partner" button in the "Getting Started" section of your dashboard (if you are, indeed, just getting started), or click "Partner Agencies" in the left-hand menu, then "All Partners", then "New Partner Agency". Then, fill in the following information, and click "Add Partner Agency". - -##### Note: This just sets up the partner so you can distribute to them. It does *not* notify or invite the partner -- that is a separate step. +READY FOR REVIEW +# Adding a single Partner +To add a single Partner, you can either Click on the "Add a Partner" button in the "Getting Started" section of your dashboard (if you are, indeed, just getting started), or click "Partner Agencies" in the left-hand menu, then "All Partners", then "New Partner Agency". Then, fill in the following information, and click "Add Partner Agency". +![Navigation to add a Partner](images/partners/partners_add_navigation.png) +![Add Partner page](images/partners/partners_add.png) +[!NOTE] This just sets up the Partner so you can distribute to them. It does *not* notify or invite the Partner -- that is a separate step. ### Name (mandatory) -This is the name of the agency. It will appear in dropdowns for you to select when filtering requests or distributions, or when entering a new distribution. +This is the name of the agency. It will appear in dropdowns for you to select when filtering Requests or Distributions, or when entering a new Distribution. ### E-mail(mandatory) -This is the primary contact for the partner. Any system emails regarding the partner [TODO: list examples] will be sent to this address. -### Default storage location -Choosing a default storage location here is a short-cut that will automatically fill in the source location in the distribution that is started when you fulfill a request from this partner. (Don't worry, you can change it.) +This is the primary contact for the Partner. Any system emails regarding the Partner, such as notifications of Distributions or reminders of Request deadlines, will be sent to this address. +### Default Storage Location +Choosing a default Storage Location here is a short-cut that will automatically fill in the Storage Location when you fulfill a Request from this Partner. (Don't worry, you can change it.) ### Group -What group, if any, does the partner belong to. Groups are very handy if you want to, say, allow only specific groups to be able to request certain items. For more details see [Partner Groups](pm_partner_groups.md) -[TODO: Check what happens if none is chosen, but there are groups. Does the partner get to request nothing or everything?] +What Partner Group, if any, does the Partner belong to. Partner Groups are very handy if you want to, say, allow only certain Partners to be able to request some Items. For more details see [Partner Groups](pm_partner_groups.md) + +[!NOTE] If you use Partner Groups, please note that any Partners that do not belong to a Partner Group will be able to choose from all the Items. So if you need to restrict certain Items to a specific group, then you're going to have to have all your Partners belonging to some Partner Group +[!NOTE] Partners who belong to Partner Groups can *only* choose the Items that have the Item Categories specified for their Partner Group. -### Do you want this partner to receive emails for distributions and reminders from the system? -Hopefully this is self-explanatory, but also see the questions about customizing reminders in [your organization](getting_started_customization.md) +### Do you want this Partner to receive emails for Distributions and Reminders from the system? +See also the questions about customizing reminders in [your organization](getting_started_customization.md) ### Quota -This is an information-only quota -- it is meant to be total items per request. We give a friendly "are you sure you wanted to order that much?" kind of warning on the partner's confirmation screen if they over order, but there is *no* actual enforcement. If entered, this value is also displayed in your view of the requests. +This is an information-only quota -- it is meant to be total Items per request. We give a friendly "are you sure you wanted to order that much?" kind of warning on the partner's confirmation screen if they over order, but there is *no* actual enforcement. If entered, this value is also displayed in your view of the requests. ### Notes -Bank-only [TODO: Double-check -- i'm 95% sure that's right] notes about the partner. +Bank-only notes about the Partner. ### Documents -Documents concerning the partner -- bank only [TODO: double check] +Documents concerning the Partner -- bank only [Prior - Importing Partners](pm_importing_partners.md) [Next - Partner Groups](pm_partner_groups.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_announcements.md b/docs/user_guide/bank/pm_announcements.md index 76deb1ded6..faa3e8d2e2 100644 --- a/docs/user_guide/bank/pm_announcements.md +++ b/docs/user_guide/bank/pm_announcements.md @@ -1,37 +1,33 @@ -DRAFT USER GUIDE +READY FOR REVIEW +# Partner Announcements -# Partner announcements +Partner Announcements are a great way to let your partners know about temporary situations -- like when you need to put some limits on what you can give out of a particular size. Some banks also use them to point to other resources. +These Announcements appear on the partner's dashboard, so they'll be there whenever the partner logs in. -Partner announcements are a great way to let your partners know about temporary situations -- like when you need to put some limits on what you can give out of a particular size. Some banks also use them to point to other resources. -These announcements appear on the partner's dashboard, so they'll be there whenever the partner logs in. +You can add multiple Announcements, with links to additional information, and either have an expiry date or delete them when they are no longer useful. The Announcements will be shown in reverse chronological order, by time entered. -You can add multiple announcements, with links to additional information, and either have an expiry date or delete them when they are no longer useful. The announcments will be shown in reverse chronological order +## How to create an Announcement -[TODO: confirm that there isn't something odd, like the ones with expiries showing first] - -## How to create an announcement - -Click on "Partner Agencies" in the left hand menu, then "Partner Announcement". This will bring up a list of all your partner announcements. +Click on "Partner Agencies" in the left hand menu, then "Partner Announcement". This will bring up a list of all your partner Announcements. ![all announcments screen](images/partners/partners_announcements_1.png) -Click the "New Announcements" button to bring up a form for a new announcement +Click the "New Announcements" button to bring up a form for a new Announcement -[TODO: Also check that the "SEnt by" actually has meaning. As org_admin1, it just says "N/A".] ![new announcement screen](images/partners/partners_announcements_2.png) Here you can fill in -- Up to 500 characters for your announcement. +- Up to 500 characters for your Announcement. - a URL for more information (useful for pointing to resources on your site) -- an expiry date. This is optional - you can leave announcements up 'forever' if you want. The expiry date is the last day the announcement will be shown [TODO: confirm that it's not the first day it's *not* shown.] +- an expiry date. This is optional - you can leave Announcements up 'forever' if you want. The expiry date is the last day the Announcement will be shown. -Then, as soon as you click "Broadcast announcement", the partners will see it when they log in. +Then, as soon as you click "Broadcast Announcement", the partners will see it when they log in. -## Changing an announcment -Simply click "edit" in the actions column beside the announcement you want to change, make your changes, and click "Broadcast announcement" +## Changing an Announcement +Simply click "edit" in the actions column beside the Announcement you want to change, make your changes, and click "Broadcast Announcement" -## Deleting an announcement -You may want to clear out your older announcements. You can click "delete" beside any announcement to remove it from your view. It also will be deleted from the partner's view, if it was not yet expired. +## Deleting an Announcement +You may want to clear out your older Announcements. You can click "delete" beside any Announcement to remove it from your view. It also will be deleted from the partner's view, if it was not yet expired. [Prior: Other partner information](pm_other_information.md) [Next: Inventory -- Items](inventory_items.md) diff --git a/docs/user_guide/bank/pm_approving_a_partner.md b/docs/user_guide/bank/pm_approving_a_partner.md index dd97f8b0c5..5685509fc5 100644 --- a/docs/user_guide/bank/pm_approving_a_partner.md +++ b/docs/user_guide/bank/pm_approving_a_partner.md @@ -1,39 +1,38 @@ -DRAFT USER GUIDE +READY FOR REVIEW +# Approving Partners -[TODO: Upcoming change review -- terminology. I feel like these are going to be done before the first pass is through so I'm leaving the screenshots out for the nonce.] +There are three paths in to reviewing a Partner's information before approving them to make Requests: -# Approving partners +1/ There is a list of Partners awaiting approvals on the dashboard. To start the approval process, click "Review Application"; -There are three paths in to reviewing a partner's information before approving them to make requests -- -1/ There is a list of partners awaiting approvals on the dashboard. To start the approval process, click "Review Application"; +![Navigation to review application_from dashboard](images/partners/partners_review_application_from_dashboard.png) -[TODO: Screenshot] +or -2/ Click "Partner Agencies" and "All Partners" in the left-hand menu, then "Review Application" beside the partner. +2/ Click "Partner Agencies" and "All Partners" in the left-hand menu, then "Review Applicant's Profile" beside the Partner. -[TODO: Screenshot] -or -3/ Click "Partner Agencies" and "All Partners" in the left-hand menu, then click the partner name. Scroll down to see the partner profile information, which starts just below the "Approve Partner" button +![Navigation to review application_from Partner_list](images/partners/partners_review_application_navigation.png) -[TODO: Screenshot] +or + +3/ Click "Partner Agencies" and "All Partners" in the left-hand menu, then click the Partner's name. Scroll down to view the Partner Profile information, and click "Approve Partner" if it has what is needed. ----------------------------------- -In any of these cases, you can view all the answers in the profile. If something needs changing, you can do it yourself by clicking the "Edit Information" button, or the partner can make the change. +---------------------------------- -See [Partner profiles](partner_profiles.md) for details on the partner profile. +In any of these cases, you can view all the answers in the profile. If something needs changing, you can do it yourself by clicking the "Edit Information" button, or the Partner can make the change. -[TODO: Link should go to the part of partner profiles that lists all the fields] +See [Partner profiles](pm_partner_profiles.md#viewing-a-partner-profile) for details on the Partner Profile. -Once you have reviewed the partner's info, and want to approve them to make requests, click the "Approve Partner " button. +Once you have reviewed the Partner's info, and want to approve them to make requests, click the "Approve Partner " button. -[TODO: Screenshot] +![](images/partners/partners_approving_1.png) -You should see a "Partner Approved!" message, and the status of the partner will show as "Approved" +You should see a "Partner Approved!" message, and the status of the Partner will show as "Approved" -[TODO: Screenshot] +![](images/partners/partners_approving_2.png) [Prior - Partner Profiles](pm_partner_profiles.md) [Next - Requesting Recertification](pm_requesting_recertification.md) diff --git a/docs/user_guide/bank/pm_editing_a_partner.md b/docs/user_guide/bank/pm_editing_a_partner.md index 53f0a37cac..4c64aa70a8 100644 --- a/docs/user_guide/bank/pm_editing_a_partner.md +++ b/docs/user_guide/bank/pm_editing_a_partner.md @@ -1,19 +1,23 @@ -DRAFT USER GUIDE +READY FOR REVIEW -# Editing a partner +# Editing a Partner -Note - this is editing a partner's basic information, not their [profile](pm_partner_profiles.md) +Note - this is editing a Partner's basic information, not their [Partner Profile](pm_partner_profiles.md). What's the difference? The basic Partner information is what you enter when you are initially creating the Partner, whereas the Partner Profile contains a high level of detail that can be entered by the Partners themselves. You can edit a partner's basic information by clicking [Partner Agencies] in the left-hand menu, then [All Partners], then click on the partner you want to work with. -That will bring up this screen: +That will bring up this screen. Click "Edit Partner Information" to bring up the Partner edit screen: -[TODO: Screenshot] +![top of View Partner screen](images/partners/partners_edit_navigation.png) -Click "Edit Partner Information" to bring up this screen: +This is the same selection of information you provided when you initially added the partner. + +![edit page for basic Partner information](images/partners/partners_edit.png) -[TODO: Screenshot] Make your changes, and click "Update Partner" to save. + + + [Prior: Partner Groups](pm_partner_groups.md)[Next: Inviting a partner](pm_inviting_a_partner.md) diff --git a/docs/user_guide/bank/pm_importing_partners.md b/docs/user_guide/bank/pm_importing_partners.md index f44ddaa43d..c2193cb85c 100644 --- a/docs/user_guide/bank/pm_importing_partners.md +++ b/docs/user_guide/bank/pm_importing_partners.md @@ -1,9 +1,9 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Importing partners -**N.B.** We've set up importing partners as a process you can only run once to prevent accidental imports and writing over existing partners. +**N.B.** We've set up importing partners as a process you only run once to prevent accidental imports and writing over existing partners. -To import partners, you will need a .csv file containing their information -- this will just be the partner business name and primary contact email. +To import partners, you will need a .csv (comma-separated variable) file containing their information -- this will just be the Partner business name and primary contact email. We do provide an example file -- to get it, click on "Partner Agencies", "All Partners", then "Import Partners". (If you are a brand-new bank you might have gotten here through the "Getting Started" instructions on the dashboard.) ![Navigation to import](images/partners/partners_importing_1.png) @@ -12,14 +12,13 @@ You will see a pop-up with a "Download example CSV" button (A) on it. Clicking That file is named partners_template.csv, and you should be able to find it in your downloads directory. -You can edit this in your favourite spreadsheet program, or just as a text file. +You can edit this in your favourite spreadsheet program, or just as a text file. But you need to save it as a .csv file When you have the information completed, navigate back to that same pop-up ( click on "Partner Agencies", "All Partners", then "Import Partners" ) Now, follow the instructions under "2. Upload your CSV file " -- click "Choose File" (B), and pick the .csv file you've edited. The file name will appear after the "Choose File" button. Then, click "Import CSV". -If you see "Partners were imported successfully!", that's good! You should review the list of partners on that page to make sure that all of the partners were imported -- if there was a badly formatted email, that partner will not appear. -If you should get a "500" error, it most likely means that your file is not formatted properly. +If you see "Partners were imported successfully!", that's good! If you should get a "500" error, it most likely means that your file is not formatted properly. [Prior - The request/distribution cycle](pm_request_distribution_cycle.md) [Next - Adding a partner](pm_adding_a_partner.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_inviting_a_partner.md b/docs/user_guide/bank/pm_inviting_a_partner.md index 9b6f2feee3..88edbca4a7 100644 --- a/docs/user_guide/bank/pm_inviting_a_partner.md +++ b/docs/user_guide/bank/pm_inviting_a_partner.md @@ -1,59 +1,61 @@ -Draft User Guide -# Inviting a partner +READY FOR REVIEW +# Inviting a Partner -Before a partner can make requests, they have to be invited and approved. +Before a Partner can make Requests, they have to be invited and approved. When they are invited, they will receive an email with a link so that they can set up their password. -These links expire[TODO: How long?], but if they don't respond in time, +These links expire in a couple of weeks, but if they don't respond in time, you can direct them to use the "Forgot your password?" function on the sign-in page (https://humanessentials.app/signin) to get a new link to set their password. ## Invite or Invite-and-Approve? -There are two options for enabling partners to request. +There are two options for enabling Partners to Request. The first, default, option is: -1/ You invite the partner. +1/ You invite the Partner. 2/ They click on the link and set up their password. -3/ They then sign in and go into their "My Organization", update the profile information you need (se [profile](pm_partner_profiles.md). +3/ They then sign in and go into their "My Organization", update whatever profile information you need (se [profile](pm_partner_profiles.md). 4/ They submit it for your approval 5/ You approve it (there may be some back and forth here!) -6/ They can now make requests. +6/ They can now make Requests. If you choose "Use One step Partner invite and approve process?" in ["My Organization"](getting_started_customization.md), then the sequence is: -1/ You invite and approve the partner. +1/ You invite and approve the Partner. 2/ They click on the link and set up their password. -3/ They can now make requests. +3/ They can now make Requests. The *disadvantage* to the second method is that it may be harder to extract any -information you do need from your partners which can impact your grant-writing and +information you do need from your Partners which can impact your grant-writing and your annual reports. -# How to invite a partner +# How to invite a Partner 1/ Click on "Partner Agencies" in the left hand menu, then "All Partners" -2/ Find the partner you wish to invite. -3/ Click on the "Invite and Approve" button for that partner. +2/ Find the Partner you wish to invite. +3/ Click on the "Invite" button for that Partner. -[TODO: Add screenshot] +![Inviting a Partner screenshot](images/partners/partners_inviting.png) -# How to invite and approve a partner -First, make sure that you have "Use one-step partner invate and approve process" chosen in ["My Organization"](getting_started_customization.md). +# How to invite and approve a Partner +First, make sure that you have "Use one-step Partner invite and approve process" chosen in ["My Organization"](getting_started_customization.md). Then... 1/ Click on "Partner Agencies" in the left hand menu, then "All Partners" -2/ Find the partner you wish to invite. -3/ Click on the "Invite and Approve" button for that partner. +2/ Find the Partner you wish to invite and approve. +3/ Click on the "Invite and Approve" button for that Partner. + + +![Inviting and approving a Partner screenshot](images/partners/partners_inviting_and_approving.png) -[TODO: Add screenshot] [Prior: Partner Groups ](pm_partner_groups.md) [ Next: Partner Profiles](pm_partner_profiles.md) diff --git a/docs/user_guide/bank/pm_making_a_partner_inactive.md b/docs/user_guide/bank/pm_making_a_partner_inactive.md index 000e7b3847..25af1aa11c 100644 --- a/docs/user_guide/bank/pm_making_a_partner_inactive.md +++ b/docs/user_guide/bank/pm_making_a_partner_inactive.md @@ -1,35 +1,35 @@ -DRAFT USER GUIDE +READY FOR REVIEW -# Making a partner inactive +# Making a Partner inactive -Unfortunately, sometimes a partner will cease operations. Should that occur, you can make them inactive. This does not remove their historical information, but they won't appear in your selections, or in the all partners list (unless you specifically choose "Deactivated") +Unfortunately, sometimes a Partner will cease operations. Should that occur, you can make them inactive. This does not remove their historical information, but they won't appear in your selections, or in the all partners list (unless you specifically choose "Deactivated") -To deactivate a partner: +To deactivate a Partner: -Go to the All Partners list (click on "Partner Agencies", then "All Partners" in the left hand menu),then click on the partner name to bring up that partner's screen. +Go to the All Partners list (click on "Partner Agencies", then "All Partners" in the left hand menu),then click on the Partner name to bring up that Partner's screen. -[TODO: Screenshot] +![Partner deactivate screenshot 1](images/partners/partners_deactivate_1.png) Then click on "Deactivate Partner". A confirmation screen will appear. Click "OK". -[TODO: Screenshot] -You'll see a message that the partner is successfully deactivated, and they will not appear in the all partners list. +![Partner deactivate screenshot 2](images/partners/partners_deactivate_2.png) -## But what if I need to see them? +You'll see a message that the Partner is successfully deactivated, and they will not appear in the default All Partners list. -If you need to see their information, go to the all partners list, and click on the "Deactivated" link. -This will bring up a list of all the deactivated partners. You can then click on them to view their information. +Note that if there has never been any activity for the Partner at all, you will be prompted to delete instead. This is permanent. -[TODO: screenshot] +## But what if I need to see them? -## Can I reactivate them? +If you need to see the information for a deactivated Partner, go to the All Partners list, and click on the "Deactivated" link (see A, below). +This will bring up a list of all the deactivated Partners. You can then click on them to view their information. -Yes. Bring up the list of deactivated partners, then click on the "Reactivate" button beside the partner. Click "Ok" on the confirmation window that pops up. +![Viewing deactivated Partners and reactivating them](images/partners/partners_viewing_and_reactivating_deactivated.png) +## Can I reactivate them? -[TODO: screenshot] +Yes. Bring up the list of deactivated Partners, then click on the "Reactivate" (B, above) button beside the partner. Click "Ok" on the confirmation window that pops up. -This will bring them up in the same status as they were when you deactivated them. +This will restore them to the same status as when you deactivated them. [Prior: Requesting recertification](pm_requesting_recertification.md) [Next: reactivating a partner](pm_partner_reactivation.md) diff --git a/docs/user_guide/bank/pm_other_information.md b/docs/user_guide/bank/pm_other_information.md index 1cfb695e12..73b3854c94 100644 --- a/docs/user_guide/bank/pm_other_information.md +++ b/docs/user_guide/bank/pm_other_information.md @@ -1,14 +1,11 @@ -DRAFT USER GUIDE +READY FOR REVIEW -There are a few other things with partners that didn't fit into any of the sections above: +There are a few other things with Partners that didn't fit into any of the sections above: -1/ You can see the families served, children served and zipcodes served -How do we get these numbers? -[TODO: confirm how we determine families, children, zipcode in this case] -- Families served -- if the partner is using child requests, this is a count of the families they have entered. -- Children served -- if the partner is using child requests, this is a count of active children (confirm), -- Zipcodes - these are directly from the zipcodes entered in the partner profiles -- [TODO: Should we also show counties served?] -2/ You can see the prior distributions for this partner, at the bottom of the view of the partner. +1/ You can see the Families served, Children served and zipcodes served, when you view a partner +- Families served -- if the partner is using Child Requests, this is a count of the Families they have entered. +- Children served -- if the partner is using Child requests, this is a count of active Children (confirm), +- Zipcodes - these are directly from the zipcodes entered in the Partner Profile +2/ You can see the prior Distributions for this partner, at the bottom of the view of the Partner. [Prior: Administering partner users](pm_partner_user_admin.md)[Next: Partner announcements](pm_announcements.md) diff --git a/docs/user_guide/bank/pm_partner_groups.md b/docs/user_guide/bank/pm_partner_groups.md index d7d977f6d3..e4c5eeba60 100644 --- a/docs/user_guide/bank/pm_partner_groups.md +++ b/docs/user_guide/bank/pm_partner_groups.md @@ -1,42 +1,41 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Partner Groups You may have reasons that some partners are treated differently than others. Reasons we've seen for this include: - Some banks have 'tiers' of partners, where some partners have different rules about what and when they can request. -- Some banks have grants that are tied to specific geographic areas, and set up specific items for those grants (that only partners in those geographic areas can request) +- Some banks have grants that are tied to specific geographic areas, and set up specific Items for those grants (that only Partners in those geographic areas can request) -Partner groups allow you to manage that. They allow you to set the item categories (see [Item Categories](inventory_items.md)) that a group can request -[TODO: Point at the precise place in Inventory Items that that is at.] +Partner Groups allow you to manage that. They allow you to set the item categories (see [Item Categories](inventory_items.md)) that a Partner Group can request -If you are going to use partner groups, you should -1/ Set up your item categories -2/ Assign items to them -3/ Set up the groups, -4/ Then assign a group to the partners on an individual basis -- so you may want to set up your groups before adding your partners. +If you are going to use Partner Groups, you should +1/ Set up your Item Categories +2/ Assign Items to them +3/ Set up the Partner Groups, +4/ Then assign a Partner Group to the Partners on an individual basis -- you may want to set up your groups before adding your partners. -# Adding a partner group +# Adding a Partner Group In the left-hand menu, click on "Partner Agencies", then "All Partners". The Partner Agencies list will appear. There are two tabs in this list "Partners" and "Groups". Click on "Groups". Then click on "New Partner Group" -![Navigation for adding a partner group](images/partners/partners_groups_1.png) +![Navigation for adding a Partner Group](images/partners/partners_groups_1.png) This will bring up a form like this (the categories will be different): -![New partner group form](images/partners/partners_groups_2.png) +![New partner Group Gorm](images/partners/partners_groups_2.png) ## Fields in the partner group form ### Name -This is the name your bank will use to refer to the partner group. It is not visible to the partners, and is not used in any reports. It does have to be unique among your partner groups. +This is the name your bank will use to refer to the Partner Group. It is not visible to the partners, and is not used in any reports. It must be unique among your Partner Groups. ### Which Item Categories Can They Request? This lists the item categories you entered (in (see [Item Categories](inventory_items.md))) -The partners who are in this group will only be able to request the items in the categories you check here. Note that they will not be able -they will not be able to request any items that are not in a category +The Partners who are in this Partner Group will only be able to request the items in the categories you check here. Note that they will not be able +they will not be able to request any Items that are not in a category -For clarity - if you do not choose any categories, they will not be able to choose any items, so if you are using partner groups, you have to use item categories. +For clarity - if you do not choose any categories, they will not be able to choose any items, so if you are using Partner Groups, you have to use Item Categories. ### Do you want to send deadline reminders to them every month? This works in conjunction with "Reminder day" and "Deadline day", which is set on an organization level (see [Getting Started - Customization](getting_started_customization.md)) # What if a partner isn't in a group? -If a partner is not in a group, they can request any item that is visible to partners. -If a partner is not in a group, they will receive reminder emails, if they have been set up on an organization level [TODO: double-check the code for that] +If a Partner is not in a Partner Group, they can request any item that is visible to partners. +If a Partner is not in a Partner Group, they will receive reminder emails, if those emails have been set up on an organization level [Prior - Adding a partner](pm_adding_a_partner.md) [Next - Inviting a partner](pm_inviting_a_partner.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_partner_profiles.md b/docs/user_guide/bank/pm_partner_profiles.md index e1a5ecd1bb..a5dfb00dfd 100644 --- a/docs/user_guide/bank/pm_partner_profiles.md +++ b/docs/user_guide/bank/pm_partner_profiles.md @@ -1,22 +1,22 @@ -DRAFT USER GUIDE - -[TODO: Screenshots throughout. Waiting for Partner Profiles rework.] +READY FOR REVIEW # Partner Profiles -Partner profiles allow you to gather and hold a lot of information about your partners that may be useful for meeting your regulatory requirements, managing their experience, and for grant-writing. +[!NOTE] We are in progress of reworking the Partner Profiles. In the interest of not duplicating effort, we have not included the screenshots in this section until that is ready for launch. + +Partner Profiles allow you to gather and hold a lot of information about your Partners that may be useful for meeting your regulatory requirements, managing their experience, and for grant-writing. -There is a *lot* of information in the profile, and the information that is needed varies from bank to bank. Once you decide what information you need from your partners, you can set up the partner profiles to show the sections that you need, customizing it through ["My organization"](getting_started_customization.md). +There is a *lot* of information in the profiles, and the information that is needed varies from bank to bank. Once you decide what information you need from your partners, you can set up the Partner Profiles to show the sections that you need, customizing it through ["My organization"](getting_started_customization.md). -## How does a partner fill in and submit their profile +## How does a Partner fill in and submit their profile -In the partner's view of the system, they can click on "Edit My Profile" to fill in all the information you want. -Once they have save this, they will also see a "Submit for Approval" button. Clicking that changes their status to "Waiting for Approval", and makes them appear in your [dashboard](essentials_dashboard.md) list of partners waiting for approval, as well as making a "Review partner's application" button appear beside them in your view of all the partners. +In the Partner's view of the system, they can click on "Edit My Profile" to fill in all the information you want. +Once they have saved this, they will also see a "Submit for Approval" button. Clicking that changes their status to "Waiting for Approval", and makes them appear in your [dashboard](essentials_dashboard.md) list of partners waiting for approval, as well as making a "Review Partner's application" button appear beside them in your view of all the Partners. -# Viewing a partner's profile -The partner's profile is viewable by clicking Partner Agencies in the left hand menu, then All Partners, then "view" beside the partner in question. Scroll down to "Partner Profile" -# Editing a partner's profile -You can edit a partner's profile clicking Partner Agencies in the left hand menu, then All Partners, then "view" beside the partner in question. Scroll down to "Partner Profile", then click "Edit Information." +# Viewing a Partner Profile +The Partner's profile is viewable by clicking Partner Agencies in the left hand menu, then All Partners, then "view" beside the Partner in question. Scroll down to "Partner Profile" +# Editing a Partner's Profile +You can edit a Partner Profile clicking Partner Agencies in the left hand menu, then All Partners, then "view" beside the partner in question. Scroll down to "Partner Profile", then click "Edit Information." # The sections The high level sections of the partner profile are: @@ -58,7 +58,7 @@ Sometimes, a large agency may have a separate address for a specific program or ### Media Information This provides a place for the agency to indicate their major communication outlets. -*If* the bank chooses to ask for this information, the partner must fill in at least one of the fields: +*If* the bank chooses to ask for this information, the Partner must fill in at least one of the fields: - Website - Facebook @@ -66,8 +66,6 @@ This provides a place for the agency to indicate their major communication outle - Instagram - No Social Media Presence - - ### Agency Stability This section is about the maturity of the agency. @@ -100,12 +98,12 @@ How does the agency get its funding? ### Area Served (County/Client Share %) This asks what county/county equivalents the agency serves and what proportion of their client share is in which area. -At this time, this only covers U.S. counties. We believe the list covers *all* of the U.S, including some areas that are not counties. +At this time, this only covers U.S. counties and equivalents. We believe the list covers *all* of the U.S, including some areas that are not tecnically counties. Let us know if you need more! The sum of the client share has to be either 0 or 100, and the numbers have to be positive whole numbers. -You start out with 1 county, but can add more with the "Add Another County button" +You start out with space for 1 county, but can add more with the "Add Another County button" ### Population Served This section has only two questions: @@ -113,7 +111,7 @@ This section has only two questions: - Do You Verify The Income Of Your Clients? - ### Race/Ethnicity of Client Base -This section is comprised of questions about the race/ethnicity of the client base and the poverty level of the client base. +This section is comprised of questions about the race/ethnicity of the client base and the poverty level of the Partner's client base. There is no check on whether the numbers add up to 100 -- because there may be overlap. ### Race/Ethnicity: @@ -132,7 +130,8 @@ Contact information for the head of the agency: - Executive Director Phone - Executive Director Email -- [TODO: Check if this is ever used for outgoing email] +This is for your information only. It is not used for any emails. + ### Primary Contact Contact information for your bank's primary contact - Primary Contact Name @@ -140,17 +139,14 @@ Contact information for your bank's primary contact - Primary Contact Cell - Primary Contact Email -[TODO: Check if 1/ Primary Contact email is defaulted on partner creation, and 2/ confirm that this is used for outgoing emails. Also what about multiple emails -- we need to either restrict to a single, or do the same thing as with the pickup person about them.] +This is for your information only. It is not used for any emails. ### Pick Up Person -The Pick Up person will receive an email when the distribution is scheduled. - -[TODO: Check -- is it only on pickups, or do they get it for deliveries too?] - +The Pick up person will receive an email (as well as the person who made the Request) when a Distribution is scheduled, if the Distribution type is "Pick up" - Pick Up Person Name - Pick Up Person's Phone # -- Pick Up Person's Email -Note this can be multiple, comma-separated emails. +- Pick Up Person's Email + - This can be multiple, comma-separated emails. ### Agency Distribution Information This section is about the agency's practices when distributing to their clients. @@ -160,14 +156,14 @@ This section is about the agency's practices when distributing to their clients. - This is meant to be the documentation required for new clients ### Additional Documents -This is a place to upload additional documents that you need from your partner. +This is a place to upload additional documents that you need from your Partner. ### Settings (not configurable) -Many banks restrict the partners to one kind of request (see [Customization](getting_started_customization.md)). If you don't, the partner can still simplify their request experience by unclicking the items that don't apply. -Only the options that you haven't already turned off will show up here, and at least one has to be checked. +Many banks restrict the Partners to one kind of request (see [Customization](getting_started_customization.md)). If you don't, the Partner can still simplify their request experience by unclicking the items that don't apply. +Only the options that you haven't already turned off will show up there, and at least one has to be checked. - Enable Quantity-based Requests - Enable Child-based Requests - Enable Requests for Individuals -[Prior: inviting a partner](pm_inviting_a_partner.md) [Next: Approving a partner](pm_approving_a_partner.md) \ No newline at end of file +[Prior: inviting a Partner](pm_inviting_a_partner.md) [Next: Approving a Partner](pm_approving_a_partner.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_partner_reactivation.md b/docs/user_guide/bank/pm_partner_reactivation.md index a1f3de7bd2..a40c13dac1 100644 --- a/docs/user_guide/bank/pm_partner_reactivation.md +++ b/docs/user_guide/bank/pm_partner_reactivation.md @@ -1,4 +1,4 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Partner reactivation @@ -10,7 +10,7 @@ This will bring up a list of all the deactivated partners. You can then click on Then click on the "Reactivate" button beside the partner. Click "Ok" on the confirmation window that pops up. -[TODO: screenshot] +![Navigation for reactivating a Partner](images/partners/partners_viewing_and_reactivating_deactivated.png) This will bring them up in the same status as they were when you deactivated them. diff --git a/docs/user_guide/bank/pm_partner_statuses.md b/docs/user_guide/bank/pm_partner_statuses.md index afddbe08d8..be107cc517 100644 --- a/docs/user_guide/bank/pm_partner_statuses.md +++ b/docs/user_guide/bank/pm_partner_statuses.md @@ -1,35 +1,46 @@ -DRAFT USER GUIDE -# Partner statuses (what can partners do when) +READY FOR REVIEW +# Partner statuses (what can Partners do when) -As an essential bank, you distribute materials (diapers, period products, etc.) to your partners who interface with the people in need of the items. +As an Essentials Bank, you distribute materials (diapers, period products, etc.) to your Partners who interface with the people in need of the items. -Depending on factors like how well-established your processes are and the capabilities of your partners, you might or might not allow/require your patners to make requests to get materials distributed to them. +Depending on factors like how well-established your processes are and the capabilities of your Partners, you might or might not allow/require your Partners to make Requests to get materials distributed to them. -There are three partner statuses, which have different impacts on how you interact with your partners. +There are three Partner statuses, which have different impacts on how you interact with your Partners. ## Uninvited -You can add partners without inviting them to the system. In this case, you can make distributions to them in the system. The partners can not sign in to the system, and so will not directly provide the information in the partner profile. This is the status of imported partners. -The partners will still get emails regarding deadlines and distributions (assuming you have set them up to do so) [TODO: expand -- what are the conditions under which partners get which emails -- ] -[TODO: Check: do we have an add and invite on the import, or just add] +You can add Partners without inviting them to the system. In this case, you can make Distributions to them in the system. The Partners can not sign in to the system, and so will not directly provide the information in the Partner Profile. This is the status of freshly imported Partners. +The Partners will still get emails regarding deadlines and Distributions (assuming you have set them up to do so through your bank's [customization](getting_started_customization.md#reminder-emails-optional) and any [Partner Groups](pm_partner_groups.md#do-you-want-to-send-deadline-reminders-to-them-every-month) they belong to. + +If you import your Partners, they will be imported as Uninvited. + ## Invited -Once you invite a partner, an email goes out to the to allow them to provide a password. [TODO: Details of email] They will then have access to the partner profile. They cannot, however, make requests. +Once you invite a Partner, we set up the Partner's email as a user, send an email to allow them to provide a password, and set their status to +"Invited". + +Sample email: + +![Details of email](images/partners/partners_invitation_email.png) + +If you use the "Invite and Approve" option, the Partner's status will be "Approved" + +Invited Partners have access to the Partner Profile. They cannot, however, make Requests. -The 'standard' sequence is for the partners to fill in some information in the partner profile, then "request approval" of the bank. The bank then reviews the information provided, and either approves, or asks for changes. +The 'standard' sequence is for the Partners to fill in some information in the Partner Profile, then "Submit for Approval" of the bank. The bank then reviews the information provided, and either approves, or asks for changes. -However, many banks get the information from their partners in other ways. Some, for instance, will fill in the information themselves while on a call with the partner. +However, many banks get the information from their Partners in other ways. Some, for instance, will fill in the information themselves while on a call with the Partner. ## Awaiting review -Once a partner has been invited, they will use the "edit my organization" function in their view of the system to provide the information. After saving the screen, they can "submit for approval" - which changes the status to "Awaiting review. This partner will now appear on your dashboard, in the "Partner Approvals" section. The partner will not be able to make requests until they are approved. +Once a Partner has been invited, they will use the "Edit my Profile" function in their view of the system to provide the information. They can save their progress, and then "save and review", then "submit for approval" - which changes the status to "Awaiting review. This Partner will now appear on your dashboard, in the "Partner Approvals" section. The Partner will not be able to make Requests until they are approved. ## Approved -Approved partners can make requests. -Note: There is no system-required information for partner approval -- you can approve them as soon as they are invited. +Approved Partners can make Requests. +Note: There is no system-required information for Partner approval -- you can approve them as soon as they are invited. ## Recertification Required -In order to keep your partner information up to date, you may (usually on an annual basis) require your partners to review and revise their profile information. The main reason to do this is to ensure you have up to date information for contact and for grant writing. Until the partner submits the updated information and you approve it, they will not be able to enter requests in the system. -Please note that the partner can update this information at any time -- you don't have to use this to allow them to update their info. +In order to keep your Partner information up to date, you may (usually on an annual basis) require your Partners to review and revise their Profile information. The main reason to do this is to ensure you have up to date information for contact and for grant writing. Until the Partner submits the updated information and you approve it, they will not be able to enter Requests in the system. +Please note that the Partner can update this information at any time -- you don't have to use this to allow them to update their information. [Prior: The Request/distribution cycle](pm_request_distribution_cycle.md) -[Next: importing partners](pm_importing_partners.md) +[Next: importing Partners](pm_importing_partners.md) diff --git a/docs/user_guide/bank/pm_partner_user_admin.md b/docs/user_guide/bank/pm_partner_user_admin.md index f0a0dea0a0..a0d975fd83 100644 --- a/docs/user_guide/bank/pm_partner_user_admin.md +++ b/docs/user_guide/bank/pm_partner_user_admin.md @@ -1,18 +1,20 @@ -DRAFT USER GUIDE +READY FOR REVIEW -# Administering partner users +# Administering Partner Users -Partners can add users themselves, but occasionally a bank may need to step in and administer partner users (for instance, if the only person who uses the system leaves the partner.) +Partners can add Users themselves, but occasionally a bank may need to step in and administer Partner Users (for instance, if the only person who uses the system leaves the Partner.) -## Where do you administer partner users? +## Where do you administer Partner Users? -If you need to administer a partner's users, click on "Partner Agencies" in the left-hand menu, then "All Partners", -then click on the specific partner you want to administer, and then "Manage Users" near the top of that screen. +If you need to administer a Partner's Users, click on "Partner Agencies" in the left-hand menu, then "All Partners", +then click on the specific Partner you want to administer. -[TODO: Navigation screenshots] +![](images/partners/partners_user_management_navigation_1.png) +Then click "Manage Users" near the top of that screen. +![](images/partners/partners_user_management_navigation_2.png) -This will bring you to a list of users for that partner. Here you can resend invitations (A), start a reset password process (B) (they will receive an email with a link to reset their password), Remove access to that partner from the user(C), or invite a new user to that partner (D). +This will bring you to a list of Users for that Partner. Here you can invite new Users, start a reset password process (B) (they will receive an email with a link to reset their password), or remove access to that Partner from the User(C). -[TODO: Annotated screenshot for partner user admin screen] +![](images/partners/partners_user_management.png) -[Prior - Partner reactivation](pm_partner_reactivation.md) [Next: Other partner information](pm_other_information.md) \ No newline at end of file +[Prior - Partner reactivation](pm_Partner_reactivation.md) [Next: Other Partner information](pm_other_information.md) \ No newline at end of file diff --git a/docs/user_guide/bank/pm_request_distribution_cycle.md b/docs/user_guide/bank/pm_request_distribution_cycle.md index 7a4fb6d12b..2f50f79ad5 100644 --- a/docs/user_guide/bank/pm_request_distribution_cycle.md +++ b/docs/user_guide/bank/pm_request_distribution_cycle.md @@ -1,34 +1,34 @@ -DRAFT USER GUIDE +READY FOR REVIEW # The Request/Distribution Cycle -You can distribute materials to your partners as soon as you have them entered into the system. However, many banks find it useful to allow their partners to request products through the system. +You can distribute materials to your Partners as soon as you have them entered into the system. However, many banks find it useful to allow their Partners to request products through the system. -To allow partners to make requests, you will have to [invite](pm_inviting_a_partner.md) them, and [approve](pm_approving_a_partner.md) them, based on a [profile](pm_partner_profiles.md) they fill out and submit. There is a shortcut to do both at once. +To allow Partners to make Requests, you will have to [invite](pm_inviting_a_partner.md) them, and [approve](pm_approving_a_partner.md) them, based on a [profile](pm_partner_profiles.md) they fill out and submit. There is a shortcut to do both at once. -For clarity - you can make distributions without requests. +For clarity - you can make Distributions without Requests. -So, how does this work? +So, how does working with Requests from Partners work? -1/ The partner fills in a request form on the system and saves it. +1/ The Partner fills in a Request form on the system and saves it. -2/ They get an email confirming that the request has been sent to you. +2/ They get an email confirming that the Request has been sent to you. -3/ This requests appears in the system in the following places: a) your [dashbboard](essentials_dashboard.md), under "Outstanding Requests", and in the [Requests](essentials_requests.md) view. +3/ This Request appears in the system in the following places: a) your [dashbboard](essentials_dashboard.md), under "Outstanding Requests", and in the [Requests](essentials_requests.md) view. -4/ You view the request, and click "Fulfill request" (you so have the option of cancelling it) +4/ You view the Request, and click "Fulfill Request" (you also have the option of cancelling it) -5/ That marks the request as "started" -- so other staff don't grab the same request to start working on it, and brings up a new [Distribution] for the partner, with the information we can pre-fill, pre-filled. +5/ That marks the Request as "started" -- so other staff don't grab the same Request to start working on it, and brings up a new [Distribution] for the partner, with the information we can pre-fill, pre-filled. 6/ You fill in the rest of the information, and make any adjustments required. -7/ Clicking "Save" creates the distribution in the system, adjusting the inventory, and sends an email to the partner with the details attached. They can also see the distribution in their "Upcoming Distributions" list. +7/ Clicking "Save" creates the Distribution in the system, adjusting the inventory, and sends an email to the partner with the details attached. They can also see the distribution in their "Upcoming Distributions" list. 8/ You set aside the products for pick-up / deliver them / ship them -9/ You can mark the distribution as complete. +9/ You can mark the Distribution as complete. This is not required, but provides a way to focus on just the upcoming ones. + +Please note that you can edit Distributions (e.g. if you have more product come in before pickup). The Partner does not get a new email if you do that. -Please note that you can edit distributions (e.g. if you have more product come in before pickup). The partner does not get a new email if you do that. -[TODO: Confirm] [Prior: Pick ups and deliveries](essentials_pick_ups.md) [Next: Partner statuses](pm_partner_statuses.md) diff --git a/docs/user_guide/bank/pm_requesting_recertification.md b/docs/user_guide/bank/pm_requesting_recertification.md index 13c6f38c9e..5f5eee95f7 100644 --- a/docs/user_guide/bank/pm_requesting_recertification.md +++ b/docs/user_guide/bank/pm_requesting_recertification.md @@ -1,23 +1,40 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Requesting Recertification -From time to time, perhaps annually, you may want to confirm that the information your partner has provided is correct. That is what "request recertification" is for. +From time to time, perhaps annually, you may want to confirm that the information your Partner has provided is still correct. That is what "request recertification" is for. +[!WARN] Once you request recertification, the Partner will not be able to enter Requests until you have approved them again!! -#### *** N.B. Once you request recertification, the partner will not be able to enter requests until you have approved them again!! *** +If you request recertification from a Partner, they will receive an email requesting that they update their information, and their status will change to "Recertification required". -If you request recertification from a partner, they will receive an email requesting that they update their information -[TODO: provide text of email], and their status will change to "Recertification required". +Here is the text of that email -To request recertification, go to the All Partners screen (click on "Partner Agencies", then "All Partners" in the left hand menu), then click the red "Request Recertification" button beside the partner. A confirmation window will pop up - check that you picked the right partner, and click "OK". +--------- +Hi [Partner name] -[TODO: provide screenshot -- that screen is likely to change before the end of writing.] +It's time to update your agency information! -This will change the status of the partner to "Recertification required". +Please log in to your account at http://humanessentials.app/users/sign_in -Once they have made their changes, you can [approve](pm_approving_a_partner.md) them again, which will restore their ability to make requests. +If no information has changed, please click Update. +If any information has changed, please amend it on the form and then click Update. -(Note: They have a "Submit for Approval" button, which would change their status to "Waiting for Approval", and make them appear in your dashboard, but you can bypass that step) +If you have any questions please contact [Bank name] at [Bank email] + +Thank you and have a great day! + +---------- + + +To request recertification, go to the All Partners screen (click on "Partner Agencies", then "All Partners" in the left hand menu), then click the red "Request Recertification" button beside the Partner. A confirmation window will pop up - check that you picked the right Partner, and click "OK". + +![](images/partners/partners_recertification.png) + +This will change the status of the Partner to "Recertification required". + +Once they have made their changes, you can [approve](pm_approving_a_partner.md) them again, which will restore their ability to make Pequests. + +[!Note] They have a "Submit for Approval" button, which would change their status to "Waiting for Approval", and make them appear in your dashboard, but you can bypass that step) [Prior -- Approving a partner](pm_approving_a_partner.md) [Next -- making a partner inactive ](pm_making_a_partner_inactive.md) diff --git a/docs/user_guide/bank/readme.md b/docs/user_guide/bank/readme.md index 1b46320234..58b164565a 100644 --- a/docs/user_guide/bank/readme.md +++ b/docs/user_guide/bank/readme.md @@ -1,6 +1,6 @@ -DRAFT USER GUIDE +READY FOR REVIEW -This user guide is meant for users of the human essentials app at essentials banks. As of October 25, 2024, it is a work in progress. If you are interested in helping out, please reach out to @cielf. +This user guide is meant for users of Human Essentials at essentials banks. As of November 2, 2024, it is a work in progress. If you are interested in helping out, please reach out to @cielf. 1. Introduction 1. [Is Human Essentials right for you?](intro_i.md) (what we help with and what we don't) @@ -64,6 +64,9 @@ This user guide is meant for users of the human essentials app at essentials ban 2. [Distributions by county](reports_distribution_by_county.md) 3. [Manufacturer donations](reports_manufacturers_donations.md) 4. [Activity graph](reports_activity_graph.md) + 5. [History](reports_history.md) 8. [User management](user_management.md) 9. [Account Management](account_management.md) 10. [But I need to do something different!](asking_for_changes.md) +11. Special Topics + 1. [Custom Units](special_custom_units.md) diff --git a/docs/user_guide/bank/reports_activity_graph.md b/docs/user_guide/bank/reports_activity_graph.md index b54dd70e4b..ec6677f79f 100644 --- a/docs/user_guide/bank/reports_activity_graph.md +++ b/docs/user_guide/bank/reports_activity_graph.md @@ -1,13 +1,13 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Activity Graph Report -This report is merely a visual representation of the number of items processed over a time period. +This report is merely a visual representation of the number of Items processed over a time period. ![activity_graph_default_view](images/reports/reports_activity_graph_1.png) -If you want to see a time period other than the current year, change the date range and click "Filter". Note, we recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. +If you want to see a time period other than the default period of 60 days prior to 30 days forward from today, change the date range and click "Filter". Note, we recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. ![activity_graph_with_date_range_gizmo](images/reports/reports_activity_graph_2.png) [Back to Manufacturers Donations](reports_manufacturers_donations.md) -[Next: User Management](user_management.md) +[Next: History](reports_history.md) diff --git a/docs/user_guide/bank/reports_annual_survey.md b/docs/user_guide/bank/reports_annual_survey.md index f9702437f3..5c066d8d8c 100644 --- a/docs/user_guide/bank/reports_annual_survey.md +++ b/docs/user_guide/bank/reports_annual_survey.md @@ -1,4 +1,4 @@ -DRAFT USER GUIDE +READY FOR REVIEW ## Annual Survey The annual survey contains information useful for completing the NDBN or Alliance for Period Supplies annual survey, but also for grant writing. Each year's annual survey becomes available January 1 of the following year. @@ -24,15 +24,15 @@ You can also extract the report to a csv file by clicking "Export Report" (B) ### Calculation notes -1/ We are in the process of changing the calculations so that items in kits appear in the totals for each value. -At time of writing, the # of disposable and cloth diapers distributed include any diapers in kits, but period supplies, adult incontinence, and other do not. -At time of writing, purchased/donated supplies do not include any kit purchases/donations. ( We only know of one bank that currently has kits donated.) +1/ We are in the process of changing the calculations so that Items in Kits appear in the totals for each value. +At time of writing, the # of disposable and cloth diapers distributed include any diapers in Kits, and period supplies also include period supplies in Kits, but adult incontinence and other do not. +At time of writing, purchased/donated supplies do not include any Kit Purchases/Donations. ( We only know of one bank that currently has kits donated.) -2/ % donated / purchased is based on the number of items acquired, not the number of items distributed. +2/ % donated / purchased is based on the number of Items acquired, not the number of Items distributed. 3/ How we calculate per person values: - If you have entered a non-zero value for an [item](inventory_items.md) in the quantity per individual field, we use that value. Otherwise, we assume 50 of the item per individual. -[TODO: When inventory_items.md is written, point the above link to the right place in it.] + If you have entered a non-zero value for an [Item](inventory_items.md#editing-an-item) in the quantity per individual field, we use that value. Otherwise, we assume 50 of the Item per individual. + [Prior: Trends](reports_trends.md) [Next: Distributions by County](reports_distributions_by_county.md) \ No newline at end of file diff --git a/docs/user_guide/bank/reports_distributions_by_county.md b/docs/user_guide/bank/reports_distributions_by_county.md index 3768c9151e..6851229eb9 100644 --- a/docs/user_guide/bank/reports_distributions_by_county.md +++ b/docs/user_guide/bank/reports_distributions_by_county.md @@ -1,17 +1,17 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Distributions by County Report ![distributions_by_county_default_view](images/reports/reports_distributions_by_county_1.png) -This report shows the total items and estimated total market value of the distributed items for the time period. +This report shows the total items and estimated total market value of the distributed Items for the time period. We use the area served information entered in each partner's [profile](pm_partner_profiles.md) to allocate the items that have been distributed to them. If that information has not been provided, the items you've distributed are put into the 'Unspecified' category. -**N.B.** If you are using kits, please note that this treats each kit as an item, rather than counting the items within the kits separately. +**N.B.** If you are using Kits, please note that this treats each Kit as an item, rather than counting the items within the Kits separately. -If you want to see a time period other than the current year, change the date range and click "Filter". We recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. +If you want to see a time period other than the default of 60 days prior to 30 days forward from today, change the date range and click "Filter". We recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. ![distributions_by_county_with_date_range_gizmo](images/reports/reports_distributions_by_county_2.png) diff --git a/docs/user_guide/bank/reports_history.md b/docs/user_guide/bank/reports_history.md new file mode 100644 index 0000000000..678e777dc9 --- /dev/null +++ b/docs/user_guide/bank/reports_history.md @@ -0,0 +1,9 @@ +READY FOR REVIEW + +# History Report +This is mainly used for troubleshooting by the Human Essentials team. You are unlikely to need it in your day-to-day work. + +It contains a record of every change that was made affecting inventory since we put it in in late 2024. + +[Prior: Activity graph](reports_activity_graph.md) +[Next: User Management](user_management.md) \ No newline at end of file diff --git a/docs/user_guide/bank/reports_itemized_reports.md b/docs/user_guide/bank/reports_itemized_reports.md index a8cdc7c483..fc8888c280 100644 --- a/docs/user_guide/bank/reports_itemized_reports.md +++ b/docs/user_guide/bank/reports_itemized_reports.md @@ -1,17 +1,17 @@ -DRAFT USER GUIDE! +READY FOR REVIEW # Itemized Reports -The itemized reports break down the inventory by item over a given time period. There are itemized reports for donations and for distributions. -The following will use the itemized report for distributions, but the itemized report for donations basically works the same way. +The itemized reports break down the inventory by Item over a given time period. There are itemized reports for Donations and for Distributions. +The following will use the itemized report for Distributions, but the itemized report for Donations basically works the same way. To show this report, click on "Reports" in the left-hand menu, then "Distributions - Itemized" ![Initial Itemized Reports view](images/reports/reports_itemized_1.png) -The initial view that comes up will show the items distributed during the current calendar year. -[TODO: Check if the filtration here is on issued_at or on the date of the line items] +The initial view that comes up will show the Items distributed during the default period of 60 days prior to, 30 days forward from today. +This is based on the "issued at" date, so even if you edit your distribution after the fact, any changes will be kept with that date. -If the amount on hand is less than the minimum quantity you provided for that item (see [Items](inventory_items.md)), then the on hand amount will be highlighted in red. +If the amount on hand is less than the minimum quantity you provided for that Item (see [Items](inventory_items.md)), then the on hand amount will be highlighted in red. You can change the timeframe that this report covers in the usual way -- use the gizmo that will appear if you click on the date range at the top of the report to set the dates, then click "Filter" diff --git a/docs/user_guide/bank/reports_manufacturers_donations.md b/docs/user_guide/bank/reports_manufacturers_donations.md index 7cb31abc49..dcbeb7ad83 100644 --- a/docs/user_guide/bank/reports_manufacturers_donations.md +++ b/docs/user_guide/bank/reports_manufacturers_donations.md @@ -1,5 +1,4 @@ -DRAFT USER GUIDE - +READY FOR REVIEW # Manufacturers Donations Report To access this report, click 'Reports", then "Donations - Manufacturers" @@ -7,18 +6,17 @@ To access this report, click 'Reports", then "Donations - Manufacturers" ![manufacturer_donations_report_default](images/reports/reports_manufacturer_donations_1.png) -This shows the number of items donated by manufacturers over the indicated time period (default current year), as well as the top manufacturers donating in that time period. -[TODO: Confirm -- is this true -- or is this still broken?] +This shows the number of items donated by Manufacturers over the indicated time period (default 60 days prior to 30 days forward from today), as well as the top Manufacturers donating in that time period. -From here, you can click on the listed manufacturers to see a breakdown of their donations. +From here, you can click on the listed Manufacturers to see a breakdown of their Donations. -Or enter a new donation (by clicking on "New Donation", naturally.) +Or enter a new Donation (by clicking on "New Donation", naturally.) -Or see all donations for the time period, by clicking on 'See more...' +Or see all Donations for the time period, by clicking on 'See more...' -If you want to see a time period other than the current year, change the date range and click "Filter". We recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. +If you want to see a time period other than the default, change the date range and click "Filter". We recommend you use the little pop-up gizmo to enter your date range, as the format of the date range is very fussy. -![manufacturer_donations_date_range_gizmo](images/reports/reports_manufacturer_donations_2.png) +![Manufacturer_Donations_date_range_gizmo](images/reports/reports_manufacturer_donations_2.png) [Back to Distributions by County](reports_distributions_by_county.md) diff --git a/docs/user_guide/bank/reports_summary_reports.md b/docs/user_guide/bank/reports_summary_reports.md index 8e8cc2c451..21840ffdbf 100644 --- a/docs/user_guide/bank/reports_summary_reports.md +++ b/docs/user_guide/bank/reports_summary_reports.md @@ -1,27 +1,30 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Summary Reports There are 3 Summary reports - one each for Distributions, Donations, and Purchases -These each allow you to quickly get the number of items dealt with in a timeframe, plus links to recent activity +These each allow you to quickly get the number of Items dealt with in a timeframe, plus links to recent activity ## Distribution Summary -This gives the number of items distributed -- initially year-to-date, but you can filter by whatever date range you wish. This filtering works against the "issued_at" date on the distribution, rather than the date it was entered. +This gives the number of Items distributed -- initially for the last 60 days (and 30 days forward), but you can filter by whatever date range you wish. This filtering works against the "issued_at" date on the Distribution, rather than the date it was entered. -The report also gives the number of items that are in distributions that have an "issued at" in the future. +The report also gives the number of Items that are in Distributions that have an "issued at" in the future. -The "recent distributions" section shows recently entered distributions that match the date filter. That is, whether or not a distribution shows here is based on the filter, but the order depends on when it was entered -- latest first. -[TODO: Marked up Screenshots] +The "Recent Distributions" section shows recently entered Distributions that match the date filter. That is, whether or not a Distribution shows here is based on the filter, but the order depends on when it was entered -- latest first. + +![Navigation to Distribution Summary report](images/reports/reports_summary_distributions.png) ## Donation Summary This is quite similar to the Distribution summary, above. -This gives the number of items donated -- initially year-to-date, but you can filter by whatever date range you wish. This filtering works against the "issued on" date on the distribution, rather than the date it was entered. + +This gives the number of Items donated -- initially for the last 60 days, but you can filter by whatever date range you wish. This filtering works against the "issued on" date on the Donation, rather than the date it was entered. It also gives the total of monetary donations recorded in the system for that period -The "recent donations" section shows recently entered donations that match the date filter. That is, whether or not a donation shows here is based on the filter, but the order depends on when it was entered -- latest first. -[TODO: Marked up Screenshots] +The "Recent Donations" section shows recently entered Donations that match the date filter. That is, whether or not a donation shows here is based on the filter, but the order depends on when it was entered -- latest first. + +![Navigation to Donation Summary report](images/reports/reports_summary_donations.png) ## Purchase Summary -This merely gives the amount spent during the period, along with recently entered purchases that match the date filter. The filter works on the issued on date, but the ordering is by the creation date of the purchase. -[TODO: Marked up Screenshots] +This presents the amounts spent during the period, along with recently entered Purchases that match the date filter. The filter works on the issued on date, but the ordering is by the creation date of the Purchase, newest first. +![Navigation to Purchase Summary report](images/reports/reports_summary_purchases.png) [Prior: Exports](exports.md)[Next: Itemized reports](reports_itemized_reports.md) \ No newline at end of file diff --git a/docs/user_guide/bank/reports_trends.md b/docs/user_guide/bank/reports_trends.md index 76dddd8735..e327afcd37 100644 --- a/docs/user_guide/bank/reports_trends.md +++ b/docs/user_guide/bank/reports_trends.md @@ -1,6 +1,6 @@ -DRAFT USER GUIDE +READY FOR REVIEW # Trends Reports -The trends reports (one each for Distributions, Donations, and Purchases) give you a picture of the activity on any item over the last 12 months. +The trends reports (one each for Distributions, Donations, and Purchases) give you a picture of the activity on any Item over the last 12 months. The examples below will use the Distributions - Trends report, but everything is basically the same for Donations- Trends and Purchases - Trends. @@ -13,19 +13,19 @@ This will bring up a blank "Monthly" Distributions report, like so: ![Blank trends report](images/reports/reports_trends_1.png) -To see the activity on a particular item, click on that item. You can click on as many as you want -- if, for instance, you want compare the distributions of multiple sizes, you can do that -- but in general, fewer will be better. +To see the activity on a particular Item, click on that Item. You can click on as many as you want -- if, for instance, you want compare the Distributions of multiple sizes, you can do that -- but in general, fewer will be better. (Our test data only has very recent data in it, so you'll see more bars, unless of course, you are a brand new bank!) ![trends report with 2 items](images/reports/reports_trends_2.png) -You can also hover over any month to see the numerical levels for the chosen items for that month. +You can also hover over any month to see the numerical levels for the chosen Items for that month. ![trends report with breakout](images/reports/reports_trends_3.png) -To clear the items - either click on them again, or click "Deselect All" +To clear the Items - either click on them again, or click "Deselect All" -You can click "Select All" to see *all* the items, but that will produce a very busy visual. +You can click "Select All" to see *all* the Items. That will produce a very busy visual, indeed. diff --git a/docs/user_guide/bank/special_custom_units.md b/docs/user_guide/bank/special_custom_units.md new file mode 100644 index 0000000000..fd76900b95 --- /dev/null +++ b/docs/user_guide/bank/special_custom_units.md @@ -0,0 +1,16 @@ +READY FOR REVIEW +# Coming Soon -- Custom Units (opt-in feature) + +The number of items throughout the bank's view of the system is the number of units (e.g. diapers), but +Partners often think in terms of packs of diapers. Because Banks were getting a lot of partners requesting the number of packs of diapers, instead of the number of diapers, we are introducing the ability for banks to allow the partners to request other units (e.g. packs) + +In short, you'll be able to add custom units that they partners can request. The partners then can ask for, say, 'packs' of diapers. You will still have to translate those to the number of items when distributing, because there is a lot of variety in pack size across brands. + + +When this is ready for release, we will update this page, but to preview: +[!NOTE] None of the following steps are available yet. +- A bank that wishes to use custom units will add those units on their organization page +- Then, for any Items that will use the custom units (you are not going to have the same units for car seats and diapers), you add them to that Item +- At that point, the Partners will see a slightly different screen that will have them pick between "units" and the custom units (say, "packs") +- When the bank sees the request, they will see the units that were chosen, but have to enter the total quantity for that item (in our example, the number of diapers) +- The units the partner chose will show wherever we are showing the requested amounts (e.g. when viewing the request, when calculating Product Totals, when fulfilling the request, and in the pdf that is attached to their Distribution email. diff --git a/docs/user_guide/bank/user_management.md b/docs/user_guide/bank/user_management.md index 4a2215762e..2e3844e284 100644 --- a/docs/user_guide/bank/user_management.md +++ b/docs/user_guide/bank/user_management.md @@ -1,56 +1,84 @@ -DRAFT USER GUIDE +READY FOR REVIEW # User Management -The bank organization admin users can manage both the staff who have access to humanessentials at the bank, and at each of the bank's partners. -The partners can manage their own users as well, but it is sometimes necessary for the bank to be able to add a user at the partner agency due to staff turnover. +The bank organization admin Users can manage both the staff who have access to humanessentials.app at the bank, and at each of the bank's Partners. +The Partners can manage their own Users as well, but it is sometimes necessary for the bank to be able to add a User at the Partner due to staff turnover. ### How do I know what access I have? If you look at the left-hand menu, and you see, down at the bottom, a section "My Organization", then you have admin access. -Otherwise, if you see "Donations", you have bank user access. -If, instead, you see "My Profile", you are signed in as a partner. -[TODO: Annotated screen shots] +Otherwise, if you see "Donations", you have bank User access. +![annotated screenshot showing where "Donations" and "My Organization" are in the menu](images/user_management/user_access_admin_and_user.png) +If, instead, you see "My Profile", you are currently signed in as a Partner. +![annotated screenshot showing where "My Profile" shows for a partner User"](images/user_management/user_access_partner.png) ## Managing Bank staff access -To manage bank staff, you must be signed in as a user with admin rights. +To manage bank staff, you must be signed in as a User with admin rights. -### Adding a user -To add a user, sign in as someone who has bank admin access, then click on "My Organization" in the left-hand menu. +### Adding a User +To add a User, sign in as someone who has bank admin access, then click on "My Organization" in the left-hand menu. Scroll down to the bottom of that page. You will see a section labelled "Users". -Check that the user is not already listed, then click on the "Invite User to this Organization" button in the bottom-right corner of the page. +Check that the User is not already listed, then click on the "Invite User to this Organization" button in the bottom-right corner of the page. -[TODO: More Screenshots] +![navigation to invite new User](images/user_management/user_invite_new_bank_user.png) -In the window that pops up, add the user's name and email, then click "Invite User". +In the window that pops up, add the User's name and email, then click "Invite User". An email like the following will be sent to that email: -![Email that goes to the invited user, with button to start password process](images/user_management/user_invite_email.png) +![Email that goes to the invited User, with button to start password process](images/user_management/user_invite_email.png) -When the new user clicks "Accept Invitation", they will be prompted to set their password. Once they do, they will have bank user level access to the system. -### Deleting a user +When the new User clicks "Accept Invitation", they will be prompted to set their password. Once they do, they will have bank User level access to the system. +### Deleting a User When someone leaves your organization, you will want to remove their access to humanessentials. To do that, -sign in as someone who has bank admin access, then click on "My Organization" in the left-hand menu. -Scroll down to the bottom of that page. You will see a section labelled "Users". -Find the user you wish to delete. -*If* they had admin access, you will first need to demote them to user, by clicking their "Demote to User" button. -Then, click the "Actions" button beside their name, and then click "Remove User". +sign in as someone who has bank admin access, then -### Giving a user admin access -To promote a user to have admin access, +(1) click on "My Organization" in the left-hand menu. +(2) Scroll down to the bottom of that page. You will see a section labelled "Users". +Find the User you wish to delete. + +*If* they had admin access, you will first need to demote them to User, by clicking their "Demote to User" button. + +Then, (3) click the "Actions" button beside their name, and +(4) click "Remove User". + + +![Steps to delete a User (who is not an admin)](images/user_management/user_delete_bank_user.png) + +Click "OK" on the confirmation screen. + + +### Giving a User admin access +To promote a User to have admin access, +(1) click on "My Organization" in the left-hand menu. +(2) Scroll down to the bottom of that page. You will see a section labelled "Users". +Find the User you wish to promote. + +Then, (3) click the "Actions" button beside their name, and +(4) Click "Promote to Admin" + + +![Steps to promote a User to admin](images/user_management/user_promote_bank_user.png) + + +Click "OK" on the confirmation screen. + + +### Removing admin access from a User. + +To remove admin access from a User, sign in as someone who has bank admin access, then (1) click on "My Organization" in the left-hand menu. +(2) Scroll down to the bottom of that page. You will see a section labelled "Users". +Find the User you wish to remove admin access from. (3) Click the "Demote to User" button beside their name and email. + +Click "OK" on the confirmation screen. -[TODO: Write this once it is fixed!] -### Removing admin access from a user. -To remove admin access from a user, sign in as someone who has bank admin access, then click on "My Organization" in the left-hand menu. -Scroll down to the bottom of that page. You will see a section labelled "Users". -Find the user you wish to remove admin access from. Click the "demote to user" button beside their name and email. ### *Help! Our admin quit and we don't have the access we need!* Send a note to info@humanessentials.app explaining the situation. We review that email on Sunday mornings. You might be able to get faster turnaround by reaching out on the human essentials slack. ## Managing Partner access -See [Administering partner users](pm_partner_user_admin.md) +See [Administering Partner Users](pm_partner_user_admin.md) -[Prior: Activity graph](reports_activity_graph.md) [Next: Account Management](account_management.md) \ No newline at end of file +[Prior: History](reports_history.md) [Next: Account Management](account_management.md) \ No newline at end of file diff --git a/docs/user_guide/style_guide.md b/docs/user_guide/documentation_style_guide.md similarity index 68% rename from docs/user_guide/style_guide.md rename to docs/user_guide/documentation_style_guide.md index a4ab6c9357..ec3af4a1a2 100644 --- a/docs/user_guide/style_guide.md +++ b/docs/user_guide/documentation_style_guide.md @@ -8,11 +8,11 @@ We want to do a final pass to make everything consistent with the style guide be ## Capitalization ### Headings -The first letter of a heading is capitalized. After that,use lower case except for nouns that are always capitalized (see below) -### Objects in the app +In documentaion, use title case for the main header for each page, then use sentence case (see this page as an example). +### When quoting the system Match the text to what is in the application. For instance, if a button says "Manage Product drives". That being said, if there is something in the app that is inconsistent, let's get that fixed. -### Always capitalize -Essentially, if it has a view in Human Essentials, capitalize it +### Otherwise, always capitalize +Essentially, if you are referring to an object that has a view in Human Essentials, capitalize it - Bank - Partner - Account @@ -25,6 +25,12 @@ Essentially, if it has a view in Human Essentials, capitalize it - Request - Inventory - Audit + +Also : +- Human Essentials +- Essentials Bank + ## Hyphenation left-hand menu in-kind value +email, not e-mail diff --git a/lib/seeds.rb b/lib/seeds.rb new file mode 100644 index 0000000000..f5cd4537ce --- /dev/null +++ b/lib/seeds.rb @@ -0,0 +1,21 @@ +module Seeds + def self.seed_base_items + # Initial starting qty for our test organizations + base_items = File.read(Rails.root.join("db", "base_items.json")) + items_by_category = JSON.parse(base_items) + + items_by_category.each do |category, entries| + entries.each do |entry| + BaseItem.find_or_create_by!( + name: entry["name"], + category: category, + partner_key: entry["key"], + updated_at: Time.zone.now, + created_at: Time.zone.now + ) + end + end + # Create global 'Kit' base item + KitCreateService.find_or_create_kit_base_item! + end +end diff --git a/lib/tasks/backup_db_rds.rake b/lib/tasks/backup_db_rds.rake index b226f6398e..272bd8ef9d 100644 --- a/lib/tasks/backup_db_rds.rake +++ b/lib/tasks/backup_db_rds.rake @@ -1,10 +1,11 @@ desc "Update the development db to what is being used in prod" task :backup_db_rds => :environment do - system("echo Performing dump of the database.") + logger = Logger.new(STDOUT) + logger.info("Performing dump of the database.") current_time = Time.current.strftime("%Y%m%d%H%M%S") - system("echo Copying of the database...") + logger.info("Copying the database...") backup_filename = "#{current_time}.rds.dump" system("PGPASSWORD='#{ENV["DIAPER_DB_PASSWORD"]}' pg_dump -Fc -v --host=#{ENV["DIAPER_DB_HOST"]} --username=#{ENV["DIAPER_DB_USERNAME"]} --dbname=#{ENV["DIAPER_DB_DATABASE"]} -f #{backup_filename}") @@ -16,6 +17,6 @@ task :backup_db_rds => :environment do storage_access_key: account_key ) - system("echo Uploading #{backup_filename}") + logger.info("Uploading #{backup_filename}") blob_client.create_block_blob("backups", backup_filename, File.read(backup_filename)) end diff --git a/lib/tasks/create_inventory_in_out_daily_view.rake b/lib/tasks/create_inventory_in_out_daily_view.rake deleted file mode 100644 index acb078f7fc..0000000000 --- a/lib/tasks/create_inventory_in_out_daily_view.rake +++ /dev/null @@ -1,55 +0,0 @@ -# Getting the daily version as we monitor the difference between inventory in/out and inventory problem. -# Use -- bring down a local copy of production, then rake create_inventory_in_out_daily_view -# -- sum the differences, then compare to previous pull. If the sum of differences has changed -# -- then there has been "inventory drift" in the non-kit items since the last pull -# -- This was meant to be a daily pull, but has been paused while some known issues are addressed. -require 'csv' -task :create_inventory_in_out_daily_view => :environment do - # Create a big ol' report that is the inventory levels of all the items on all the storage locations. - # Let's organize it by organization - - headers = ["Organization ID","Organization Name", "Storage Location ID", "Storage Location Name", "Item ID", "Item Name", "Inventory In", "Inventory Out", "Expected", "Current Inventory", "Diff", "Inventory item created", "Inventory Item last updated"] - - file = "#{Rails.root}/public/inventory_check_#{Time.now}.csv" - CSV.open(file, 'w', write_headers: true, headers:headers) do |writer| - organizations = Organization.alphabetized - organizations.each do |org| - puts "#{org.id}, #{org.name}" - storage_locations = org.storage_locations.active_locations.order(:name) - storage_locations.each do |loc| - puts "#{org.id}, #{org.name}, #{loc.id}, #{loc.name}" - line_items_in = ItemsInQuery.new(organization: org, storage_location: loc).call - line_items_out = ItemsOutQuery.new(organization: org, storage_location: loc).call - inventory = loc.inventory_items - line_items_in.each do |line_in| - in_item_id = line_in.item_id - in_quantity = line_in.quantity - line_out = line_items_out.where(item_id: in_item_id).first - if(line_out.nil?) - out_quantity = 0 - else - out_quantity = line_out.quantity - end - expected = in_quantity - out_quantity - inventory_item = inventory.where(item_id: in_item_id).first - if inventory_item.nil? - inventory_quantity = 0 - difference = expected - inventory_quantity - writer << [org.id, org.name, loc.id, loc.name, in_item_id, line_in.item.name, in_quantity, out_quantity, expected, inventory_quantity, difference, "N/A","N/A"] - else - inventory_quantity = inventory_item.quantity - difference = expected - inventory_quantity - writer << [org.id, org.name, loc.id, loc.name, in_item_id, line_in.item.name, in_quantity, out_quantity, expected, inventory_quantity, difference, inventory_item.created_at, inventory_item.updated_at] - end - - - # puts "#{org.id}, #{org.name}, #{loc.id}, #{loc.name}, #{in_item_id}, #{line_in.item.name}, #{in_quantity},#{out_quantity}, #{expected}, #{inventory_quantity},#{difference}" - - end - - end - end - - end - -end \ No newline at end of file diff --git a/lib/tasks/create_no_kit_daily_view.rake b/lib/tasks/create_no_kit_daily_view.rake deleted file mode 100644 index 1727ae694c..0000000000 --- a/lib/tasks/create_no_kit_daily_view.rake +++ /dev/null @@ -1,80 +0,0 @@ -#Getting the daily version as we monitor the difference between inventory in/out and inventory problem. Version without kits -# Use -- bring down a local copy of production, then rake create_no_kit_daily_view -# -- sum the differences, then compare to previous pull. If the sum of differences has changed -# -- then there has been "inventory drift" in the non-kit items since the last pull -# -- This was meant to be a daily pull, but has been paused while some known issues are addressed. -require 'csv' -task :create_no_kit_item_daily_view => :environment do - # Create a big ol' report that is the inventory levels of all the items on all the storage locations. - # Let's organize it by organization - - headers = ["Organization ID","Organization Name", "Storage Location ID", "Storage Location Name", "Item ID", "Item Name", "Inventory In", "Inventory Out", "Expected", "Current Inventory", "Diff", "Inventory item created", "Inventory Item last updated"] - - file = "#{Rails.root}/public/nki_inventory_check_#{Time.now}.csv" -CSV.open(file, 'w', write_headers: true, headers:headers) do |writer| - organizations = Organization.alphabetized - organizations.each do |org| - puts "#{org.id}, #{org.name}" - storage_locations = org.storage_locations.active_locations.order(:name) - storage_locations.each do |loc| - puts "#{org.id}, #{org.name}, #{loc.id}, #{loc.name}" - line_items_in = ItemsInQuery.new(organization: org, storage_location: loc).call - line_items_out = ItemsOutQuery.new(organization: org, storage_location: loc).call - inventory = loc.inventory_items - line_items_in.each do |line_in| - next if item_is_in_a_kit?(org, line_in) - next if item_is_a_kit?(org, line_in) - in_item_id = line_in.item_id - in_quantity = line_in.quantity - line_out = line_items_out.where(item_id: in_item_id).first - if(line_out.nil?) - out_quantity = 0 - else - out_quantity = line_out.quantity - end - expected = in_quantity - out_quantity - inventory_item = inventory.where(item_id: in_item_id).first - if inventory_item.nil? - inventory_quantity = 0 - difference = expected - inventory_quantity - writer << [org.id, org.name, loc.id, loc.name, in_item_id, line_in.item.name, in_quantity, out_quantity, expected, inventory_quantity, difference, "N/A","N/A"] - else - inventory_quantity = inventory_item.quantity - difference = expected - inventory_quantity - puts "writing out #{org.name} #{in_item_id} #{line_in.item.name}" - writer << [org.id, org.name, loc.id, loc.name, in_item_id, line_in.item.name, in_quantity, out_quantity, expected, inventory_quantity, difference, inventory_item.created_at, inventory_item.updated_at] - end - - - # puts "#{org.id}, #{org.name}, #{loc.id}, #{loc.name}, #{in_item_id}, #{line_in.item.name}, #{in_quantity},#{out_quantity}, #{expected}, #{inventory_quantity},#{difference}" - - end - - end - end - -end - -end - -def item_is_in_a_kit?(org, li) - kits = Kit.where(organization: org) - kits.each do |kit| - items = kit.items - items.each do |item| - if item.id == li.item_id - puts "item #{item.id} #{item.name} is in a kit" - return true - end - end - end - false -end - -def item_is_a_kit?(org, li) - if(li.item.kit_id) - puts "item #{li.item.id} #{li.item.name} is a kit" - return true - end - false -end \ No newline at end of file diff --git a/lib/tasks/initiate_reminder_deadline_job.rake b/lib/tasks/initiate_reminder_deadline_job.rake deleted file mode 100644 index 45d3a10779..0000000000 --- a/lib/tasks/initiate_reminder_deadline_job.rake +++ /dev/null @@ -1,7 +0,0 @@ -desc "This task is called by the Heroku scheduler add-on to initiate the ReminderDeadlineJob periodically" -task :initiate_reminder_deadline_job => :environment do - puts "Initiating the Reminder Deadline job" - ReminderDeadlineJob.perform_now - - puts "Done!" -end diff --git a/public/403.html b/public/403.html index e3af451eb5..63ffb8baf6 100644 --- a/public/403.html +++ b/public/403.html @@ -1,66 +1,220 @@ - + - The page you were looking for is forbidden (403) - - + + + + +
+ + + + + +
+ - - -
-

The page you were looking for is forbidden.

+
+
+
+
+

403 Error Page

+
+
+ +
+
+
+
+ + +
+
+

403

+
+
+

Oops! The page you were looking for is forbidden.

+ +

+ You don't have access to this page. +

+

+
+
+
+
-

If you are the application owner check the logs for more information.

+ + + + +
+
+ + + diff --git a/public/404.html b/public/404.html index b612547fc2..410e1424bc 100644 --- a/public/404.html +++ b/public/404.html @@ -1,67 +1,220 @@ - + - The page you were looking for doesn't exist (404) - - + + + + +
+ + + + + +
+ - - -
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

+
+
+
+
+

404 Error Page

+
+
+ +
-

If you are the application owner check the logs for more information.

+
+ + +
+
+

404

+
+
+

Oops! Page not found.

+ +

+ We could not find the page you were looking for. +

+ +
+
+
+ +
+
+ + + + +
+
+ + + diff --git a/public/422.html b/public/422.html index a21f82b3bd..f0fdc49bd8 100644 --- a/public/422.html +++ b/public/422.html @@ -1,67 +1,220 @@ - + - The change you wanted was rejected (422) - - + + + + +
+ + + + + +
+ - - -
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

+
+
+
+
+

422 Error Page

+
+
+ +
+
+
+
+ + +
+
+

422

+
+
+

Oops! The change you wanted was rejected.

+

+ Maybe you tried to change something you didn't have access to. +

+

-

If you are the application owner check the logs for more information.

+
+ +
+
+ + + + +
+
+ + + + diff --git a/public/500.html b/public/500.html index 061abc587d..9578136f7e 100644 --- a/public/500.html +++ b/public/500.html @@ -1,66 +1,217 @@ - + - We're sorry, but something went wrong (500) - - + + + + +
+ + + + + +
+ - - -
-

We're sorry, but something went wrong.

+
+
+
+
+

500 Error Page

+
+
+ +
+
+
+
+ +
+
+

500

+
+
+

Oops! Something went wrong.

+

+ We will work on fixing that right away. +

+
+
+
+
-

If you are the application owner check the logs for more information.

+ + + + +
+
+ + + diff --git a/spec/controllers/distributions_controller_spec.rb b/spec/controllers/distributions_controller_spec.rb index ec8210a941..a04a165377 100644 --- a/spec/controllers/distributions_controller_spec.rb +++ b/spec/controllers/distributions_controller_spec.rb @@ -9,9 +9,57 @@ end describe "POST #create" do - context "when distribution causes inventory quantity to be below minimum quantity" do - let(:item) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5) } - let(:storage_location) { create(:storage_location, :with_items, item: item, item_quantity: 20, organization: organization) } + let(:available_item) { create(:item, name: "Available Item", organization: organization, on_hand_minimum_quantity: 5) } + let!(:first_storage_location) { create(:storage_location, :with_items, item: available_item, item_quantity: 20, organization: organization) } + let!(:second_storage_location) { create(:storage_location, :with_items, item: available_item, item_quantity: 20, organization: organization) } + context "when distribution causes inventory to remain above minimum quantity for an organization" do + let(:params) do + { + organization_name: organization.id, + distribution: { + partner_id: partner.id, + storage_location_id: first_storage_location.id, + line_items_attributes: + { + "0": { item_id: first_storage_location.items.first.id, quantity: 10 } + } + } + } + end + + subject { post :create, params: params.merge(format: :turbo_stream) } + + it "does not display an error" do + subject + + expect(flash[:alert]).to be_nil + end + + context "when distribution causes inventory to fall below minimum quantity for a storage location" do + let(:params) do + { + organization_name: organization.id, + distribution: { + partner_id: partner.id, + storage_location_id: second_storage_location.id, + line_items_attributes: + { + "0": { item_id: second_storage_location.items.first.id, quantity: 18 } + } + } + } + end + it "does not display an error" do + subject + expect(flash[:notice]).to eq("Distribution created!") + expect(flash[:error]).to be_nil + end + end + end + + context "when distribution causes inventory quantity to be below minimum quantity for an organization" do + let(:first_item) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5) } + let(:storage_location) { create(:storage_location, :with_items, item: first_item, item_quantity: 20, organization: organization) } let(:params) do { organization_name: organization.id, @@ -31,11 +79,44 @@ it "redirects with a flash notice and a flash error" do expect(subject).to have_http_status(:redirect) expect(flash[:notice]).to eq("Distribution created!") - expect(flash[:error]).to eq("The following items have fallen below the minimum on hand quantity: Item 1") + expect(flash[:alert]).to eq("The following items have fallen below the minimum on hand quantity, bank-wide: Item 1") + end + + context "when distribution causes inventory quantity to be below recommended quantity for an organization" do + let(:second_item) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let(:storage_location) { create(:storage_location, organization: organization) } + let(:params) do + { + organization_name: organization.id, + distribution: { + partner_id: partner.id, + storage_location_id: storage_location.id, + line_items_attributes: + { + "0": { item_id: storage_location.items.first.id, quantity: 18 }, + "1": { item_id: storage_location.items.second.id, quantity: 15 } + } + } + } + end + before do + TestInventory.create_inventory(organization, { + storage_location.id => { + first_item.id => 20, + second_item.id => 20 + } + }) + end + it "displays an error for both minimum and recommended quantity for an organization" do + expect(subject).to have_http_status(:redirect) + expect(flash[:notice]).to eq("Distribution created!") + expect(flash[:alert]).to include("The following items have fallen below the recommended on hand quantity, bank-wide: Item 2") + expect(flash[:alert]).to include("The following items have fallen below the minimum on hand quantity, bank-wide: Item 1") + end end end - context "multiple line_items that have inventory quantity below minimum quantity" do + context "multiple line_items that have inventory quantity below minimum quantity for an organization" do let(:item1) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } let(:storage_location) { create(:storage_location, organization: organization) } @@ -67,14 +148,13 @@ it "redirects with a flash notice and a flash error" do expect(subject).to have_http_status(:redirect) expect(flash[:notice]).to eq("Distribution created!") - expect(flash[:error]).to include("The following items have fallen below the minimum on hand quantity") - expect(flash[:error]).to include("Item 1") - expect(flash[:error]).to include("Item 2") - expect(flash[:alert]).to be_nil + expect(flash[:alert]).to include("The following items have fallen below the minimum on hand quantity, bank-wide") + expect(flash[:alert]).to include("Item 1") + expect(flash[:alert]).to include("Item 2") end end - context "multiple line_items that have inventory quantity below recommended quantity" do + context "multiple line_items that have inventory quantity below recommended quantity for an organization" do let(:item1) { create(:item, name: "Item 1", organization: organization, on_hand_recommended_quantity: 5) } let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_recommended_quantity: 5) } let(:storage_location) { create(:storage_location, organization: organization) } @@ -106,7 +186,7 @@ it "redirects with a flash notice and a flash alert" do expect(subject).to have_http_status(:redirect) expect(flash[:notice]).to eq("Distribution created!") - expect(flash[:alert]).to eq("The following items have fallen below the recommended on hand quantity: Item 1, Item 2") + expect(flash[:alert]).to eq("The following items have fallen below the recommended on hand quantity, bank-wide: Item 1, Item 2") end end @@ -136,7 +216,7 @@ end describe "PUT #update" do - context "when distribution causes inventory quantity to be below recommended quantity" do + context "when distribution causes inventory quantity to be below recommended quantity for an organization" do let(:item1) { create(:item, name: "Item 1", organization: organization, on_hand_recommended_quantity: 5) } let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_recommended_quantity: 5) } let(:storage_location) { create(:storage_location, organization: organization) } @@ -169,11 +249,11 @@ it "redirects with a flash notice and a flash error" do expect(subject).to have_http_status(:redirect) expect(flash[:notice]).to eq("Distribution updated!") - expect(flash[:alert]).to eq("The following items have fallen below the recommended on hand quantity: Item 1, Item 2") + expect(flash[:alert]).to eq("The following items have fallen below the recommended on hand quantity, bank-wide: Item 1, Item 2") end end - context "when distribution causes inventory quantity to be below minimum quantity" do + context "when distribution causes inventory quantity to be below minimum quantity for an organization" do let(:item1) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5) } let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 5) } let(:storage_location) { create(:storage_location) } @@ -206,8 +286,7 @@ it "redirects with a flash notice and a flash error" do expect(subject).to have_http_status(:redirect) expect(flash[:notice]).to eq("Distribution updated!") - expect(flash[:error]).to eq("The following items have fallen below the minimum on hand quantity: Item 1, Item 2") - expect(flash[:alert]).to be_nil + expect(flash[:alert]).to eq("The following items have fallen below the minimum on hand quantity, bank-wide: Item 1, Item 2") end end diff --git a/spec/controllers/donations_controller_spec.rb b/spec/controllers/donations_controller_spec.rb index a42080161a..70f7bf3a27 100644 --- a/spec/controllers/donations_controller_spec.rb +++ b/spec/controllers/donations_controller_spec.rb @@ -66,8 +66,7 @@ donation_params = { source: donation.source, line_items_attributes: line_item_params } expect do put :update, params: { id: donation.id, donation: donation_params } - end.to change { donation.storage_location.inventory_items.first.quantity }.by(5) - .and change { + end.to change { View::Inventory.new(donation.organization_id) .quantity_for(storage_location: donation.storage_location_id, item_id: line_item.item_id) }.by(5) @@ -92,35 +91,6 @@ end.to change { original_storage_location.size }.by(-10) # removes the whole donation of 10 expect(new_storage_location.size).to eq 8 end - - # TODO this test is invalid in event-world since it's handled by the aggregate - it "rolls back updates if quantity would go below 0" do - next if Event.read_events?(organization) - - donation = create(:donation, :with_items, item_quantity: 10) - original_storage_location = donation.storage_location - - # adjust inventory so that updating will set quantity below 0 - inventory_item = original_storage_location.inventory_items.last - inventory_item.quantity = 5 - inventory_item.save! - - new_storage_location = create(:storage_location) - line_item = donation.line_items.first - line_item_params = { - "0" => { - item_id: line_item.item_id, - quantity: "1", - id: line_item.id - } - } - donation_params = { source: donation.source, storage_location: new_storage_location, line_items_attributes: line_item_params } - put :update, params: { id: donation.id, donation: donation_params } - expect(response).not_to redirect_to(edit_donation_path(donation)) - expect(original_storage_location.size).to eq 5 - expect(new_storage_location.size).to eq 0 - expect(donation.reload.line_items.first.quantity).to eq 10 - end end describe "when removing a line item" do @@ -132,8 +102,7 @@ donation_params = { source: donation.source } expect do put :update, params: { id: donation.id, donation: donation_params } - end.to change { donation.storage_location.inventory_items.first.quantity }.by(-1 * item_quantity) - .and change { + end.to change { View::Inventory.new(donation.organization_id) .quantity_for(storage_location: donation.storage_location_id, item_id: item_id) }.by(-1 * item_quantity) diff --git a/spec/events/inventory_aggregate_spec.rb b/spec/events/inventory_aggregate_spec.rb index 4c65ad97c9..30f184449c 100644 --- a/spec/events/inventory_aggregate_spec.rb +++ b/spec/events/inventory_aggregate_spec.rb @@ -435,7 +435,7 @@ kit.line_items = [] kit.line_items << build(:line_item, quantity: 20, item: item1, itemizable: kit) kit.line_items << build(:line_item, quantity: 5, item: item2, itemizable: kit) - KitDeallocateEvent.publish(kit, storage_location1, 2) + KitDeallocateEvent.publish(kit, storage_location1.id, 2) # 30 + (20*2) = 70, 10 + (5*2) = 20 described_class.handle(KitDeallocateEvent.last, inventory) @@ -462,38 +462,6 @@ } )) end - - it "should process a snapshot event" do - InventoryItem.delete_all - - storage_location1.inventory_items.create!(quantity: 5, item_id: item1.id) - storage_location1.inventory_items.create!(quantity: 10, item_id: item2.id) - storage_location2.inventory_items.create!(quantity: 15, item_id: item2.id) - storage_location2.inventory_items.create!(quantity: 20, item_id: item3.id) - SnapshotEvent.publish(organization) - - described_class.handle(SnapshotEvent.last, inventory) - result = InventoryAggregate.inventory_for(organization.id) - expect(result).to eq(EventTypes::Inventory.new( - organization_id: organization.id, - storage_locations: { - storage_location1.id => EventTypes::EventStorageLocation.new( - id: storage_location1.id, - items: { - item1.id => EventTypes::EventItem.new(item_id: item1.id, quantity: 5, storage_location_id: storage_location1.id), - item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 10, storage_location_id: storage_location1.id) - } - ), - storage_location2.id => EventTypes::EventStorageLocation.new( - id: storage_location2.id, - items: { - item2.id => EventTypes::EventItem.new(item_id: item2.id, quantity: 15, storage_location_id: storage_location2.id), - item3.id => EventTypes::EventItem.new(item_id: item3.id, quantity: 20, storage_location_id: storage_location2.id) - } - ) - } - )) - end end describe "multiple events" do @@ -650,7 +618,7 @@ DonationEvent.publish(donation) travel 1.minute - SnapshotEvent.publish_from_events(organization) + SnapshotEvent.publish(organization) # check inventory at this point inventory = described_class.inventory_for(organization.id) @@ -704,7 +672,7 @@ donation.save! attributes = {line_items_attributes: {"0": {item_id: item1.id, quantity: 40}, "1": {item_id: item2.id, quantity: 25}}} - ItemizableUpdateService.call(itemizable: donation, type: :increase, event_class: DonationEvent, params: attributes) + ItemizableUpdateService.call(itemizable: donation, event_class: DonationEvent, params: attributes) result = InventoryAggregate.inventory_for(organization.id) expect(result).to eq(EventTypes::Inventory.new( @@ -725,7 +693,7 @@ )) attributes = {line_items_attributes: {"0": {item_id: item1.id, quantity: 35}, "1": {item_id: item2.id, quantity: 30}}} - ItemizableUpdateService.call(itemizable: donation, type: :increase, event_class: DonationEvent, params: attributes) + ItemizableUpdateService.call(itemizable: donation, event_class: DonationEvent, params: attributes) result = InventoryAggregate.inventory_for(organization.id) expect(result).to eq(EventTypes::Inventory.new( @@ -750,8 +718,6 @@ describe "validation" do context "current event is incorrect" do it "should raise a bare error" do - next unless Event.read_events?(organization) # only relevant if flag is on - donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) donation.line_items << build(:line_item, quantity: 50, item: item1) DonationEvent.publish(donation) @@ -768,8 +734,6 @@ context "subsequent event is incorrect" do it "should handle negative quantities" do - next unless Event.read_events?(organization) # only relevant if flag is on - donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) donation.line_items << build(:line_item, quantity: 100, item: item1, itemizable: donation) DonationEvent.publish(donation) @@ -781,8 +745,6 @@ end it "should add the event to the message" do - next unless Event.read_events?(organization) # only relevant if flag is on - travel_to Time.zone.local(2023, 5, 5) donation = FactoryBot.create(:donation, organization: organization, storage_location: storage_location1) donation.line_items << build(:line_item, quantity: 50, item: item1, itemizable: donation) diff --git a/spec/factories/donations.rb b/spec/factories/donations.rb index 7791bba25b..6534b28bf1 100644 --- a/spec/factories/donations.rb +++ b/spec/factories/donations.rb @@ -62,7 +62,6 @@ end after(:create) do |instance, evaluator| - evaluator.storage_location.increase_inventory(instance.line_item_values) DonationEvent.publish(instance) end end diff --git a/spec/factories/line_items.rb b/spec/factories/line_items.rb index 913be2b479..d6bdddc5ad 100644 --- a/spec/factories/line_items.rb +++ b/spec/factories/line_items.rb @@ -51,10 +51,5 @@ itemizable_type { "Transfer" } itemizable_id { create(:transfer).id } end - - trait :kit do - itemizable_type { "Kit" } - itemizable_id { create(:kit).id } - end end end diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index 30ef97daa7..f5fa0aad71 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -34,6 +34,7 @@ # account_request_id :integer # ndbn_member_id :bigint # +require 'seeds' FactoryBot.define do factory :organization do @@ -61,7 +62,7 @@ trait :with_items do after(:create) do |instance, evaluator| - seed_base_items if BaseItem.count.zero? # seeds 45 base items if none exist + Seeds.seed_base_items if BaseItem.count.zero? # seeds 45 base items if none exist Organization.seed_items(instance) # creates 1 item for each base item end end diff --git a/spec/factories/partners.rb b/spec/factories/partners.rb index ba34f27bbe..894dd6ff62 100644 --- a/spec/factories/partners.rb +++ b/spec/factories/partners.rb @@ -40,6 +40,10 @@ status { :awaiting_review } end + trait :deactivated do + status { :deactivated } + end + after(:create) do |partner, evaluator| next if evaluator.try(:without_profile) diff --git a/spec/factories/purchases.rb b/spec/factories/purchases.rb index 054d7c0856..b10e9cbd2d 100644 --- a/spec/factories/purchases.rb +++ b/spec/factories/purchases.rb @@ -51,7 +51,6 @@ end after(:create) do |instance, evaluator| - evaluator.storage_location.increase_inventory(instance.line_item_values) PurchaseEvent.publish(instance) end end diff --git a/spec/factories/requests.rb b/spec/factories/requests.rb index 987527a7da..017a36b787 100644 --- a/spec/factories/requests.rb +++ b/spec/factories/requests.rb @@ -7,6 +7,7 @@ # discard_reason :text # discarded_at :datetime # request_items :jsonb +# request_type :string # status :integer default("pending") # created_at :datetime not null # updated_at :datetime not null @@ -57,6 +58,18 @@ def random_request_items status { 'fulfilled' } end + trait :quantity do + request_type { 'quantity' } + end + + trait :individual do + request_type { 'individual' } + end + + trait :child do + request_type { 'child' } + end + trait :pending do status { 'pending' } end diff --git a/spec/factories/transfers.rb b/spec/factories/transfers.rb index 7b410dae7c..913656ab14 100644 --- a/spec/factories/transfers.rb +++ b/spec/factories/transfers.rb @@ -43,7 +43,6 @@ end after(:create) do |instance, evaluator| - evaluator.from.increase_inventory(instance.line_item_values) end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 0648985cad..cd7e87ddef 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -134,4 +134,51 @@ def current_organization; end end end end + + describe "#set_default_location for purchase" do + helper do + def current_organization; end + end + + before(:each) do + allow(helper).to receive(:current_organization).and_return(organization) + end + + context "returns storage_location_id if present" do + let(:purchase) { build(:purchase, storage_location_id: 2) } + subject { helper.default_location(purchase) } + + it { is_expected.to eq(2) } + end + + context "returns current_organization intake_location if storage_location_id is not present" do + let(:organization) { build(:organization, intake_location: 1) } + let(:purchase) { build(:purchase, storage_location_id: nil) } + + before do + allow(helper).to receive(:current_organization).and_return(organization) + end + + subject { helper.default_location(purchase) } + + it { is_expected.to eq(1) } + end + end + + describe "#default_location for source_object" do + helper do + def current_organization; end + end + + before(:each) do + allow(helper).to receive(:current_organization).and_return(organization) + end + + context "returns storage_location_id if present" do + let(:donation) { build(:donation, storage_location_id: 2) } + subject { helper.default_location(donation) } + + it { is_expected.to eq(2) } + end + end end diff --git a/spec/helpers/historical_trends_helper_spec.rb b/spec/helpers/historical_trends_helper_spec.rb index d22b7d93f3..53fccf4564 100644 --- a/spec/helpers/historical_trends_helper_spec.rb +++ b/spec/helpers/historical_trends_helper_spec.rb @@ -2,22 +2,25 @@ RSpec.describe HistoricalTrendsHelper do describe "#last_12_months" do - it "returns the last 12 months starting from July when the current month is June" do + it "returns the last 12 months starting from July 2023 when the current month is June 2024" do allow_any_instance_of(Time).to receive(:month).and_return(6) + allow_any_instance_of(Time).to receive(:year).and_return(2024) - expect(last_12_months).to eq(["Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun"]) + expect(last_12_months).to eq(["Jul 2023", "Aug 2023", "Sep 2023", "Oct 2023", "Nov 2023", "Dec 2023", "Jan 2024", "Feb 2024", "Mar 2024", "Apr 2024", "May 2024", "Jun 2024"]) end - it "returns the last 12 months starting from Feb when the current month is Jan" do + it "returns the last 12 months starting from Feb 1999 when the current month is Jan 2000" do allow_any_instance_of(Time).to receive(:month).and_return(1) + allow_any_instance_of(Time).to receive(:year).and_return(2000) - expect(last_12_months).to eq(["Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"]) + expect(last_12_months).to eq(["Feb 1999", "Mar 1999", "Apr 1999", "May 1999", "Jun 1999", "Jul 1999", "Aug 1999", "Sep 1999", "Oct 1999", "Nov 1999", "Dec 1999", "Jan 2000"]) end - it "returns the last 12 months starting from Jan when the current month is Dec" do + it "returns the last 12 months starting from Jan 2010 when the current month is Dec 2010" do allow_any_instance_of(Time).to receive(:month).and_return(12) + allow_any_instance_of(Time).to receive(:year).and_return(2010) - expect(last_12_months).to eq(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]) + expect(last_12_months).to eq(["Jan 2010", "Feb 2010", "Mar 2010", "Apr 2010", "May 2010", "Jun 2010", "Jul 2010", "Aug 2010", "Sep 2010", "Oct 2010", "Nov 2010", "Dec 2010"]) end end end diff --git a/spec/helpers/partners_helper_spec.rb b/spec/helpers/partners_helper_spec.rb new file mode 100644 index 0000000000..f9c2b46131 --- /dev/null +++ b/spec/helpers/partners_helper_spec.rb @@ -0,0 +1,11 @@ +describe PartnersHelper, type: :helper do + describe "partial_display_name" do + it "returns the humanized name by default" do + expect(helper.partial_display_name("agency_stability")).to eq("Agency stability") + end + + it "returns the custom display name when overridden" do + expect(helper.partial_display_name("attached_documents")).to eq("Additional Documents") + end + end +end diff --git a/spec/helpers/purchases_helper_spec.rb b/spec/helpers/purchases_helper_spec.rb index 631a7b03ed..420f1d4705 100644 --- a/spec/helpers/purchases_helper_spec.rb +++ b/spec/helpers/purchases_helper_spec.rb @@ -4,25 +4,5 @@ def current_organization end end - - context "returns purchase storage_location_id if present" do - let(:purchase) { build(:purchase, storage_location_id: 2) } - subject { helper.new_purchase_default_location(purchase) } - - it { is_expected.to eq(2) } - end - - context "returns current_organization intake_location if purchase storage_location_id is not present" do - let(:organization) { build(:organization, intake_location: 1) } - let(:purchase) { build(:purchase, storage_location_id: nil) } - - before do - allow(helper).to receive(:current_organization).and_return(organization) - end - - subject { helper.new_purchase_default_location(purchase) } - - it { is_expected.to eq(1) } - end end end diff --git a/spec/inventory.rb b/spec/inventory.rb index d6f3a36182..7aea7826d9 100644 --- a/spec/inventory.rb +++ b/spec/inventory.rb @@ -16,7 +16,6 @@ def clear_inventory(storage_location) } ) ) - storage_location.inventory_items.delete_all end # Pass in a hash of storage location ID -> { item ID -> quantity}. Blows away any current @@ -37,22 +36,6 @@ def create_inventory(organization, hash) ) ) end - create_inventory_items_from_events(organization.id) - end - - # @param organization_id [Integer] - def create_inventory_items_from_events(organization_id) - inventory = View::Inventory.new(organization_id) - InventoryItem.joins(:storage_location) - .where(storage_locations: {organization_id: organization_id}) - .delete_all - inventory.all_items.each do |item| - InventoryItem.create!( - item_id: item.item_id, - storage_location_id: item.storage_location_id, - quantity: item.quantity - ) - end end end end diff --git a/spec/mailers/custom_devise_mailer_spec.rb b/spec/mailers/custom_devise_mailer_spec.rb index cf497921c2..4943481cd8 100644 --- a/spec/mailers/custom_devise_mailer_spec.rb +++ b/spec/mailers/custom_devise_mailer_spec.rb @@ -7,6 +7,7 @@ let(:partner) do partner = create(:partner, :uninvited) partner.primary_user.delete + partner.organization.update!(invitation_text: "Custom Invitation Text") partner.reload partner end @@ -17,6 +18,7 @@ it "invites to primary user" do expect(mail.subject).to eq("You've been invited to be a partner with #{user.partner.organization.name}") + expect(mail.html_part.body).to include(partner.organization.invitation_text) expect(mail.html_part.body).to include("You've been invited to become a partner with #{user.partner.organization.name}!") end end @@ -32,15 +34,21 @@ end context "when user is invited" do - let(:user) { create(:user) } + let(:invitation_sent_at) { Time.zone.now } + let(:user) { create(:user, invitation_sent_at: invitation_sent_at) } it "invites to user" do expect(mail.subject).to eq("Your Human Essentials App Account Approval") expect(mail.html_part.body).to include("Your request has been approved and you're invited to become an user of the Human Essentials inventory management system!") end - it "has invite expiration message" do - expect(mail.html_part.body).to include("For security reasons these invitations expire. This invitation will expire in 8 hours or if a new password reset is triggered.") + it "has invite expiration message and reset instructions" do + expect(mail.html_part.body).to include("This invitation will expire at #{user.invitation_due_at.strftime("%B %d, %Y %I:%M %p")} GMT or if a new password reset is triggered.") + end + + it "has reset instructions" do + expect(mail.html_part.body).to match(%r{

If your invitation has an expired message, go here and enter your email address to reset your password.

}) + expect(mail.html_part.body).to include("Feel free to ignore this email if you are not interested or if you feel it was sent by mistake.") end end end diff --git a/spec/mailers/partner_mailer_spec.rb b/spec/mailers/partner_mailer_spec.rb index 9aae8949ea..30403054bc 100644 --- a/spec/mailers/partner_mailer_spec.rb +++ b/spec/mailers/partner_mailer_spec.rb @@ -28,7 +28,7 @@ it "renders the body with text that indicates the result and a link to their dashboard" do expect(subject.body.encoded).to include("Hi #{partner.name}") - expect(subject.body.encoded).to include("#{partner.organization.name} has approved your application.") + expect(subject.body.encoded).to include("You have been approved to make requests for essentials to #{partner.organization.name}.") expect(subject.body.encoded).to include("/partners/dashboard") end end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 236ecd1147..a01a01971e 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -22,7 +22,7 @@ let(:mail) { ActionMailer::Base.deliveries.last } it "sends an email with instructions" do - expect(mail.body.encoded).to include("For security reasons these invitations expire. This invitation will expire in 8 hours or if a new password reset is triggered.") + expect(mail.body.encoded).to include("For security reasons these invitations expire. This invitation will expire in 6 hours or if a new password reset is triggered.") end end end diff --git a/spec/models/distribution_spec.rb b/spec/models/distribution_spec.rb index a3c7996515..faa8e804d4 100644 --- a/spec/models/distribution_spec.rb +++ b/spec/models/distribution_spec.rb @@ -51,15 +51,6 @@ expect(d).not_to be_valid end - it "ensures that any included items are found in the associated storage location" do - unless Event.read_events?(organization) # not relevant in event world - d = build(:distribution) - item_missing = create(:item, name: "missing") - d.line_items << build(:line_item, item: item_missing) - expect(d).not_to be_valid - end - end - it "ensures that the issued at is no earlier than 2000" do d = build(:distribution, issued_at: "1999-12-31") expect(d).not_to be_valid @@ -148,6 +139,34 @@ end end + describe "in_last_12_months >" do + context "when the current date is December 31, 2023" do + before do + travel_to Time.zone.local(2023, 12, 31) + end + + after do + travel_back + end + + it "includes distributions issued within the last 12 months" do + included_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2023, 1, 1)) + excluded_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2022, 12, 30)) + distributions = Distribution.in_last_12_months + expect(distributions).to include(included_distribution) + expect(distributions).not_to include(excluded_distribution) + end + + it "includes distributions up to the current date and excludes future ones" do + current_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2023, 12, 31)) + future_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2024, 1, 1)) + distributions = Distribution.in_last_12_months + expect(distributions).to include(current_distribution) + expect(distributions).not_to include(future_distribution) + end + end + end + describe "by_item_id >" do it "only returns distributions with given item id" do # create 2 items with unique ids @@ -185,6 +204,39 @@ expect(Distribution.by_location(location_1.id)).not_to include(dist2) end end + + describe "with_diapers >" do + let(:disposable_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } + let(:cloth_diaper_item) { create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) } + let(:non_diaper_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + + it "only includes distributions with disposable or cloth_diaper items" do + dist1 = create(:distribution, :with_items, item: disposable_item) + dist2 = create(:distribution, :with_items, item: cloth_diaper_item) + dist3 = create(:distribution, :with_items, item: non_diaper_item) + + distributions = Distribution.with_diapers + expect(distributions.count).to eq(2) + expect(distributions).to include(dist1) + expect(distributions).to include(dist2) + expect(distributions).not_to include(dist3) + end + end + + describe "with_period_supplies >" do + let(:period_supplies_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + let(:non_period_supplies_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } + + it "only includes distributions with period supplies items" do + dist1 = create(:distribution, :with_items, item: period_supplies_item) + dist2 = create(:distribution, :with_items, item: non_period_supplies_item) + + distributions = Distribution.with_period_supplies + expect(distributions.count).to eq(1) + expect(distributions).to include(dist1) + expect(distributions).not_to include(dist2) + end + end end context "Callbacks >" do diff --git a/spec/models/inventory_item_spec.rb b/spec/models/inventory_item_spec.rb deleted file mode 100644 index 065739c2e5..0000000000 --- a/spec/models/inventory_item_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# == Schema Information -# -# Table name: inventory_items -# -# id :integer not null, primary key -# quantity :integer default(0) -# created_at :datetime not null -# updated_at :datetime not null -# item_id :integer -# storage_location_id :integer -# - -RSpec.describe InventoryItem, type: :model do - context "Validations >" do - describe "quantity >" do - it { should validate_presence_of(:quantity) } - it { should validate_numericality_of(:quantity) } - it { should validate_numericality_of(:quantity).is_greater_than_or_equal_to(0) } - it { should validate_numericality_of(:quantity).is_less_than(2**31) } - it { should validate_presence_of(:storage_location_id) } - it { should validate_presence_of(:item_id) } - end - end - - it "initializes the quantity to 0 if it was not specified" do - expect(InventoryItem.new.quantity).to eq(0) - end - - context "Filtering >" do - describe "->by_partner_key" do - it "shows the Inventory Items by partner_key" do - create(:base_item, partner_key: "UniqueString") - create(:inventory_item, item: create(:item, partner_key: "UniqueString")) - expect(InventoryItem.by_partner_key("UniqueString").size).to eq(1) - end - end - end - - describe "versioning" do - it { is_expected.to be_versioned } - end -end diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb index 2ab392edd8..5436ab0b0c 100644 --- a/spec/models/item_spec.rb +++ b/spec/models/item_spec.rb @@ -46,7 +46,7 @@ expect(subject.class).to respond_to :class_filter end - it "->by_size returns all items with the same size, per their BaseItem parent" do + specify "->by_size returns all items with the same size, per their BaseItem parent" do size4 = create(:base_item, size: "4", name: "Size 4 Diaper") size_z = create(:base_item, size: "Z", name: "Size Z Diaper") @@ -56,7 +56,30 @@ expect(Item.by_size("4").length).to eq(2) end - it "->alphabetized retrieves items in alphabetical order" do + specify "->housing_a_kit returns all items which belongs_to (house) a kit" do + name = "test kit" + kit_params = attributes_for(:kit, name: name) + kit_params[:line_items_attributes] = [{item_id: create(:item).id, quantity: 1}] # shouldn't be counted + KitCreateService.new(organization_id: organization.id, kit_params: kit_params).call + + create(:item) # shouldn't be counted + expect(Item.housing_a_kit.count).to eq(1) + expect(Item.housing_a_kit.first.name = name) + end + + specify "->loose returns all items which do not belongs_to a kit" do + name = "A" + item = create(:item, name: name, organization: organization) + + kit_params = attributes_for(:kit) + kit_params[:line_items_attributes] = [{item_id: item.id, quantity: 1}] + KitCreateService.new(organization_id: organization.id, kit_params: kit_params).call # shouldn't be counted + + expect(Item.loose.count).to eq(1) + expect(Item.loose.first.name = name) + end + + specify "->alphabetized retrieves items in alphabetical order" do item_c = create(:item, name: "C") item_b = create(:item, name: "B") item_a = create(:item, name: "A") @@ -66,7 +89,7 @@ expect(Item.alphabetized.map(&:name)).to eq(alphabetized_list) end - it "->active shows items that are still active" do + specify "->active shows items that are still active" do inactive_item = create(:line_item, :purchase).item item = create(:item) inactive_item.deactivate! @@ -198,15 +221,6 @@ end context "Methods >" do - describe "storage_locations_containing" do - it "retrieves all storage locations that contain an item" do - item = create(:item) - storage_location = create(:storage_location, :with_items, item: item, item_quantity: 12) - create(:storage_location) - expect(Item.storage_locations_containing(item).first).to eq(storage_location) - end - end - describe "barcodes_for" do it "retrieves all BarcodeItems associated with an item" do item = create(:item) @@ -371,6 +385,19 @@ end end + describe '#is_in_kit?' do + it "is true for items that are in a kit and false otherwise" do + item_not_in_kit = create(:item, organization: organization) + item_in_kit = create(:item, organization: organization) + + kit_params = attributes_for(:kit) + kit_params[:line_items_attributes] = [{item_id: item_in_kit.id, quantity: 1}] + KitCreateService.new(organization_id: organization.id, kit_params: kit_params).call + expect(item_in_kit.is_in_kit?).to be true + expect(item_not_in_kit.is_in_kit?).to be false + end + end + describe "other?" do it "is true for items that are partner_key 'other'" do item = create(:item, base_item: create(:base_item, name: "Base")) @@ -453,4 +480,47 @@ describe "versioning" do it { is_expected.to be_versioned } end + + describe "kit items" do + context "with kit and regular items" do + let(:organization) { create(:organization) } + let(:kit) { create(:kit, organization: organization) } + let(:kit_item) { create(:item, kit: kit, organization: organization) } + let(:regular_item) { create(:item, organization: organization) } + + describe "#can_delete?" do + it "returns false for kit items" do + expect(kit_item.can_delete?).to be false + end + + it "returns true for regular items" do + expect(regular_item.can_delete?).to be true + end + end + + describe "#deactivate!" do + it "deactivates both the kit item and its associated kit" do + kit_item.deactivate! + expect(kit_item.reload.active).to be false + expect(kit.reload.active).to be false + end + + it "only deactivates regular items" do + regular_item.deactivate! + expect(regular_item.reload.active).to be false + end + end + + describe "#validate_destroy" do + it "prevents deletion of kit items" do + expect { kit_item.destroy! }.to raise_error(ActiveRecord::RecordNotDestroyed) + expect(kit_item.errors[:base]).to include("Cannot delete item - it has already been used!") + end + + it "allows deletion of regular items" do + expect { regular_item.destroy! }.not_to raise_error + end + end + end + end end diff --git a/spec/models/kit_spec.rb b/spec/models/kit_spec.rb index 989fb83ce2..700bdd15d4 100644 --- a/spec/models/kit_spec.rb +++ b/spec/models/kit_spec.rb @@ -113,19 +113,20 @@ it 'should return false' do item = create(:item, :active, organization: organization, kit: kit) storage_location = create(:storage_location, :with_items, organization: organization, item: item) + kit.reload TestInventory.create_inventory(organization, { storage_location.id => { kit.item.id => 10 } }) - expect(kit.reload.can_deactivate?(nil)).to eq(false) + expect(kit.reload.can_deactivate?).to eq(false) end end context 'without inventory items' do it 'should return true' do - expect(kit.reload.can_deactivate?(nil)).to eq(true) + expect(kit.reload.can_deactivate?).to eq(true) end end end diff --git a/spec/models/organization_stats_spec.rb b/spec/models/organization_stats_spec.rb index 21248d0769..3bd87ef268 100644 --- a/spec/models/organization_stats_spec.rb +++ b/spec/models/organization_stats_spec.rb @@ -1,12 +1,7 @@ # == No Schema Information # RSpec.describe OrganizationStats, type: :model do - let(:partners) { [] } - let(:storage_locations) { [] } - let(:donation_sites) { [] } - let(:current_org) do - double("org", partners: partners, storage_locations: storage_locations, donation_sites: donation_sites) - end + let(:current_org) { create(:organization) } subject { described_class.new(current_org) } @@ -20,7 +15,9 @@ end context "current org is not nil >" do - let(:partners) { %w(element1 element2) } + before(:each) do + FactoryBot.create_list(:partner, 2, organization: current_org) + end it "should return actual count of partners" do expect(subject.partners_added).to eq(2) @@ -38,7 +35,9 @@ end context "current org is not nil >" do - let(:storage_locations) { %w(loc1 loc2 loc3) } + before(:each) do + create_list(:storage_location, 3, organization: current_org) + end it "should return actual count of locations" do expect(subject.storage_locations_added).to eq(3) @@ -56,7 +55,9 @@ end context "current org is not nil >" do - let(:donation_sites) { %w(site1 site2 site3) } + before(:each) do + create_list(:donation_site, 3, organization: current_org) + end it "should return actual count of donation sites" do expect(subject.donation_sites_added).to eq(3) @@ -74,8 +75,16 @@ end context "current org is not nil + locations have items >" do - let(:storage_location_1) { create :storage_location, :with_items } + let(:storage_location_1) { create :storage_location } + let(:item) { create(:item, organization: current_org) } let(:storage_locations) { [storage_location_1] } + before(:each) do + TestInventory.create_inventory(current_org, { + storage_location_1.id => { + item.id => 50 + } + }) + end it "should return storage location" do expect(subject.locations_with_inventory).to include(storage_location_1) diff --git a/spec/models/partner_spec.rb b/spec/models/partner_spec.rb index c632759004..a3e0a06060 100644 --- a/spec/models/partner_spec.rb +++ b/spec/models/partner_spec.rb @@ -299,6 +299,8 @@ let(:agency_type) { Partner::AGENCY_TYPES["OTHER"] } let(:other_agency_type) { "Another Agency Name" } let(:notes) { "Some notes" } + let(:providing_diapers) { {value: "N", index: 13} } + let(:providing_period_supplies) { {value: "N", index: 14} } before do partner.profile.update({ @@ -317,7 +319,16 @@ partner.update(notes: notes) end - it "should has the info in the columns order" do + it "should have the expected info in the columns order" do + county_1 = create(:county, name: "High County, Maine", region: "Maine") + county_2 = create(:county, name: "laRue County, Louisiana", region: "Louisiana") + county_3 = create(:county, name: "Ste. Anne County, Louisiana", region: "Louisiana") + create(:partners_served_area, partner_profile: partner.profile, county: county_1, client_share: 50) + create(:partners_served_area, partner_profile: partner.profile, county: county_2, client_share: 40) + create(:partners_served_area, partner_profile: partner.profile, county: county_3, client_share: 10) + partner.profile.reload # not sure if this is needed + # county ordering is a bit esoteric -- it is human alphabetical by county within region (region is state) + correctly_ordered_counties = "laRue County, Louisiana; Ste. Anne County, Louisiana; High County, Maine" expect(partner.csv_export_attributes).to eq([ partner.name, partner.email, @@ -330,9 +341,77 @@ contact_name, contact_phone, contact_email, - notes + notes, + correctly_ordered_counties, + providing_diapers[:value], + providing_period_supplies[:value] ]) end + + context "when partner has a distribution in the last 12 months" do + let(:distribution) { create(:distribution, partner: partner) } + + shared_examples "providing_diapers check" do |scope| + before do + providing_diapers[:value] = "Y" + + case scope + when :disposable + item = create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) + when :cloth_diapers + item = create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) + end + + create(:line_item, item: item, itemizable: distribution) + end + + it "should have Y as providing_diapers" do + expect(partner.csv_export_attributes[providing_diapers[:index]]).to eq(providing_diapers[:value]) + end + end + + context "with a disposable item" do + include_examples "providing_diapers check", :disposable + end + + context "with a cloth diaper item" do + include_examples "providing_diapers check", :cloth_diapers + end + + context "with a period supplies item" do + before do + providing_period_supplies[:value] = "Y" + + item = create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) + create(:line_item, item: item, itemizable: distribution) + end + + it "should have Y as providing_period_supplies" do + expect(partner.csv_export_attributes[providing_period_supplies[:index]]).to eq(providing_period_supplies[:value]) + end + end + end + + context "when partner only has distribution older than a 12 months" do + let(:distribution) { create(:distribution, issued_at: (12.months.ago.beginning_of_day - 1.day), partner: partner) } + let(:disposable_diapers_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } + let(:cloth_diapers_item) { create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) } + let(:period_supplies_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + + before do + create(:line_item, item: disposable_diapers_item, itemizable: distribution) + create(:line_item, item: cloth_diapers_item, itemizable: distribution) + create(:line_item, item: period_supplies_item, itemizable: distribution) + end + + it "should have N as providing_diapers" do + expect(partner.csv_export_attributes[providing_diapers[:index]]).to eq(providing_diapers[:value]) + end + + it "should have N as providing_period_supplies" do + expect(partner.csv_export_attributes[providing_period_supplies[:index]]).to eq(providing_period_supplies[:value]) + end + end end describe '#quantity_year_to_date' do diff --git a/spec/models/partners/profile_spec.rb b/spec/models/partners/profile_spec.rb index 76aaac100e..f82cb7e976 100644 --- a/spec/models/partners/profile_spec.rb +++ b/spec/models/partners/profile_spec.rb @@ -97,10 +97,19 @@ subject { build(:partner_profile, enable_child_based_requests: false, enable_individual_requests: false, enable_quantity_based_requests: false) } context "no settings are set to true" do - it "should not be valid" do + it "sets error at base when feature flag disabled for partner step form" do + allow(Flipper).to receive(:enabled?).with("partner_step_form").and_return(false) + expect(subject).to_not be_valid expect(subject.errors[:base]).to include("At least one request type must be set") end + + it "sets error at field level when feature flag enabled for partner step form" do + allow(Flipper).to receive(:enabled?).with("partner_step_form").and_return(true) + + expect(subject).to_not be_valid + expect(subject.errors[:enable_child_based_requests]).to include("At least one request type must be set") + end end context "at least one request type is set to true" do @@ -256,15 +265,33 @@ end context "multiple" do - it "sums the client shares " do + it "sums the client shares and sets error at base when feature flag disabled for partner step form" do + allow(Flipper).to receive(:enabled?).with("partner_step_form").and_return(false) + + profile = create(:partner_profile) + county1 = create(:county, name: "county1", region: "region1") + county2 = create(:county, name: "county2", region: "region2") + create(:partners_served_area, partner_profile: profile, county: county1, client_share: 50) + create(:partners_served_area, partner_profile: profile, county: county2, client_share: 49) + profile.reload + expect(profile.client_share_total).to eq(99) + expect(profile.valid?).to eq(false) + expect(profile.errors[:base]).to include("Total client share must be 0 or 100") + end + + it "sets error at field level when feature flag enabled for partner step form" do + allow(Flipper).to receive(:enabled?).with("partner_step_form").and_return(true) + profile = create(:partner_profile) county1 = create(:county, name: "county1", region: "region1") county2 = create(:county, name: "county2", region: "region2") create(:partners_served_area, partner_profile: profile, county: county1, client_share: 50) create(:partners_served_area, partner_profile: profile, county: county2, client_share: 49) profile.reload + expect(profile.client_share_total).to eq(99) expect(profile.valid?).to eq(false) + expect(profile.errors[:client_share]).to include("Total client share must be 0 or 100") end it "is valid if client share sum is 100" do @@ -280,6 +307,25 @@ end end + describe "county_list" do + it "provides a county list in human-alpha by county within region order" do + county_1 = create(:county, name: "High County, Maine", region: "Maine") + county_2 = create(:county, name: "laRue County, Louisiana", region: "Louisiana") + county_3 = create(:county, name: "Ste. Anne County, Louisiana", region: "Louisiana") + county_4 = create(:county, name: "Other County, Louisiana", region: "Louisiana") + profile = create(:partner_profile) + profile_2 = create(:partner_profile) + create(:partners_served_area, partner_profile: profile, county: county_1, client_share: 50) + create(:partners_served_area, partner_profile: profile, county: county_2, client_share: 40) + create(:partners_served_area, partner_profile: profile, county: county_3, client_share: 10) + create(:partners_served_area, partner_profile: profile_2, county: county_4) + profile.reload + profile_2.reload + ## This is human-alpha by county within region (i.e. state) + expect(profile.county_list_by_region).to eq("laRue County, Louisiana; Ste. Anne County, Louisiana; High County, Maine") + end + end + describe "versioning" do it { is_expected.to be_versioned } end diff --git a/spec/models/partners/served_area_spec.rb b/spec/models/partners/served_area_spec.rb index 949d99ed80..df604027df 100644 --- a/spec/models/partners/served_area_spec.rb +++ b/spec/models/partners/served_area_spec.rb @@ -11,19 +11,27 @@ # RSpec.describe Partners::ServedArea, type: :model do - it { should belong_to(:partner_profile) } - it { should belong_to(:county) } + describe "validations" do + let(:served_area_nil_client_share) { build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: nil) } + it { should belong_to(:partner_profile) } + it { should belong_to(:county) } - it "must only allow integer client shares" do - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 50)).to be_valid - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 50.5)).not_to be_valid - end + it "displays a user friendly error message for nil client shares" do + expect(served_area_nil_client_share).not_to be_valid + expect(served_area_nil_client_share.errors.messages[:client_share]).to match_array(["Client share must be between 1 and 100 inclusive", "is not a number"]) + end + + it "must only allow integer client shares" do + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 50)).to be_valid + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 50.5)).not_to be_valid + end - it "must only allow valid client share values" do - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 0)).not_to be_valid - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 101)).not_to be_valid - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 1)).to be_valid - expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 100)).to be_valid + it "must only allow valid client share values" do + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 0)).not_to be_valid + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 101)).not_to be_valid + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 1)).to be_valid + expect(build(:partners_served_area, partner_profile: create(:partner_profile), county: create(:county), client_share: 100)).to be_valid + end end describe "versioning" do diff --git a/spec/models/request_spec.rb b/spec/models/request_spec.rb index 6a794b651b..473447797c 100644 --- a/spec/models/request_spec.rb +++ b/spec/models/request_spec.rb @@ -7,6 +7,7 @@ # discard_reason :text # discarded_at :datetime # request_items :jsonb +# request_type :string # status :integer default("pending") # created_at :datetime not null # updated_at :datetime not null @@ -127,6 +128,14 @@ end end + describe "request_type_label" do + let(:request) { build(:request, request_type: "individual") } + + it "returns the the first letter of the request_type capitalized" do + expect(request.request_type_label).to eq("I") + end + end + describe "versioning" do it { is_expected.to be_versioned } end diff --git a/spec/models/storage_location_spec.rb b/spec/models/storage_location_spec.rb index 05bc74d677..5ebdb20ba2 100644 --- a/spec/models/storage_location_spec.rb +++ b/spec/models/storage_location_spec.rb @@ -45,16 +45,6 @@ end context "Filtering >" do - it "->containing yields only inventories that have that item" do - item = create(:item) - item2 = create(:item) - storage_location = create(:storage_location, :with_items, item: item, item_quantity: 5) - create(:storage_location, :with_items, item: item2, item_quantity: 5) - results = StorageLocation.containing(item.id) - expect(results.length).to eq(1) - expect(results.first).to eq(storage_location) - end - it "->active_locations yields only storage locations that haven't been discarded" do create(:storage_location, name: "Active Location") create(:storage_location, name: "Inactive Location", discarded_at: Time.zone.now) @@ -62,73 +52,38 @@ expect(results.length).to eq(1) expect(results.first.discarded_at).to be_nil end + + it "->with_transfers_to yields storage locations with transfers to an organization" do + storage_location1 = create(:storage_location, name: "loc1", organization: organization) + storage_location2 = create(:storage_location, name: "loc2", organization: organization) + storage_location3 = create(:storage_location, name: "loc3", organization: organization) + storage_location4 = create(:storage_location, name: "loc4", organization: create(:organization)) + storage_location5 = create(:storage_location, name: "loc5", organization: storage_location4.organization) + create(:transfer, from: storage_location3, to: storage_location1, organization: organization) + create(:transfer, from: storage_location3, to: storage_location2, organization: organization) + create(:transfer, from: storage_location5, to: storage_location4, organization: storage_location4.organization) + + expect(StorageLocation.with_transfers_to(organization).to_a).to match_array([storage_location1, storage_location2]) + end + + it "->with_transfers_from yields storage locations with transfers from an organization" do + storage_location1 = create(:storage_location, name: "loc1", organization: organization) + storage_location2 = create(:storage_location, name: "loc2", organization: organization) + storage_location3 = create(:storage_location, name: "loc3", organization: organization) + storage_location4 = create(:storage_location, name: "loc4", organization: create(:organization)) + storage_location5 = create(:storage_location, name: "loc5", organization: storage_location4.organization) + create(:transfer, from: storage_location3, to: storage_location1, organization: organization) + create(:transfer, from: storage_location3, to: storage_location2, organization: organization) + create(:transfer, from: storage_location5, to: storage_location4, organization: storage_location4.organization) + + expect(StorageLocation.with_transfers_from(organization).to_a).to match_array([storage_location3]) + end end context "Methods >" do let(:item) { create(:item) } subject { create(:storage_location, :with_items, item_quantity: 10, item: item, organization: organization) } - describe "increase_inventory" do - context "With existing inventory" do - let(:donation) { create(:donation, :with_items, item_quantity: 66, organization: organization) } - - it "increases inventory quantities from an itemizable object" do - expect do - subject.increase_inventory(donation.line_item_values) - end.to change { subject.size }.by(66) - end - end - - context "when providing a new item that does not yet exist" do - let(:mystery_item) { create(:item, organization: organization) } - let(:donation_with_new_items) { create(:donation, :with_items, organization: organization, item_quantity: 10, item: mystery_item) } - - it "creates those new inventory items in the storage location" do - expect do - subject.increase_inventory(donation_with_new_items.line_item_values) - end.to change { subject.inventory_items.count }.by(1) - end - end - end - - describe "decrease_inventory" do - let(:item) { create(:item, organization: organization) } - let(:distribution) { create(:distribution, :with_items, item: item, item_quantity: 66, organization: organization) } - - it "decreases inventory quantities from an itemizable object" do - storage_location = create(:storage_location, :with_items, item_quantity: 100, item: item, organization: organization) - expect do - storage_location.decrease_inventory(distribution.line_item_values) - end.to change { storage_location.size }.by(-66) - end - - context "when there is insufficient inventory available" do - let(:distribution_but_too_much) { create(:distribution, :with_items, item: item, item_quantity: 9001, organization: organization) } - - it "gives informative errors" do - next if Event.read_events?(organization) - - storage_location = create(:storage_location, :with_items, item_quantity: 10, item: item, organization: organization) - expect do - storage_location.decrease_inventory(distribution_but_too_much.line_item_values).errors - end.to raise_error(Errors::InsufficientAllotment) - end - - it "does not change inventory quantities if there is an error" do - next if Event.read_events?(organization) - - storage_location = create(:storage_location, :with_items, item_quantity: 10, item: item, organization: organization) - starting_size = storage_location.size - begin - storage_location.decrease_inventory(distribution.line_item_values) - rescue Errors::InsufficientAllotment, InventoryError - end - storage_location.reload - expect(storage_location.size).to eq(starting_size) - end - end - end - describe "StorageLocation.items_inventoried" do it "returns a collection of items that are stored within inventories" do items = create_list(:item, 3, organization: organization) @@ -253,33 +208,6 @@ expect(storage_location.longitude).not_to eq(nil) end end - - describe "csv_export_attributes" do - it "returns an array of storage location attributes, followed by inventory item quantities that are sorted by alphabetized item names" do - item1 = create(:item, name: "C") - item2 = create(:item, name: "B") - item3 = create(:item, name: "A") - inactive_item = create(:item, name: "inactive item", active: false) - name = "New Storage Location" - address = "1500 Remount Road, Front Royal, VA 22630" - warehouse_type = "Warehouse with loading bay" - square_footage = rand(1000..10000) - storage_location = create(:storage_location, name: name, address: address, warehouse_type: warehouse_type, square_footage: square_footage) - quantity1 = rand(100..1000) - quantity2 = rand(100..1000) - quantity3 = rand(100..1000) - TestInventory.create_inventory(storage_location.organization, { - storage_location.id => { - item1.id => quantity1, - item2.id => quantity2, - item3.id => quantity3, - inactive_item.id => 1 - } - }) - sum = quantity1 + quantity2 + quantity3 - expect(storage_location.csv_export_attributes).to eq([name, address, square_footage, warehouse_type, sum, quantity3, quantity2, quantity1]) - end - end end describe "versioning" do diff --git a/spec/models/transfer_spec.rb b/spec/models/transfer_spec.rb index 2ca192938d..e3f9d17151 100644 --- a/spec/models/transfer_spec.rb +++ b/spec/models/transfer_spec.rb @@ -69,21 +69,6 @@ end end - context "Methods >" do - it "`self.storage_locations_transferred_to` and `..._from` constrains appropriately" do - storage_location1 = create(:storage_location, name: "loc1", organization: organization) - storage_location2 = create(:storage_location, name: "loc2", organization: organization) - storage_location3 = create(:storage_location, name: "loc3", organization: organization) - storage_location4 = create(:storage_location, name: "loc4", organization: create(:organization)) - storage_location5 = create(:storage_location, name: "loc5", organization: storage_location4.organization) - create(:transfer, from: storage_location3, to: storage_location1, organization: organization) - create(:transfer, from: storage_location3, to: storage_location2, organization: organization) - create(:transfer, from: storage_location5, to: storage_location4, organization: storage_location4.organization) - expect(Transfer.storage_locations_transferred_to_in(organization).to_a).to match_array([storage_location1, storage_location2]) - expect(Transfer.storage_locations_transferred_from_in(organization).to_a).to match_array([storage_location3]) - end - end - describe "versioning" do it { is_expected.to be_versioned } end diff --git a/spec/pdfs/donation_pdf_spec.rb b/spec/pdfs/donation_pdf_spec.rb index 21a8ee2174..5ceac9d3eb 100644 --- a/spec/pdfs/donation_pdf_spec.rb +++ b/spec/pdfs/donation_pdf_spec.rb @@ -5,6 +5,17 @@ create(:donation, organization: organization, donation_site: donation_site, source: Donation::SOURCES[:donation_site], comment: "A donation comment") end + let(:product_drive) { create(:product_drive, name: "Second Best Product Drive") } + let(:product_drive_participant) { + create(:product_drive_participant, business_name: "A Good Place to Collect Diapers", address: "1500 Remount Road, Front Royal, VA 22630", email: "good@place.is") + } + let(:product_drive_donation) do + create(:donation, organization: organization, product_drive: product_drive, source: Donation::SOURCES[:product_drive], + product_drive_participant: product_drive_participant, comment: "A product drive donation") + end + let(:product_drive_donation_without_participant) do + create(:donation, organization: organization, product_drive: product_drive, source: Donation::SOURCES[:product_drive], comment: "A product drive donation without participant") + end let(:item1) { FactoryBot.create(:item, name: "Item 1", package_size: 50, value_in_cents: 100) } let(:item2) { FactoryBot.create(:item, name: "Item 2", value_in_cents: 200) } let(:item3) { FactoryBot.create(:item, name: "Item 3", value_in_cents: 300) } @@ -65,4 +76,22 @@ expect(pdf_test.page(1).text).to include("Total Items Received") end end + + context "product drive donation" do + it "renders correctly" do + pdf = described_class.new(organization, product_drive_donation) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + expect(pdf_test.page(1).text).to include("A Good Place to Collect Diapers") + expect(pdf_test.page(1).text).to include("good@place.is") + expect(pdf_test.page(1).text).to include("1500 Remount Road, Front Royal, VA 22630") + expect(pdf_test.page(1).text).to include("A product drive donation") + end + + it "renders correctly without a product drive participant" do + pdf = described_class.new(organization, product_drive_donation_without_participant) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + expect(pdf_test.page(1).text).to include("Product Drive -- Second Best Product Drive") + expect(pdf_test.page(1).text).to include("A product drive donation") + end + end end diff --git a/spec/queries/low_inventory_query_spec.rb b/spec/queries/low_inventory_query_spec.rb index 5ea25583d2..fd7454e851 100644 --- a/spec/queries/low_inventory_query_spec.rb +++ b/spec/queries/low_inventory_query_spec.rb @@ -84,4 +84,32 @@ }) } end + + context "when items are in multiple storage locations" do + let(:recommended_quantity) { 300 } + let(:secondary_storage_location) { create :storage_location, organization: organization } + let!(:secondary_purchase) { + create :purchase, + :with_items, + organization: organization, + storage_location: secondary_storage_location, + item: item, + item_quantity: inventory_item_quantity, + issued_at: Time.current + } + + it { + expect(subject.count).to eq 1 + } + + it { + is_expected.to include({ + id: item.id, + name: item.name, + on_hand_minimum_quantity: 0, + on_hand_recommended_quantity: 300, + total_quantity: 200 + }) + } + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a8130707cd..bcfb3b3fb6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -170,10 +170,6 @@ def self.capybara_tmp_path Capybara.server = :puma, { Silent: true } end - config.before(:each) do - allow(Event).to receive(:read_events?).and_return(true) - end - config.before do Faker::UniqueGenerator.clear # Clears used values to avoid retry limit exceeded error end @@ -242,21 +238,3 @@ def await_select2(select2, container = nil, &block) find("#{container} select option[data-select2-id=\"#{current_id.to_i + 1}\"]", wait: 10) end - -def seed_base_items - base_items = File.read(Rails.root.join("db", "base_items.json")) - items_by_category = JSON.parse(base_items) - base_items_data = items_by_category.map do |category, entries| - entries.map do |entry| - { - name: entry["name"], - category: category, - partner_key: entry["key"], - updated_at: Time.zone.now, - created_at: Time.zone.now - } - end - end.flatten - - BaseItem.create!(base_items_data) -end diff --git a/spec/requests/admin/base_items_requests_spec.rb b/spec/requests/admin/base_items_requests_spec.rb index ee04d33c1e..b32fa7cf33 100644 --- a/spec/requests/admin/base_items_requests_spec.rb +++ b/spec/requests/admin/base_items_requests_spec.rb @@ -1,12 +1,20 @@ RSpec.describe "Admin::BaseItems", type: :request do - let(:organization) { create(:organization) } + let(:organization) { create(:organization, :with_items) } let(:user) { create(:user, organization: organization) } + let(:super_admin) { create(:super_admin, organization: organization) } let(:organization_admin) { create(:organization_admin, organization: organization) } - # TODO: should this be testing something? context "while signed in as a super admin" do before do - sign_in(@super_admin) + sign_in(super_admin) + end + + it "doesn't let you delete the Kit base item" do + kit_base_item = KitCreateService.find_or_create_kit_base_item! + delete admin_base_item_path(id: kit_base_item.id) + expect(flash[:alert]).to include("You cannot delete the Kits base item") + expect(response).to be_redirect + expect(BaseItem.exists?(kit_base_item.id)).to be true end end @@ -15,52 +23,11 @@ sign_in(organization_admin) end - describe "GET #new" do - it "returns http success" do - get new_admin_base_item_path - expect(response).to have_http_status(:found) - end - end - - describe "POST #create" do - it "redirects" do - post admin_base_items_path(organization: attributes_for(:organization)) - expect(response).to be_redirect - end - end - describe "GET #index" do - it "returns http success" do + it "denies access and redirects" do get admin_base_items_path - expect(response).to have_http_status(:found) - end - end - - describe "GET #show" do - it "returns http success" do - get admin_base_item_path(id: organization.id) - expect(response).to have_http_status(:found) - end - end - - describe "GET #edit" do - it "returns http success" do - get edit_admin_base_item_path(id: organization.id) - expect(response).to have_http_status(:found) - end - end - - describe "PUT #update" do - it "redirect" do - put admin_base_item_path(id: organization.id, organization: { name: "Foo" }) - expect(response).to be_redirect - end - end - - describe "DELETE #destroy" do - it "redirects" do - delete admin_base_item_path(id: organization.id) - expect(response).to be_redirect + expect(flash[:error]).to eq("Access Denied.") + expect(response).to redirect_to(dashboard_path) end end end diff --git a/spec/requests/admin/organizations_requests_spec.rb b/spec/requests/admin/organizations_requests_spec.rb index 9b0410eb2e..ff49d755c5 100644 --- a/spec/requests/admin/organizations_requests_spec.rb +++ b/spec/requests/admin/organizations_requests_spec.rb @@ -85,6 +85,14 @@ expect(subject).to render_template("new") expect(flash[:error]).to be_present end + + it "preserves user attributes" do + post admin_organizations_path({ organization: invalid_params }) + + expect(subject).to render_template("new") + expect(response.body).to include(invalid_params[:user][:name]) + expect(response.body).to include(invalid_params[:user][:email]) + end end end diff --git a/spec/requests/audits_requests_spec.rb b/spec/requests/audits_requests_spec.rb index 80d21de719..61ad5a38a8 100644 --- a/spec/requests/audits_requests_spec.rb +++ b/spec/requests/audits_requests_spec.rb @@ -50,6 +50,21 @@ get new_audit_path expect(response).to be_successful end + + it 'only includes active items in the line item select dropdown' do + create(:item, name: "TestActiveItem", organization: organization) + create(:item, name: "TestInactiveItem", organization: organization, active: false) + + get new_audit_path + expect(response).to have_http_status(:ok) + + html = Nokogiri::HTML(response.body) + options = html.css('select[name="audit[line_items_attributes][0][item_id]"] option') + option_values = options.map { |option| option.text.strip } + + expect(option_values).to include('TestActiveItem') + expect(option_values).not_to include('TestInactiveItem') + end end describe "GET #edit" do diff --git a/spec/requests/distributions_requests_spec.rb b/spec/requests/distributions_requests_spec.rb index 34e06e4af5..710e1bbfc8 100644 --- a/spec/requests/distributions_requests_spec.rb +++ b/spec/requests/distributions_requests_spec.rb @@ -62,7 +62,7 @@ end describe "GET #index" do - let(:item) { create(:item, organization: organization) } + let(:item) { create(:item, value_in_cents: 100, organization: organization) } let!(:distribution) { create(:distribution, :with_items, :past, item: item, item_quantity: 10, organization: organization) } it "returns http success" do @@ -103,6 +103,109 @@ expect(response.body).to match(/Has Inactive Items/) end end + + context "with filters" do + it "shows all active partners in dropdown filter unrestricted by current filter" do + inactive_partner_name = create(:partner, :deactivated, organization:).name + active_partner_name = distribution.partner.name + + # Filter by date with no distributions + params = { filters: { date_range: "January 1,9999 - January 1,9999"} } + + get distributions_path, params: params + page = Nokogiri::HTML(response.body) + partner_select = page.at_css("select[name='filters[by_partner]']") + + expect(partner_select).to be_present + expect(partner_select.text).to include(active_partner_name) + expect(partner_select.text).not_to include(inactive_partner_name) + end + end + + context "when filtering by item id" do + let!(:item_2) { create(:item, value_in_cents: 100, organization: organization) } + let(:params) { { filters: { by_item_id: item.id } } } + + before do + distribution.line_items << create(:line_item, item: item_2, quantity: 10) + end + + it "shows value and quantity for that item in distributions" do + get distributions_path, params: params + + page = Nokogiri::HTML(response.body) + item_quantity, item_value = page.css("table tbody tr td.numeric") + + # total value/quantity of distribution + expect(distribution.total_quantity).to eq(20) + expect(distribution.value_per_itemizable).to eq(2000) + + # displays quantity of filtered item in distribution + # displays total value of distribution + expect(item_quantity.text).to eq("10") + expect(item_value.text).to eq("$20.00") + end + + it "changes the total quantity header" do + get distributions_path, params: params + + page = Nokogiri::HTML(response.body) + item_total_header, item_value_header = page.css("table thead tr th.numeric") + + expect(item_total_header.text).to eq("Total #{item.name}") + expect(item_value_header.text).to eq("Total Value") + end + end + + context "when filtering by item category id" do + let!(:item_category) { create(:item_category, organization:) } + let!(:item_category_2) { create(:item_category, organization:) } + let!(:item_2) { create(:item, item_category: item_category_2, value_in_cents: 100, organization: organization) } + let(:params) { { filters: { by_item_category_id: item.item_category_id } } } + + before do + item.update(item_category: item_category) + distribution.line_items << create(:line_item, item: item_2, quantity: 10) + end + + it "shows value and quantity for that item category in distributions" do + get distributions_path, params: params + + page = Nokogiri::HTML(response.body) + item_quantity, item_value = page.css("table tbody tr td.numeric") + + # total value/quantity of distribution + expect(distribution.total_quantity).to eq(20) + expect(distribution.value_per_itemizable).to eq(2000) + + # displays quantity of filtered item in distribution + # displays total value of distribution + expect(item_quantity.text).to eq("10") + expect(item_value.text).to eq("$20.00") + end + + it "changes the total quantity header" do + get distributions_path, params: params + + page = Nokogiri::HTML(response.body) + item_total_header, item_value_header = page.css("table thead tr th.numeric") + + expect(item_total_header.text).to eq("Total in #{item_category.name}") + expect(item_value_header.text).to eq("Total Value") + end + + it "doesn't show duplicate distributions" do + # Add another item in given category so that a JOIN clauses would produce duplicates + item.update(item_category: item_category_2, value_in_cents: 50) + + get distributions_path, params: params + + page = Nokogiri::HTML(response.body) + distribution_rows = page.css("table tbody tr") + + expect(distribution_rows.count).to eq(1) + end + end end describe "POST #create" do @@ -130,6 +233,20 @@ expect(response).to have_error end + it "renders #new on failure with only active items in dropdown" do + create(:item, organization: organization, name: 'Active Item') + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + post distributions_path(distribution: { comment: nil, partner_id: nil, storage_location_id: nil }, format: :turbo_stream) + expect(response).to have_http_status(400) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select.line_item_name").text.split("\n") + + expect(selectable_items).to include("Active Item") + expect(selectable_items).not_to include("Inactive Item") + end + context "Deactivated partners should not be displayed in partner dropdown" do before do create(:partner, name: 'Active Partner', organization: organization, status: "approved") @@ -172,6 +289,18 @@ expect(page.css('#distribution_storage_location_id option[selected]')).to be_empty end + it "should only show active items in item dropdown" do + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + get new_distribution_path(default_params) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select#barcode_item_barcodeable_id").text.split("\n") + + expect(selectable_items).to include("Item 1", "Item 2") + expect(selectable_items).not_to include("Inactive Item") + end + context "with org default but no partner default" do it "selects org default" do organization.update!(default_storage_location: storage_location.id) @@ -407,37 +536,6 @@ end.to change { original_storage_location.size }.by(10) # removes the whole distribution of 10 - increasing inventory expect(new_storage_location.size).to eq 25 end - - # TODO this test is invalid in event-world since it's handled by the aggregate - it "rollsback updates if quantity would go below 0" do - next if Event.read_events?(organization) - - distribution = create(:distribution, :with_items, item_quantity: 10, organization: organization) - original_storage_location = distribution.storage_location - - # adjust inventory so that updating will set quantity below 0 - inventory_item = original_storage_location.inventory_items.last - inventory_item.quantity = 5 - inventory_item.save! - - new_storage_location = create(:storage_location) - line_item = distribution.line_items.first - line_item_params = { - "0" => { - "_destroy" => "false", - item_id: line_item.item_id, - quantity: "20", - id: line_item.id - } - } - distribution_params = { storage_location_id: new_storage_location.id, line_items_attributes: line_item_params } - expect do - put :update, params: { id: donation.id, distribution: distribution_params } - end.to raise_error(NameError) - expect(original_storage_location.size).to eq 5 - expect(new_storage_location.size).to eq 0 - expect(distribution.reload.line_items.first.quantity).to eq 10 - end end context "mail follow up" do @@ -500,6 +598,19 @@ expect(response.body).to include("Active Partner") end + it "should only show active items in item dropdown" do + create(:item, organization: organization, name: 'Active Item') + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + get edit_distribution_path(id: distribution.id) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select#barcode_item_barcodeable_id").text.split("\n") + + expect(selectable_items).to include("Active Item") + expect(selectable_items).not_to include("Inactive Item") + end + context 'with units' do let!(:request) { create(:request, diff --git a/spec/requests/donations_requests_spec.rb b/spec/requests/donations_requests_spec.rb index 963a5244c6..cfaea749c3 100644 --- a/spec/requests/donations_requests_spec.rb +++ b/spec/requests/donations_requests_spec.rb @@ -1,5 +1,6 @@ RSpec.describe "Donations", type: :request do let(:organization) { create(:organization) } + let(:storage_location) { create(:storage_location, name: "Pawane Location", organization: organization) } let(:user) { create(:user, organization: organization) } let(:organization_admin) { create(:organization_admin, organization: organization) } @@ -86,6 +87,19 @@ end end + describe "GET #new" do + subject do + organization.update!(default_storage_location: storage_location) + get new_donation_path + response + end + + it { is_expected.to be_successful } + it "should include the storage location name" do + expect(subject.body).to include("Pawane Location") + end + end + describe "GET #print" do let(:item) { create(:item) } let!(:donation) { create(:donation, :with_items, item: item) } @@ -247,11 +261,7 @@ put donation_path(id: donation.id, donation: edited_donation) - if Event.read_events?(organization) - expect(flash[:alert]).to include("Error updating donation: Could not reduce quantity") - else # TODO remove this branch when switching to events - expect(flash[:alert]).to include("Error updating donation: Requested items exceed the available inventory") - end + expect(flash[:alert]).to include("Error updating donation: Could not reduce quantity") expect(response.body).to include("Edit - Donations - #{original_source}") expect(response.body).to include("Editing Donation\n from #{original_source}") diff --git a/spec/requests/items_requests_spec.rb b/spec/requests/items_requests_spec.rb index 8a2110645d..0e1f0adc65 100644 --- a/spec/requests/items_requests_spec.rb +++ b/spec/requests/items_requests_spec.rb @@ -222,7 +222,7 @@ it 'shows custom request units when flipper enabled' do Flipper.enable(:enable_packs) get item_path(id: item.id) - print(response.body) + expect(response.body).to include('Custom Units') expect(response.body).to include("ITEM1; ITEM2") end diff --git a/spec/requests/purchases_requests_spec.rb b/spec/requests/purchases_requests_spec.rb index d60b23f484..3e51348c1e 100644 --- a/spec/requests/purchases_requests_spec.rb +++ b/spec/requests/purchases_requests_spec.rb @@ -1,5 +1,6 @@ RSpec.describe "Purchases", type: :request do let(:organization) { create(:organization) } + let(:storage_location) { create(:storage_location, name: "Pawane Location", organization: organization) } let(:user) { create(:user, organization: organization) } let(:organization_admin) { create(:organization_admin, organization: organization) } @@ -36,6 +37,27 @@ expect(subject.body).to include("Comments") expect(subject.body).to include("Purchase Comment") end + + describe "pagination" do + around do |ex| + Kaminari.config.default_per_page = 2 + ex.run + Kaminari.config.default_per_page = 50 + end + before do + item = create(:item, organization: organization) + purchase_1 = create(:purchase, organization: organization, comment: "Singleton", issued_at: 1.day.ago) + create(:line_item, item: item, itemizable: purchase_1, quantity: 2) + purchase_2 = create(:purchase, organization: organization, comment: "Twins", issued_at: 2.days.ago) + create(:line_item, item: item, itemizable: purchase_2, quantity: 2) + purchase_3 = create(:purchase, organization: organization, comment: "Fates", issued_at: 3.days.ago) + create(:line_item, item: item, itemizable: purchase_3, quantity: 2) + end + + it "puts the right number of purchases on the page" do + expect(subject.body).to include(" View").twice + end + end end context "csv" do @@ -48,11 +70,15 @@ describe "GET #new" do subject do + organization.update!(default_storage_location: storage_location) get new_purchase_path response end it { is_expected.to be_successful } + it "should include the storage location name" do + expect(subject.body).to include("Pawane Location") + end end describe "POST#create" do @@ -120,11 +146,10 @@ purchase_params = { source: "Purchase Site", line_items_attributes: line_item_params } expect do put purchase_path(id: purchase.id, purchase: purchase_params) - end.to change { purchase.storage_location.inventory_items.first.quantity }.by(5) - .and change { - View::Inventory.new(organization.id) - .quantity_for(storage_location: purchase.storage_location_id, item_id: line_item.item_id) - }.by(5) + end.to change { + View::Inventory.new(organization.id) + .quantity_for(storage_location: purchase.storage_location_id, item_id: line_item.item_id) + }.by(5) end describe "when removing a line item" do @@ -141,8 +166,7 @@ purchase_params = { source: "Purchase Site", line_items_attributes: line_item_params } expect do put purchase_path(id: purchase.id, purchase: purchase_params) - end.to change { purchase.storage_location.inventory_items.first.quantity }.by(-10) - .and change { + end.to change { View::Inventory.new(organization.id) .quantity_for(storage_location: purchase.storage_location_id, item_id: line_item.item_id) }.by(-10) @@ -169,36 +193,6 @@ end.to change { original_storage_location.size }.by(-10) # removes the whole purchase of 10 expect(new_storage_location.size).to eq 8 end - - # TODO this test is invalid in event-world since it's handled by the aggregate - it "rollsback updates if quantity would go below 0" do - next if Event.read_events?(organization) - - purchase = create(:purchase, :with_items, item_quantity: 10) - original_storage_location = purchase.storage_location - - # adjust inventory so that updating will set quantity below 0 - inventory_item = original_storage_location.inventory_items.last - inventory_item.quantity = 5 - inventory_item.save! - - new_storage_location = create(:storage_location) - line_item = purchase.line_items.first - line_item_params = { - "0" => { - "_destroy" => "false", - item_id: line_item.item_id, - quantity: "1", - id: line_item.id - } - } - purchase_params = { storage_location: new_storage_location, line_items_attributes: line_item_params } - put purchase_path(id: purchase.id, purchase: purchase_params) - expect(response).not_to redirect_to(anything) - expect(original_storage_location.size).to eq 5 - expect(new_storage_location.size).to eq 0 - expect(purchase.reload.line_items.first.quantity).to eq 10 - end end end diff --git a/spec/requests/reports/purchases_summary_requests_spec.rb b/spec/requests/reports/purchases_summary_requests_spec.rb index 3f9a47d2c4..cc9f5b3399 100644 --- a/spec/requests/reports/purchases_summary_requests_spec.rb +++ b/spec/requests/reports/purchases_summary_requests_spec.rb @@ -10,6 +10,10 @@ describe "GET #index" do it "shows a list of recent purchases" do get reports_purchases_summary_path + expect(response.body).to include("Total spent on diapers") + expect(response.body).to include("Total spent on adult incontinence") + expect(response.body).to include("Total spent on other") + expect(response.body).to include("Total items") expect(response.body).to include("Recent purchases") end end diff --git a/spec/requests/storage_locations_requests_spec.rb b/spec/requests/storage_locations_requests_spec.rb index ded6d9943d..648da84f14 100644 --- a/spec/requests/storage_locations_requests_spec.rb +++ b/spec/requests/storage_locations_requests_spec.rb @@ -9,7 +9,12 @@ end describe "GET #index" do - before { create(:storage_location, name: "Test Storage Location", address: "123 Donation Site Way", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } + let!(:storage_location) do + create(:storage_location, + name: "Test Storage Location", + address: "123 Donation Site Way", + warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) + end context "html" do let(:response_format) { 'html' } @@ -34,10 +39,61 @@ end end end + + context "with empty storage location" do + it "shows a deactivate button" do + get storage_locations_path(format: response_format) + page = Nokogiri::HTML(response.body) + deactivate_link = page.at_css("a[href='#{storage_location_deactivate_path(storage_location)}']") + expect(deactivate_link.attr("class")).not_to match(/disabled/) + end + end + + context "with nonempty storage location" do + before do + TestInventory.create_inventory(storage_location.organization, + { storage_location.id => { create(:item, name: "A").id => 1 } }) + end + + it "shows a disabled deactivate button" do + get storage_locations_path(format: response_format) + page = Nokogiri::HTML(response.body) + deactivate_link = page.at_css("a[href='#{storage_location_deactivate_path(storage_location)}']") + expect(deactivate_link.attr("class")).to match(/disabled/) + end + end end context "csv" do let(:response_format) { 'csv' } + + # Addresses used for storage locations must have associated geocoder stubs. + # See calls to Geocoder::Lookup::Test.add_stub in spec/rails_helper.rb + let(:storage_location_with_duplicate_item) { create(:storage_location, name: "Storage Location with Duplicate Items", address: "1500 Remount Road, Front Royal, VA 22630", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } + let(:storage_location_with_items) { create(:storage_location, name: "Storage Location with Items", address: "123 Donation Site Way", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } + let(:storage_location_with_unique_item) { create(:storage_location, name: "Storage Location with Unique Items", address: "Smithsonian Conservation Center new", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } + let(:item1) { create(:item, name: 'A') } + let(:item2) { create(:item, name: 'B') } + let(:item3) { create(:item, name: 'C') } + let(:item4) { create(:item, name: 'D') } + let!(:inactive_item) { create(:item, name: 'inactive item', active: false) } + + before do + TestInventory.create_inventory(storage_location_with_items.organization, { + storage_location_with_items.id => { + item1.id => 1, + item2.id => 1, + item3.id => 1 + }, + storage_location_with_duplicate_item.id => { + item3.id => 1 + }, + storage_location_with_unique_item.id => { + item4.id => 5 + } + }) + end + it "succeeds" do get storage_locations_path(format: response_format) expect(response).to be_successful @@ -45,6 +101,7 @@ it "includes headers followed by alphabetized item names" do storage_location_with_items = create(:storage_location) + Item.delete_all item1 = create(:item, name: 'C') item2 = create(:item, name: 'B') item3 = create(:item, name: 'A') @@ -66,48 +123,17 @@ expect(response.body.split("\n")[0]).to eq([StorageLocation.csv_export_headers, item3.name, item2.name, item1.name].join(',')) end - context "when read_events feature toggle is enabled" do - # Addresses used for storage locations must have associated geocoder stubs. - # See calls to Geocoder::Lookup::Test.add_stub in spec/rails_helper.rb - let(:storage_location_with_duplicate_item) { create(:storage_location, name: "Storage Location with Duplicate Items", address: "1500 Remount Road, Front Royal, VA 22630", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } - let(:storage_location_with_items) { create(:storage_location, name: "Storage Location with Items", address: "123 Donation Site Way", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } - let(:storage_location_with_unique_item) { create(:storage_location, name: "Storage Location with Unique Items", address: "Smithsonian Conservation Center new", warehouse_type: StorageLocation::WAREHOUSE_TYPES.first) } - let(:item1) { create(:item, name: 'A') } - let(:item2) { create(:item, name: 'B') } - let(:item3) { create(:item, name: 'C') } - let(:item4) { create(:item, name: 'D') } - let!(:inactive_item) { create(:item, name: 'inactive item', active: false) } - - before do - allow(Event).to receive(:read_events?).and_return(true) - - TestInventory.create_inventory(storage_location_with_items.organization, { - storage_location_with_items.id => { - item1.id => 1, - item2.id => 1, - item3.id => 1 - }, - storage_location_with_duplicate_item.id => { - item3.id => 1 - }, - storage_location_with_unique_item.id => { - item4.id => 5 - } - }) - end - - it "Generates csv with Storage Location fields, alphabetized item names, item quantities lined up in their columns, and zeroes for no inventory" do - get storage_locations_path(format: response_format) - # The first address below is quoted since it contains commas - csv = <<~CSV - Name,Address,Square Footage,Warehouse Type,Total Inventory,A,B,C,D - Storage Location with Duplicate Items,"1500 Remount Road, Front Royal, VA 22630",100,Residential space used,1,0,0,1,0 - Storage Location with Items,123 Donation Site Way,100,Residential space used,3,1,1,1,0 - Storage Location with Unique Items,Smithsonian Conservation Center new,100,Residential space used,5,0,0,0,5 - Test Storage Location,123 Donation Site Way,100,Residential space used,0,0,0,0,0 - CSV - expect(response.body).to eq(csv) - end + it "Generates csv with Storage Location fields, alphabetized item names, item quantities lined up in their columns, and zeroes for no inventory" do + get storage_locations_path(format: response_format) + # The first address below is quoted since it contains commas + csv = <<~CSV + Name,Address,Square Footage,Warehouse Type,Total Inventory,A,B,C,D + Storage Location with Duplicate Items,"1500 Remount Road, Front Royal, VA 22630",100,Residential space used,1,0,0,1,0 + Storage Location with Items,123 Donation Site Way,100,Residential space used,3,1,1,1,0 + Storage Location with Unique Items,Smithsonian Conservation Center new,100,Residential space used,5,0,0,0,5 + Test Storage Location,123 Donation Site Way,100,Residential space used,0,0,0,0,0 + CSV + expect(response.body).to eq(csv) end end end @@ -231,94 +257,63 @@ expect(response.body).to include("200") end - context "with version date set", versioning: true do - let(:inventory_item) { storage_location.inventory_items.first } - - context "with a version found" do - context "with events_read on" do - before(:each) { allow(Event).to receive(:read_events?).and_return(true) } - context "before active events" do - it "should show the version specified" do - travel 1.day do - inventory_item.update!(quantity: 100) - end - travel 1.week do - inventory_item.update!(quantity: 300) - end - travel 8.days do - SnapshotEvent.delete_all - SnapshotEvent.publish(organization) - end - travel 2.weeks do - get storage_location_path(storage_location, format: response_format, - version_date: 9.days.ago.to_date.to_fs(:db)) - expect(response).to be_successful - expect(response.body).to include("Smithsonian") - expect(response.body).to include("Test Item") - expect(response.body).to include("100") - end - end - end + context "with version date set" do + let!(:inventory_item) { + InventoryItem.create!(storage_location_id: storage_location.id, + item_id: item.id, + quantity: 200) + } - context "with active events" do - it 'should show the right version' do - travel 1.day do - TestInventory.create_inventory(organization, { - storage_location.id => { - item.id => 100, - item2.id => 0 - } - }) - end - travel 1.week do - TestInventory.create_inventory(organization, { - storage_location.id => { - item.id => 300, - item2.id => 0 - } - }) - end - travel 2.weeks do - get storage_location_path(storage_location, format: response_format, - version_date: 9.days.ago.to_date.to_fs(:db)) - expect(response).to be_successful - expect(response.body).to include("Smithsonian") - expect(response.body).to include("Test Item") - expect(response.body).to include("100") - end - end + context "before active events" do + it "should show the version specified" do + travel 1.day do + inventory_item.update!(quantity: 100) end - end - context "with events_read off" do - before(:each) { allow(Event).to receive(:read_events?).and_return(false) } - it "should show the version specified" do - travel 1.day do - inventory_item.update!(quantity: 100) - end - travel 1.week do - inventory_item.update!(quantity: 300) - end - travel 2.weeks do - get storage_location_path(storage_location, format: response_format, - version_date: 9.days.ago.to_date.to_fs(:db)) - expect(response).to be_successful - expect(response.body).to include("Smithsonian") - expect(response.body).to include("Test Item") - expect(response.body).to include("100") - end + travel 1.week do + inventory_item.update!(quantity: 300) + end + travel 8.days do + SnapshotEvent.delete_all + SnapshotEvent.publish(organization) + end + travel 2.weeks do + get storage_location_path(storage_location, format: response_format, + version_date: 9.days.ago.to_date.to_fs(:db)) + expect(response).to be_successful + expect(response.body).to include("Smithsonian") + expect(response.body).to include("Test Item") + expect(response.body).to include("100") end end end - context "with no version found" do - it "should show N/A" do - get storage_location_path(storage_location, format: response_format, - version_date: 1.week.ago.to_date.to_fs(:db)) - expect(response).to be_successful - expect(response.body).to include("Smithsonian") - expect(response.body).to include("Test Item") - # event world doesn't care about versions - expect(response.body).to include("N/A") unless Event.read_events?(organization) + context "with active events" do + it 'should show the right version' do + travel 1.day do + TestInventory.create_inventory(organization, { + storage_location.id => { + item.id => 100, + item2.id => 0 + } + }) + end + travel 1.week do + TestInventory.create_inventory(organization, { + storage_location.id => { + item.id => 300, + item2.id => 0 + } + }) + end + + travel 2.weeks do + get storage_location_path(storage_location, format: response_format, + version_date: 9.days.ago.to_date.to_fs(:db)) + expect(response).to be_successful + expect(response.body).to include("Smithsonian") + expect(response.body).to include("Test Item") + expect(response.body).to include("100") + end end end end @@ -378,8 +373,6 @@ def item_to_h(view_item) end let(:storage_location) { create(:storage_location, :with_items, organization: organization) } - let(:inventory_items_at_storage_location) { storage_location.inventory_items.map(&:to_h) } - let(:inactive_inventory_items) { organization.inventory_items.inactive.map(&:to_h) } let(:items_at_storage_location) do View::Inventory.new(organization.id).items_for_location(storage_location.id).map(&method(:item_to_h)) end @@ -393,12 +386,11 @@ def item_to_h(view_item) it "returns a collection that only includes items at the storage location" do get inventory_storage_location_path(storage_location, format: :json) expect(response.parsed_body).to eq(items_at_storage_location) - expect(response.parsed_body).to eq(inventory_items_at_storage_location) end it "returns items sorted alphabetically by item name" do get inventory_storage_location_path(storage_location, format: :json) - sorted_items = inventory_items_at_storage_location.sort_by { |item| item['item_name'].downcase } + sorted_items = items_at_storage_location.sort_by { |item| item['item_name'].downcase } expect(response.parsed_body).to eq(sorted_items) end end @@ -411,7 +403,6 @@ def item_to_h(view_item) get inventory_storage_location_path(storage_location, format: :json, include_deactivated_items: true) organization.items.first.update(active: true) expect(response.parsed_body).to eq(items_at_storage_location + inactive_items) - expect(response.parsed_body).to eq(inventory_items_at_storage_location + inactive_inventory_items) end end diff --git a/spec/services/adjustment_create_service_spec.rb b/spec/services/adjustment_create_service_spec.rb index 43a60cb668..1a8d21d573 100644 --- a/spec/services/adjustment_create_service_spec.rb +++ b/spec/services/adjustment_create_service_spec.rb @@ -9,8 +9,6 @@ let!(:storage_location) { create(:storage_location, :with_items, item_count: 2, item_quantity: 100, organization: organization) } let!(:item_1) { storage_location.items.first } let!(:item_2) { storage_location.items.second } - let!(:inventory_item_1) { InventoryItem.where(storage_location_id: storage_location.id, item_id: storage_location.items.first.id).first } - let!(:inventory_item_2) { InventoryItem.where(storage_location_id: storage_location.id, item_id: storage_location.items.second.id).first } # These can't be `let` variables because they need to be recalculated each time. def item1_inventory_quantity @@ -27,7 +25,7 @@ def item2_inventory_quantity expect do adjustment_params = {user_id: user.id, organization_id: organization.id, storage_location_id: storage_location.id, line_items_attributes: {"0": {item_id: storage_location.items.first.id, quantity: 5}}} subject.new(adjustment_params).call - end.to change { inventory_item_1.reload.quantity }.by(5).and change { item1_inventory_quantity }.by(5) + end.to change { item1_inventory_quantity }.by(5) expect(AdjustmentEvent.count).to eq(1) event = AdjustmentEvent.last expect(event.data).to eq(EventTypes::InventoryPayload.new( @@ -57,7 +55,7 @@ def item2_inventory_quantity expect do adjustment_params = {user_id: user.id, organization_id: organization.id, storage_location_id: storage_location.id, line_items_attributes: {"0": {item_id: storage_location.items.first.id, quantity: -5}}} subject.new(adjustment_params).call - end.to change { inventory_item_1.reload.quantity }.by(-5).and change { item1_inventory_quantity }.by(-5) + end.to change { item1_inventory_quantity }.by(-5) expect(AdjustmentEvent.count).to eq(1) event = AdjustmentEvent.last expect(event.data).to eq(EventTypes::InventoryPayload.new( @@ -95,7 +93,7 @@ def item2_inventory_quantity "2": {item_id: storage_location.items.first.id, quantity: 2} }} subject.new(adjustment_params).call - end.to change { inventory_item_1.reload.quantity }.by(1).and change { item1_inventory_quantity }.by(1) + end.to change { item1_inventory_quantity }.by(1) adjustment = Adjustment.last expect(adjustment.line_items.count).to eq(1) expect(adjustment.line_items[0].quantity).to eq(1) @@ -112,7 +110,7 @@ def item2_inventory_quantity "2": {item_id: item_1.id, quantity: 2} }} subject.new(adjustment_params).call - end.to change { inventory_item_1.reload.quantity }.by(-7).and change { item1_inventory_quantity }.by(-7) + end.to change { item1_inventory_quantity }.by(-7) adjustment = Adjustment.last expect(adjustment.line_items.count).to eq(1) expect(adjustment.line_items[0].quantity).to eq(-7) @@ -128,7 +126,7 @@ def item2_inventory_quantity "0": {item_id: item_1.id, quantity: quantity} }} subject.new(adjustment_params).call - end.to change { inventory_item_1.reload.quantity }.by(0).and change { item1_inventory_quantity }.by(0) + end.to change { item1_inventory_quantity }.by(0) end it "gives an error if we attempt to adjust inventory below 0" do @@ -141,7 +139,7 @@ def item2_inventory_quantity }} result = subject.new(adjustment_params).call expect(result.adjustment.errors.size).to be > 0 - expect(result.adjustment.errors[:inventory][0]).to include("items exceed the available inventory") + expect(result.adjustment.errors[:base][0]).to include("Could not reduce quantity") end it "handles adjustments to multiple items" do @@ -154,8 +152,6 @@ def item2_inventory_quantity "2": {item_id: item_1.id, quantity: -2} }} subject.new(adjustment_params).call - expect(inventory_item_1.reload.quantity).to eq(103) - expect(inventory_item_2.reload.quantity).to eq(102) adjustment = Adjustment.last expect(adjustment.line_items.count).to eq(2) line_item_1 = adjustment.line_items.where(item_id: item_1.id).first diff --git a/spec/services/allocate_kit_inventory_service_spec.rb b/spec/services/allocate_kit_inventory_service_spec.rb deleted file mode 100644 index 55d8ab4403..0000000000 --- a/spec/services/allocate_kit_inventory_service_spec.rb +++ /dev/null @@ -1,193 +0,0 @@ -RSpec.describe AllocateKitInventoryService, type: :service do - let(:organization) { create(:organization) } - let(:item) { create(:item, name: "Item", organization: organization, on_hand_minimum_quantity: 15) } - let(:item_out_of_stock) { create(:item, name: "Item out of stock", organization: organization, on_hand_minimum_quantity: 0) } - - let(:storage_location) { create(:storage_location, organization: organization) } - let(:item_inventory) { storage_location.inventory_items.where(item_id: item.id).first } - let(:item_out_of_stock_inventory) { storage_location.inventory_items.where(item_id: item_out_of_stock.id)&.first } - - before(:each) do - TestInventory.create_inventory(organization, { - storage_location.id => { - item.id => 15, - item_out_of_stock.id => 0 - } - }) - end - - describe "#error" do - let(:kit) { Kit.create(params) } - - context "when the Store location organization doesn't match" do - let(:wrong_organization) { create(:organization) } - let(:wrong_storage) { create(:storage_location, organization: wrong_organization) } - let(:params) do - { - organization_id: organization.id, - line_items_attributes: { - "0": { item_id: item_out_of_stock.id, quantity: 5 } - } - } - end - - it "returns error" do - service = AllocateKitInventoryService.new(kit: kit, storage_location: wrong_storage, increase_by: 1).allocate - expect(service.error).to include("Storage location kit doesn't match") - end - end - end - - describe "#allocate" do - let(:kit) do - kit_creation_service = KitCreateService.new(organization_id: organization.id, kit_params: params).tap(&:call) - kit_creation_service.kit - end - let(:kit_item_inventory) { InventoryItem.find_by(storage_location_id: storage_location.id, item_id: kit.item.id) } - let(:inventory_out) { - KitAllocation.find_by(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_out") - } - let(:inventory_in) { - KitAllocation.find_by(storage_location_id: storage_location.id, kit_id: kit.id, - organization_id: kit.organization.id, kit_allocation_type: "inventory_in") - } - - context "when inventory items are available" do - let(:quantity_of_items) { 1 } - let(:increase_by) { 2 } - let(:params) do - { - organization_id: organization.id, - name: Faker::Appliance.equipment, - line_items_attributes: { - "0": { item_id: item.id, quantity: quantity_of_items } - } - } - end - - it "allocates items, increases the quantity of the Kit Item and inventory in, and decreases inventory out" do - quantity_before_allocate = item_inventory.quantity - kit_quantity_before_allocate = kit_item_inventory.quantity - - service = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: increase_by).allocate - - # Check to see that the correct amount of the associated item - # had decreased in quantity - expect(item_inventory.reload.quantity).to eq(quantity_before_allocate - (quantity_of_items * increase_by)) - - # Check that the kit's item quantity was increased by the correct amount - expect(kit_item_inventory.reload.quantity).to eq(kit_quantity_before_allocate + increase_by) - inventory = View::Inventory.new(organization.id) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: item.id)).to eq(13) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: kit.item.id)).to eq(2) - - # Check that Inventory out decreased by allocated kit's line_items and their respective quantities - expect(inventory_out.line_items.count).to eq(kit.line_items.count) - expect(inventory_out.line_items.first.item_id).to eq(kit.line_items.first.item_id) - expect(inventory_out.line_items.first.quantity).to eq(kit.line_items.first.quantity * -increase_by) - - # Check inventory in increased by number of kits allocated - expect(inventory_in.line_items.first.quantity).to eq(increase_by) - - expect(service.error).to be_nil - end - - context "When kit is allocated more then once" do - let(:second_increase_by) { 3 } - - before do - item_inventory - @first_call = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: increase_by).allocate - @second_call = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: second_increase_by).allocate - end - - it "increases the already existed inventory in and decreases the already existed inventory out" do - expect(@first_call.error).to be_nil - expect(@second_call.error).to be_nil - - # Check inventory out decreases both time with the increase_by value - expect(inventory_out.line_items.first.quantity).to eq(kit.line_items.first.quantity * -(increase_by + second_increase_by)) - - # Check inventory in increase both time with increase_by value - expect(inventory_in.line_items.first.quantity).to eq(increase_by + second_increase_by) - end - end - - context "when there are more then one line items" do - let(:params) do - { - organization_id: organization.id, - name: Faker::Appliance.equipment, - line_items_attributes: { - "0": { item_id: item.id, quantity: quantity_of_items }, - "1": { item_id: item.id, quantity: 2 } - } - } - end - - context "when there kit is allocated once" do - before do - item_inventory - @first_call = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: increase_by).allocate - end - - it "inventory out for that kit contains both line_items with their respective quantity" do - expect(inventory_out.line_items[0].quantity).to eq(kit.line_items[0].quantity * -increase_by) - expect(inventory_out.line_items[1].quantity).to eq(kit.line_items[1].quantity * -increase_by) - end - end - - context "when same kit is allocated multiple times" do - let(:second_increase_by) { 3 } - before do - item_inventory - @first_call = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: increase_by).allocate - @second_call = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: second_increase_by).allocate - end - - it "inventory out for that kit contains both line_items with their respective quantity" do - expect(@first_call.error).to be_nil - expect(@second_call.error).to be_nil - - expect(inventory_in.line_items.first.quantity).to eq(increase_by + second_increase_by) - expect(inventory_out.line_items[0].quantity).to eq(kit.line_items[0].quantity * -(increase_by + second_increase_by)) - expect(inventory_out.line_items[1].quantity).to eq(kit.line_items[1].quantity * -(increase_by + second_increase_by)) - end - end - end - end - - context "when more than one kit is requested" do - let(:params) do - { - organization_id: organization.id, - name: Faker::Appliance.equipment, - line_items_attributes: { - "0": { item_id: item.id, quantity: quantity_of_items } - } - } - end - - context "but one of the items is out of stock" do - let(:quantity_of_items) { item_inventory.quantity } - let(:quantity_of_kits) { 2 } - - it "returns error and does not change kit or item quantity" do - kit_quantity_before_allocate = kit_item_inventory.quantity - - service = AllocateKitInventoryService.new(kit: kit, storage_location: storage_location, increase_by: quantity_of_kits).allocate - - message = Event.read_events?(organization) ? "Could not reduce quantity" : "items exceed the available inventory" - expect(service.error).to include(message) - - expect(item_inventory.reload.quantity).to eq(quantity_of_items) - expect(kit_item_inventory.reload.quantity).to eq(kit_quantity_before_allocate) - inventory = View::Inventory.new(organization.id) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: item.id)).to eq(quantity_of_items) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: kit.item.id)).to eq(kit_quantity_before_allocate) - end - end - end - end -end diff --git a/spec/services/deallocate_kit_inventory_service_spec.rb b/spec/services/deallocate_kit_inventory_service_spec.rb deleted file mode 100644 index 5ffc934e9a..0000000000 --- a/spec/services/deallocate_kit_inventory_service_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -RSpec.describe DeallocateKitInventoryService, type: :service do - let(:organization) { create(:organization) } - let(:item1) { create(:item, name: "Item11", organization: organization, on_hand_minimum_quantity: 5) } - let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 1) } - - let(:storage_location) { create(:storage_location, organization: organization) } - let(:item_inventory1) { storage_location.inventory_items.find_by(item_id: item1.id) } - let(:item_inventory2) { storage_location.inventory_items.find_by(item_id: item2.id) } - - let(:inventory_in) { create(:kit_allocation, storage_location: storage_location, organization_id: organization.id, kit_id: kit.id, kit_allocation_type: "inventory_in") } - let(:inventory_out) { create(:kit_allocation, storage_location: storage_location, organization_id: organization.id, kit_id: kit.id, kit_allocation_type: "inventory_out") } - - before(:each) do - TestInventory.create_inventory(organization, { - storage_location.id => { - item1.id => 5, - item2.id => 1 - } - }) - end - - describe "#error" do - let(:kit) do - kit_creation_service = KitCreateService.new(organization_id: organization.id, kit_params: params).tap(&:call) - kit_creation_service.kit - end - let(:kit_item_inventory) { InventoryItem.find_by(storage_location_id: storage_location.id, item_id: kit.item.id) } - - context "when the storage location organization doesn't match" do - let(:wrong_organization) { create(:organization) } - let(:wrong_storage) { create(:storage_location, organization: wrong_organization) } - let(:params) do - { - organization_id: organization.id, - name: Faker::Appliance.name, - line_items_attributes: { - "0": { item_id: item1.id, quantity: 5 } - } - } - end - - it "returns error" do - service = DeallocateKitInventoryService.new(kit: kit, storage_location: wrong_storage, decrease_by: 1).deallocate - expect(service.error).to include("Storage location kit doesn't match") - end - end - end - - describe "#deallocate" do - let(:kit) do - kit_creation_service = KitCreateService.new(organization_id: organization.id, kit_params: params).tap(&:call) - kit_creation_service.kit - end - let(:kit_item_inventory) { InventoryItem.find_by(storage_location_id: storage_location.id, item_id: kit.item.id) } - - before do - TestInventory.create_inventory(organization, - { - storage_location.id => { - kit.item.id => 100 - } - }) - end - - context "when inventory items are available" do - let(:decrease_by) { 2 } - let(:quantity_of_items1) { 2 } - let(:quantity_of_items2) { 3 } - let(:params) do - { - organization_id: organization.id, - name: Faker::Appliance.name, - line_items_attributes: { - "0": { item_id: item1.id, quantity: quantity_of_items1 }, - "1": { item_id: item2.id, quantity: quantity_of_items2 } - } - } - end - - context "when decrease_by is equal to kit's quantity in inventory_in" do - before do - inventory_out.line_items.create!(item_id: item1.id, quantity: -1 * (quantity_of_items1 * decrease_by)) - inventory_out.line_items.create!(item_id: item2.id, quantity: -1 * (quantity_of_items2 * decrease_by)) - inventory_in.line_items.create!(item_id: kit.item.id, quantity: decrease_by) - end - - it "increases the quantity of the loose items contained in the kit and decrease kit quantity, and delete inventory_in and inventory_out" do - before_deallocate1 = item_inventory1.quantity - before_deallocate2 = item_inventory2.quantity - kit_item_inventory_quantity = kit_item_inventory.quantity - service = DeallocateKitInventoryService.new(kit: kit, storage_location: storage_location, decrease_by: decrease_by).deallocate - - expect(service.error).to be_nil - - expect(item_inventory1.reload.quantity).to eq(before_deallocate1 + (quantity_of_items1 * decrease_by)) - expect(item_inventory2.reload.quantity).to eq(before_deallocate2 + (quantity_of_items2 * decrease_by)) - expect(kit_item_inventory.reload.quantity).to eq(kit_item_inventory_quantity - decrease_by) - inventory = View::Inventory.new(organization.id) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: item1.id)) - .to eq(before_deallocate1 + (quantity_of_items1 * decrease_by)) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: item2.id)) - .to eq(before_deallocate2 + (quantity_of_items2 * decrease_by)) - expect(inventory.quantity_for(storage_location: storage_location.id, item_id: kit.item.id)) - .to eq(kit_item_inventory_quantity - decrease_by) - - # Invetory out and inventory in should be deleted on de-allocation - expect(KitAllocation.find_by(id: inventory_in.id).present?).to be_falsey - expect(KitAllocation.find_by(id: inventory_out.id).present?).to be_falsey - end - end - - context "when decrease_by is less then kit's quantity in inventory_in" do - let(:inventory_quantity) { 3 } - before do - inventory_out.line_items.create!(item_id: item1.id, quantity: -1 * (quantity_of_items1 * inventory_quantity)) - inventory_out.line_items.create!(item_id: item2.id, quantity: -1 * (quantity_of_items2 * inventory_quantity)) - inventory_in.line_items.create!(item_id: kit.item.id, quantity: inventory_quantity) - end - - it "will add items in inventory_out and remove kits for decrease_by from inventory_in" do - first_line_item_quantity_before = inventory_out.reload.line_items[0].quantity - second_line_item_quantity_before = inventory_out.reload.line_items[1].quantity - kit_quantity = inventory_in.reload.line_items.first.quantity - service = DeallocateKitInventoryService.new(kit: kit, storage_location: storage_location, decrease_by: decrease_by).deallocate - - expect(service.error).to be_nil - - # inventoy out increased with decrease_by - expect(inventory_out.reload.line_items[0].quantity).to eq(first_line_item_quantity_before + (quantity_of_items1 * decrease_by)) - expect(inventory_out.reload.line_items[1].quantity).to eq(second_line_item_quantity_before + (quantity_of_items2 * decrease_by)) - - # invetory in decreased with decrease_by - expect(inventory_in.reload.line_items.first.quantity).to eq(kit_quantity - decrease_by) - end - end - - context "when decrease_by is greater then kit's quantity in inventory_in" do - before do - inventory_out.line_items.create!(item_id: item1.id, quantity: -1 * quantity_of_items1) - inventory_out.line_items.create!(item_id: item2.id, quantity: -1 * quantity_of_items2) - inventory_in.line_items.create!(item_id: kit.item.id, quantity: 1) - end - - it "raises error about inconsistent inventory in or out" do - service = DeallocateKitInventoryService.new(kit: kit, storage_location: storage_location, decrease_by: decrease_by).deallocate - - expect(service.error).to eq("Inconsistent inventory in") - end - end - - context "when kit_allocation not exist for inventory_in or inventory_out" do - let(:inventory_in) { nil } - let(:inventory_out) { nil } - - it "raises error about KitAllocation not found" do - service = DeallocateKitInventoryService.new(kit: kit, storage_location: storage_location, decrease_by: decrease_by).deallocate - expect(service.error).to eq("KitAllocation not found for given kit") - end - end - end - end -end diff --git a/spec/services/distribution_create_service_spec.rb b/spec/services/distribution_create_service_spec.rb index 3d1a7e91b7..32fb7d1ab8 100644 --- a/spec/services/distribution_create_service_spec.rb +++ b/spec/services/distribution_create_service_spec.rb @@ -101,8 +101,7 @@ it "preserves the Insufficiency error and is unsuccessful" do result = subject.new(too_much_dist).call - error_class = Event.read_events?(organization) ? InventoryError : Errors::InsufficientAllotment - expect(result.error).to be_instance_of(error_class) + expect(result.error).to be_instance_of(InventoryError) expect(result).not_to be_success end end @@ -124,8 +123,7 @@ it "preserves the Insufficiency error and is unsuccessful" do result = subject.new(too_much_dist).call - error_class = Event.read_events?(organization) ? InventoryError : Errors::InsufficientAllotment - expect(result.error).to be_instance_of(error_class) + expect(result.error).to be_instance_of(InventoryError) expect(result).not_to be_success end end diff --git a/spec/services/distribution_destroy_service_spec.rb b/spec/services/distribution_destroy_service_spec.rb index e765bc9a91..8845b6a14e 100644 --- a/spec/services/distribution_destroy_service_spec.rb +++ b/spec/services/distribution_destroy_service_spec.rb @@ -42,7 +42,6 @@ before do allow(distribution).to receive(:storage_location).and_return(fake_storage_location) allow(distribution).to receive(:line_item_values).and_return(fake_items) - allow(fake_storage_location).to receive(:increase_inventory) end it 'should destroy the Distribution' do @@ -57,7 +56,6 @@ it 'should increase the inventory of the storage location' do subject - expect(fake_storage_location).to have_received(:increase_inventory).with(fake_items) end end @@ -83,31 +81,8 @@ end context 'and the increase inventory operations fails' do - let(:fake_storage_location) { instance_double(StorageLocation) } - let(:fake_insufficient_allotment_error) do - Errors::InsufficientAllotment.new( - fake_error_message, - fake_insufficient_items - ) - end - let(:fake_error_message) { Faker::Lorem.sentence } - let(:fake_insufficient_items) do - [ - { - item_id: Faker::Number.number, - item: Faker::Lorem.word, - quantity_on_hand: Faker::Number.number, - quantity_requested: Faker::Number.number - } - ] - end - before do - allow(distribution).to receive(:storage_location).and_return(fake_storage_location) - allow(distribution).to receive(:line_item_values).and_return(fake_insufficient_items) - allow(fake_storage_location).to receive(:increase_inventory) - .with(fake_insufficient_items) - .and_raise(fake_insufficient_allotment_error) + allow(DistributionDestroyEvent).to receive(:publish).and_raise('OH NOES') end it 'should not delete the Distribution' do @@ -117,7 +92,7 @@ it 'should not be successful and have the error message' do result = subject expect(result).not_to be_success - expect(result.error).to be_instance_of(Errors::InsufficientAllotment) + expect(result.error.message).to eq('OH NOES') end end end diff --git a/spec/services/distributions_by_county_report_service_spec.rb b/spec/services/distributions_by_county_report_service_spec.rb index 1b330722ee..2a7a20e657 100644 --- a/spec/services/distributions_by_county_report_service_spec.rb +++ b/spec/services/distributions_by_county_report_service_spec.rb @@ -1,6 +1,6 @@ RSpec.describe DistributionByCountyReportService, type: :service do let(:year) { Time.current.year } - let(:issued_at_last_year) { Time.current.utc.change(year: year - 1).to_datetime } + let(:issued_at_last_year) { Time.current.change(year: year - 1).to_datetime } let(:distributions) { [] } include_examples "distribution_by_county" diff --git a/spec/services/donation_destroy_service_spec.rb b/spec/services/donation_destroy_service_spec.rb index 68a1251ee2..336effc659 100644 --- a/spec/services/donation_destroy_service_spec.rb +++ b/spec/services/donation_destroy_service_spec.rb @@ -37,80 +37,19 @@ end context 'when storage_location fails to decrease the inventory of the donation' do - let(:fake_organization) { instance_double(Organization, short_name: 'org_name', donations: fake_organization_donations) } - let(:fake_organization_donations) { instance_double('donations') } - let(:fake_donation) { instance_double(Donation, storage_location: fake_storage_location) } - let(:fake_storage_location) { instance_double(StorageLocation) } - let(:fake_insufficient_allotment_error) do - Errors::InsufficientAllotment.new( - fake_error_message, - fake_insufficient_items - ) - end - let(:fake_error_message) { Faker::Lorem.sentence } - let(:fake_insufficient_items) do - [ - { - item_id: Faker::Number.number, - item: Faker::Lorem.word, - quantity_on_hand: Faker::Number.number, - quantity_requested: Faker::Number.number - } - ] - end before do - allow(Organization).to receive(:find) - .with(organization_id) - .and_return(fake_organization) - allow(fake_organization_donations).to receive(:find) - .with(donation_id) - .and_return(fake_donation) - allow(fake_donation).to receive(:line_item_values).and_return(fake_insufficient_items) - allow(fake_storage_location).to receive(:decrease_inventory) - .with(fake_insufficient_items) - .and_raise(fake_insufficient_allotment_error) + allow(DonationDestroyEvent).to receive(:publish).and_raise('OH NOES') end it 'to not be a success' do result = subject.call expect(result).not_to be_success - expect(result.error).to be_instance_of(Errors::InsufficientAllotment) end end context 'when the donation destroy fails' do - let(:fake_organization) { instance_double(Organization, short_name: 'org_name', donations: fake_organization_donations) } - let(:fake_organization_donations) { instance_double('donations') } - let(:fake_donation) { - instance_double(Donation, - storage_location: fake_storage_location, - storage_location_id: 12, - id: 5, - line_items: [], - organization_id: organization_id) - } - let(:fake_storage_location) { instance_double(StorageLocation) } - let(:fake_insufficient_items) do - [ - { - item_id: Faker::Number.number, - item: Faker::Lorem.word, - quantity_on_hand: Faker::Number.number, - quantity_requested: Faker::Number.number - } - ] - end - before do - allow(Organization).to receive(:find) - .with(organization_id) - .and_return(fake_organization) - allow(fake_organization_donations).to receive(:find) - .with(donation_id) - .and_return(fake_donation) - allow(fake_donation).to receive(:line_item_values).and_return(fake_insufficient_items) - allow(fake_storage_location).to receive(:decrease_inventory).with(fake_insufficient_items) - allow(fake_donation).to receive(:destroy!).and_raise('boom') + allow(DonationDestroyEvent).to receive(:publish).and_raise('OH NOES') end it 'to not be a success' do diff --git a/spec/services/exports/export_request_service_spec.rb b/spec/services/exports/export_request_service_spec.rb index cca32a18ce..fb91b5e5d1 100644 --- a/spec/services/exports/export_request_service_spec.rb +++ b/spec/services/exports/export_request_service_spec.rb @@ -1,82 +1,318 @@ RSpec.describe Exports::ExportRequestService do let(:org) { create(:organization) } + let(:item_2t) { create :item, name: "2T Diapers" } let(:item_3t) { create :item, name: "3T Diapers" } + let(:item_4t) do + create :item, name: "4T Diapers" do |item| + create(:item_unit, item: item, name: "pack") + end + end + + let(:item_deleted1) { create :item, :inactive, name: "Inactive Diapers1" } + let(:item_deleted2) { create :item, :inactive, name: "Inactive Diapers2" } + + let!(:partner) { create :partner, organization: org, name: "Howdy Partner" } let!(:request_3t) do create(:request, :started, + :child, + :with_item_requests, organization: org, + partner: partner, request_items: [{ item_id: item_3t.id, quantity: 150 }]) end - let(:item_2t) { create :item, name: "2T Diapers" } let!(:request_2t) do create(:request, :fulfilled, + :individual, + :with_item_requests, organization: org, + partner: partner, request_items: [{ item_id: item_2t.id, quantity: 100 }]) end + let!(:request_with_deleted_items) do - create(:request, + request = create(:request, :fulfilled, + :with_item_requests, organization: org, - request_items: [{ item_id: 0, quantity: 200 }, { item_id: -1, quantity: 200 }]) - end - - let!(:unique_items) do - [ - {item_id: item_3t.id, quantity: 2}, - {item_id: item_2t.id, quantity: 3} - ] + partner: partner, + request_items: [{ item_id: item_deleted1.id, quantity: 200 }, { item_id: item_deleted2.id, quantity: 200 }]) + item_deleted1.delete + item_deleted2.delete + request.reload end - let!(:item_quantities) do - unique_items.each_with_object(Hash.new(0)) do |item, hsh| - hsh[item[:item_id]] += item[:quantity] - end - end - - let!(:request_with_items) do + let!(:request_with_multiple_items) do create( :request, :started, + :with_item_requests, organization: org, - request_items: unique_items + partner: partner, + request_items: [ + {item_id: item_3t.id, quantity: 2}, + {item_id: item_2t.id, quantity: 3}, + {item_id: item_4t.id, quantity: 4, request_unit: "pack"} + ] ) end + let!(:request_4t) do + create(:request, + :started, + :quantity, + :with_item_requests, + organization: org, + partner: partner, + request_items: [ + { item_id: item_4t.id, quantity: 77, request_unit: "" } + ]) + end + + let!(:request_4t_pack) do + create(:request, + :started, + :quantity, + :with_item_requests, + organization: org, + partner: partner, + request_items: [ + { item_id: item_4t.id, quantity: 1, request_unit: "pack" } + ]) + end + subject do described_class.new(Request.all).generate_csv_data end - describe ".generate_csv_data" do - let(:expected_headers) do - expected_headers_item_headers = [item_2t, item_3t].map(&:name).sort - expected_headers_item_headers << Exports::ExportRequestService::DELETED_ITEMS_COLUMN_HEADER - %w(Date Requestor Status) + expected_headers_item_headers + context "with custom units feature enabled" do + before do + Flipper.enable(:enable_packs) + end + + describe ".generate_csv_data" do + it "includes headers as the first row with ordered item names alphabetically with deleted item included at the end" do + expect(subject.first).to eq([ + "Date", + "Requestor", + "Type", + "Status", + "2T Diapers", + "3T Diapers", + "4T Diapers", + "4T Diapers - packs", + "" + ]) + end + + it "includes rows for each request" do + expect(subject.count).to eq(7) + end + + it "has expected data for the 3T Diapers request" do + expect(subject[1]).to eq([ + request_3t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Child", + "Started", + 0, # 2T Diapers + 150, # 3T Diapers + 0, # 4T Diapers + 0, # 4T Diapers - packs + 0 # + ]) + end + + it "has expected data for the 2T Diapers request" do + expect(subject[2]).to eq([ + request_2t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Individual", + "Fulfilled", + 100, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 0, # 4T Diapers - packs + 0 # + ]) + end + + it "has expected data for the request with deleted items" do + expect(subject[3]).to eq([ + request_with_deleted_items.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + nil, + "Fulfilled", + 0, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 0, # 4T Diapers - packs + 400 # + ]) + end + + it "has expected data for the request with multiple items" do + expect(subject[4]).to eq([ + request_with_multiple_items.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + nil, + "Started", + 3, # 2T Diapers + 2, # 3T Diapers + 0, # 4T Diapers + 4, # 4T Diapers - packs + 0 # + ]) + end + + it "has expected data for the request with 4T diapers without pack unit" do + expect(subject[5]).to eq([ + request_4t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Quantity", + "Started", + 0, # 2T Diapers + 0, # 3T Diapers + 77, # 4T Diapers + 0, # 4T Diapers - packs + 0 # + ]) + end + + it "has expected data for the request with 4T diapers with pack unit" do + expect(subject[6]).to eq([ + request_4t_pack.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Quantity", + "Started", + 0, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 1, # 4T Diapers - packs + 0 # + ]) + end + + it "has expected data even when the unit was deleted" do + item_4t.request_units.destroy_all + expect(subject[6]).to eq([ + request_4t_pack.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Quantity", + "Started", + 0, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 1, # 4T Diapers - packs + 0 # + ]) + end end + end - it "includes headers as the first row with ordered item names alphabetically with deleted item included at the end" do - expect(subject.first).to eq(expected_headers) + context "with custom units feature disabled" do + before do + Flipper.disable(:enable_packs) end - it "includes rows for each request with correct columns of item quantity" do - expect(subject.second).to include(request_3t.created_at.strftime("%m/%d/%Y").to_s) + describe ".generate_csv_data" do + it "includes headers as the first row with ordered item names alphabetically with deleted item included at the end" do + expect(subject.first).to eq([ + "Date", + "Requestor", + "Type", + "Status", + "2T Diapers", + "3T Diapers", + "4T Diapers", + "" + ]) + end + + it "includes rows for each request" do + expect(subject.count).to eq(7) + end + + it "has expected data for the 3T Diapers request" do + expect(subject[1]).to eq([ + request_3t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Child", + request_3t.status.humanize, + 0, # 2T Diapers + 150, # 3T Diapers + 0, # 4T Diapers + 0 # + ]) + end - item_2t_column_idx = expected_headers.each_with_index.to_h[item_2t.name] - item_3t_column_idx = expected_headers.each_with_index.to_h[item_3t.name] + it "has expected data for the 2T Diapers request" do + expect(subject[2]).to eq([ + request_2t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Individual", + "Fulfilled", + 100, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 0 # + ]) + end - expect(subject.second[item_3t_column_idx]).to eq(150) + it "has expected data for the request with deleted items" do + expect(subject[3]).to eq([ + request_with_deleted_items.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + nil, + "Fulfilled", + 0, # 2T Diapers + 0, # 3T Diapers + 0, # 4T Diapers + 400 # + ]) + end - expect(subject.third).to include(request_2t.created_at.strftime("%m/%d/%Y").to_s) - expect(subject.third[item_2t_column_idx]).to eq(100) + it "has expected data for the request with multiple items" do + expect(subject[4]).to eq([ + request_with_multiple_items.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + nil, + "Started", + 3, # 2T Diapers + 2, # 3T Diapers + 4, # 4T Diapers + 0 # + ]) + end - expect(subject.fourth).to include(request_3t.created_at.strftime("%m/%d/%Y").to_s) - item_column_idx = expected_headers.each_with_index.to_h[Exports::ExportRequestService::DELETED_ITEMS_COLUMN_HEADER] - expect(subject.fourth[item_column_idx]).to eq(400) + it "has expected data for the request with 4T diapers without pack unit" do + expect(subject[5]).to eq([ + request_4t.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Quantity", + "Started", + 0, # 2T Diapers + 0, # 3T Diapers + 77, # 4T Diapers + 0 # + ]) + end - expect(subject.fifth[item_2t_column_idx]).to eq(item_quantities[item_2t.id]) - expect(subject.fifth[item_3t_column_idx]).to eq(item_quantities[item_3t.id]) + it "has expected data for the request with 4T diapers with pack unit" do + expect(subject[6]).to eq([ + request_4t_pack.created_at.strftime("%m/%d/%Y").to_s, + "Howdy Partner", + "Quantity", + "Started", + 0, # 2T Diapers + 0, # 3T Diapers + 1, # 4T Diapers + 0 # + ]) + end end end end diff --git a/spec/services/historical_trend_service_spec.rb b/spec/services/historical_trend_service_spec.rb index 5b94b00fb4..99ce523206 100644 --- a/spec/services/historical_trend_service_spec.rb +++ b/spec/services/historical_trend_service_spec.rb @@ -4,22 +4,29 @@ let(:service) { described_class.new(organization.id, type) } describe "#series" do - let!(:item1) { create(:item, organization: organization, name: "Item 1") } - let!(:item2) { create(:item, organization: organization, name: "Item 2") } + let(:item1) { create(:item, organization: organization, name: "Item 1") } + let(:item2) { create(:item, organization: organization, name: "Item 2") } + let(:donation1) { create(:donation, organization:, issued_at: Date.current) } + let(:donation2) { create(:donation, organization:, issued_at: 2.months.ago) } let!(:line_items) do (0..11).map do |n| - create(:line_item, item: item1, itemizable_type: type, quantity: 10 * (n + 1), created_at: n.months.ago) + create(:line_item, item: item1, itemizable_type: type, itemizable_id: donation1.id, quantity: 10, created_at: n.months.ago) end end - let!(:line_item2) { create(:line_item, item: item2, itemizable_type: type, quantity: 60, created_at: 6.months.ago) } - let!(:line_item3) { create(:line_item, item: item2, itemizable_type: type, quantity: 30, created_at: 3.months.ago) } + let!(:line_item2) { create(:line_item, item: item2, itemizable_type: type, itemizable_id: donation2.id, quantity: 60, created_at: 6.months.ago) } + let!(:line_item3) { create(:line_item, item: item2, itemizable_type: type, itemizable_id: donation2.id, quantity: 30, created_at: 3.months.ago) } it "returns an array of items with their monthly data" do expected_result = [ - {name: "Item 1", data: [120, 110, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10], visible: false}, - {name: "Item 2", data: [0, 0, 0, 0, 0, 60, 0, 0, 30, 0, 0, 0], visible: false} + {name: "Item 1", data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120], visible: false}, + {name: "Item 2", data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 0, 0], visible: false} ] expect(service.series).to eq(expected_result) end + + it "the last data point is the quantity for the current month" do + item1_quantities = service.series.first[:data] + expect(item1_quantities.last).to be(line_items.pluck(:quantity).sum) + end end end diff --git a/spec/services/inventory_check_service_spec.rb b/spec/services/inventory_check_service_spec.rb index 7f6aa9efa6..9182d68eb8 100644 --- a/spec/services/inventory_check_service_spec.rb +++ b/spec/services/inventory_check_service_spec.rb @@ -3,56 +3,143 @@ subject { InventoryCheckService } describe "call" do - let(:item1) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } - let(:item2) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } - - context "error" do + context "when on hand quantity is below the minimum for the organization" do + let(:first_item) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let(:second_item) { create(:item, name: "Item 2", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } let(:storage_location) do storage_location = create(:storage_location, organization: organization) TestInventory.create_inventory(storage_location.organization, { storage_location.id => { - item1.id => 4, - item2.id => 4 + first_item.id => 4, + second_item.id => 4 } }) storage_location end + let(:distribution) { create(:distribution, storage_location_id: storage_location.id) } + before do + create(:line_item, item: first_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) + create(:line_item, item: second_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) + end it "should set the error" do - distribution = create(:distribution, storage_location_id: storage_location.id) - create(:line_item, item: item1, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) - create(:line_item, item: item2, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) - result = subject.new(distribution.reload).call - expect(result.error).to eq("The following items have fallen below the minimum on hand quantity: Item 1, Item 2") - expect(result.alert).to be_nil + expect(result.minimum_alert).to eq("The following items have fallen below the minimum on hand quantity, bank-wide: Item 1, Item 2") + expect(result.recommended_alert).to be_nil end end - context "alert" do - let(:storage_location) do + context "when on hand quantity is above the minimum for the organization" do + let(:available_item) { create(:item, name: "Available Item", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let!(:first_storage_location) do + first_storage_location = create(:storage_location, organization: organization) + TestInventory.create_inventory(first_storage_location.organization, { + first_storage_location.id => { + available_item.id => 9 + } + }) + + first_storage_location + end + let!(:second_storage_location) do + second_storage_location = create(:storage_location, organization: organization) + TestInventory.create_inventory(second_storage_location.organization, { + second_storage_location.id => { + available_item.id => 4 + } + }) + + second_storage_location + end + + context "when on hand quantity is below the minimum for one storage location" do + let(:distribution) { create(:distribution, storage_location_id: second_storage_location.id) } + before do + create(:line_item, item: available_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 2) + end + it "should not set the error" do + result = subject.new(distribution.reload).call + + expect(result.minimum_alert).to be_nil + end + end + end + + context "when on hand quantity is above the recommended amount for the organization" do + let(:first_item) { create(:item, name: "Item 1", organization: organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let!(:storage_location) do storage_location = create(:storage_location, organization: organization) TestInventory.create_inventory(storage_location.organization, { storage_location.id => { - item1.id => 9, - item2.id => 9 + first_item.id => 20 + } + }) + + storage_location + end + let(:distribution) { create(:distribution, storage_location_id: storage_location.id) } + before do + create(:line_item, item: first_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 2) + end + it "should not set the alert" do + result = subject.new(distribution.reload).call + + expect(result.recommended_alert).to be_nil + expect(result.minimum_alert).to be_nil + end + + context "when on hand quantity is below the recommended amount for one storage location" do + let!(:second_storage_location) do + second_storage_location = create(:storage_location, organization: organization) + TestInventory.create_inventory(second_storage_location.organization, { + second_storage_location.id => { + first_item.id => 8 + } + }) + + second_storage_location + end + let(:distribution) { create(:distribution, storage_location_id: second_storage_location.id) } + before do + create(:line_item, item: first_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 2) + end + it "should not set the alert" do + result = subject.new(distribution.reload).call + + expect(result.recommended_alert).to be_nil + expect(result.minimum_alert).to be_nil + end + end + end + + context "when on hand quantity is below the recommended amount for the organization" do + let(:somewhat_stocked_organization) { create(:organization) } + let(:first_item) { create(:item, name: "Item 1", organization: somewhat_stocked_organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let(:second_item) { create(:item, name: "Item 2", organization: somewhat_stocked_organization, on_hand_minimum_quantity: 5, on_hand_recommended_quantity: 10) } + let!(:storage_location) do + storage_location = create(:storage_location, organization: somewhat_stocked_organization) + TestInventory.create_inventory(storage_location.organization, { + storage_location.id => { + first_item.id => 9, + second_item.id => 9 } }) storage_location end + let(:distribution) { create(:distribution, storage_location_id: storage_location.id) } + before do + create(:line_item, item: first_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) + create(:line_item, item: second_item, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) + end it "should set the alert" do - distribution = create(:distribution, storage_location_id: storage_location.id) - create(:line_item, item: item1, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) - create(:line_item, item: item2, itemizable_type: "Distribution", itemizable_id: distribution.id, quantity: 16) - result = subject.new(distribution.reload).call - expect(result.alert).to eq("The following items have fallen below the recommended on hand quantity: Item 1, Item 2") - expect(result.error).to be_nil + expect(result.recommended_alert).to eq("The following items have fallen below the recommended on hand quantity, bank-wide: Item 1, Item 2") + expect(result.minimum_alert).to be_nil end end end diff --git a/spec/services/item_create_service_spec.rb b/spec/services/item_create_service_spec.rb index d4e172f1ba..54e005ea44 100644 --- a/spec/services/item_create_service_spec.rb +++ b/spec/services/item_create_service_spec.rb @@ -24,10 +24,6 @@ allow(organization).to receive(:items).and_return(fake_organization_items) allow(fake_organization_items).to receive(:new).with(item_params).and_return(fake_organization_item) allow(organization).to receive(:storage_locations).and_return(fake_organization_storage_locations) - - # Add a spy on InventoryItem to ensure this method is being - # called in our specs and service object. - allow(InventoryItem).to receive(:create!) end context 'when there are no issues' do @@ -43,13 +39,6 @@ # Assert that the service object calls the expected method. expect(fake_organization_item).to have_received(:save!) - fake_organization_storage_locations.each do |sl| - expect(InventoryItem).to have_received(:create!).with( - storage_location_id: sl.id, - item_id: fake_organization_item.id, - quantity: 0 - ) - end end end @@ -79,24 +68,6 @@ expect(subject.error).to eq(fake_error) end end - - context 'because a inventory item creation failed and raised an error' do - let(:fake_error) { StandardError.new('random-error') } - - before do - allow(InventoryItem).to receive(:create!).with( - storage_location_id: fake_organization_storage_locations.first.id, - item_id: fake_organization_item.id, - quantity: 0 - ).and_raise(fake_error) - end - - it 'should return a OpenStruct with the raised error' do - expect(subject).to be_a_kind_of(OpenStruct) - expect(subject.success?).to eq(false) - expect(subject.error).to eq(fake_error) - end - end end end end diff --git a/spec/services/itemizable_update_service_spec.rb b/spec/services/itemizable_update_service_spec.rb index 2eac909587..77ba98fee1 100644 --- a/spec/services/itemizable_update_service_spec.rb +++ b/spec/services/itemizable_update_service_spec.rb @@ -47,7 +47,6 @@ subject do described_class.call(itemizable: itemizable, params: attributes, - type: :increase, event_class: DonationEvent) end @@ -114,7 +113,7 @@ end subject do - described_class.call(itemizable: itemizable, params: attributes, type: :decrease) + described_class.call(itemizable: itemizable, params: attributes, event_class: DistributionEvent) end it "should update quantity in same storage location" do @@ -159,9 +158,6 @@ end describe "events" do - before(:each) do - allow(Event).to receive(:read_events?).and_return(true) - end describe "with donations" do let(:itemizable) do line_items = [ @@ -174,9 +170,6 @@ line_items: line_items, issued_at: 1.day.ago) end - before(:each) do - allow(Event).to receive(:read_events?).and_return(true) - end let(:attributes) do { issued_at: 2.days.ago, @@ -188,7 +181,7 @@ expect(DonationEvent.count).to eq(1) expect(View::Inventory.total_inventory(organization.id)).to eq(60) - described_class.call(itemizable: itemizable, params: attributes, type: :increase, event_class: DonationEvent) + described_class.call(itemizable: itemizable, params: attributes, event_class: DonationEvent) expect(DonationEvent.count).to eq(2) expect(View::Inventory.total_inventory(organization.id)).to eq(95) @@ -198,7 +191,7 @@ expect(DonationEvent.count).to eq(0) expect(View::Inventory.total_inventory(organization.id)).to eq(40) - described_class.call(itemizable: itemizable, params: attributes, type: :increase, event_class: DonationEvent) + described_class.call(itemizable: itemizable, params: attributes, event_class: DonationEvent) expect(DonationEvent.count).to eq(0) expect(UpdateExistingEvent.count).to eq(1) @@ -235,7 +228,7 @@ expect(DistributionEvent.count).to eq(1) expect(View::Inventory.total_inventory(organization.id)).to eq(40) - described_class.call(itemizable: itemizable, params: attributes, type: :decrease, event_class: DistributionEvent) + described_class.call(itemizable: itemizable, params: attributes, event_class: DistributionEvent) expect(DistributionEvent.count).to eq(2) expect(View::Inventory.total_inventory(organization.id)).to eq(42) @@ -245,7 +238,7 @@ expect(DistributionEvent.count).to eq(0) expect(View::Inventory.total_inventory(organization.id)).to eq(50) - described_class.call(itemizable: itemizable, params: attributes, type: :decrease, event_class: DistributionEvent) + described_class.call(itemizable: itemizable, params: attributes, event_class: DistributionEvent) expect(DistributionEvent.count).to eq(0) expect(UpdateExistingEvent.count).to eq(1) diff --git a/spec/services/partner_profile_update_service_spec.rb b/spec/services/partner_profile_update_service_spec.rb index 25b2446bc5..97108d73ba 100644 --- a/spec/services/partner_profile_update_service_spec.rb +++ b/spec/services/partner_profile_update_service_spec.rb @@ -88,7 +88,7 @@ expect(profile.served_areas.size).to eq(2) result = PartnerProfileUpdateService.new(profile.partner, partner_params, incorrect_attributes_missing_client_share).call expect(result.success?).to eq(false) - expect(result.error.to_s).to include("Validation failed: Served areas client share is not a number, Served areas client share is not included in the list") + expect(result.error.to_s).to include("Validation failed: Served areas client share is not a number, Served areas client share Client share must be between 1 and 100 inclusive") profile.reload expect(profile.served_areas.size).to eq(2) end diff --git a/spec/services/partners/family_request_create_service_spec.rb b/spec/services/partners/family_request_create_service_spec.rb index a51e5ff9a7..1cbe16d5bf 100644 --- a/spec/services/partners/family_request_create_service_spec.rb +++ b/spec/services/partners/family_request_create_service_spec.rb @@ -96,6 +96,28 @@ expect(first_item_request.quantity.to_i).to eq(first_item_request.item.default_quantity) expect(second_item_request.quantity.to_i).to eq(second_item_request.item.default_quantity * 4) end + + context "with for_families false" do + let(:for_families) { false } + + it "creates a request of type individual" do + expect { subject }.to change { Request.count }.by(1) + + partner_request = Request.last + expect(partner_request.request_type).to eq("individual") + end + end + + context "with for_families true" do + let(:for_families) { true } + + it "creates a request of type child" do + expect { subject }.to change { Request.count }.by(1) + + partner_request = Request.last + expect(partner_request.request_type).to eq("child") + end + end end context 'without children' do @@ -120,7 +142,7 @@ let(:fake_request_create_service) { instance_double(Partners::RequestCreateService, call: -> {}, errors: [], partner_request: -> {}) } before do - allow(Partners::RequestCreateService).to receive(:new).with(partner_user_id: partner_user.id, comments: comments, for_families: false, item_requests_attributes: contain_exactly(*expected_item_request_attributes)).and_return(fake_request_create_service) + allow(Partners::RequestCreateService).to receive(:new).with(partner_user_id: partner_user.id, request_type: "individual", comments: comments, item_requests_attributes: contain_exactly(*expected_item_request_attributes)).and_return(fake_request_create_service) end it 'should send the correct request payload to the Partners::RequestCreateService and call it' do diff --git a/spec/services/partners/request_create_service_spec.rb b/spec/services/partners/request_create_service_spec.rb index 6da6d9be68..fbc9be8fb0 100644 --- a/spec/services/partners/request_create_service_spec.rb +++ b/spec/services/partners/request_create_service_spec.rb @@ -4,11 +4,13 @@ let(:args) do { partner_user_id: partner_user.id, + request_type: request_type, comments: comments, item_requests_attributes: item_requests_attributes } end let(:partner_user) { partner.primary_user } + let(:request_type) { nil } let(:partner) { create(:partner) } let(:comments) { Faker::Lorem.paragraph } let(:item_requests_attributes) do @@ -89,6 +91,36 @@ expect(Partners::ItemRequest.count).to eq(item_requests_attributes.count) end + context "when request_type is child" do + let(:request_type) { "child" } + + it "creates a request with of that type" do + expect { subject }.to change { Request.count }.by(1) + + expect(Request.last.request_type).to eq("child") + end + end + + context "when request_type is individual" do + let(:request_type) { "individual" } + + it "creates a request with of that type" do + expect { subject }.to change { Request.count }.by(1) + + expect(Request.last.request_type).to eq("individual") + end + end + + context "when request_type is quantity" do + let(:request_type) { "quantity" } + + it "creates a request with of that type" do + expect { subject }.to change { Request.count }.by(1) + + expect(Request.last.request_type).to eq("quantity") + end + end + context 'when we have duplicate item as part of request' do let(:duplicate_item) { FactoryBot.create(:item) } let(:unique_item) { FactoryBot.create(:item) } @@ -149,12 +181,14 @@ let(:args) do { partner_user_id: partner_user.id, + request_type: request_type, comments: comments, item_requests_attributes: item_requests_attributes } end let(:partner_user) { partner.primary_user } let(:partner) { create(:partner) } + let(:request_type) { "child" } let(:comments) { Faker::Lorem.paragraph } let(:item) { FactoryBot.create(:item) } let(:item_requests_attributes) do diff --git a/spec/services/partners/section_error_service_spec.rb b/spec/services/partners/section_error_service_spec.rb new file mode 100644 index 0000000000..b2c6b3435b --- /dev/null +++ b/spec/services/partners/section_error_service_spec.rb @@ -0,0 +1,47 @@ +require "rails_helper" + +RSpec.describe Partners::SectionErrorService, type: :service do + describe ".sections_with_errors" do + subject { Partners::SectionErrorService.sections_with_errors(error_keys) } + + context "when error keys map to multiple sections" do + let(:error_keys) { [:website, :pick_up_email, :enable_quantity_based_requests] } + + it "returns an array with each section containing an error" do + expect(subject).to contain_exactly("media_information", "pick_up_person", "partner_settings") + end + end + + context "when error keys map to the same section multiple times" do + let(:error_keys) { [:website, :twitter, :facebook] } + + it "returns a unique array with only one instance of the section" do + expect(subject).to eq(["media_information"]) + end + end + + context "when error keys include fields not mapped to any section" do + let(:error_keys) { [:website, :unknown_field, :enable_quantity_based_requests] } + + it "excludes nil values for unmapped fields and returns unique sections" do + expect(subject).to eq(["media_information", "partner_settings"]) + end + end + + context "when none of the error keys match any section" do + let(:error_keys) { [:unknown_field_1, :unknown_field_2] } + + it "returns an empty array when no sections match" do + expect(subject).to be_empty + end + end + + context "when error keys are empty" do + let(:error_keys) { [] } + + it "returns an empty array" do + expect(subject).to eq([]) + end + end + end +end diff --git a/spec/services/reports/children_served_report_service_spec.rb b/spec/services/reports/children_served_report_service_spec.rb index e660760693..88b66dc536 100644 --- a/spec/services/reports/children_served_report_service_spec.rb +++ b/spec/services/reports/children_served_report_service_spec.rb @@ -105,64 +105,4 @@ }) end end - describe "#disposable_diapers_from_kits_total" do - it "calculates the number of disposable diapers that have been distributed within kits this year" do - organization = create(:organization) - - # create disposable/ nondisposable base items - create(:base_item, name: "Toddler Disposable Diaper", partner_key: "toddler diapers", category: "disposable diaper") - create(:base_item, name: "Infant Disposable Diaper", partner_key: "infant diapers", category: "infant disposable diaper") - create(:base_item, name: "Infant Cloth Diaper", partner_key: "infant cloth diapers", category: "cloth diaper") - create(:base_item, name: "Adult Brief LXL Test", partner_key: "adult lxl test", category: "Diapers - Adult Briefs") - - # create disposable/ nondisposable items - toddler_disposable_kit_item = create(:item, name: "Toddler Disposable Diaper", partner_key: "toddler diapers", organization: organization) - infant_disposable_kit_item = create(:item, name: "Infant Disposable Diapers", partner_key: "infant diapers", organization: organization) - infant_cloth_kit_item = create(:item, name: "Infant Cloth Diapers", partner_key: "infant cloth diapers", organization: organization) - adult_brief_kit_item = create(:item, name: "Adult Brief L/XL", partner_key: "adult lxl test", organization: organization) - - # create line items that contain the d/nd items - toddler_disposable_line_item = create(:line_item, item: toddler_disposable_kit_item, quantity: 5) - infant_disposable_line_item = create(:line_item, item: infant_disposable_kit_item, quantity: 5) - infant_cloth_line_item = create(:line_item, item: infant_cloth_kit_item, quantity: 5) - adult_brief_line_item = create(:line_item, item: adult_brief_kit_item, quantity: 5) - - # create kits that contain the d/nd line items - toddler_disposable_kit = create(:kit, organization: organization, line_items: [toddler_disposable_line_item]) - infant_disposable_kit = create(:kit, organization: organization, line_items: [infant_disposable_line_item]) - infant_cloth_kit = create(:kit, organization: organization, line_items: [infant_cloth_line_item]) - adult_brief_kit = create(:kit, organization: organization, line_items: [adult_brief_line_item]) - - # create items which have the kits - create(:base_item, name: "Unrelated Base", partner_key: "unrelated base", category: "unrelated base") - infant_disposable_dist_item = create(:item, name: "Dist Item 1", organization: organization, partner_key: "unrelated base", kit: toddler_disposable_kit) - toddler_disposable_dist_item = create(:item, name: "Dist Item 2", organization: organization, partner_key: "unrelated base", kit: infant_disposable_kit) - infant_cloth_dist_item = create(:item, name: "Dist Item 3", organization: organization, partner_key: "unrelated base", kit: infant_cloth_kit) - adult_brief_dist_item = create(:item, name: "Dist Item 4", organization: organization, partner_key: "unrelated base", kit: adult_brief_kit) - - within_time = Time.zone.parse("2020-05-31 14:00:00") - - # create empty distributions - infant_distribution = create(:distribution, organization: organization, issued_at: within_time) - toddler_distribution = create(:distribution, organization: organization, issued_at: within_time) - adult_distribution = create(:distribution, organization: organization, issued_at: within_time) - - # add line items to distributions which contain the d/nd kits - create(:line_item, quantity: 10, item: toddler_disposable_dist_item, itemizable: toddler_distribution) - create(:line_item, quantity: 10, item: infant_disposable_dist_item, itemizable: infant_distribution) - create(:line_item, quantity: 10, item: infant_cloth_dist_item, itemizable: infant_distribution) - create(:line_item, quantity: 10, item: adult_brief_dist_item, itemizable: adult_distribution) - - service = described_class.new(organization: organization, year: within_time.year) - - # Find distributions, that has a - # Line item, that has an - # Item, which has a - # Kit, which has a - # Line item, which has an - # Item, which is a disposable diaper. - # And then add all those quantities up - expect(service.disposable_diapers_from_kits_total).to eq(100) - end - end end diff --git a/spec/services/requests_total_items_service_spec.rb b/spec/services/requests_total_items_service_spec.rb index 54c5480962..dd25bd6187 100644 --- a/spec/services/requests_total_items_service_spec.rb +++ b/spec/services/requests_total_items_service_spec.rb @@ -4,48 +4,72 @@ subject { described_class.new(requests: requests).calculate } context 'when the request items is not blank' do - let(:sample_items) { create_list(:item, 3, organization: organization) } + let(:sample_items) do + create_list(:item, 3, :with_unit, organization: organization, unit: "bundle") do |item, n| + item.name = "item_name_#{n}" + item.save! + end + end let(:item_names) { sample_items.pluck(:name) } let(:item_ids) { sample_items.pluck(:id) } let(:requests) do - [ - create(:request, request_items: item_ids.map { |k| { "item_id" => k, "quantity" => 20 } }), - create(:request, request_items: item_ids.map { |k| { "item_id" => k, "quantity" => 10 } }) + local_requests = [ + create(:request, :with_item_requests, request_items: item_ids.map { |k| { "item_id" => k, "quantity" => 20 } }), + create(:request, :with_item_requests, request_items: item_ids.map { |k| { "item_id" => k, "quantity" => 10, "request_unit" => "bundle" } }), + create(:request, :with_item_requests, request_items: item_ids.map { |k| { "item_id" => k, "quantity" => 50, "request_unit" => "bundle" } }) ] + Request.where(id: local_requests.map(&:id)) end it 'return items with correct quantities calculated' do - expect(subject.first.last).to eq(30) + expect(subject.first.last).to eq(80) end it 'return the names of items correctly' do - expect(subject.keys).to eq(item_names) + expect(subject.keys).to eq([ + "item_name_0", + "item_name_1", + "item_name_2" + ]) end - end - context 'when item name is nil' do - let(:item) do - i = build(:item, name: nil) - i.save(validate: false) - i - end - let(:requests) do - [create(:request, request_items: [{ "item_id" => item.id, "quantity" => 20 }])] - end + context 'when custom request units are specified and enabled' do + before do + Flipper.enable(:enable_packs) + end + + it 'returns the names of items correctly' do + expect(subject.keys).to eq([ + "item_name_0", + "item_name_1", + "item_name_2", + "item_name_0 - bundles", + "item_name_1 - bundles", + "item_name_2 - bundles" + ]) + end - it 'return Unknown Item' do - expect(subject.first.first).to eq('*Unknown Item*') + it 'returns items with correct quantities calculated' do + expect(subject).to eq({ + "item_name_0" => 20, + "item_name_0 - bundles" => 60, + "item_name_1" => 20, + "item_name_1 - bundles" => 60, + "item_name_2" => 20, + "item_name_2 - bundles" => 60 + }) + end end end context 'when provided with requests that have no request items' do - let(:requests) { [create(:request, request_items: {})] } + let(:requests) { Request.where(id: [create(:request, :with_item_requests, request_items: {})].map(&:id)) } it { is_expected.to be_blank } end context 'when provided requests is nil' do - let(:requests) { nil } + let(:requests) { Request.where(id: nil) } it { is_expected.to be_blank } end diff --git a/spec/services/transfer_destroy_service_spec.rb b/spec/services/transfer_destroy_service_spec.rb index 388b57d1e4..0e75af925a 100644 --- a/spec/services/transfer_destroy_service_spec.rb +++ b/spec/services/transfer_destroy_service_spec.rb @@ -7,8 +7,8 @@ # Create a double StorageLocation that behaves like how we want to use # it within the service object. The benefit is that we aren't testing # ActiveRecord and the database. - let(:fake_from) { instance_double(StorageLocation, increase_inventory: -> {}, id: 1) } - let(:fake_to) { instance_double(StorageLocation, decrease_inventory: -> {}, id: 1) } + let(:fake_from) { instance_double(StorageLocation, id: 1) } + let(:fake_to) { instance_double(StorageLocation, id: 1) } let(:fake_items) do [ { @@ -30,12 +30,6 @@ allow(transfer).to receive(:to).and_return(fake_to) allow(transfer).to receive(:destroy!) allow(transfer).to receive(:line_item_values).and_return(fake_items) - - # Now that that the `transfer.from` and `transfer.to` is stubbed - # to return the doubles of StorageLocation, we must program them - # to expect the `increase_inventory` and `decrease_inventory` - allow(fake_from).to receive(:increase_inventory).with(fake_items) - allow(fake_to).to receive(:decrease_inventory).with(fake_items) end context 'when there are no issues' do @@ -50,8 +44,6 @@ subject # Assert that the service object calls the expected method. - expect(fake_from).to have_received(:increase_inventory).with(fake_items) - expect(fake_to).to have_received(:decrease_inventory).with(fake_items) expect(transfer).to have_received(:destroy!) end end @@ -69,32 +61,15 @@ end end - context 'because undoing the transfer inventory changes by increasing the inventory of `from` failed' do - let(:fake_error) { Errors::InsufficientAllotment.new('msg') } - + context 'because the event publish failed' do before do - allow(transfer).to receive(:line_item_values).and_return(fake_items) - allow(fake_from).to receive(:increase_inventory).with(fake_items).and_raise(fake_error) + allow(TransferDestroyEvent).to receive(:publish).and_raise('OH NOES') end it 'should return a OpenStruct with the raised error' do expect(subject).to be_a_kind_of(OpenStruct) expect(subject.success?).to eq(false) - expect(subject.error).to eq(fake_error) - end - end - - context 'because undoing the transfer inventory changes by decreasing the inventory of `to` failed' do - let(:fake_error) { Errors::InsufficientAllotment.new('random-error') } - before do - allow(transfer).to receive(:line_item_values).and_return(fake_items) - allow(fake_to).to receive(:decrease_inventory).with(transfer.line_item_values).and_raise(fake_error) - end - - it 'should return a OpenStruct with the raised error' do - expect(subject).to be_a_kind_of(OpenStruct) - expect(subject.success?).to eq(false) - expect(subject.error).to eq(fake_error) + expect(subject.error.message).to eq('OH NOES') end end diff --git a/spec/support/date_range_picker_shared_example.rb b/spec/support/date_range_picker_shared_example.rb index 554b3b4f7d..54ce71e7a8 100644 --- a/spec/support/date_range_picker_shared_example.rb +++ b/spec/support/date_range_picker_shared_example.rb @@ -20,11 +20,30 @@ def date_range_picker_select_range(range_name) let(:user) { create(:user, organization: organization) } let!(:very_old) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2000, 7, 31), :organization => organization) } + let!(:two_months_ago) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2019, 5, 31), :organization => organization) } let!(:recent) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2019, 7, 24), :organization => organization) } let!(:today) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2019, 7, 31), :organization => organization) } + let!(:one_month_ahead) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2019, 8, 31), :organization => organization) } let!(:one_year_ahead) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2020, 7, 31), :organization => organization) } let!(:two_years_ahead) { create(described_class.to_s.underscore.to_sym, date_field.to_sym => Time.zone.local(2021, 7, 31), :organization => organization) } + context "when choosing 'Default'" do + before do + sign_out user + travel_to Time.zone.local(2019, 7, 31) + sign_in user + end + + after do + travel_back + end + + it "shows only 4 records" do + visit subject + expect(page).to have_css("table tbody tr", count: 4) + end + end + context "when choosing 'All Time'" do before do sign_out user @@ -41,7 +60,7 @@ def date_range_picker_select_range(range_name) date_range = "#{Time.zone.local(1919, 7, 1).to_formatted_s(:date_picker)} - #{Time.zone.local(2020, 7, 31).to_formatted_s(:date_picker)}" fill_in "filters_date_range", with: date_range find(:id, 'filters_date_range').native.send_keys(:enter) - expect(page).to have_css("table tbody tr", count: 4) + expect(page).to have_css("table tbody tr", count: 6) end end diff --git a/spec/support/distribution_by_county_shared_example.rb b/spec/support/distribution_by_county_shared_example.rb index 89ff48a9a2..5ae26c67ad 100644 --- a/spec/support/distribution_by_county_shared_example.rb +++ b/spec/support/distribution_by_county_shared_example.rb @@ -6,7 +6,7 @@ let(:organization_admin) { create(:organization_admin, organization: organization) } let(:item_1) { create(:item, value_in_cents: 1050, organization: organization) } - let(:issued_at_present) { Time.current.utc.to_datetime } + let(:issued_at_present) { Time.current.to_datetime } let(:partner_1) { p1 = create(:partner, organization: organization) p1.profile.served_areas << create_list(:partners_served_area, 4, partner_profile: p1.profile, client_share: 25) diff --git a/spec/system/adjustment_system_spec.rb b/spec/system/adjustment_system_spec.rb index e2c6a2b9d6..62838e697d 100644 --- a/spec/system/adjustment_system_spec.rb +++ b/spec/system/adjustment_system_spec.rb @@ -75,7 +75,7 @@ expect do click_button "Save" end.not_to change { storage_location.size } - expect(page).to have_content("items exceed the available inventory") + expect(page).to have_content("Could not reduce quantity") end it "politely informs the user if they try to adjust down below zero, even if they use multiple adjustments to do so" do @@ -101,7 +101,7 @@ expect do click_button "Save" end.not_to change { storage_location.size } - expect(page).to have_content("items exceed the available inventory") + expect(page).to have_content("Could not reduce quantity") expect(page).to have_field("adjustment_line_items_attributes_0_quantity", with: "-18") end end diff --git a/spec/system/audit_system_spec.rb b/spec/system/audit_system_spec.rb index 4b7fa5bd0c..69a0218ece 100644 --- a/spec/system/audit_system_spec.rb +++ b/spec/system/audit_system_spec.rb @@ -48,6 +48,88 @@ find('option', text: item.name.to_s) end + + context "when adding new items" do + let!(:existing_barcode) { create(:barcode_item) } + let(:item_with_barcode) { existing_barcode.item } + + it "allows user to add items by barcode" do + visit new_audit_path + + within "#audit_line_items" do + # Scan existing barcode + expect(page).to have_xpath("//input[@id='_barcode-lookup-0']") + Barcode.boop(existing_barcode.value) + + # Ensure item quantity and name have been filled in + expect(page).to have_field "_barcode-lookup-0", with: existing_barcode.value + expect(page).to have_field "audit_line_items_attributes_0_quantity", with: existing_barcode.quantity.to_s + expect(page).to have_field "audit_line_items_attributes_0_item_id", with: existing_barcode.item.id.to_s + end + end + + it "allows auditing items that are not in a storage location", :js do + item = create(:item, name: "TestItemNotInStorageLocation", organization: organization) + audit_quantity = 1234 + visit new_audit_path + + await_select2("#audit_line_items_attributes_0_item_id") do + select storage_location.name, from: "Storage location" + end + select item.name, from: "audit_line_items_attributes_0_item_id" + fill_in "audit_line_items_attributes_0_quantity", with: audit_quantity + + accept_confirm do + click_button "Confirm Audit" + end + expect(page.find(".alert-info")).to have_content "Audit is confirmed" + expect(page).to have_content(item.name) + expect(page).to have_content(audit_quantity) + + accept_confirm do + click_link "Finalize Audit" + end + expect(page.find(".alert-info")).to have_content "Audit is Finalized" + + event = Event.last + expect(event.type).to eq "AuditEvent" + event_line_item = Event.last.data.items.first + expect(event_line_item.item_id).to eq item.id + expect(event_line_item.quantity).to eq audit_quantity + end + + it "allows user to add items that do not yet have a barcode", :js do + item_without_barcode = create(:item) + new_barcode = "00000000" + new_item_name = item_without_barcode.name + + visit new_audit_path + + # Scan new barcode + within "#audit_line_items" do + expect(page).to have_xpath("//input[@id='_barcode-lookup-0']") + Barcode.boop(new_barcode) + end + + # Item lookup finds no barcode and responds by prompting user to choose an item and quantity + within "#newBarcode" do + fill_in "Quantity", with: 10 + select new_item_name, from: "Item" + expect(page).to have_field("barcode_item_quantity", with: '10') + expect(page).to have_field("barcode_item_value", with: new_barcode) + click_on "Save" + end + + within "#audit_line_items" do + # Ensure item fields have been filled in + expect(page).to have_field "audit_line_items_attributes_0_quantity", with: '10' + expect(page).to have_field "audit_line_items_attributes_0_item_id", with: item_without_barcode.id.to_s + + # Ensure new line item was added and has focus + expect(page).to have_field("_barcode-lookup-1", focused: true) + end + end + end end context "when viewing the audits index" do diff --git a/spec/system/distribution_system_spec.rb b/spec/system/distribution_system_spec.rb index 0b310d76f5..5a98ee1ba0 100644 --- a/spec/system/distribution_system_spec.rb +++ b/spec/system/distribution_system_spec.rb @@ -233,7 +233,7 @@ end expect(page).not_to have_content('New Distribution') - expect(page).to have_content("The following items have fallen below the minimum on hand quantity: #{item.name}") + expect(page).to have_content("The following items have fallen below the minimum on hand quantity, bank-wide: #{item.name}") end end @@ -268,7 +268,7 @@ click_button "Yes, it's correct" end - expect(page).to have_content("The following items have fallen below the recommended on hand quantity: #{item.name}") + expect(page).to have_content("The following items have fallen below the recommended on hand quantity, bank-wide: #{item.name}") end end @@ -304,8 +304,7 @@ end.not_to change { Distribution.count } expect(page).to have_content("New Distribution") - message = Event.read_events?(organization) ? 'Could not reduce quantity' : 'items exceed the available inventory' - expect(page.find(".alert")).to have_content message + expect(page.find(".alert")).to have_content('Could not reduce quantity') end end context "when there is a default storage location" do @@ -386,8 +385,7 @@ click_on "Save", match: :first end.not_to change { distribution.line_items.first.quantity } within ".alert" do - message = Event.read_events?(organization) ? 'Could not reduce quantity' : 'items exceed the available inventory' - expect(page).to have_content message + expect(page).to have_content('Could not reduce quantity') end end @@ -581,16 +579,10 @@ click_on "Save" expect(page).to have_no_content "Distribution updated!" - message = 'items exceed the available inventory' - number = 999_999 - if Event.read_events?(organization) - message = 'Could not reduce quantity' - number = 999_899 - end - expect(page).to have_content(/#{message}/i) - expect(page).to have_content number, count: 1 + expect(page).to have_content(/Could not reduce quantity/i) + expect(page).to have_content 999_899, count: 1 within ".alert" do - expect(page).to have_content number + expect(page).to have_content 999_899 end expect(Distribution.first.line_items.count).to eq 1 end @@ -885,7 +877,6 @@ expect(page).to have_content("Distribution Complete") expect(page).to have_link("Distribution Complete") - expect(storage_location.inventory_items.first.quantity).to eq(0) expect(View::Inventory.new(organization.id) .quantity_for(item_id: item.id, storage_location: storage_location.id)).to eq(0) diff --git a/spec/system/distributions_by_county_system_spec.rb b/spec/system/distributions_by_county_system_spec.rb index a9ca7c8de9..808e7fa29a 100644 --- a/spec/system/distributions_by_county_system_spec.rb +++ b/spec/system/distributions_by_county_system_spec.rb @@ -1,8 +1,8 @@ RSpec.feature "Distributions by County", type: :system do include_examples "distribution_by_county" - let(:year) { Time.current.year } - let(:issued_at_last_year) { Time.current.utc.change(year: year - 1).to_datetime } + let(:current_year) { Time.current.year } + let(:issued_at_last_year) { Time.current.change(year: current_year - 1).to_datetime } before do sign_in(user) @@ -18,8 +18,9 @@ partner_1.profile.served_areas.each do |served_area| expect(page).to have_text(served_area.county.name) end - expect(page).to have_text("50", count: 4) - expect(page).to have_text("$525.00", count: 4) + + expect(page).to have_css("table tbody tr td", text: "50", exact_text: true, count: 4) + expect(page).to have_css("table tbody tr td", text: "$525.00", exact_text: true, count: 4) end it("works for this year") do @@ -31,8 +32,53 @@ partner_1.profile.served_areas.each do |served_area| expect(page).to have_text(served_area.county.name) end - expect(page).to have_text("25", count: 4) - expect(page).to have_text("$262.50", count: 4) + + expect(page).to have_css("table tbody tr td", text: "25", exact_text: true, count: 4) + expect(page).to have_css("table tbody tr td", text: "$262.50", exact_text: true, count: 4) + end + + it("works for prior year") do + # Should NOT return distribution issued before previous calendar year + last_day_of_two_years_ago = Time.current.beginning_of_day.change(year: current_year - 2, month: 12, day: 31).to_datetime + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: last_day_of_two_years_ago) + + # Should return distribution issued during previous calendar year + one_year_ago = issued_at_last_year + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: one_year_ago) + + # Should NOT return distribution issued after previous calendar year + first_day_of_current_year = Time.current.end_of_day.change(year: current_year, month: 1, day: 1).to_datetime + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: first_day_of_current_year) + + visit_distribution_by_county_with_specified_date_range("Prior Year") + + partner_1.profile.served_areas.each do |served_area| + expect(page).to have_text(served_area.county.name) + end + expect(page).to have_css("table tbody tr td", text: "25", exact_text: true, count: 4) + expect(page).to have_css("table tbody tr td", text: "$262.50", exact_text: true, count: 4) + end + + it("works for last 12 months") do + # Should NOT return disitribution issued before 12 months ago + one_year_and_one_day_ago = 1.year.ago.prev_day.beginning_of_day.to_datetime + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: one_year_and_one_day_ago) + + # Should return distribution issued during previous 12 months + today = issued_at_present + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: today) + + # Should NOT return distribution issued in the future + tomorrow = 1.day.from_now.end_of_day.to_datetime + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: tomorrow) + + visit_distribution_by_county_with_specified_date_range("Last 12 Months") + + partner_1.profile.served_areas.each do |served_area| + expect(page).to have_text(served_area.county.name) + end + expect(page).to have_css("table tbody tr td", text: "25", exact_text: true, count: 4) + expect(page).to have_css("table tbody tr td", text: "$262.50", exact_text: true, count: 4) end end diff --git a/spec/system/donation_system_spec.rb b/spec/system/donation_system_spec.rb index e635d3c89e..b38710011a 100644 --- a/spec/system/donation_system_spec.rb +++ b/spec/system/donation_system_spec.rb @@ -453,6 +453,9 @@ qty = page.find(:xpath, '//input[@id="donation_line_items_attributes_0_quantity"]').value expect(qty).to eq(@existing_barcode.quantity.to_s) + + # the form should add another empty line + expect(page).to have_field("_barcode-lookup-1", focused: true) end it "Updates the line item when the same barcode is scanned twice", :js do diff --git a/spec/system/kit_system_spec.rb b/spec/system/kit_system_spec.rb index 5a3750e16d..c79a6d5a38 100644 --- a/spec/system/kit_system_spec.rb +++ b/spec/system/kit_system_spec.rb @@ -52,6 +52,32 @@ expect(page).to have_content("#{quantity_per_kit} #{item.name}") end + it "can add items correctly" do + visit new_kit_path + new_barcode = "1234567890" + quantity = "1" + + find(:id, "_barcode-lookup-0").set(new_barcode).send_keys(:enter) + + within "#newBarcode" do + expect(page).to have_field("Quantity", with: "") + fill_in "Quantity", with: quantity + + expect(page).to have_field("Item", with: "") + select(Item.last.name, from: "barcode_item[barcodeable_id]") + end + + within ".modal-footer" do + click_button "Save" + end + + expect(page).to have_content("Barcode Added to Inventory") + # Check that item details have been filled in via javascript + expect(page).to have_field("kit_line_items_attributes_0_quantity", with: quantity) + # Check that new field has been added via javascript + expect(page).to have_css(".line_item_section", count: 2) + end + it 'can allocate and deallocate quantity per storage location from kit index' do visit kits_path @@ -62,16 +88,9 @@ original_item_1_count = inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id) original_item_2_count = inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id) - original_ii_kit_count = existing_kit.inventory_items.find_by(storage_location_id: storage_location.id)&.quantity || 0 - original_ii_item_1_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id).quantity - original_ii_item_2_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id).quantity - expect(original_kit_count).to eq(0) - expect(original_kit_count).to eq(original_ii_kit_count) expect(original_item_1_count).to eq(50) - expect(original_item_1_count).to eq(original_ii_item_1_count) expect(original_item_2_count).to eq(50) - expect(original_item_2_count).to eq(original_ii_item_2_count) select storage_location.name, from: 'kit_adjustment_storage_location_id' @@ -83,12 +102,9 @@ inventory.reload # Check that the kit quantity increased by the expected amount - expect(existing_kit.reload.inventory_items.find_by(storage_location_id: storage_location.id).quantity).to eq(2) expect(inventory.quantity_for(item_id: existing_kit.item.id, storage_location: storage_location.id)).to eq(2) # Ensure each of the contained items decrease the correct amount - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id).quantity).to eq(40) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id).quantity).to eq(44) expect(inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id)).to eq(40) expect(inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id)).to eq(44) @@ -100,11 +116,6 @@ click_on 'Save' inventory.reload - # Ensure each of the contained items decrease the correct amount - expect(existing_kit.reload.inventory_items.find_by(storage_location_id: storage_location.id).quantity).to eq(0) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id).quantity).to eq(50) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id).quantity).to eq(50) - expect(inventory.quantity_for(item_id: existing_kit.item.id, storage_location: storage_location.id)).to eq(0) expect(inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id)).to eq(50) expect(inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id)).to eq(50) @@ -138,16 +149,9 @@ original_item_1_count = inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id) original_item_2_count = inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id) - original_ii_kit_count = existing_kit.inventory_items.find_by(storage_location_id: storage_location.id)&.quantity || 0 - original_ii_item_1_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id)&.quantity || 0 - original_ii_item_2_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id)&.quantity || 0 - expect(original_kit_count).to eq(0) - expect(original_kit_count).to eq(original_ii_kit_count) expect(original_item_1_count).to eq(0) - expect(original_item_1_count).to eq(original_ii_item_1_count) expect(original_item_2_count).to eq(0) - expect(original_item_2_count).to eq(original_ii_item_2_count) select storage_location.name, from: 'kit_adjustment_storage_location_id' @@ -157,10 +161,6 @@ click_on 'Save' inventory.reload - expect(existing_kit.reload.inventory_items.find_by(storage_location_id: storage_location.id)&.quantity || 0).to eq(0) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id)&.quantity || 0).to eq(0) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id)&.quantity || 0).to eq(0) - expect(inventory.quantity_for(item_id: existing_kit.item.id, storage_location: storage_location.id)).to eq(0) expect(inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id)).to eq(0) expect(inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id)).to eq(0) @@ -189,16 +189,9 @@ original_item_1_count = inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id) original_item_2_count = inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id) - original_ii_kit_count = existing_kit.inventory_items.find_by(storage_location_id: storage_location.id)&.quantity || 0 - original_ii_item_1_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id).quantity - original_ii_item_2_count = storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id).quantity - expect(original_kit_count).to eq(0) - expect(original_kit_count).to eq(original_ii_kit_count) expect(original_item_1_count).to eq(50) - expect(original_item_1_count).to eq(original_ii_item_1_count) expect(original_item_2_count).to eq(50) - expect(original_item_2_count).to eq(original_ii_item_2_count) select storage_location.name, from: 'kit_adjustment_storage_location_id' @@ -208,10 +201,6 @@ click_on 'Save' inventory.reload - expect(existing_kit.reload.inventory_items.find_by(storage_location_id: storage_location.id)&.quantity || 0).to eq(0) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_1.id).quantity).to eq(50) - expect(storage_location.inventory_items.find_by(item_id: existing_kit_item_2.id).quantity).to eq(50) - expect(inventory.quantity_for(item_id: existing_kit.item.id, storage_location: storage_location.id)).to eq(0) expect(inventory.quantity_for(item_id: existing_kit_item_1.id, storage_location: storage_location.id)).to eq(50) expect(inventory.quantity_for(item_id: existing_kit_item_2.id, storage_location: storage_location.id)).to eq(50) diff --git a/spec/system/partner_system_spec.rb b/spec/system/partner_system_spec.rb index b54b9fc42b..59c6d771e5 100644 --- a/spec/system/partner_system_spec.rb +++ b/spec/system/partner_system_spec.rb @@ -380,6 +380,106 @@ end end + describe "#edit_profile" do + let!(:partner) { create(:partner, name: "Frank") } + subject { edit_profile_path(partner.id) } + + context "when step-wise editing is enabled" do + before do + Flipper.enable(:partner_step_form) + visit subject + end + + it "displays all sections in a closed state by default" do + within ".accordion" do + expect(page).to have_css("#agency_information.accordion-collapse.collapse", visible: false) + expect(page).to have_css("#program_delivery_address.accordion-collapse.collapse", visible: false) + + partner.partials_to_show.each do |partial| + expect(page).to have_css("##{partial}.accordion-collapse.collapse", visible: false) + end + end + end + + it "allows sections to be opened, closed, filled in any order, and reviewed" do + # Media + find("button[data-bs-target='#media_information']").click + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + within "#media_information" do + fill_in "Website", with: "https://www.example.com" + end + find("button[data-bs-target='#media_information']").click + expect(page).to have_css("#media_information.accordion-collapse.collapse", visible: false) + + # Executive director + find("button[data-bs-target='#executive_director']").click + expect(page).to have_css("#executive_director.accordion-collapse.collapse.show", visible: true) + within "#executive_director" do + fill_in "Executive Director Name", with: "Lisa Smith" + end + + # Save Progress + all("input[type='submit'][value='Save Progress']").last.click + expect(page).to have_css(".alert-success", text: "Details were successfully updated.") + + # Save and Review + all("input[type='submit'][value='Save and Review']").last.click + expect(current_path).to eq(partner_path(partner.id)) + expect(page).to have_css(".alert-success", text: "Details were successfully updated.") + end + + it "displays the edit view with sections containing validation errors expanded" do + # Open up Media section and clear out website value + find("button[data-bs-target='#media_information']").click + within "#media_information" do + fill_in "Website", with: "" + end + + # Open Pick up person section and fill in 4 email addresses + find("button[data-bs-target='#pick_up_person']").click + within "#pick_up_person" do + fill_in "Pick Up Person's Email", with: "email1@example.com, email2@example.com, email3@example.com, email4@example.com" + end + + # Open Partner Settings section and uncheck all options + find("button[data-bs-target='#partner_settings']").click + within "#partner_settings" do + uncheck "Enable Quantity-based Requests" if has_checked_field?("Enable Quantity-based Requests") + uncheck "Enable Child-based Requests (unclick if you only do bulk requests)" if has_checked_field?("Enable Child-based Requests (unclick if you only do bulk requests)") + uncheck "Enable Requests for Individuals" if has_checked_field?("Enable Requests for Individuals") + end + + # Save Progress + all("input[type='submit'][value='Save Progress']").last.click + + # Expect an alert-danger message containing validation errors + expect(page).to have_css(".alert-danger", text: /There is a problem/) + expect(page).to have_content("No social media presence must be checked if you have not provided any of Website, Twitter, Facebook, or Instagram.") + expect(page).to have_content("Enable child based requests At least one request type must be set") + expect(page).to have_content("Pick up email can't have more than three email addresses") + + # Expect media section, executive director section, and partner settings section to be opened + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#pick_up_person.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#partner_settings.accordion-collapse.collapse.show", visible: true) + + # Try to Submit and Review from error state + all("input[type='submit'][value='Save and Review']").last.click + + # Expect an alert-danger message containing validation errors + expect(page).to have_css(".alert-danger", text: /There is a problem/) + expect(page).to have_content("No social media presence must be checked if you have not provided any of Website, Twitter, Facebook, or Instagram.") + expect(page).to have_content("Enable child based requests At least one request type must be set") + expect(page).to have_content("Pick up email can't have more than three email addresses") + + # Expect media section, executive director section, and partner settings section to be opened + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#pick_up_person.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#partner_settings.accordion-collapse.collapse.show", visible: true) + end + end + end + describe "#approve_partner" do let(:tooltip_message) do "Partner has not requested approval yet. Partners are able to request approval by going into 'My Organization' and clicking 'Request Approval' button." diff --git a/spec/system/partners/approval_process_spec.rb b/spec/system/partners/approval_process_spec.rb index 26d69cbcd4..24b78341f2 100644 --- a/spec/system/partners/approval_process_spec.rb +++ b/spec/system/partners/approval_process_spec.rb @@ -28,7 +28,7 @@ before do click_on 'My Profile' assert page.has_content? 'Uninvited' - click_on 'Update Information' + all('a', text: 'Update Information').last.click fill_in 'Other Agency Type', with: 'Lorem' @@ -41,7 +41,7 @@ click_on 'Update Information' assert page.has_content? 'Details were successfully updated.' - find_link(text: 'Submit for Approval').click + all('a', text: 'Submit for Approval').last.click assert page.has_content? 'You have submitted your details for approval.' assert page.has_content? 'Awaiting Review' end @@ -77,7 +77,7 @@ login_as(partner_user) visit partner_user_root_path click_on 'My Profile' - click_on 'Submit for Approval' + all('a', text: 'Submit for Approval').last.click end it "should render an error message", :aggregate_failures do diff --git a/spec/system/partners/profile_edit_system_spec.rb b/spec/system/partners/profile_edit_system_spec.rb new file mode 100644 index 0000000000..3d35a5e580 --- /dev/null +++ b/spec/system/partners/profile_edit_system_spec.rb @@ -0,0 +1,102 @@ +RSpec.describe "Partners profile edit", type: :system, js: true do + let!(:partner1) { create(:partner, status: "invited") } + let(:partner1_user) { partner1.primary_user } + + context "step-wise editing is enabled" do + before do + Flipper.enable(:partner_step_form) + login_as(partner1_user) + visit edit_partners_profile_path + end + + it "displays all sections in a closed state by default" do + within ".accordion" do + expect(page).to have_css("#agency_information.accordion-collapse.collapse", visible: false) + expect(page).to have_css("#program_delivery_address.accordion-collapse.collapse", visible: false) + + partner1.partials_to_show.each do |partial| + expect(page).to have_css("##{partial}.accordion-collapse.collapse", visible: false) + end + + expect(page).to have_css("#partner_settings.accordion-collapse.collapse", visible: false) + end + end + + it "allows sections to be opened, closed, filled in any order, and submit for approval" do + # Media + find("button[data-bs-target='#media_information']").click + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + within "#media_information" do + fill_in "Website", with: "https://www.example.com" + end + find("button[data-bs-target='#media_information']").click + expect(page).to have_css("#media_information.accordion-collapse.collapse", visible: false) + + # Executive director + find("button[data-bs-target='#executive_director']").click + expect(page).to have_css("#executive_director.accordion-collapse.collapse.show", visible: true) + within "#executive_director" do + fill_in "Executive Director Name", with: "Lisa Smith" + end + + # Save Progress + all("input[type='submit'][value='Save Progress']").last.click + expect(page).to have_css(".alert-success", text: "Details were successfully updated.") + + # Submit and Review + all("input[type='submit'][value='Save and Review']").last.click + expect(current_path).to eq(partners_profile_path) + expect(page).to have_css(".alert-success", text: "Details were successfully updated.") + end + + it "displays the edit view with sections containing validation errors expanded" do + # Open up Media section and clear out website value + find("button[data-bs-target='#media_information']").click + within "#media_information" do + fill_in "Website", with: "" + end + + # Open Pick up person section and fill in 4 email addresses + find("button[data-bs-target='#pick_up_person']").click + within "#pick_up_person" do + fill_in "Pick Up Person's Email", with: "email1@example.com, email2@example.com, email3@example.com, email4@example.com" + end + + # Open Partner Settings section and uncheck all options + find("button[data-bs-target='#partner_settings']").click + within "#partner_settings" do + uncheck "Enable Quantity-based Requests" if has_checked_field?("Enable Quantity-based Requests") + uncheck "Enable Child-based Requests (unclick if you only do bulk requests)" if has_checked_field?("Enable Child-based Requests (unclick if you only do bulk requests)") + uncheck "Enable Requests for Individuals" if has_checked_field?("Enable Requests for Individuals") + end + + # Save Progress + all("input[type='submit'][value='Save Progress']").last.click + + # Expect an alert-danger message containing validation errors + expect(page).to have_css(".alert-danger", text: /There is a problem/) + expect(page).to have_content("No social media presence must be checked if you have not provided any of Website, Twitter, Facebook, or Instagram.") + expect(page).to have_content("Enable child based requests At least one request type must be set") + expect(page).to have_content("Pick up email can't have more than three email addresses") + + # Expect media section, executive director section, and partner settings section to be opened + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#pick_up_person.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#partner_settings.accordion-collapse.collapse.show", visible: true) + + # Try to Submit and Review from error state + all("input[type='submit'][value='Save and Review']").last.click + + # Expect an alert-danger message containing validation errors + expect(page).to have_css(".alert-danger", text: /There is a problem/) + expect(page).to have_content("No social media presence must be checked if you have not provided any of Website, Twitter, Facebook, or Instagram.") + expect(page).to have_content("Enable child based requests At least one request type must be set") + expect(page).to have_content("Pick up email can't have more than three email addresses") + + # Expect media section, executive director section, and partner settings section to be opened + expect(page).to have_css("#media_information.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#pick_up_person.accordion-collapse.collapse.show", visible: true) + expect(page).to have_css("#partner_settings.accordion-collapse.collapse.show", visible: true) + end + end +end diff --git a/spec/system/product_drive_system_spec.rb b/spec/system/product_drive_system_spec.rb index c60bc0122a..6f24b8409a 100644 --- a/spec/system/product_drive_system_spec.rb +++ b/spec/system/product_drive_system_spec.rb @@ -29,7 +29,7 @@ it "Shows the expected filters with the expected values and in alphabetical order for name filter" do expect(page.find("select[name='filters[by_name]']").find(:xpath, 'option[2]').text).to eq "Alpha Test name 3" expect(page.has_select?('filters[by_name]', with_options: @product_drives.map(&:name))).to be true - expect(page.has_field?('filters_date_range', with: this_year)) + expect(page.has_field?('filters_date_range', with: default_date)) end it "shows the expected product drives" do diff --git a/spec/system/request_system_spec.rb b/spec/system/request_system_spec.rb index d274d2cda0..ce79f92089 100644 --- a/spec/system/request_system_spec.rb +++ b/spec/system/request_system_spec.rb @@ -25,11 +25,11 @@ subject { requests_path } before do - create(:request, :started, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '12' }]) - create(:request, :started, partner: partner1, request_items: [{ "item_id": item2.id, "quantity": '13' }]) - create(:request, :started, partner: partner2, request_items: [{ "item_id": item1.id, "quantity": '14' }]) - create(:request, :fulfilled, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '15' }]) - create(:request, :pending, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '16' }]) + create(:request, :with_item_requests, :started, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '12' }]) + create(:request, :with_item_requests, :started, partner: partner1, request_items: [{ "item_id": item2.id, "quantity": '13' }]) + create(:request, :with_item_requests, :started, partner: partner2, request_items: [{ "item_id": item1.id, "quantity": '14' }]) + create(:request, :with_item_requests, :fulfilled, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '15' }]) + create(:request, :with_item_requests, :pending, partner: partner1, request_items: [{ "item_id": item1.id, "quantity": '16' }]) end it "lists requests" do diff --git a/spec/system/storage_location_system_spec.rb b/spec/system/storage_location_system_spec.rb index 15b460f483..c579901546 100644 --- a/spec/system/storage_location_system_spec.rb +++ b/spec/system/storage_location_system_spec.rb @@ -134,8 +134,7 @@ location1 = create(:storage_location, :with_items) visit subject - expect(accept_confirm { click_on "Deactivate", match: :first }).to include "Are you sure you want to deactivate #{location1.name}" - expect(page.find(".alert")).to have_content "Cannot deactivate storage location containing inventory items with non-zero quantities" + expect(page).to have_link('Deactivate', class: "disabled", href: "/storage_locations/#{location1.id}/deactivate") end it "Allows user to deactivate and reactivate storage locations" do diff --git a/spec/system/transfer_system_spec.rb b/spec/system/transfer_system_spec.rb index 42069c5881..c127bd13b4 100644 --- a/spec/system/transfer_system_spec.rb +++ b/spec/system/transfer_system_spec.rb @@ -34,8 +34,6 @@ def create_transfer(amount, from_name, to_name) inventory = View::Inventory.new(organization.id) original_from_storage_item_count = inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id) - original_from_ii_storage_item_count = from_storage_location.inventory_items.find_by(item_id: item.id).quantity - expect(original_from_storage_item_count).to eq(original_from_ii_storage_item_count) original_to_storage_item_count = 0 transfer_amount = 10 @@ -44,8 +42,6 @@ def create_transfer(amount, from_name, to_name) inventory.reload # Ensure the that the transfer has changed the inventory quantities - expect(from_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).not_to eq(original_from_storage_item_count) - expect(to_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(transfer_amount) expect(inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id)).not_to eq(original_from_storage_item_count) expect(inventory.quantity_for(storage_location: to_storage_location.id, item_id: item.id)).to eq(transfer_amount) @@ -58,8 +54,6 @@ def create_transfer(amount, from_name, to_name) inventory.reload # Assert that the original inventory counts have been restored. - expect(from_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(original_from_storage_item_count) - expect(to_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(original_to_storage_item_count) expect(inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id)).to eq(original_from_storage_item_count) expect(inventory.quantity_for(storage_location: to_storage_location.id, item_id: item.id)).to eq(original_to_storage_item_count) end @@ -70,33 +64,25 @@ def create_transfer(amount, from_name, to_name) inventory = View::Inventory.new(organization.id) original_from_storage_item_count = inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id) - original_from_ii_storage_item_count = from_storage_location.inventory_items.find_by(item_id: item.id).quantity - expect(original_from_storage_item_count).to eq(original_from_ii_storage_item_count) transfer_amount = 10 create_transfer(transfer_amount.to_s, from_storage_location.name, to_storage_location.name) inventory.reload - expect(from_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(original_from_storage_item_count - transfer_amount) - expect(to_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(transfer_amount) expect(inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id)).to eq(original_from_storage_item_count - transfer_amount) expect(inventory.quantity_for(storage_location: to_storage_location.id, item_id: item.id)).to eq(transfer_amount) - allow_any_instance_of(StorageLocation).to receive(:decrease_inventory).and_raise( - Errors::InsufficientAllotment.new('error-msg', []) - ) + allow(TransferDestroyEvent).to receive(:publish).and_raise('OH NOES') accept_confirm do click_link 'Delete' end - expect(page).to have_content(/error-msg/) + expect(page).to have_content(/OH NOES/) inventory.reload # Assert that the inventory did not change in response # to the raised error. - expect(from_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(original_from_storage_item_count - transfer_amount) - expect(to_storage_location.reload.inventory_items.find_by(item_id: item.id).quantity).to eq(transfer_amount) expect(inventory.quantity_for(storage_location: from_storage_location.id, item_id: item.id)).to eq(original_from_storage_item_count - transfer_amount) expect(inventory.quantity_for(storage_location: to_storage_location.id, item_id: item.id)).to eq(transfer_amount) end