From 4c38cc2eadc6d9271c85017f025e51bbfa5b83a4 Mon Sep 17 00:00:00 2001 From: Jade Stewart Date: Wed, 27 Nov 2024 11:25:24 -0700 Subject: [PATCH] Revert "Merge branch '4513_incontinence_supplies' of github.com:jadekstewart3/human-essentials into 4513_incontinence_supplies" This reverts commit 42fb8ef654c25b97c707d44befdb681a28b8bab5, reversing changes made to 8d09039430d2c5ee4be2f042f0a9d80b5d0517d2. --- .devcontainer/.env.codespaces | 14 - .devcontainer/Dockerfile | 9 - .devcontainer/devcontainer.json | 42 - .devcontainer/docker-compose.yml | 35 - .devcontainer/launch.json.codespaces | 32 - .devcontainer/post-create.sh | 19 - .github/PULL_REQUEST_TEMPLATE.md | 1 - .github/dependabot.yml | 5 +- .github/workflows/brakeman.yml | 8 +- .github/workflows/factory-bot-lint.yml | 8 +- .github/workflows/plantuml.yml | 18 + .github/workflows/rspec-events.yml | 90 ++ .github/workflows/rspec-system-events.yml | 93 ++ .github/workflows/rspec-system.yml | 8 +- .github/workflows/rspec.yml | 8 +- .github/workflows/ruby_lint.yml | 4 + .rubocop_todo.yml | 1 + CONTRIBUTING.md | 252 +---- Gemfile | 23 +- Gemfile.lock | 420 ++++----- README.md | 178 +++- app/assets/stylesheets/application.scss | 29 +- app/assets/stylesheets/custom.scss | 16 - .../simple_form-bootstrap/_form_abbr.scss | 5 +- .../admin/account_requests_controller.rb | 18 +- .../admin/organizations_controller.rb | 9 +- app/controllers/admin/users_controller.rb | 10 + app/controllers/application_controller.rb | 22 +- app/controllers/audits_controller.rb | 25 +- app/controllers/concerns/importable.rb | 8 +- app/controllers/dashboard_controller.rb | 25 +- app/controllers/distributions_controller.rb | 72 +- app/controllers/donations_controller.rb | 28 +- app/controllers/errors_controller.rb | 9 + app/controllers/item_categories_controller.rb | 2 +- app/controllers/items_controller.rb | 61 +- app/controllers/kits_controller.rb | 45 +- app/controllers/organizations_controller.rb | 34 +- app/controllers/partner_groups_controller.rb | 27 +- app/controllers/partner_users_controller.rb | 70 -- app/controllers/partners/base_controller.rb | 11 - .../partners/children_controller.rb | 6 +- .../partners/dashboards_controller.rb | 4 +- .../partners/family_requests_controller.rb | 54 +- .../individuals_requests_controller.rb | 16 - .../partners/profiles_controller.rb | 27 +- .../partners/requests_controller.rb | 33 +- app/controllers/partners/users_controller.rb | 1 - app/controllers/partners_controller.rb | 12 + .../product_drive_participants_controller.rb | 2 +- app/controllers/profiles_controller.rb | 29 +- app/controllers/purchases_controller.rb | 3 +- app/controllers/reports_controller.rb | 63 -- .../requests/cancelation_controller.rb | 2 +- app/controllers/requests_controller.rb | 26 +- .../storage_locations_controller.rb | 53 +- app/events/audit_event.rb | 2 +- app/events/event_differ.rb | 104 +++ app/events/event_types/event_line_item.rb | 11 - app/events/inventory_aggregate.rb | 16 +- app/events/kit_deallocate_event.rb | 4 +- app/events/snapshot_event.rb | 36 +- app/events/update_existing_event.rb | 63 -- app/helpers/application_helper.rb | 8 +- app/helpers/dashboard_helper.rb | 40 + app/helpers/date_range_helper.rb | 8 +- app/helpers/distribution_helper.rb | 2 +- app/helpers/donations_helper.rb | 28 - app/helpers/historical_trends_helper.rb | 14 +- app/helpers/items_helper.rb | 5 - app/helpers/partners_helper.rb | 21 - app/javascript/application.js | 3 +- .../controllers/accordion_controller.js | 19 - .../controllers/confirmation_controller.js | 111 --- .../controllers/form_input_controller.js | 1 - .../controllers/item_units_controller.js | 49 - .../password_visibility_controller.js | 13 - .../controllers/select2_controller.js | 22 +- app/javascript/utils/barcode_items.js | 7 +- .../utils/distributions_and_transfers.js | 1 - app/jobs/backup_db_rds.rb | 24 - app/jobs/reminder_deadline_job.rb | 1 - app/mailers/distribution_mailer.rb | 6 +- app/mailers/requests_confirmation_mailer.rb | 8 +- app/models/account_request.rb | 16 +- app/models/audit.rb | 3 +- app/models/concerns/issued_at.rb | 7 - app/models/concerns/itemizable.rb | 24 + app/models/concerns/provideable.rb | 4 +- app/models/distribution.rb | 36 +- app/models/donation.rb | 4 - app/models/donation_site.rb | 1 - app/models/errors.rb | 6 - app/models/event.rb | 20 +- app/models/inventory_discrepancy.rb | 15 + app/models/inventory_item.rb | 15 +- app/models/item.rb | 85 +- app/models/item_unit.rb | 20 - app/models/kit.rb | 10 +- app/models/line_item.rb | 15 +- app/models/manufacturer.rb | 11 +- app/models/organization.rb | 14 +- app/models/organization_stats.rb | 4 +- app/models/partner.rb | 41 +- app/models/partners/child.rb | 10 +- app/models/partners/family.rb | 2 +- app/models/partners/item_request.rb | 22 - app/models/partners/profile.rb | 41 +- app/models/partners/served_area.rb | 2 +- app/models/product_drive_participant.rb | 3 - app/models/purchase.rb | 14 - app/models/request.rb | 18 +- app/models/request_item.rb | 11 +- app/models/storage_location.rb | 148 ++- app/models/transfer.rb | 9 +- app/models/unit.rb | 14 - app/models/user.rb | 4 - app/models/vendor.rb | 2 - app/models/view/inventory.rb | 25 - app/pdfs/distribution_pdf.rb | 261 ++---- app/pdfs/donation_pdf.rb | 195 ---- app/pdfs/picklists_pdf.rb | 167 ---- ...y_storage_collection_and_quantity_query.rb | 70 +- .../items_by_storage_collection_query.rb | 36 + app/queries/low_inventory_query.rb | 22 - app/services/adjustment_create_service.rb | 12 + .../allocate_kit_inventory_service.rb | 98 ++ .../deallocate_kit_inventory_service.rb | 106 +++ app/services/distribution_create_service.rb | 1 + app/services/distribution_destroy_service.rb | 1 + ...distribution_itemized_breakdown_service.rb | 17 +- app/services/distribution_update_service.rb | 4 +- app/services/donation_create_service.rb | 1 + app/services/donation_destroy_service.rb | 1 + .../donation_itemized_breakdown_service.rb | 11 +- .../export_distributions_csv_service.rb | 11 +- .../exports/export_donations_csv_service.rb | 3 - .../exports/export_request_service.rb | 56 +- app/services/historical_trend_service.rb | 40 +- app/services/inventory_check_service.rb | 23 +- app/services/item_create_service.rb | 17 +- app/services/itemizable_update_service.rb | 54 +- app/services/organization_update_service.rb | 24 +- app/services/partner_create_service.rb | 32 +- .../partner_profile_update_service.rb | 2 - .../partners/family_request_create_service.rb | 9 - .../partners/request_create_service.rb | 52 +- .../partners/section_error_service.rb | 30 - app/services/purchase_create_service.rb | 1 + app/services/purchase_destroy_service.rb | 1 + app/services/reports.rb | 2 +- ...rvice.rb => acquisition_report_service.rb} | 112 ++- .../reports/children_served_report_service.rb | 1 - .../reports/partner_info_report_service.rb | 2 +- .../reports/period_supply_report_service.rb | 77 +- .../reports/summary_report_service.rb | 2 +- app/services/requests_total_items_service.rb | 34 +- .../storage_location_deactivate_service.rb | 8 +- app/services/transfer_create_service.rb | 4 +- app/services/transfer_destroy_service.rb | 6 + app/views/account_requests/new.html.erb | 2 +- app/views/adjustments/index.html.erb | 4 +- .../_open_account_request.html.erb | 7 +- .../_rejection_modal.html.erb | 25 +- .../admin/account_requests/index.html.erb | 11 + app/views/admin/base_items/show.html.erb | 2 +- app/views/admin/dashboard.html.erb | 2 +- app/views/admin/organizations/_list.html.erb | 53 +- app/views/admin/organizations/index.html.erb | 6 +- app/views/admin/users/_list.html.erb | 1 + app/views/admin/users/_roles.html.erb | 2 +- app/views/audits/_form.html.erb | 6 +- app/views/audits/edit.html.erb | 2 +- app/views/audits/index.html.erb | 2 +- app/views/audits/new.html.erb | 2 +- app/views/audits/show.html.erb | 27 +- .../_barcode_item_lookup.html.erb | 2 +- app/views/barcode_items/create.js.erb | 11 +- app/views/barcode_items/edit.html.erb | 2 +- app/views/barcode_items/index.html.erb | 2 +- app/views/barcode_items/new.html.erb | 2 +- app/views/barcode_items/show.html.erb | 2 +- .../broadcast_announcements/edit.html.erb | 2 +- .../broadcast_announcements/index.html.erb | 4 +- .../broadcast_announcements/new.html.erb | 2 +- app/views/dashboard/_announcements.html.erb | 32 - .../_distribution.html.erb | 0 .../{reports => dashboard}/_donation.html.erb | 0 .../_getting_started_prompt.html.erb | 214 ++--- .../_itemized_distributions_partial.html.erb | 21 + .../_itemized_donations_partial.html.erb | 19 + .../dashboard/_low_inventory_report.html.erb | 37 - app/views/dashboard/_manufacturer.html.erb | 5 + .../dashboard/_outstanding_requests.html.erb | 35 - .../dashboard/_partner_approvals.html.erb | 34 - app/views/dashboard/_product_drive.html.erb | 5 + .../{reports => dashboard}/_purchase.html.erb | 2 +- app/views/dashboard/index.html.erb | 361 +++++++- .../distributions/_distribution_row.html.erb | 5 +- .../_distribution_total.html.erb | 2 - app/views/distributions/_form.html.erb | 34 +- app/views/distributions/edit.html.erb | 2 +- app/views/distributions/index.html.erb | 19 +- app/views/distributions/new.html.erb | 6 +- app/views/distributions/pickup_day.html.erb | 4 +- app/views/distributions/schedule.html.erb | 2 +- app/views/distributions/show.html.erb | 6 +- app/views/distributions/validate.html.erb | 42 - app/views/donation_sites/edit.html.erb | 2 +- app/views/donation_sites/index.html.erb | 4 +- app/views/donation_sites/new.html.erb | 2 +- app/views/donation_sites/show.html.erb | 2 +- app/views/donations/_donation_form.html.erb | 27 +- app/views/donations/_donation_row.html.erb | 1 - app/views/donations/edit.html.erb | 10 +- app/views/donations/index.html.erb | 2 +- app/views/donations/new.html.erb | 2 +- app/views/donations/show.html.erb | 16 +- app/views/errors/insufficient.html.erb | 3 + .../errors/internal_server_error.html.erb | 28 + app/views/errors/not_found.html.erb | 31 + app/views/events/_event_row.html.erb | 2 +- app/views/items/_form.html.erb | 6 - app/views/items/_item_categories.html.erb | 2 +- app/views/items/_item_list.html.erb | 15 +- app/views/items/_item_row.html.erb | 11 +- app/views/items/_items_inventory.html.erb | 2 +- .../_items_quantity_and_location.html.erb | 2 +- app/views/items/_kits.html.erb | 2 +- app/views/items/edit.html.erb | 2 +- app/views/items/index.html.erb | 2 +- app/views/items/new.html.erb | 2 +- app/views/items/show.html.erb | 49 +- app/views/kits/_form.html.erb | 8 +- app/views/kits/_table.html.erb | 22 +- app/views/kits/allocations.html.erb | 2 +- app/views/kits/index.html.erb | 2 +- app/views/kits/new.html.erb | 1 - app/views/layouts/_lte_admin_navbar.html.erb | 55 +- app/views/layouts/_lte_admin_sidebar.html.erb | 37 +- app/views/layouts/_lte_navbar.html.erb | 51 +- app/views/layouts/_lte_sidebar.html.erb | 217 ++--- app/views/layouts/application.html.erb | 10 +- app/views/layouts/application_old.html.erb | 2 +- .../layouts/partners/application.html.erb | 1 - .../partners/navigation/_navbar.html.erb | 2 +- .../partners/navigation/_sidebar.html.erb | 4 +- .../line_items/_line_item_fields.html.erb | 44 +- app/views/manufacturers/edit.html.erb | 2 +- app/views/manufacturers/index.html.erb | 2 +- app/views/manufacturers/new.html.erb | 2 +- app/views/manufacturers/show.html.erb | 2 +- .../partner_approval_request.html.erb | 2 +- .../partner_approval_request.text.erb | 2 +- app/views/organizations/_details.html.erb | 36 - app/views/organizations/edit.html.erb | 28 +- app/views/partner_groups/edit.html.erb | 2 +- app/views/partner_groups/new.html.erb | 2 +- .../application_approved.html.erb | 2 +- .../application_approved.text.erb | 2 +- app/views/partner_users/_form.html.erb | 20 - app/views/partner_users/_user.html.erb | 32 - app/views/partner_users/_users.html.erb | 69 -- app/views/partner_users/index.html.erb | 40 - .../partners/_partner_groups_table.html.erb | 5 +- app/views/partners/_partner_row.html.erb | 4 +- app/views/partners/_partners_table.html.erb | 2 +- app/views/partners/_statuses.html.erb | 2 +- .../partners/children/_child.json.jbuilder | 2 +- app/views/partners/children/_form.html.erb | 32 +- app/views/partners/children/show.html.erb | 4 +- .../dashboards/_requests_in_progress.html.erb | 12 +- app/views/partners/edit.html.erb | 2 +- .../partners/family_requests/_list.html.erb | 6 +- .../partners/family_requests/new.html.erb | 19 +- app/views/partners/index.html.erb | 2 +- .../individuals_requests/new.html.erb | 19 +- app/views/partners/new.html.erb | 2 +- app/views/partners/profiles/edit.html.erb | 5 +- .../profiles/edit/_pick_up_person.html.erb | 2 +- .../profiles/step/_accordion_section.html.erb | 24 - ...ncy_distribution_information_form.html.erb | 15 - .../step/_agency_information_form.html.erb | 51 - .../step/_agency_stability_form.html.erb | 52 -- .../profiles/step/_area_served_form.html.erb | 26 - .../step/_attached_documents_form.html.erb | 15 - .../step/_executive_director_form.html.erb | 26 - .../profiles/step/_form_actions.html.erb | 6 - .../step/_media_information_form.html.erb | 22 - .../_organizational_capacity_form.html.erb | 13 - .../step/_partner_settings_form.html.erb | 22 - .../step/_pick_up_person_form.html.erb | 11 - .../step/_population_served_form.html.erb | 58 -- .../_program_delivery_address_form.html.erb | 21 - .../step/_sources_of_funding_form.html.erb | 17 - .../partners/profiles/step/edit.html.erb | 46 - app/views/partners/requests/_error.html.erb | 3 - .../partners/requests/_item_request.html.erb | 16 +- app/views/partners/requests/_success.html.erb | 2 +- app/views/partners/requests/new.html.erb | 26 +- app/views/partners/requests/show.html.erb | 9 +- app/views/partners/requests/validate.html.erb | 53 -- app/views/partners/show.html.erb | 110 ++- .../product_drive_participants/_form.html.erb | 6 +- .../product_drive_participants/edit.html.erb | 2 +- .../product_drive_participants/index.html.erb | 2 +- .../product_drive_participants/new.html.erb | 2 +- .../product_drive_participants/show.html.erb | 4 +- app/views/product_drives/edit.html.erb | 2 +- app/views/product_drives/index.html.erb | 4 +- app/views/product_drives/new.html.erb | 2 +- app/views/product_drives/show.html.erb | 2 +- app/views/profiles/step/edit.html.erb | 36 - app/views/purchases/_purchase_form.html.erb | 3 +- app/views/purchases/_purchase_row.html.erb | 2 - app/views/purchases/edit.html.erb | 2 +- app/views/purchases/index.html.erb | 12 +- app/views/purchases/new.html.erb | 2 +- app/views/purchases/show.html.erb | 38 +- app/views/reports/_manufacturer.html.erb | 5 - app/views/reports/_product_drive.html.erb | 5 - app/views/reports/activity_graph.html.erb | 51 - .../reports/annual_reports/show.html.erb | 2 +- .../reports/distributions_summary.html.erb | 24 - app/views/reports/donations_summary.html.erb | 27 - .../reports/itemized_distributions.html.erb | 37 - app/views/reports/itemized_donations.html.erb | 37 - .../manufacturer_donations_summary.html.erb | 33 - .../reports/product_drives_summary.html.erb | 29 - app/views/reports/purchases_summary.html.erb | 29 - .../_calculate_product_totals.html.erb | 2 +- app/views/requests/_request_row.html.erb | 2 +- app/views/requests/cancelation/new.html.erb | 2 +- app/views/requests/index.html.erb | 6 - app/views/requests/show.html.erb | 23 +- .../confirmation_email.html.erb | 9 +- .../served_areas/_served_area_fields.html.erb | 6 +- app/views/shared/_card.html.erb | 82 -- app/views/shared/_csv_import_modal.html.erb | 7 +- app/views/shared/_filtered_card.html.erb | 37 - app/views/static/index.html.erb | 10 +- app/views/static/privacypolicy.html.erb | 94 +- .../_inventory_item_row.html.erb | 11 + app/views/storage_locations/_source.html.erb | 1 + .../_storage_location_row.html.erb | 5 +- app/views/storage_locations/edit.html.erb | 2 +- app/views/storage_locations/index.html.erb | 6 +- .../storage_locations/inventory.json.jbuilder | 5 + app/views/storage_locations/new.html.erb | 2 +- app/views/storage_locations/show.html.erb | 22 +- app/views/transfers/index.html.erb | 2 +- app/views/transfers/new.html.erb | 2 +- app/views/transfers/show.html.erb | 2 +- app/views/users/_add_user_modal.erb | 2 +- app/views/users/_organization_user.html.erb | 42 +- app/views/users/sessions/new.html.erb | 23 +- .../shared/_account_management_menu.html.erb | 10 +- app/views/vendors/edit.html.erb | 2 +- app/views/vendors/index.html.erb | 2 +- app/views/vendors/new.html.erb | 2 +- app/views/vendors/show.html.erb | 2 +- bin/setup | 4 +- clock.rb | 8 - config/application.rb | 1 + config/brakeman.ignore | 52 +- config/database.yml | 1 - config/environments/development.rb | 5 - config/initializers/devise.rb | 7 - config/initializers/postgres.rb | 15 + config/initializers/simple_form_bootstrap.rb | 7 +- config/locales/en.yml | 4 +- config/locales/roles.en.yml | 2 - config/routes.rb | 280 +++--- db/base_items.json | 7 +- db/migrate/20231110194231_clear_events.rb | 1 + .../20240331141135_clear_events_again.rb | 1 + ..._distribution_printing_to_organizations.rb | 9 - ..._distribution_printing_to_organizations.rb | 8 - db/migrate/20240426135118_add_pack_models.rb | 16 - ...0211_add_request_units_to_item_requests.rb | 5 - ..._signature_fields_flag_to_organizations.rb | 11 - ...fill_include_signature_on_organizations.rb | 7 - .../20240512140954_clear_events_once_again.rb | 9 - ...stributed_started_requests_as_fulfilled.rb | 14 - ...40527151622_add_ai_liners_to_base_items.rb | 8 - ...1155348_dedup_item_requests_in_requests.rb | 58 -- ...ctivate_users_and_remove_org_user_roles.rb | 15 - ...174254_create_join_table_children_items.rb | 8 - ..._backfill_partner_child_requested_items.rb | 15 - ...808_add_issued_at_index_to_distribution.rb | 7 - ...20240718010905_drop_partner_users_table.rb | 9 - ...0240811140508_clear_events_with_feeling.rb | 9 - db/migrate/20240825141541_fix_bad_kits.rb | 25 - ...30015517_fix_invalid_distribution_event.rb | 12 - ...kfill_vendor_business_name_from_contact.rb | 5 - ...8_remove_excess_kit_village_diaper_bank.rb | 6 - ..._fix_invalid_distribution_event20241112.rb | 12 - db/schema.rb | 79 +- db/seeds.rb | 158 +--- .../architecture/barcode-retrieval.svg | 0 .../0001-record-architecture-decisions.md | 0 ...-models-as-they-move-through-the-system.md | 0 ...titenancy-instead-of-multiple-instances.md | 0 ...arate-application-intended-for-partners.md | 0 ...006-instantiating-items-from-base-items.md | 0 .../decisions/0007-barcode-querying.md | 0 ...08-merging-partner-base-and-diaper-base.md | 0 ...0009-stick-with-adminlte-for-app-design.md | 0 .../architecture/decisions/README.md | 0 {docs => doc}/architecture/multi-tenancy.svg | 0 {docs => doc}/architecture/overview.md | 0 .../architecture/partner-request.svg | 0 {docs => doc}/architecture/physical-flow.svg | 0 {docs => doc}/architecture/timeline-flow.svg | 0 {docs => doc}/templates/release-template.md | 0 docs/.gitignore | 1 - docs/Gemfile | 5 - docs/Gemfile.lock | 303 ------ docs/_config.yml | 10 - docs/_layouts/default.html | 56 -- docs/readme.md | 13 - docs/user_guide/bank/account_management.md | 30 - docs/user_guide/bank/asking_for_changes.md | 12 - .../bank/community_donation_sites.md | 54 -- .../bank/community_manufacturers.md | 26 - .../community_product_drive_participants.md | 42 - .../bank/community_product_drives.md | 90 -- docs/user_guide/bank/community_vendors.md | 41 - docs/user_guide/bank/essentials_dashboard.md | 28 - .../bank/essentials_distributions.md | 112 --- docs/user_guide/bank/essentials_donations.md | 143 --- docs/user_guide/bank/essentials_pick_ups.md | 40 - docs/user_guide/bank/essentials_purchases.md | 105 --- docs/user_guide/bank/essentials_requests.md | 135 --- docs/user_guide/bank/exports.md | 358 -------- .../bank/getting_started_access_levels.md | 49 - .../bank/getting_started_choices.md | 20 - .../bank/getting_started_customization.md | 215 ----- .../bank/getting_started_donation_sites.md | 15 - .../bank/getting_started_inventory.md | 31 - .../bank/getting_started_partners.md | 34 - .../bank/getting_started_storage_locations.md | 33 - .../bank/getting_started_user_management.md | 43 - .../account_management_account_settings.png | Bin 234200 -> 0 bytes ...management_account_settings_navigation.png | Bin 383771 -> 0 bytes .../account_management_logout.png | Bin 382385 -> 0 bytes .../account_management_my_organization.png | Bin 382238 -> 0 bytes .../donation_sites/add_new_donation_site.jpg | Bin 132218 -> 0 bytes .../donation_sites/create_donation_site.jpg | Bin 194629 -> 0 bytes .../deactivate_donation_site.jpg | Bin 196739 -> 0 bytes .../donation_sites/donation_sites.jpg | Bin 195753 -> 0 bytes .../donation_sites/donation_sites_details.jpg | Bin 167572 -> 0 bytes .../donation_sites/edit_donation_site.jpg | Bin 150591 -> 0 bytes .../donation_sites/export_donation_sites.jpg | Bin 195207 -> 0 bytes .../manufacturers/edit_manufacturer.jpg | Bin 127370 -> 0 bytes .../manufacturers/manufacturer_details.jpg | Bin 131985 -> 0 bytes .../manufacturers/manufacturers_page.jpg | Bin 135492 -> 0 bytes .../manufacturers/new_manufacturer.jpg | Bin 136283 -> 0 bytes .../add_participant.jpg | Bin 144439 -> 0 bytes .../edit_participant.jpg | Bin 163322 -> 0 bytes .../export_participants.jpg | Bin 144021 -> 0 bytes .../participant_details.jpg | Bin 122605 -> 0 bytes .../product_drive_page.jpg | Bin 145055 -> 0 bytes .../community_product_drives_add.png | Bin 217711 -> 0 bytes ...ommunity_product_drives_add_navigation.png | Bin 116967 -> 0 bytes ...unity_product_drives_export_navigation.png | Bin 396532 -> 0 bytes .../community_product_drives_modify.png | Bin 256723 -> 0 bytes ...unity_product_drives_modify_navigation.png | Bin 524557 -> 0 bytes .../community_product_drives_navigation.png | Bin 116765 -> 0 bytes .../community_product_drives_view.png | Bin 188391 -> 0 bytes ...mmunity_product_drives_view_navigation.png | Bin 116409 -> 0 bytes .../images/community/vendors/add_vendor.jpg | Bin 127296 -> 0 bytes .../images/community/vendors/edit_vendors.jpg | Bin 160670 -> 0 bytes .../community/vendors/export_vendors.jpg | Bin 122291 -> 0 bytes .../images/community/vendors/new_vendor.jpg | Bin 149881 -> 0 bytes .../community/vendors/vendor_details.jpg | Bin 182841 -> 0 bytes .../images/community/vendors/vendors_page.jpg | Bin 134202 -> 0 bytes .../dashboard/essentials_dashboard_1.png | Bin 202588 -> 0 bytes .../dashboard/essentials_dashboard_2.png | Bin 219843 -> 0 bytes ...sentials_distribution_print_navigation.png | Bin 99876 -> 0 bytes .../essentials_distributions_edit.png | Bin 152166 -> 0 bytes ...sentials_distributions_edit_navigation.png | Bin 99042 -> 0 bytes ...ntials_distributions_export_navigation.png | Bin 94072 -> 0 bytes ...essentials_distributions_export_sample.png | Bin 172811 -> 0 bytes .../essentials_distributions_filter.png | Bin 79554 -> 0 bytes .../essentials_distributions_navigation.png | Bin 95531 -> 0 bytes .../essentials_distributions_printout.png | Bin 211489 -> 0 bytes .../essentials_distributions_view.png | Bin 120573 -> 0 bytes ...sentials_distributions_view_navigation.png | Bin 87284 -> 0 bytes .../donations/essentials_donations_1.png | Bin 94807 -> 0 bytes .../donations/essentials_donations_2.png | Bin 120318 -> 0 bytes .../donations/essentials_donations_3.png | Bin 38900 -> 0 bytes .../donations/essentials_donations_4.png | Bin 92681 -> 0 bytes .../donations/essentials_donations_5.png | Bin 442103 -> 0 bytes .../donations/essentials_donations_6.png | Bin 53807 -> 0 bytes .../donations/essentials_donations_7.png | Bin 409317 -> 0 bytes .../donations/essentials_donations_8.png | Bin 167831 -> 0 bytes .../essentials/pick_ups/add_calendar.jpg | Bin 28030 -> 0 bytes .../essentials/pick_ups/copy_calendar_url.png | Bin 46339 -> 0 bytes .../distribution_from_source_to_partner.jpg | Bin 180727 -> 0 bytes .../essentials/pick_ups/other_calendars.jpg | Bin 123751 -> 0 bytes .../essentials/pick_ups/pickup&delivery.jpg | Bin 159632 -> 0 bytes .../specific_day_distribution_schedule.jpg | Bin 170069 -> 0 bytes .../purchases/essentials_purchaces_view.png | Bin 374880 -> 0 bytes .../purchases/essentials_purchases_1.png | Bin 138338 -> 0 bytes .../purchases/essentials_purchases_2.png | Bin 83967 -> 0 bytes .../purchases/essentials_purchases_3.png | Bin 141101 -> 0 bytes .../purchases/essentials_purchases_4.png | Bin 246738 -> 0 bytes ...essentials_purchases_delete_navigation.png | Bin 86185 -> 0 bytes .../essentials_purchases_edit_navigation.png | Bin 87047 -> 0 bytes ...essentials_purchases_export_navigation.png | Bin 183665 -> 0 bytes .../essentials_requests_cancel_confirm.png | Bin 228976 -> 0 bytes .../essentials_requests_cancel_email.png | Bin 302917 -> 0 bytes .../essentials_requests_cancel_navigation.png | Bin 374566 -> 0 bytes .../essentials_requests_export_navigation.png | Bin 376850 -> 0 bytes .../essentials_requests_navigation.png | Bin 108618 -> 0 bytes .../requests/essentials_requests_picklist.png | Bin 158415 -> 0 bytes ...ls_requests_print_picklists_navigation.png | Bin 375515 -> 0 bytes .../essentials_requests_product_totals.png | Bin 572294 -> 0 bytes ...als_requests_product_totals_navigation.png | Bin 109632 -> 0 bytes .../requests/essentials_requests_view.png | Bin 374826 -> 0 bytes .../essentials_requests_view_navigation.png | Bin 109752 -> 0 bytes ...ibution_printout_customizable_sections.png | Bin 230006 -> 0 bytes .../gs_customization_navigation_1.png | Bin 66952 -> 0 bytes .../gs_customization_navigation_2.png | Bin 67614 -> 0 bytes .../gs_customization_top_of_edit.png | Bin 377708 -> 0 bytes .../getting_started_donation_sites_1.png | Bin 62240 -> 0 bytes .../getting_started_donation_sites_2.png | Bin 360887 -> 0 bytes .../inventory/gs_inventory_1.png | Bin 193432 -> 0 bytes .../inventory/gs_inventory_2.png | Bin 191433 -> 0 bytes .../inventory/gs_inventory_3.png | Bin 108406 -> 0 bytes .../partners/gs_just_starting_step_2.png | Bin 82300 -> 0 bytes .../gs_just_starting_step_2_import.png | Bin 84374 -> 0 bytes .../gs_just_starting_step_1.png | Bin 136998 -> 0 bytes .../gs_storage_locations_navigation.png | Bin 100269 -> 0 bytes .../new_storage_location.png | Bin 490797 -> 0 bytes .../storage_location_index.png | Bin 185995 -> 0 bytes .../user_admin/gs_user_admin_demote_admin.png | Bin 75978 -> 0 bytes .../user_admin/gs_user_admin_invite_user.png | Bin 73498 -> 0 bytes .../user_admin/gs_user_admin_navigation.png | Bin 80050 -> 0 bytes .../user_admin/gs_user_admin_promote_user.png | Bin 83270 -> 0 bytes .../user_admin/gs_user_admin_remove_user.png | Bin 81435 -> 0 bytes .../inventory_adjustments_navigation.png | Bin 90761 -> 0 bytes .../inventory/inventory_adjustments_new.png | Bin 100750 -> 0 bytes .../inventory_adjustments_new_navigation.png | Bin 83789 -> 0 bytes .../inventory_adjustments_result.png | Bin 86238 -> 0 bytes .../inventory/inventory_adjustments_view.png | Bin 84014 -> 0 bytes .../inventory_adjustments_view_navigation.png | Bin 84214 -> 0 bytes .../inventory/inventory_audits_confirm.png | Bin 139801 -> 0 bytes .../inventory/inventory_audits_delete.png | Bin 74021 -> 0 bytes .../inventory/inventory_audits_finalize.png | Bin 118739 -> 0 bytes .../images/inventory/inventory_audits_new.png | Bin 154426 -> 0 bytes .../inventory_audits_new_navigation.png | Bin 78785 -> 0 bytes .../inventory/inventory_audits_progress.png | Bin 177278 -> 0 bytes .../inventory/inventory_audits_resume.png | Bin 73135 -> 0 bytes .../inventory_audits_view_navigation.png | Bin 104113 -> 0 bytes .../inventory/inventory_barcodes_delete.png | Bin 171725 -> 0 bytes .../inventory_barcodes_edit_navigation.png | Bin 173334 -> 0 bytes .../inventory/inventory_barcodes_export.png | Bin 173359 -> 0 bytes .../inventory_barcodes_export_navigation.png | Bin 173191 -> 0 bytes .../inventory_barcodes_navigation.png | Bin 167625 -> 0 bytes .../inventory/inventory_barcodes_new.png | Bin 90224 -> 0 bytes .../inventory_barcodes_new_navigation.png | Bin 172155 -> 0 bytes .../inventory_category_view_navigation.png | Bin 252886 -> 0 bytes .../inventory_item_category_edit.png | Bin 101890 -> 0 bytes ...nventory_item_category_edit_navigation.png | Bin 258128 -> 0 bytes .../inventory_item_category_navigation.png | Bin 260170 -> 0 bytes .../inventory/inventory_item_category_new.png | Bin 88317 -> 0 bytes ...inventory_item_category_new_navigation.png | Bin 260844 -> 0 bytes .../inventory_item_category_view.png | Bin 183771 -> 0 bytes .../inventory_item_location_navigation.png | Bin 226827 -> 0 bytes .../inventory_items_delete_vs_deactivate.png | Bin 219073 -> 0 bytes .../images/inventory/inventory_items_edit.png | Bin 161800 -> 0 bytes .../inventory_items_edit_navigation.png | Bin 149140 -> 0 bytes .../inventory_items_inventory_navigation.png | Bin 216876 -> 0 bytes .../inventory_items_kits_navigation.png | Bin 159480 -> 0 bytes .../inventory/inventory_items_navigation.png | Bin 148373 -> 0 bytes .../inventory_items_reactivation.png | Bin 88781 -> 0 bytes .../images/inventory/inventory_items_view.png | Bin 145226 -> 0 bytes .../inventory_items_view_navigation.png | Bin 148482 -> 0 bytes .../inventory/inventory_kits_allocation.png | Bin 144209 -> 0 bytes .../inventory_kits_allocation_post_save.png | Bin 152263 -> 0 bytes .../inventory/inventory_kits_deactivate.png | Bin 129609 -> 0 bytes ...tory_kits_modify_allocation_navigation.png | Bin 136619 -> 0 bytes .../images/inventory/inventory_kits_new.png | Bin 117339 -> 0 bytes .../inventory_kits_new_navigation.png | Bin 106225 -> 0 bytes .../inventory/inventory_kits_reactivate.png | Bin 135216 -> 0 bytes ...nventory_storage_location_reactivation.png | Bin 100404 -> 0 bytes ...entory_storage_location_view_inventory.png | Bin 184173 -> 0 bytes .../inventory_storage_locations_add.png | Bin 117094 -> 0 bytes ...ntory_storage_locations_add_navigation.png | Bin 396487 -> 0 bytes .../inventory_storage_locations_coming_in.png | Bin 91028 -> 0 bytes .../inventory_storage_locations_going_out.png | Bin 89173 -> 0 bytes ...inventory_storage_locations_navigation.png | Bin 393514 -> 0 bytes .../inventory/inventory_transfers_delete.png | Bin 50190 -> 0 bytes .../inventory/inventory_transfers_export.png | Bin 89186 -> 0 bytes .../inventory_transfers_export_navigation.png | Bin 50852 -> 0 bytes .../inventory_transfers_navigation.png | Bin 49618 -> 0 bytes .../inventory/inventory_transfers_new.png | Bin 87260 -> 0 bytes .../inventory_transfers_new_navigation.png | Bin 50295 -> 0 bytes .../inventory/inventory_transfers_view.png | Bin 74993 -> 0 bytes .../inventory_transfers_view_navigation.png | Bin 50468 -> 0 bytes .../bank/images/partners/partners_add.png | Bin 119872 -> 0 bytes .../partners/partners_add_navigation.png | Bin 91099 -> 0 bytes .../partners/partners_announcements_1.png | Bin 150604 -> 0 bytes .../partners/partners_announcements_2.png | Bin 338566 -> 0 bytes .../images/partners/partners_approving_1.png | Bin 106209 -> 0 bytes .../images/partners/partners_approving_2.png | Bin 185074 -> 0 bytes .../images/partners/partners_deactivate_1.png | Bin 157118 -> 0 bytes .../images/partners/partners_deactivate_2.png | Bin 128557 -> 0 bytes .../bank/images/partners/partners_edit.png | Bin 145563 -> 0 bytes .../partners/partners_edit_navigation.png | Bin 83472 -> 0 bytes .../images/partners/partners_groups_1.png | Bin 72657 -> 0 bytes .../images/partners/partners_groups_2.png | Bin 103071 -> 0 bytes .../images/partners/partners_importing_1.png | Bin 100718 -> 0 bytes .../images/partners/partners_importing_2.png | Bin 112686 -> 0 bytes .../partners/partners_invitation_email.png | Bin 83421 -> 0 bytes .../images/partners/partners_inviting.png | Bin 173341 -> 0 bytes .../partners_inviting_and_approving.png | Bin 155681 -> 0 bytes .../partners_profile_edit_navigation_1.png | Bin 130312 -> 0 bytes .../partners_profile_edit_navigation_2.png | Bin 97792 -> 0 bytes .../partners/partners_recertification.png | Bin 125064 -> 0 bytes .../partners_recertification_email.png | Bin 37652 -> 0 bytes ...ners_review_application_from_dashboard.png | Bin 194549 -> 0 bytes ...partners_review_application_navigation.png | Bin 155759 -> 0 bytes .../partners/partners_user_management.png | Bin 121951 -> 0 bytes .../partners_user_management_navigation_1.png | Bin 126225 -> 0 bytes .../partners_user_management_navigation_2.png | Bin 111301 -> 0 bytes ...s_viewing_and_reactivating_deactivated.png | Bin 109308 -> 0 bytes .../reports/reports_activity_graph_1.png | Bin 120633 -> 0 bytes .../reports/reports_activity_graph_2.png | Bin 423962 -> 0 bytes .../reports/reports_annual_survey_1.png | Bin 75931 -> 0 bytes .../reports/reports_annual_survey_2.png | Bin 84367 -> 0 bytes .../reports/reports_annual_survey_3.png | Bin 404991 -> 0 bytes .../reports/reports_annual_survey_4.png | Bin 369553 -> 0 bytes .../reports/reports_annual_survey_5.png | Bin 351734 -> 0 bytes .../reports_distributions_by_county_1.png | Bin 416524 -> 0 bytes .../reports_distributions_by_county_2.png | Bin 391996 -> 0 bytes .../images/reports/reports_itemized_1.png | Bin 74590 -> 0 bytes .../reports_manufacturer_donations_1.png | Bin 402083 -> 0 bytes .../reports_manufacturer_donations_2.png | Bin 432301 -> 0 bytes .../reports/reports_summary_distributions.png | Bin 131542 -> 0 bytes .../reports/reports_summary_donations.png | Bin 156307 -> 0 bytes .../reports/reports_summary_purchases.png | Bin 184035 -> 0 bytes .../bank/images/reports/reports_trends_1.png | Bin 54193 -> 0 bytes .../bank/images/reports/reports_trends_2.png | Bin 175957 -> 0 bytes .../bank/images/reports/reports_trends_3.png | Bin 389225 -> 0 bytes .../user_access_admin_and_user.png | Bin 142430 -> 0 bytes .../user_management/user_access_partner.png | Bin 112451 -> 0 bytes .../user_management/user_delete_bank_user.png | Bin 103495 -> 0 bytes .../user_management/user_demote_admin.png | Bin 88669 -> 0 bytes .../user_management/user_invite_email.png | Bin 260613 -> 0 bytes .../user_invite_new_bank_user.png | Bin 106736 -> 0 bytes .../user_promote_bank_user.png | Bin 103240 -> 0 bytes docs/user_guide/bank/intro_i.md | 36 - docs/user_guide/bank/intro_ii.md | 35 - docs/user_guide/bank/inventory_adjustments.md | 41 - docs/user_guide/bank/inventory_audits.md | 81 -- docs/user_guide/bank/inventory_barcodes.md | 47 - docs/user_guide/bank/inventory_items.md | 180 ---- docs/user_guide/bank/inventory_kits.md | 104 --- .../bank/inventory_storage_locations.md | 61 -- docs/user_guide/bank/inventory_transfers.md | 59 -- docs/user_guide/bank/pm_adding_a_partner.md | 31 - docs/user_guide/bank/pm_announcements.md | 33 - .../user_guide/bank/pm_approving_a_partner.md | 38 - docs/user_guide/bank/pm_editing_a_partner.md | 23 - docs/user_guide/bank/pm_importing_partners.md | 24 - docs/user_guide/bank/pm_inviting_a_partner.md | 61 -- .../bank/pm_making_a_partner_inactive.md | 35 - docs/user_guide/bank/pm_other_information.md | 11 - docs/user_guide/bank/pm_partner_groups.md | 41 - docs/user_guide/bank/pm_partner_profiles.md | 169 ---- .../bank/pm_partner_reactivation.md | 18 - docs/user_guide/bank/pm_partner_statuses.md | 46 - docs/user_guide/bank/pm_partner_user_admin.md | 20 - .../bank/pm_request_distribution_cycle.md | 37 - .../bank/pm_requesting_recertification.md | 40 - docs/user_guide/bank/readme.md | 72 -- .../user_guide/bank/reports_activity_graph.md | 13 - docs/user_guide/bank/reports_annual_survey.md | 38 - .../bank/reports_distributions_by_county.md | 20 - docs/user_guide/bank/reports_history.md | 9 - .../bank/reports_itemized_reports.md | 19 - .../bank/reports_manufacturers_donations.md | 24 - .../bank/reports_summary_reports.md | 30 - docs/user_guide/bank/reports_trends.md | 33 - docs/user_guide/bank/special_custom_units.md | 16 - docs/user_guide/bank/user_management.md | 84 -- docs/user_guide/documentation_style_guide.md | 36 - lib/tasks/backup_db_rds.rake | 22 - .../create_inventory_in_out_daily_view.rake | 55 ++ lib/tasks/create_no_kit_daily_view.rake | 80 ++ lib/tasks/fetch_latest_db.rake | 26 +- lib/tasks/initiate_reminder_deadline_job.rake | 7 + public/403.html | 256 +----- public/404.html | 257 ++---- public/422.html | 257 ++---- public/500.html | 253 +---- ..._sites_template.csv => donation_sites.csv} | 0 .../{partners_template.csv => partners.csv} | 0 ...ons_template.csv => storage_locations.csv} | 0 sbv.md | 4 +- .../admin/account_requests_controller_spec.rb | 15 - spec/controllers/admins_controller_spec.rb | 22 +- .../application_controller_spec.rb | 17 +- .../distributions_controller_spec.rb | 82 +- .../donation_sites_controller_spec.rb | 15 +- spec/controllers/donations_controller_spec.rb | 98 +- spec/controllers/help_controller_spec.rb | 11 +- spec/controllers/items_controller_spec.rb | 86 +- spec/controllers/transfers_controller_spec.rb | 25 +- spec/events/event_differ_spec.rb | 75 ++ spec/events/inventory_aggregate_spec.rb | 204 ++-- spec/factories/adjustments.rb | 2 +- spec/factories/audits.rb | 2 +- spec/factories/barcode_items.rb | 2 +- spec/factories/distributions.rb | 5 +- spec/factories/donation_site.rb | 11 + spec/factories/donation_sites.rb | 28 - spec/factories/donations.rb | 11 +- spec/factories/item_units.rb | 20 - spec/factories/items.rb | 11 +- spec/factories/kits.rb | 2 +- spec/factories/manufacturers.rb | 2 +- spec/factories/organizations.rb | 11 +- spec/factories/partner_groups.rb | 2 +- spec/factories/partner_user.rb | 16 + spec/factories/partners.rb | 4 +- spec/factories/partners/child.rb | 15 + spec/factories/partners/children.rb | 35 - spec/factories/partners/families.rb | 49 - spec/factories/partners/family.rb | 22 + spec/factories/partners/item_requests.rb | 24 - spec/factories/partners/profile.rb | 7 + spec/factories/partners/profiles.rb | 90 -- spec/factories/partners/user.rb | 16 + spec/factories/product_drive.rb | 9 + spec/factories/product_drive_participants.rb | 2 +- spec/factories/product_drives.rb | 22 - spec/factories/purchases.rb | 3 +- spec/factories/requests.rb | 71 +- spec/factories/storage_locations.rb | 2 +- spec/factories/transfers.rb | 3 +- spec/factories/units.rb | 16 - spec/factories/users.rb | 22 +- spec/factories/users_roles.rb | 14 - spec/factories/vendor.rb | 2 +- .../files/partners_with_invalid_email.csv | 4 - spec/helpers/application_helper_spec.rb | 22 +- spec/helpers/historical_trends_helper_spec.rb | 15 +- spec/helpers/partners_helper_spec.rb | 11 - spec/helpers/product_drive_helper_spec.rb | 2 + spec/helpers/purchases_helper_spec.rb | 2 + spec/helpers/ui_helper_spec.rb | 2 + spec/inventory.rb | 17 + spec/jobs/historical_data_cache_job_spec.rb | 2 + spec/jobs/reminder_deadline_job_spec.rb | 2 + spec/mailers/account_request_mailer_spec.rb | 2 +- spec/mailers/custom_devise_mailer_spec.rb | 2 +- spec/mailers/distribution_mailer_spec.rb | 16 +- spec/mailers/organization_mailer_spec.rb | 10 +- spec/mailers/partner_mailer_spec.rb | 4 +- spec/mailers/reminder_deadline_mailer_spec.rb | 4 +- spec/mailers/request_mailer_spec.rb | 2 +- .../requests_confirmation_mailer_spec.rb | 35 +- spec/mailers/user_mailer_spec.rb | 2 +- spec/models/account_request_spec.rb | 33 +- spec/models/adjustment_spec.rb | 27 +- spec/models/audit_spec.rb | 41 +- spec/models/barcode_item_spec.rb | 42 +- spec/models/base_item_spec.rb | 33 +- spec/models/broadcast_announcement_spec.rb | 38 +- spec/models/concerns/deadlinable_spec.rb | 2 + spec/models/county_spec.rb | 1 + spec/models/distribution_spec.rb | 80 +- spec/models/donation_site_spec.rb | 106 +-- spec/models/donation_spec.rb | 13 +- spec/models/event_spec.rb | 3 +- spec/models/inventory_item_spec.rb | 60 ++ spec/models/item_category_spec.rb | 1 + spec/models/item_spec.rb | 182 ++-- spec/models/item_unit_spec.rb | 25 - spec/models/kit_allocation_spec.rb | 3 + spec/models/kit_spec.rb | 68 +- spec/models/line_item_spec.rb | 41 +- spec/models/manufacturer_spec.rb | 13 +- spec/models/ndbn_member_spec.rb | 1 + spec/models/organization_spec.rb | 58 +- spec/models/organization_stats_spec.rb | 30 +- spec/models/partner_group_spec.rb | 4 + spec/models/partner_spec.rb | 87 +- .../partners/authorized_family_member_spec.rb | 3 +- .../partners/child_item_request_spec.rb | 1 + spec/models/partners/child_spec.rb | 4 +- spec/models/partners/family_request_spec.rb | 2 + spec/models/partners/family_spec.rb | 2 + spec/models/partners/item_request_spec.rb | 20 +- spec/models/partners/profile_spec.rb | 121 +-- spec/models/partners/served_area_spec.rb | 31 +- spec/models/product_drive_participant_spec.rb | 13 +- spec/models/product_drive_spec.rb | 21 +- spec/models/purchase_spec.rb | 19 +- spec/models/question_spec.rb | 1 + spec/models/request_item_spec.rb | 2 +- spec/models/request_spec.rb | 75 +- spec/models/role_spec.rb | 1 + spec/models/storage_location_spec.rb | 155 +++- spec/models/transfer_spec.rb | 40 +- spec/models/unit_spec.rb | 25 - spec/models/user_spec.rb | 16 +- spec/models/vendor_spec.rb | 9 +- spec/models/view/inventory_spec.rb | 2 +- spec/pdfs/distribution_pdf_spec.rb | 129 +-- spec/pdfs/donation_pdf_spec.rb | 68 -- spec/pdfs/picklists_pdf_spec.rb | 87 -- spec/queries/items_in_query_spec.rb | 30 +- spec/queries/items_in_total_query_spec.rb | 46 +- spec/queries/items_out_query_spec.rb | 15 +- spec/queries/items_out_total_query_spec.rb | 15 +- spec/queries/low_inventory_query_spec.rb | 115 --- spec/rails_helper.rb | 76 +- spec/requests/account_requests_spec.rb | 4 +- spec/requests/adjustments_requests_spec.rb | 39 +- .../admin/account_requests_requests_spec.rb | 10 +- .../admin/barcode_items_requests_spec.rb | 6 +- .../admin/base_items_requests_spec.rb | 17 +- .../admin/broadcast_announcements_spec.rb | 24 +- spec/requests/admin/ndbn_members_spec.rb | 2 + .../admin/organizations_requests_spec.rb | 38 +- spec/requests/admin/partners_requests_spec.rb | 8 +- spec/requests/admin/questions_spec.rb | 4 +- spec/requests/admin/users_requests_spec.rb | 39 +- spec/requests/admin_requests_spec.rb | 10 +- spec/requests/attachments_requests_spec.rb | 2 +- spec/requests/audits_requests_spec.rb | 71 +- spec/requests/barcode_items_requests_spec.rb | 41 +- spec/requests/broadcast_announcements_spec.rb | 15 +- spec/requests/dashboard_requests_spec.rb | 29 +- spec/requests/distributions_by_county_spec.rb | 12 +- spec/requests/distributions_requests_spec.rb | 300 ++---- spec/requests/donation_sites_requests_spec.rb | 23 +- spec/requests/donations_requests_spec.rb | 159 +--- spec/requests/events_requests_spec.rb | 29 +- .../requests/item_categories_requests_spec.rb | 31 +- spec/requests/items_requests_spec.rb | 137 +-- spec/requests/kit_requests_spec.rb | 29 +- spec/requests/organization_requests_spec.rb | 385 ++------ spec/requests/partner_group_spec.rb | 46 - spec/requests/partner_groups_requests_spec.rb | 121 --- spec/requests/partner_users_requests_spec.rb | 133 --- .../partners/children_requests_spec.rb | 16 +- .../partners/dashboard_requests_spec.rb | 75 +- spec/requests/partners/distributions_spec.rb | 2 + .../family_requests_controller_spec.rb | 14 +- .../requests/partners/family_requests_spec.rb | 2 + .../individuals_requests_controller_spec.rb | 6 +- .../partners/profiles_requests_county_spec.rb | 2 + .../partners/profiles_requests_spec.rb | 18 +- spec/requests/partners/requests_spec.rb | 51 +- spec/requests/partners/user_requests_spec.rb | 58 +- spec/requests/partners_requests_spec.rb | 169 ++-- ...roduct_drive_participants_requests_spec.rb | 38 +- spec/requests/product_drives_requests_spec.rb | 127 +-- spec/requests/profiles_requests_spec.rb | 30 +- spec/requests/purchases_requests_spec.rb | 182 ++-- spec/requests/reports/activity_graph_spec.rb | 33 - .../reports/annual_reports_requests_spec.rb | 22 +- .../distributions_summary_requests_spec.rb | 92 -- .../reports/donations_summary_spec.rb | 97 -- .../reports/itemized_distributions_spec.rb | 55 -- .../reports/itemized_donations_spec.rb | 55 -- .../manufacturer_donations_summary_spec.rb | 79 -- .../reports/product_drives_summary_spec.rb | 98 -- .../purchases_summary_requests_spec.rb | 87 -- spec/requests/requests_requests_spec.rb | 104 +-- spec/requests/sessions_requests_spec.rb | 18 +- spec/requests/static_requests_spec.rb | 15 +- .../storage_locations_requests_spec.rb | 275 ++---- spec/requests/transfers_requests_spec.rb | 35 +- .../users/omniauth_callbacks_requests_spec.rb | 10 +- spec/requests/users_requests_spec.rb | 101 +- spec/requests/vendors_requests_spec.rb | 33 +- spec/routing/account_requests_routing_spec.rb | 2 + spec/services/add_role_service_spec.rb | 6 +- .../adjustment_create_service_spec.rb | 49 +- .../allocate_kit_inventory_service_spec.rb | 192 ++++ spec/services/calendar_service_spec.rb | 16 +- .../deallocate_kit_inventory_service_spec.rb | 161 ++++ .../distribution_create_service_spec.rb | 39 +- .../distribution_destroy_service_spec.rb | 31 +- .../services/donation_destroy_service_spec.rb | 67 +- .../export_distributions_csv_service_spec.rb | 49 +- .../export_donations_csv_service_spec.rb | 2 - .../exports/export_report_csv_service_spec.rb | 2 + .../exports/export_request_service_spec.rb | 294 +----- .../services/historical_trend_service_spec.rb | 23 +- spec/services/inventory_check_service_spec.rb | 10 +- spec/services/item_create_service_spec.rb | 29 + .../itemizable_update_service_spec.rb | 165 +--- spec/services/kit_create_service_spec.rb | 10 +- .../organization_update_service_spec.rb | 58 +- .../services/partner_approval_service_spec.rb | 2 + spec/services/partner_create_service_spec.rb | 2 + ...er_fetch_requestable_items_service_spec.rb | 4 +- spec/services/partner_invite_service_spec.rb | 2 + .../partner_profile_update_service_spec.rb | 41 +- ...er_request_recertification_service_spec.rb | 2 + .../family_request_create_service_spec.rb | 22 +- ...tch_partners_to_remind_now_service_spec.rb | 2 + .../partners/request_approval_service_spec.rb | 2 + .../partners/request_create_service_spec.rb | 66 +- .../partners/section_error_service_spec.rb | 47 - spec/services/partners/update_family_spec.rb | 2 + spec/services/profile_update_service_spec.rb | 2 + spec/services/remove_role_service_spec.rb | 4 +- ....rb => acquisition_report_service_spec.rb} | 156 ++-- .../adult_incontinence_report_service_spec.rb | 9 +- .../children_served_report_service_spec.rb | 13 +- .../other_products_report_service_spec.rb | 4 +- .../partner_info_report_service_spec.rb | 69 +- .../period_supply_report_service_spec.rb | 105 +-- .../reports/summary_report_service_spec.rb | 110 ++- .../requests_total_items_service_spec.rb | 65 +- .../service_object_errors_mixin_spec.rb | 2 + .../text_interpolator_service_spec.rb | 2 + .../services/transfer_destroy_service_spec.rb | 35 +- spec/services/user_invite_service_spec.rb | 25 +- spec/support/csv_import_shared_example.rb | 6 +- .../date_range_picker_shared_example.rb | 41 +- .../distribution_by_county_shared_example.rb | 2 +- spec/support/inventory_assistant.rb | 9 +- spec/support/itemizable_shared_example.rb | 48 +- .../pages/organization_dashboard_page.rb | 150 +-- ...organization_distributions_summary_page.rb | 62 -- .../organization_new_distribution_page.rb | 6 + .../pages/organization_new_donation_page.rb | 8 + .../pages/organization_new_purchase_page.rb | 8 + spec/support/pages/organization_page.rb | 8 +- .../organization_purchases_summary_page.rb | 48 - ...ion_reports_product_drives_summary_page.rb | 58 -- spec/support/provideable_shared_example.rb | 6 + spec/system/account_request_system_spec.rb | 5 +- spec/system/account_system_spec.rb | 7 +- spec/system/adjustment_system_spec.rb | 42 +- .../admin/account_requests_system_spec.rb | 48 +- .../system/admin/barcode_items_system_spec.rb | 22 +- spec/system/admin/base_items_system_spec.rb | 26 +- spec/system/admin/dashboard_system_spec.rb | 30 + .../system/admin/organizations_system_spec.rb | 116 +-- spec/system/admin/users_system_spec.rb | 67 +- spec/system/annual_reports_system_spec.rb | 17 +- spec/system/audit_system_spec.rb | 110 +-- spec/system/authorization_system_spec.rb | 11 +- spec/system/barcode_item_system_spec.rb | 39 +- spec/system/dashboard_system_spec.rb | 868 ++++++++++++++---- spec/system/distribution_system_spec.rb | 477 +++------- .../distributions_by_county_system_spec.rb | 12 +- spec/system/donation_site_system_spec.rb | 12 +- spec/system/donation_system_spec.rb | 90 +- spec/system/item_system_spec.rb | 55 +- spec/system/kit_system_spec.rb | 124 ++- spec/system/layout_system_spec.rb | 10 +- spec/system/log_in_system_spec.rb | 21 +- spec/system/manage_system_spec.rb | 20 +- spec/system/manufacturer_system_spec.rb | 21 +- spec/system/navigation_system_spec.rb | 30 +- spec/system/organization_system_spec.rb | 173 +++- spec/system/partner_system_spec.rb | 224 ++--- spec/system/partners/approval_process_spec.rb | 4 +- spec/system/partners/children_system_spec.rb | 36 - .../partners/family_requests_system_spec.rb | 80 +- .../partners/managing_requests_system_spec.rb | 26 +- .../partners/profile_edit_system_spec.rb | 102 -- spec/system/partners/requests_system_spec.rb | 86 -- .../product_drive_participant_system_spec.rb | 32 +- spec/system/product_drive_system_spec.rb | 16 +- .../system/profile_served_area_system_spec.rb | 14 +- spec/system/purchase_system_spec.rb | 80 +- spec/system/question_system_spec.rb | 5 +- spec/system/request_system_spec.rb | 46 +- spec/system/sign_in_system_spec.rb | 11 +- spec/system/storage_location_system_spec.rb | 17 +- spec/system/transfer_system_spec.rb | 67 +- spec/system/vendor_system_spec.rb | 16 +- 985 files changed, 8957 insertions(+), 18525 deletions(-) delete mode 100644 .devcontainer/.env.codespaces delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/docker-compose.yml delete mode 100644 .devcontainer/launch.json.codespaces delete mode 100755 .devcontainer/post-create.sh create mode 100644 .github/workflows/plantuml.yml create mode 100644 .github/workflows/rspec-events.yml create mode 100644 .github/workflows/rspec-system-events.yml create mode 100644 app/controllers/errors_controller.rb delete mode 100644 app/controllers/partner_users_controller.rb delete mode 100644 app/controllers/reports_controller.rb create mode 100644 app/events/event_differ.rb delete mode 100644 app/events/update_existing_event.rb delete mode 100644 app/helpers/donations_helper.rb delete mode 100644 app/javascript/controllers/accordion_controller.js delete mode 100644 app/javascript/controllers/confirmation_controller.js delete mode 100644 app/javascript/controllers/item_units_controller.js delete mode 100644 app/javascript/controllers/password_visibility_controller.js delete mode 100644 app/jobs/backup_db_rds.rb create mode 100644 app/models/inventory_discrepancy.rb delete mode 100644 app/models/item_unit.rb delete mode 100644 app/models/unit.rb delete mode 100644 app/pdfs/donation_pdf.rb delete mode 100644 app/pdfs/picklists_pdf.rb create mode 100644 app/queries/items_by_storage_collection_query.rb delete mode 100644 app/queries/low_inventory_query.rb create mode 100644 app/services/allocate_kit_inventory_service.rb create mode 100644 app/services/deallocate_kit_inventory_service.rb delete mode 100644 app/services/partners/section_error_service.rb rename app/services/reports/{diaper_report_service.rb => acquisition_report_service.rb} (57%) delete mode 100644 app/views/dashboard/_announcements.html.erb rename app/views/{reports => dashboard}/_distribution.html.erb (100%) rename app/views/{reports => dashboard}/_donation.html.erb (100%) create mode 100644 app/views/dashboard/_itemized_distributions_partial.html.erb create mode 100644 app/views/dashboard/_itemized_donations_partial.html.erb delete mode 100644 app/views/dashboard/_low_inventory_report.html.erb create mode 100644 app/views/dashboard/_manufacturer.html.erb delete mode 100644 app/views/dashboard/_outstanding_requests.html.erb delete mode 100644 app/views/dashboard/_partner_approvals.html.erb create mode 100644 app/views/dashboard/_product_drive.html.erb rename app/views/{reports => dashboard}/_purchase.html.erb (98%) delete mode 100644 app/views/distributions/validate.html.erb create mode 100644 app/views/errors/insufficient.html.erb create mode 100644 app/views/errors/internal_server_error.html.erb create mode 100644 app/views/errors/not_found.html.erb delete mode 100644 app/views/partner_users/_form.html.erb delete mode 100644 app/views/partner_users/_user.html.erb delete mode 100644 app/views/partner_users/_users.html.erb delete mode 100644 app/views/partner_users/index.html.erb delete mode 100644 app/views/partners/profiles/step/_accordion_section.html.erb delete mode 100644 app/views/partners/profiles/step/_agency_distribution_information_form.html.erb delete mode 100644 app/views/partners/profiles/step/_agency_information_form.html.erb delete mode 100644 app/views/partners/profiles/step/_agency_stability_form.html.erb delete mode 100644 app/views/partners/profiles/step/_area_served_form.html.erb delete mode 100644 app/views/partners/profiles/step/_attached_documents_form.html.erb delete mode 100644 app/views/partners/profiles/step/_executive_director_form.html.erb delete mode 100644 app/views/partners/profiles/step/_form_actions.html.erb delete mode 100644 app/views/partners/profiles/step/_media_information_form.html.erb delete mode 100644 app/views/partners/profiles/step/_organizational_capacity_form.html.erb delete mode 100644 app/views/partners/profiles/step/_partner_settings_form.html.erb delete mode 100644 app/views/partners/profiles/step/_pick_up_person_form.html.erb delete mode 100644 app/views/partners/profiles/step/_population_served_form.html.erb delete mode 100644 app/views/partners/profiles/step/_program_delivery_address_form.html.erb delete mode 100644 app/views/partners/profiles/step/_sources_of_funding_form.html.erb delete mode 100644 app/views/partners/profiles/step/edit.html.erb delete mode 100644 app/views/partners/requests/validate.html.erb delete mode 100644 app/views/profiles/step/edit.html.erb delete mode 100644 app/views/reports/_manufacturer.html.erb delete mode 100644 app/views/reports/_product_drive.html.erb delete mode 100644 app/views/reports/activity_graph.html.erb delete mode 100644 app/views/reports/distributions_summary.html.erb delete mode 100644 app/views/reports/donations_summary.html.erb delete mode 100644 app/views/reports/itemized_distributions.html.erb delete mode 100644 app/views/reports/itemized_donations.html.erb delete mode 100644 app/views/reports/manufacturer_donations_summary.html.erb delete mode 100644 app/views/reports/product_drives_summary.html.erb delete mode 100644 app/views/reports/purchases_summary.html.erb delete mode 100644 app/views/shared/_card.html.erb delete mode 100644 app/views/shared/_filtered_card.html.erb create mode 100644 app/views/storage_locations/_inventory_item_row.html.erb create mode 100644 app/views/storage_locations/inventory.json.jbuilder create mode 100644 config/initializers/postgres.rb delete mode 100644 config/locales/roles.en.yml delete mode 100644 db/migrate/20240411181624_add_hide_options_for_distribution_printing_to_organizations.rb delete mode 100644 db/migrate/20240411183741_backfill_add_hide_options_for_distribution_printing_to_organizations.rb delete mode 100644 db/migrate/20240426135118_add_pack_models.rb delete mode 100644 db/migrate/20240503200211_add_request_units_to_item_requests.rb delete mode 100644 db/migrate/20240506184235_add_signature_fields_flag_to_organizations.rb delete mode 100644 db/migrate/20240506184943_backfill_include_signature_on_organizations.rb delete mode 100644 db/migrate/20240512140954_clear_events_once_again.rb delete mode 100644 db/migrate/20240519201258_mark_distributed_started_requests_as_fulfilled.rb delete mode 100644 db/migrate/20240527151622_add_ai_liners_to_base_items.rb delete mode 100644 db/migrate/20240601155348_dedup_item_requests_in_requests.rb delete mode 100644 db/migrate/20240624185108_reactivate_users_and_remove_org_user_roles.rb delete mode 100644 db/migrate/20240703174254_create_join_table_children_items.rb delete mode 100644 db/migrate/20240704214509_backfill_partner_child_requested_items.rb delete mode 100644 db/migrate/20240711020808_add_issued_at_index_to_distribution.rb delete mode 100644 db/migrate/20240718010905_drop_partner_users_table.rb delete mode 100644 db/migrate/20240811140508_clear_events_with_feeling.rb delete mode 100644 db/migrate/20240825141541_fix_bad_kits.rb delete mode 100644 db/migrate/20240830015517_fix_invalid_distribution_event.rb delete mode 100644 db/migrate/20241002205346_backfill_vendor_business_name_from_contact.rb delete mode 100644 db/migrate/20241106184508_remove_excess_kit_village_diaper_bank.rb delete mode 100644 db/migrate/20241112184800_fix_invalid_distribution_event20241112.rb rename {docs => doc}/architecture/barcode-retrieval.svg (100%) rename {docs => doc}/architecture/decisions/0001-record-architecture-decisions.md (100%) rename {docs => doc}/architecture/decisions/0002-items-are-tracked-using-associative-models-as-they-move-through-the-system.md (100%) rename {docs => doc}/architecture/decisions/0003-multitenancy-instead-of-multiple-instances.md (100%) rename {docs => doc}/architecture/decisions/0005-extract-all-partner-operations-into-a-separate-application-intended-for-partners.md (100%) rename {docs => doc}/architecture/decisions/0006-instantiating-items-from-base-items.md (100%) rename {docs => doc}/architecture/decisions/0007-barcode-querying.md (100%) rename {docs => doc}/architecture/decisions/0008-merging-partner-base-and-diaper-base.md (100%) rename {docs => doc}/architecture/decisions/0009-stick-with-adminlte-for-app-design.md (100%) rename {docs => doc}/architecture/decisions/README.md (100%) rename {docs => doc}/architecture/multi-tenancy.svg (100%) rename {docs => doc}/architecture/overview.md (100%) rename {docs => doc}/architecture/partner-request.svg (100%) rename {docs => doc}/architecture/physical-flow.svg (100%) rename {docs => doc}/architecture/timeline-flow.svg (100%) rename {docs => doc}/templates/release-template.md (100%) delete mode 100644 docs/.gitignore delete mode 100644 docs/Gemfile delete mode 100644 docs/Gemfile.lock delete mode 100644 docs/_config.yml delete mode 100644 docs/_layouts/default.html delete mode 100644 docs/readme.md delete mode 100644 docs/user_guide/bank/account_management.md delete mode 100644 docs/user_guide/bank/asking_for_changes.md delete mode 100644 docs/user_guide/bank/community_donation_sites.md delete mode 100644 docs/user_guide/bank/community_manufacturers.md delete mode 100644 docs/user_guide/bank/community_product_drive_participants.md delete mode 100644 docs/user_guide/bank/community_product_drives.md delete mode 100644 docs/user_guide/bank/community_vendors.md delete mode 100644 docs/user_guide/bank/essentials_dashboard.md delete mode 100644 docs/user_guide/bank/essentials_distributions.md delete mode 100644 docs/user_guide/bank/essentials_donations.md delete mode 100644 docs/user_guide/bank/essentials_pick_ups.md delete mode 100644 docs/user_guide/bank/essentials_purchases.md delete mode 100644 docs/user_guide/bank/essentials_requests.md delete mode 100644 docs/user_guide/bank/exports.md delete mode 100644 docs/user_guide/bank/getting_started_access_levels.md delete mode 100644 docs/user_guide/bank/getting_started_choices.md delete mode 100644 docs/user_guide/bank/getting_started_customization.md delete mode 100644 docs/user_guide/bank/getting_started_donation_sites.md delete mode 100644 docs/user_guide/bank/getting_started_inventory.md delete mode 100644 docs/user_guide/bank/getting_started_partners.md delete mode 100644 docs/user_guide/bank/getting_started_storage_locations.md delete mode 100644 docs/user_guide/bank/getting_started_user_management.md delete mode 100644 docs/user_guide/bank/images/account_management/account_management_account_settings.png delete mode 100644 docs/user_guide/bank/images/account_management/account_management_account_settings_navigation.png delete mode 100644 docs/user_guide/bank/images/account_management/account_management_logout.png delete mode 100644 docs/user_guide/bank/images/account_management/account_management_my_organization.png delete mode 100644 docs/user_guide/bank/images/community/donation_sites/add_new_donation_site.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/create_donation_site.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/deactivate_donation_site.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/donation_sites.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/donation_sites_details.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/edit_donation_site.jpg delete mode 100644 docs/user_guide/bank/images/community/donation_sites/export_donation_sites.jpg delete mode 100644 docs/user_guide/bank/images/community/manufacturers/edit_manufacturer.jpg delete mode 100644 docs/user_guide/bank/images/community/manufacturers/manufacturer_details.jpg delete mode 100644 docs/user_guide/bank/images/community/manufacturers/manufacturers_page.jpg delete mode 100644 docs/user_guide/bank/images/community/manufacturers/new_manufacturer.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drive_participants/add_participant.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drive_participants/edit_participant.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drive_participants/export_participants.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drive_participants/participant_details.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drive_participants/product_drive_page.jpg delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_add.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_add_navigation.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_export_navigation.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_modify.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_modify_navigation.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_navigation.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_view.png delete mode 100644 docs/user_guide/bank/images/community/product_drives/community_product_drives_view_navigation.png delete mode 100644 docs/user_guide/bank/images/community/vendors/add_vendor.jpg delete mode 100644 docs/user_guide/bank/images/community/vendors/edit_vendors.jpg delete mode 100644 docs/user_guide/bank/images/community/vendors/export_vendors.jpg delete mode 100644 docs/user_guide/bank/images/community/vendors/new_vendor.jpg delete mode 100644 docs/user_guide/bank/images/community/vendors/vendor_details.jpg delete mode 100644 docs/user_guide/bank/images/community/vendors/vendors_page.jpg delete mode 100644 docs/user_guide/bank/images/essentials/dashboard/essentials_dashboard_1.png delete mode 100644 docs/user_guide/bank/images/essentials/dashboard/essentials_dashboard_2.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distribution_print_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_export_sample.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_filter.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_printout.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view.png delete mode 100644 docs/user_guide/bank/images/essentials/distributions/essentials_distributions_view_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_1.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_2.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_3.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_4.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_5.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_6.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_7.png delete mode 100644 docs/user_guide/bank/images/essentials/donations/essentials_donations_8.png delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/add_calendar.jpg delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/copy_calendar_url.png delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/distribution_from_source_to_partner.jpg delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/other_calendars.jpg delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/pickup&delivery.jpg delete mode 100644 docs/user_guide/bank/images/essentials/pick_ups/specific_day_distribution_schedule.jpg delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchaces_view.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_1.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_2.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_3.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_4.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_delete_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/purchases/essentials_purchases_export_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_confirm.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_email.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_cancel_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_export_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_picklist.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_print_picklists_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_product_totals_navigation.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_view.png delete mode 100644 docs/user_guide/bank/images/essentials/requests/essentials_requests_view_navigation.png delete mode 100644 docs/user_guide/bank/images/getting_started/customization/gs_customization_distribution_printout_customizable_sections.png delete mode 100644 docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_1.png delete mode 100644 docs/user_guide/bank/images/getting_started/customization/gs_customization_navigation_2.png delete mode 100644 docs/user_guide/bank/images/getting_started/customization/gs_customization_top_of_edit.png delete mode 100644 docs/user_guide/bank/images/getting_started/donation_sites/getting_started_donation_sites_1.png delete mode 100644 docs/user_guide/bank/images/getting_started/donation_sites/getting_started_donation_sites_2.png delete mode 100644 docs/user_guide/bank/images/getting_started/inventory/gs_inventory_1.png delete mode 100644 docs/user_guide/bank/images/getting_started/inventory/gs_inventory_2.png delete mode 100644 docs/user_guide/bank/images/getting_started/inventory/gs_inventory_3.png delete mode 100644 docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2.png delete mode 100644 docs/user_guide/bank/images/getting_started/partners/gs_just_starting_step_2_import.png delete mode 100644 docs/user_guide/bank/images/getting_started/storage_locations/gs_just_starting_step_1.png delete mode 100644 docs/user_guide/bank/images/getting_started/storage_locations/gs_storage_locations_navigation.png delete mode 100644 docs/user_guide/bank/images/getting_started/storage_locations/new_storage_location.png delete mode 100644 docs/user_guide/bank/images/getting_started/storage_locations/storage_location_index.png delete mode 100644 docs/user_guide/bank/images/getting_started/user_admin/gs_user_admin_demote_admin.png delete mode 100644 docs/user_guide/bank/images/getting_started/user_admin/gs_user_admin_invite_user.png delete mode 100644 docs/user_guide/bank/images/getting_started/user_admin/gs_user_admin_navigation.png delete mode 100644 docs/user_guide/bank/images/getting_started/user_admin/gs_user_admin_promote_user.png delete mode 100644 docs/user_guide/bank/images/getting_started/user_admin/gs_user_admin_remove_user.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_result.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_view.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_adjustments_view_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_confirm.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_delete.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_finalize.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_progress.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_resume.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_audits_view_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_delete.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_export.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_export_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_barcodes_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_category_view_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_edit.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_category_view.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_item_location_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_delete_vs_deactivate.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_edit.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_inventory_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_kits_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_reactivation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_view.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_items_view_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_allocation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_allocation_post_save.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_deactivate.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_modify_allocation_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_kits_reactivate.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_location_reactivation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_location_view_inventory.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_locations_add.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_locations_add_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_locations_coming_in.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_locations_going_out.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_storage_locations_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_delete.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_export.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_export_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_new.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_new_navigation.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_view.png delete mode 100644 docs/user_guide/bank/images/inventory/inventory_transfers_view_navigation.png delete mode 100644 docs/user_guide/bank/images/partners/partners_add.png delete mode 100644 docs/user_guide/bank/images/partners/partners_add_navigation.png delete mode 100644 docs/user_guide/bank/images/partners/partners_announcements_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_announcements_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_approving_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_approving_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_deactivate_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_deactivate_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_edit.png delete mode 100644 docs/user_guide/bank/images/partners/partners_edit_navigation.png delete mode 100644 docs/user_guide/bank/images/partners/partners_groups_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_groups_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_importing_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_importing_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_invitation_email.png delete mode 100644 docs/user_guide/bank/images/partners/partners_inviting.png delete mode 100644 docs/user_guide/bank/images/partners/partners_inviting_and_approving.png delete mode 100644 docs/user_guide/bank/images/partners/partners_profile_edit_navigation_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_profile_edit_navigation_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_recertification.png delete mode 100644 docs/user_guide/bank/images/partners/partners_recertification_email.png delete mode 100644 docs/user_guide/bank/images/partners/partners_review_application_from_dashboard.png delete mode 100644 docs/user_guide/bank/images/partners/partners_review_application_navigation.png delete mode 100644 docs/user_guide/bank/images/partners/partners_user_management.png delete mode 100644 docs/user_guide/bank/images/partners/partners_user_management_navigation_1.png delete mode 100644 docs/user_guide/bank/images/partners/partners_user_management_navigation_2.png delete mode 100644 docs/user_guide/bank/images/partners/partners_viewing_and_reactivating_deactivated.png delete mode 100644 docs/user_guide/bank/images/reports/reports_activity_graph_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_activity_graph_2.png delete mode 100644 docs/user_guide/bank/images/reports/reports_annual_survey_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_annual_survey_2.png delete mode 100644 docs/user_guide/bank/images/reports/reports_annual_survey_3.png delete mode 100644 docs/user_guide/bank/images/reports/reports_annual_survey_4.png delete mode 100644 docs/user_guide/bank/images/reports/reports_annual_survey_5.png delete mode 100644 docs/user_guide/bank/images/reports/reports_distributions_by_county_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_distributions_by_county_2.png delete mode 100644 docs/user_guide/bank/images/reports/reports_itemized_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_manufacturer_donations_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_manufacturer_donations_2.png delete mode 100644 docs/user_guide/bank/images/reports/reports_summary_distributions.png delete mode 100644 docs/user_guide/bank/images/reports/reports_summary_donations.png delete mode 100644 docs/user_guide/bank/images/reports/reports_summary_purchases.png delete mode 100644 docs/user_guide/bank/images/reports/reports_trends_1.png delete mode 100644 docs/user_guide/bank/images/reports/reports_trends_2.png delete mode 100644 docs/user_guide/bank/images/reports/reports_trends_3.png delete mode 100644 docs/user_guide/bank/images/user_management/user_access_admin_and_user.png delete mode 100644 docs/user_guide/bank/images/user_management/user_access_partner.png delete mode 100644 docs/user_guide/bank/images/user_management/user_delete_bank_user.png delete mode 100644 docs/user_guide/bank/images/user_management/user_demote_admin.png delete mode 100644 docs/user_guide/bank/images/user_management/user_invite_email.png delete mode 100644 docs/user_guide/bank/images/user_management/user_invite_new_bank_user.png delete mode 100644 docs/user_guide/bank/images/user_management/user_promote_bank_user.png delete mode 100644 docs/user_guide/bank/intro_i.md delete mode 100644 docs/user_guide/bank/intro_ii.md delete mode 100644 docs/user_guide/bank/inventory_adjustments.md delete mode 100644 docs/user_guide/bank/inventory_audits.md delete mode 100644 docs/user_guide/bank/inventory_barcodes.md delete mode 100644 docs/user_guide/bank/inventory_items.md delete mode 100644 docs/user_guide/bank/inventory_kits.md delete mode 100644 docs/user_guide/bank/inventory_storage_locations.md delete mode 100644 docs/user_guide/bank/inventory_transfers.md delete mode 100644 docs/user_guide/bank/pm_adding_a_partner.md delete mode 100644 docs/user_guide/bank/pm_announcements.md delete mode 100644 docs/user_guide/bank/pm_approving_a_partner.md delete mode 100644 docs/user_guide/bank/pm_editing_a_partner.md delete mode 100644 docs/user_guide/bank/pm_importing_partners.md delete mode 100644 docs/user_guide/bank/pm_inviting_a_partner.md delete mode 100644 docs/user_guide/bank/pm_making_a_partner_inactive.md delete mode 100644 docs/user_guide/bank/pm_other_information.md delete mode 100644 docs/user_guide/bank/pm_partner_groups.md delete mode 100644 docs/user_guide/bank/pm_partner_profiles.md delete mode 100644 docs/user_guide/bank/pm_partner_reactivation.md delete mode 100644 docs/user_guide/bank/pm_partner_statuses.md delete mode 100644 docs/user_guide/bank/pm_partner_user_admin.md delete mode 100644 docs/user_guide/bank/pm_request_distribution_cycle.md delete mode 100644 docs/user_guide/bank/pm_requesting_recertification.md delete mode 100644 docs/user_guide/bank/readme.md delete mode 100644 docs/user_guide/bank/reports_activity_graph.md delete mode 100644 docs/user_guide/bank/reports_annual_survey.md delete mode 100644 docs/user_guide/bank/reports_distributions_by_county.md delete mode 100644 docs/user_guide/bank/reports_history.md delete mode 100644 docs/user_guide/bank/reports_itemized_reports.md delete mode 100644 docs/user_guide/bank/reports_manufacturers_donations.md delete mode 100644 docs/user_guide/bank/reports_summary_reports.md delete mode 100644 docs/user_guide/bank/reports_trends.md delete mode 100644 docs/user_guide/bank/special_custom_units.md delete mode 100644 docs/user_guide/bank/user_management.md delete mode 100644 docs/user_guide/documentation_style_guide.md delete mode 100644 lib/tasks/backup_db_rds.rake create mode 100644 lib/tasks/create_inventory_in_out_daily_view.rake create mode 100644 lib/tasks/create_no_kit_daily_view.rake create mode 100644 lib/tasks/initiate_reminder_deadline_job.rake rename public/{donation_sites_template.csv => donation_sites.csv} (100%) rename public/{partners_template.csv => partners.csv} (100%) rename public/{storage_locations_template.csv => storage_locations.csv} (100%) delete mode 100644 spec/controllers/admin/account_requests_controller_spec.rb create mode 100644 spec/events/event_differ_spec.rb create mode 100644 spec/factories/donation_site.rb delete mode 100644 spec/factories/donation_sites.rb delete mode 100644 spec/factories/item_units.rb create mode 100644 spec/factories/partner_user.rb create mode 100644 spec/factories/partners/child.rb delete mode 100644 spec/factories/partners/children.rb delete mode 100644 spec/factories/partners/families.rb create mode 100644 spec/factories/partners/family.rb delete mode 100644 spec/factories/partners/item_requests.rb create mode 100644 spec/factories/partners/profile.rb delete mode 100644 spec/factories/partners/profiles.rb create mode 100644 spec/factories/partners/user.rb create mode 100644 spec/factories/product_drive.rb delete mode 100644 spec/factories/product_drives.rb delete mode 100644 spec/factories/units.rb delete mode 100644 spec/factories/users_roles.rb delete mode 100644 spec/fixtures/files/partners_with_invalid_email.csv delete mode 100644 spec/helpers/partners_helper_spec.rb create mode 100644 spec/models/inventory_item_spec.rb delete mode 100644 spec/models/item_unit_spec.rb delete mode 100644 spec/models/unit_spec.rb delete mode 100644 spec/pdfs/donation_pdf_spec.rb delete mode 100644 spec/pdfs/picklists_pdf_spec.rb delete mode 100644 spec/queries/low_inventory_query_spec.rb delete mode 100644 spec/requests/partner_group_spec.rb delete mode 100644 spec/requests/partner_groups_requests_spec.rb delete mode 100644 spec/requests/partner_users_requests_spec.rb delete mode 100644 spec/requests/reports/activity_graph_spec.rb delete mode 100644 spec/requests/reports/distributions_summary_requests_spec.rb delete mode 100644 spec/requests/reports/donations_summary_spec.rb delete mode 100644 spec/requests/reports/itemized_distributions_spec.rb delete mode 100644 spec/requests/reports/itemized_donations_spec.rb delete mode 100644 spec/requests/reports/manufacturer_donations_summary_spec.rb delete mode 100644 spec/requests/reports/product_drives_summary_spec.rb delete mode 100644 spec/requests/reports/purchases_summary_requests_spec.rb create mode 100644 spec/services/allocate_kit_inventory_service_spec.rb create mode 100644 spec/services/deallocate_kit_inventory_service_spec.rb delete mode 100644 spec/services/partners/section_error_service_spec.rb rename spec/services/reports/{diaper_report_service_spec.rb => acquisition_report_service_spec.rb} (53%) delete mode 100644 spec/support/pages/organization_distributions_summary_page.rb create mode 100644 spec/support/pages/organization_new_distribution_page.rb create mode 100644 spec/support/pages/organization_new_donation_page.rb create mode 100644 spec/support/pages/organization_new_purchase_page.rb delete mode 100644 spec/support/pages/organization_purchases_summary_page.rb delete mode 100644 spec/support/pages/organization_reports_product_drives_summary_page.rb create mode 100644 spec/system/admin/dashboard_system_spec.rb delete mode 100644 spec/system/partners/children_system_spec.rb delete mode 100644 spec/system/partners/profile_edit_system_spec.rb delete mode 100644 spec/system/partners/requests_system_spec.rb diff --git a/.devcontainer/.env.codespaces b/.devcontainer/.env.codespaces deleted file mode 100644 index fbbb31c4b7..0000000000 --- a/.devcontainer/.env.codespaces +++ /dev/null @@ -1,14 +0,0 @@ -# This is the .env that is used by the codespaces environment. -# See the .devcontainer/devcontainer.json file for how this is configured. -PG_USERNAME=postgres -PG_PASSWORD=postgres -DELAYED_JOB_USERNAME='admin' -DELAYED_JOB_PASSWORD='password' -FLIPPER_USERNAME="admin" -FLIPPER_PASSWORD="password" -RECAPTCHA_SITE_KEY=6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI -RECAPTCHA_PRIVATE_KEY=6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe - -# [OPTIONAL] - Used to fetch copies of production DB -AZURE_STORAGE_ACCOUNT_NAME= -AZURE_STORAGE_ACCESS_KEY= diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 662df208d9..0000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -# Installs Ruby 3.2.2. When human-essentials moves to a newer version of ruby, -# it will be more efficient to change the image. -# See https://github.com/devcontainers/images/blob/main/src/ruby/history/ -FROM mcr.microsoft.com/devcontainers/ruby:dev-3.2-buster -RUN export DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get -y install vim curl gpg postgresql postgresql-contrib -RUN cd /tmp -RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ - && apt-get -y install ./google-chrome-stable_current_amd64.deb \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 27e89fdc40..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,42 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/postgres -{ - "dockerComposeFile": "docker-compose.yml", - "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} - }, - "forwardPorts": [3000, 5432, 6080], - "portsAttributes": { - "3000": { - "label": "Application", - "onAutoForward": "silent" - }, - "5432": { - "label": "Database", - "onAutoForward": "silent" - }, - "6080": { - "label": "Desktop", - "onAutoForward": "silent" - } - }, - "workspaceFolder": "/workspaces/human-essentials", - "service": "app", - "customizations": { - "vscode": { - "extensions": ["Shopify.ruby-extensions-pack"], - "settings": { - "rubyLsp.rubyVersionManager": { - "identifier": "rvm" - } - } - } - }, - - // DOCKER env variable passed to Cuprite to enable --no-sandbox so Chrome can run in Docker - "remoteEnv": { - "DOCKER": "true" - }, - - "postCreateCommand": "bash -i .devcontainer/post-create.sh" - } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index d04b16f23d..0000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: '3.8' - -services: - app: - build: - context: .. - dockerfile: .devcontainer/Dockerfile - - volumes: - - ../..:/workspaces:cached - - # Increase shared memory for Chrome to run in Fluxbox - shm_size: "2gb" - - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - - # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. - network_mode: service:postgres - - postgres: - image: postgres:latest - restart: always - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - POSTGRES_USER: postgres - POSTGRES_DB: postgres - POSTGRES_PASSWORD: postgres - # Note that these ports are ignored by the devcontainer. - # Instead, the ports are specified in .devcontainer/devcontainer.json. - # ports: - # - "5432:5432" -volumes: - postgres-data: diff --git a/.devcontainer/launch.json.codespaces b/.devcontainer/launch.json.codespaces deleted file mode 100644 index dd375f5f89..0000000000 --- a/.devcontainer/launch.json.codespaces +++ /dev/null @@ -1,32 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "ruby_lsp", - "request": "launch", - "name": "Debug rspec at cursor with browser", - "program": "bundle exec rspec ${file}:${lineNumber}", - "env": { - "NOT_HEADLESS": "true" - } - }, - { - "type": "ruby_lsp", - "request": "launch", - "name": "Debug with Events rspec at cursor with browser", - "program": "bundle exec rspec ${file}:${lineNumber}", - "env": { - "NOT_HEADLESS": "true", - "EVENTS_READ": "true" - } - }, - { - "type": "ruby_lsp", - "request": "attach", - "name": "Attach to a live server" - } - ] -} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100755 index e1dcaa2fcc..0000000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,19 +0,0 @@ -RUBY_VERSION="$(cat .ruby-version | tr -d '\n')" - -# copy the file only if it doesn't already exist -cp -n .devcontainer/.env.codespaces .env -mkdir -p .vscode && cp -n .devcontainer/launch.json.codespaces .vscode/launch.json - -# If the project's required ruby version changes from 3.2.2, this command -# will download and compile the correct version, but it will take a long time. -if [ "$RUBY_VERSION" != "3.2.2" ]; then - rvm install $RUBY_VERSION - rvm use $RUBY_VERSION - echo "Ruby $RUBY_VERSION installed" -fi - -nvm install node -rbenv init bash -rbenv init zsh - -bin/setup diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d5cd5277e9..2c0cc5b874 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,6 @@ - I have added tests that prove my fix is effective or that my feature works, - New and existing unit tests pass locally with my changes ("bundle exec rake"), - Title include "WIP" if work is in progress. -- I acknowledge that I will *not* force push my branch once reviews have started. --> diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9fa5a6792e..073ae85883 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: bundler directory: "/" schedule: - interval: monthly + interval: daily open-pull-requests-limit: 10 ignore: - dependency-name: bootstrap @@ -23,9 +23,6 @@ updates: - dependency-name: geocoder versions: - 1.6.7 - - dependency-name: strong_migrations - versions: - - 1.8.0 - dependency-name: devise_invitable versions: - 2.0.4 diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index c564ab3ad6..b1f3d03c88 100644 --- a/.github/workflows/brakeman.yml +++ b/.github/workflows/brakeman.yml @@ -2,11 +2,11 @@ name: brakeman on: push: - paths-ignore: - - 'doc/**' - - '*.md' - - 'bin/*' + branches: + - main pull_request: + branches: + - main paths-ignore: - 'doc/**' - '*.md' diff --git a/.github/workflows/factory-bot-lint.yml b/.github/workflows/factory-bot-lint.yml index 21811ff1ab..f31604b2c4 100644 --- a/.github/workflows/factory-bot-lint.yml +++ b/.github/workflows/factory-bot-lint.yml @@ -2,11 +2,11 @@ name: factory bot lint on: push: - paths-ignore: - - 'doc/**' - - '*.md' - - 'bin/*' + branches: + - main pull_request: + branches: + - main paths-ignore: - 'doc/**' - '*.md' diff --git a/.github/workflows/plantuml.yml b/.github/workflows/plantuml.yml new file mode 100644 index 0000000000..95f1ea6eb1 --- /dev/null +++ b/.github/workflows/plantuml.yml @@ -0,0 +1,18 @@ +name: Generate PlantUML +on: push +jobs: + generate_plantuml: + runs-on: ubuntu-latest + name: plantuml + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: plantuml + id: plantuml + uses: grassedge/generate-plantuml-action@v1.5 + with: + message: "Render PlantUML files" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rspec-events.yml b/.github/workflows/rspec-events.yml new file mode 100644 index 0000000000..0dadf9c726 --- /dev/null +++ b/.github/workflows/rspec-events.yml @@ -0,0 +1,90 @@ +name: rspec-events + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths-ignore: + - "doc/**" + - "*.md" + - "bin/*" + +jobs: + rspec-events: + runs-on: ubuntu-latest + + services: + db: + image: postgres:12.3 + env: + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + # https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix + strategy: + fail-fast: false + matrix: + # Set N number of parallel jobs you want to run tests on. + # Use higher number if you have slow tests to split them on more parallel jobs. + # Remember to update ci_node_index below to 0..N-1 + ci_node_total: [2] + # set N-1 indexes for parallel jobs + # When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc + ci_node_index: [0, 1] + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Install PostgreSQL client + run: | + sudo apt-get -yqq install libpq-dev + - name: Build App + env: + POSTGRES_HOST: localhost + DATABASE_HOST: localhost + PG_USERNAME: postgres + PG_PASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PORT: 5432 + RAILS_ENV: test + run: | + bundle exec rake db:create + bundle exec rake db:schema:load + - name: Run rspec with events + env: + POSTGRES_HOST: localhost + DATABASE_HOST: localhost + PG_USERNAME: postgres + PG_PASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PORT: 5432 + PGHOST: localhost + PGUSER: postgres + RAILS_ENV: test + KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: ${{ secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC }} + KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }} + KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }} + KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true + KNAPSACK_PRO_LOG_LEVEL: info + KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN: "{spec/system/**{,/*/**}/*_spec.rb,spec/requests/**{,/*/**}/*_spec.rb}" + EVENTS_READ: true + run: | + RUBYOPT='-W:no-deprecated -W:no-experimental' bin/knapsack_pro_rspec + - name: Upload artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: failed-browser-tests + path: tmp/screenshots diff --git a/.github/workflows/rspec-system-events.yml b/.github/workflows/rspec-system-events.yml new file mode 100644 index 0000000000..23e31f1fb0 --- /dev/null +++ b/.github/workflows/rspec-system-events.yml @@ -0,0 +1,93 @@ +name: rspec-system-events + +on: + push: + branches: + - main + pull_request: + branches: + - main + paths-ignore: + - "doc/**" + - "*.md" + - "bin/*" + +jobs: + rspec-system-events: + runs-on: ubuntu-latest + + services: + db: + image: postgres:12.3 + env: + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + # https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix + strategy: + fail-fast: false + matrix: + # Set N number of parallel jobs you want to run tests on. + # Use higher number if you have slow tests to split them on more parallel jobs. + # Remember to update ci_node_index below to 0..N-1 + ci_node_total: [6] + # set N-1 indexes for parallel jobs + # When you run 2 parallel jobs then first job will have index 0, the second job will have index 1 etc + ci_node_index: [0, 1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Install PostgreSQL client + run: | + sudo apt-get -yqq install libpq-dev + - name: Build App with asset compilation + env: + POSTGRES_HOST: localhost + DATABASE_HOST: localhost + PG_USERNAME: postgres + PG_PASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PORT: 5432 + RAILS_ENV: test + run: | + bundle exec rake db:create + bundle exec rake db:schema:load + bundle exec rails assets:precompile + - name: Run rspec with events + env: + POSTGRES_HOST: localhost + DATABASE_HOST: localhost + PG_USERNAME: postgres + PG_PASSWORD: password + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PORT: 5432 + PGHOST: localhost + PGUSER: postgres + RAILS_ENV: test + KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: ${{ secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC }} + KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }} + KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }} + KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true + KNAPSACK_PRO_LOG_LEVEL: info + KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/**{,/*/**}/*_spec.rb,spec/requests/**{,/*/**}/*_spec.rb}" + EVENTS_READ: true + run: | + RUBYOPT='-W:no-deprecated -W:no-experimental' bin/knapsack_pro_rspec + - name: Upload artifacts + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: failed-browser-tests + path: | + tmp/capybara + tmp/screenshots diff --git a/.github/workflows/rspec-system.yml b/.github/workflows/rspec-system.yml index 2402b60d87..d9b7c68e76 100644 --- a/.github/workflows/rspec-system.yml +++ b/.github/workflows/rspec-system.yml @@ -2,11 +2,11 @@ name: rspec-system on: push: - paths-ignore: - - "doc/**" - - "*.md" - - "bin/*" + branches: + - main pull_request: + branches: + - main paths-ignore: - "doc/**" - "*.md" diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index 177272d45d..a4bd758938 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -2,11 +2,11 @@ name: rspec on: push: - paths-ignore: - - "doc/**" - - "*.md" - - "bin/*" + branches: + - main pull_request: + branches: + - main paths-ignore: - "doc/**" - "*.md" diff --git a/.github/workflows/ruby_lint.yml b/.github/workflows/ruby_lint.yml index 16d3548d95..f85057735a 100644 --- a/.github/workflows/ruby_lint.yml +++ b/.github/workflows/ruby_lint.yml @@ -2,11 +2,15 @@ name: rubocop lint on: push: + branches: + - main paths-ignore: - "doc/**" - "*.md" - "bin/*" pull_request: + branches: + - main paths-ignore: - "doc/**" - "*.md" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fc9a7dccba..12940bac4f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -225,6 +225,7 @@ 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..8f14e91706 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,234 +1,48 @@ -# Welcome Contributors! 👋 +# Contributing We ♥ contributors! By participating in this project, you agree to abide by the Ruby for Good [code of conduct](https://github.com/rubyforgood/human-essentials/blob/main/code-of-conduct.md). -If you're new here, here are some things you should know: -- A great introductory overview of the application is available at the [wiki](https://github.com/rubyforgood/human-essentials/wiki/Application-Overview). -- Issues tagged "Help Wanted" are self-contained and great for new contributors -- Pull Requests are reviewed within a week or so -- Ensure your build passes linting and tests and addresses the issue requirements -- This project relies entirely on volunteers, so please be patient with communication - -# Communication 💬 -If you have any questions about an issue, comment on the issue, open a new issue, or ask in [the RubyForGood slack](https://join.slack.com/t/rubyforgood/shared_invite/zt-2k5ezv241-Ia2Iac3amxDS8CuhOr69ZA). human-essentials has a `#human-essentials` channel in the Slack. Our channel in slack also contains a zoom link for office hours every day office hours are held. - -Many helpful members are available to answer your questions. Just ask, and someone will be there to help you! - +If you have any questions about an issue, comment on the issue, open a new issue or ask in [the RubyForGood slack](https://join.slack.com/t/rubyforgood/shared_invite/zt-21pyz2ab8-H6JgQfGGI0Ab6MfNOZRIQA). human-essentials has a `#human-essentials` channel in the Slack. Our channel in slack also contains a zoom link for office hours every day office hours are held. + You won't be yelled at for giving your best effort. The worst that can happen is that you'll be politely asked to change something. We appreciate any sort of contributions, and don't want a wall of rules to get in the way of that. -# Getting Started -## Local Environment 🛠️ - -#### Install WSL2 first if Using Windows -Follow [documentation](https://docs.microsoft.com/en-us/windows/wsl/install-win10) from Microsoft for enabling and installing Windows Subsystem For Linux 2 on your machine. - -Make sure to install **Ubuntu** as your Linux distribution. (This should be default.) - -**Note:** If you run into any issues with a command not running, restart your machine. - - -1. Install Ruby - - Install the version specified in [`.ruby-version`](.ruby-version). - - Visit the [Install Ruby on Rails](https://gorails.com/setup/osx/12-monterey) guide by GoRails for Ubuntu, Windows, and macOSX setup. ⚠️ Follow only the Installing Ruby step, as our project setup differs ⚠️ It is highly recommended you use a ruby version manager such as [rbenv](https://github.com/rbenv/rbenv), [asdf](https://asdf-vm.com/), or [rvm](https://rvm.io/). - - Verify that your Ruby installation works by running `ruby -v`. -2. Install Postgres - - Follow one of these guides: [MacOSX](https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-macos), [Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-ubuntu-18-04). - - Create a `database.yml` file on `config/` directory with your database configurations. You can also copy the existing files called [`database.yml.example`](config/database.yml.example) and [`.env.example`](.env.example) and change the credentials. -3. Clone the project and switch to its directory -4. Run `bin/setup` -5. Run `bin/start` and visit http://localhost:3000/ to see the human essentials page. -6. Log in as a sample user with the default [credentials](#credentials). - -## Credentials - These credentials also work for [staging](https://staging.humanessentials.app/): - -
- Super Users 🦸🏽‍♀️ - - ``` - username: superadmin@example.com - password: password! - ``` -
- -
- Bank Users 🏦 - - ``` - Organization Admin - Email: org_admin1@example.com - Password: password! - - User - Email: user_1@example.com - Password: password! - ``` -
- -
- Partner Users 👥 - - ``` - Verified Partner - Email: verified@example.com - Password: password! - - Invited Partner - Email: invited@pawneehomeless.com - Password: password! - - Unverified Partner - Email: unverified@pawneepregnancy.com - Password: password! - - Recertification Required Partner - Email: recertification_required@example.com - Password: password! - - Waiting Approval Partner - Email: waiting@example.com - Password: password! - - Another approved partner (with all groups): - Email: approved_2@example.com - Pasword: password! - ``` -
- -## Codespaces and Dev Container - EXPERIMENTAL 🛠️ - -[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/rubyforgood/human-essentials/tree/main?quickstart=1) - -[![Clone and open in VSCode Dev Container](https://img.shields.io/static/v1?label=Dev%20Containers&message=Clone%20and%20Open%20in%20VSCode&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rubyforgood/human-essentials) - -1. Create the container: - - To run the container on a Github VM, follow the Codespace link above. You can connect to the Codespace using VSCode or the VSCode web editor. - - Or follow instructions to [create a new Codespace.](https://docs.github.com/en/codespaces/developing-in-a-codespace/creating-a-codespace-for-a-repository) - - To clone this repo and run the container locally, follow instructions to [install VSCode and Docker](https://code.visualstudio.com/docs/devcontainers/containers). Click the Dev Container link above. Don't forget to add a git remote pointing to your fork once the container is setup and you want to push changes. -2. Wait for the container to start. This will take a few (10-15) minutes since Ruby needs to be installed, the database needs to be created, and the `bin/setup` script needs to run -3. Run `bin/start`. On the Ports tab, visit the forwarded port 3000 URL marked as Application to see the human essentials page. -4. Login as a sample user with the default [credentials](#credentials). +## Contributing Steps +### Issues +All work is organized by issues. +[Find issues here.](https://github.com/rubyforgood/human-essentials/issues) -## Troubleshooting 👷🏼‍♀️ +If you would like to contribute, please ask for an issue to be assigned to you. +If you would like to contribute something that is not represented by an issue, please make an issue and assign yourself. +Only take multiple issues if they are related and you can solve all of them at the same time with the same pull request. -Please let us know by opening up an issue! We have many new contributors come through and it is likely what you experienced will happen to them as well. +### Pull Requests +If you are so inclined, you can open a draft PR as you continue to work on it. -- *"My RBENV installation didn't work!"* - The rbenv repository provides a [rbenv-doctor script](https://github.com/rbenv/rbenv-installer#rbenv-doctor) to verify the installation and check if a ruby version is installed +1. Follow [the setup guide](https://github.com/rubyforgood/human-essentials#%EF%B8%8F-getting-started) to get the project working locally. -# Wiki Contribution Workflow -1. Follow this [SO post](https://stackoverflow.com/a/56480628/13342792) to force push the main repo's Wiki to your fork's Wiki. -2. Make edits to your fork's Wiki. -3. Create a documentation issue about your changes. Make sure to note which pages you changed and link to your fork's Wiki. -4. Someone will review and approve your changes and merge them into the main Wiki following this [SO post](https://stackoverflow.com/a/56810747/13342792) +1. Run the tests. We only take pull requests with passing tests, and it's great to know that you have a clean slate: `bundle exec rspec` -# 🤝 Code Contribution Workflow +1. Add a test for your change. If you are adding functionality or fixing a bug, you should add a test! -1. **Identify an unassigned issue**. Read more [here](#issues) about how to pick a good issue. -2. **Assign it** to avoid duplicated efforts (or request assignment by adding a comment). -3. **Fork the repo** if you're not a contributor yet. Read about becoming a contributor [here](#becoming-a-repo-contributor). -4. **Create a new branch** for the issue using the format `XXX-brief-description-of-feature`, where `XXX` is the issue number. -5. **Commit fixes locally** using descriptive messages that indicate the affected parts of the app. Read debugging tips [here](#debugging). -6. If you create a new model run `bundle exec annotate` from the root of the app -7. **Create RSpec tests** to validate that your work fixes the issue (if you need help with this, please reach out!). Read guidelines [here](#writing-browsersystemfeature-testsspecs). -8. **Run the tests** and make sure all tests pass successfully; if any fail, fix the issues causing the failures. Read guidelines [here](#test-before-submitting-pull-requests). -9. **Final commit** if tests needed fixing. -10. **Squash smaller commits.** Read guidelines [here](#squashing-commits). -11. **Push** up the branch -12. **Create a pull request** and indicate the addressed issue (e.g. `Resolves #1`) in the title, which will ensure the issue gets closed automatically when the pull request gets merged. Read PR guidelines [here](#pull-requests). -13. **Code review**: At this point, someone will work with you on doing a code review. The automated tests will run linting, rspec, and brakeman tests. If the automated tests give :+1: to the PR merging, we can then do any additional (staging) testing as needed. +1. Run linters and fix any linting errors they brings up. + - `bin/lint` -14. **Merge**: Finally if all looks good the core team will merge your code in; if your feature branch was in this main repository, the branch will be deleted after the PR is merged. +1. Push to your branch/fork and submit a pull request. Include the issue number (ex. `Resolves #1`) in the PR description. This will ensure the issue gets closed automatically when the pull request gets merged. -15. Deploys are currently done about once a week! Read the deployment process [here](#deployment-process). +## 🤝 Contributing Guidelines -## Issues Please feel free to contribute! While we welcome all contributions to this app, pull-requests that address outstanding Issues *and* have appropriate test coverage for them will be strongly prioritized. In particular, addressing issues that are tagged with the next milestone should be prioritized higher. -All work is organized by issues. -[Find issues here.](https://github.com/rubyforgood/human-essentials/issues) - -If you would like to contribute, please ask for an issue to be assigned to you. -If you would like to contribute something that is not represented by an issue, please make an issue and assign yourself. -Only take multiple issues if they are related and you can solve all of them at the same time with the same pull request. - -## Becoming a Repo Contributor - -Users that are frequent contributors and are involved in discussion (join the slack channel! :)) may be given direct Contributor access to the Repo so they can submit Pull Requests directly instead of Forking first. - -## Debugging -If starting server directly, via `rail s` or `rail console`, or built-in debugger in RubyMine, or running `bundle exec rspec path/to/spec.rb:line_no`, then you can use `binding.pry` to debug. Drop the pry where you want the execution to pause. - -If starting via Procfile with `bin/start`, then drop a ``binding.remote_pry`` into the line where you want execution to pause at. Then run ``pry-remote`` in the terminal to connect to it. -https://github.com/Mon-Ouie/pry-remote - -If you want to connect via Shopify Ruby LSP VSCode extension or rdbg, start the server with `bundle exec rdbg -O -n -c -- bin/rails server -p 3000` - -### Codespaces -When running tests in browser, visit the forwarded port 6080 URL to see the browser in Codespaces. You can also visit this port to access the GUI desktop in Codespaces. - -In VSCode Run and Debug view, there are some helpful defaults for running RSpec tests in browser at your cursor as well as attaching to a live server. Make sure the Ruby LSP server is started before debugging. - -## Squashing commits - -Consider the balance of "polluting the git log with commit messages" vs. "providing useful detail about the history of changes in the git log". If you have several smaller commits that serve a one purpose, you are encouraged to squash them into a single commit. There's no hard and fast rule here about this (for now), just use your best judgement. Please don't squash other people's commits. Everyone who contributes here deserves credit for their work! :) - -Only commit the schema.rb only if you have committed anything that would change the DB schema (i.e. a migration). - -## Pull Requests -### Stay scoped - -Try to keep your PRs limited to one particular issue, and don't make changes that are out of scope for that issue. If you notice something that needs attention but is out of scope, please [create a new issue](https://github.com/rubyforgood/human-essentials/issues/new). - -### In-flight pull requests - -If you are so inclined, you can open a draft PR as you continue to work on it. Sometimes we want to get a PR up there and going so that other people can review it or provide feedback, but maybe it's incomplete. This is OK, but if you do it, please tag your PR with `in-progress` label so that we know not to review / merge it. - -## Tests 🧪 -### Writing Browser/System/Feature Tests/Specs - -Add a test for your change. If you are adding functionality or fixing a bug, you should add a test! - -If you are inexperienced in writing tests or get stuck on one, please reach out for help :) - -#### Guidelines -- Prefer request tests over system tests (which run much slower) unless you need to test Javascript or other interactivity -- When creating factories, in each RSpec test, hard code all values that you check with a RSpec matcher. Don't check FactoryBot default values. See [#4217](https://github.com/rubyforgood/human-essentials/issues/4217) for why. -- Keep individual tests tightly scoped, only test the endpoint that you want to test. E.g. create inventory directly using `TestInventory` rather than using an additional endpoint. -- You probably don't need to write new tests when simple re-stylings are done (ie. the page may look slightly different but the Test suite is unaffected by those changes). - -#### Useful Tips -- If you need to see a browser/system spec run in the browser, you can use the following env variable - ``` - NOT_HEADLESS=true bundle exec rspec - ``` -- We've added [magic_test](https://github.com/bullet-train-co/magic_test) which makes creating browser specs much easier. It allows you to record actions on the browser running the specs and easily paste them into the spec. You can do this by adding `magic_test` within your system spec: - ```rb - it "does some browser stuff" do - magic_test - end - ``` - and run the spec using this command: - ``` - MAGIC_TEST=1 NOT_HEADLESS=true bundle exec rspec ` - ``` - **See videos of it in action [here](https://twitter.com/andrewculver/status/1366062684802846721)** -- Helpful classes for viewing and modifying inventory include `View::Inventory`, `TestInventory` and various `CreateService` services, see the [Event Sourcing wiki page](https://github.com/rubyforgood/human-essentials/wiki/Event-Sourcing). - -### Test before submitting pull requests -Before submitting a pull request, run all tests and lints. Fix any broken tests and lints before submitting a pull request. - -#### Continuous Integration -- There are Github Actions workflows which will run all tests in parallel using Knapsack and lints whenever you push a commit to your fork. -- 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 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. - -```ruby - it "works!" do - pending("Need to implement this") - expect(my_code).to be_valid - end -``` +To contribute, do these things: + + * **Identify an issue** you want to work on that is not currently assigned to anyone + * **Assign it** or have it assigned to yourself (so that no one else works on it while you are) + * (If not already a Contributor, fork the repo first) + * **Checkout a new issue branch** -- there's no absolute requirements on this, but we encourage the branch name format `XXX-brief-description-of-feature` where `XXX` is the issue number. + * **Do the work** -- discuss any questions on the Issues as needed (we try to be pretty good about answering questions!) + * (If you created a new model, run `bundle exec annotate` from the root of the app) + * **Create tests** to provide proof that your work fixes the Issue (if you need help with this, please reach out!) + * **Commit locally**, using descriptive commit messages that acknowledge, to the best of your ability, the parts of the app that are affected by the commit. + * **Run the tests** and make sure they run green; if they don't, fix whatever broke so that the tests pass + * **Final commit** if any tests had to be fixed + * **Push** up the branch + * **Create a Pull Request** - Please indicate which issue it addresses in your pull-request title. diff --git a/Gemfile b/Gemfile index 1744d1218e..8024236d71 100644 --- a/Gemfile +++ b/Gemfile @@ -10,11 +10,11 @@ end # User management and login workflow. gem "devise", '>= 4.7.1' # Postgres database adapter. -gem "pg", "~> 1.5.7" +gem "pg", "~> 1.5.6" # Web server. gem "puma" # Rails web framework. -gem "rails", "7.1.3.4" +gem "rails", "7.0.8" ###### MODELS / DATABASE ####### @@ -32,8 +32,6 @@ gem "paper_trail" # Associates users with roles. gem "rolify", "~> 6.0" # Enforces "safe" migrations. -# Pinned to 1.8.0 because 2.0.0 no longer support postgres v10 -# And as of now we are using postgres v10 in production gem "strong_migrations", "1.8.0" # used in events gem 'dry-struct' @@ -105,6 +103,7 @@ gem "clockwork" # These are gems that aren't used directly, only as dependencies for other gems. # Technically they don't need to be in this Gemfile at all, but we are pinning them to # specific versions for compatibility reasons. +gem "mini_racer", "~> 0.12.0" gem "nokogiri", ">= 1.10.4" gem "image_processing" gem "sprockets", "~> 4.2.1" @@ -125,7 +124,7 @@ group :development, :test, :staging do # Generate models based on factory definitions. gem 'factory_bot_rails' # Ensure the database is in a clean state on every test. - gem "database_cleaner-active_record", '~> 2.2' + gem "database_cleaner-active_record", '~> 2.1' # Generate fake data for use in tests. gem 'faker' end @@ -147,17 +146,15 @@ group :development, :test do gem "pry-remote" # Add-on for command line to create a simple debugger. gem "pry-nav" - # Debugger which supports rdbg and Shopify Ruby LSP VSCode extension - gem "debug", ">= 1.0.0" # RSpec behavioral testing framework for Rails. - gem "rspec-rails", "~> 7.0.1" + gem "rspec-rails", "~> 6.1.2" # Static analysis / linter. gem "rubocop" # Rails add-on for static analysis. gem 'rubocop-performance' - gem "rubocop-rails", "~> 2.25.1" + gem "rubocop-rails", "~> 2.24.1" # Default rules for Rubocop. - gem "standard", "~> 1.40" + gem "standard", "~> 1.35" # Erb linter. gem "erb_lint" end @@ -199,11 +196,9 @@ group :test do # More concise test ("should") matchers gem 'shoulda-matchers', '~> 6.2' # Mock HTTP requests and ensure they are not called during tests. - gem "webmock", "~> 3.24" + gem "webmock", "~> 3.23" # Interface capybara to chrome headless gem "cuprite" - # Read PDF files for tests - gem "pdf-reader" end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem @@ -215,6 +210,6 @@ if %w(mingw mswin x64_mingw jruby).include?(RUBY_PLATFORM) end # Use Redis for Action Cable -gem "redis", "~> 5.3" +gem "redis", "~> 5.2" gem "importmap-rails", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index 480023b0e5..d777bb0d1f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,84 +1,73 @@ GEM remote: https://rubygems.org/ specs: - Ascii85 (1.1.1) - actioncable (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + actioncable (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - zeitwerk (~> 2.6) - actionmailbox (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionmailbox (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.4) - actionpack (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionmailer (7.0.8) + actionpack (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activesupport (= 7.0.8) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.2) - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) - nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4) - rack-session (>= 1.0.1) + rails-dom-testing (~> 2.0) + actionpack (7.0.8) + actionview (= 7.0.8) + activesupport (= 7.0.8) + rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.4) - actionpack (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.8) + actionpack (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.0.8) + activesupport (= 7.0.8) builder (~> 3.1) - erubi (~> 1.11) - rails-dom-testing (~> 2.2) - rails-html-sanitizer (~> 1.6) - activejob (7.1.3.4) - activesupport (= 7.1.3.4) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.8) + activesupport (= 7.0.8) globalid (>= 0.3.6) - activemodel (7.1.3.4) - activesupport (= 7.1.3.4) - activerecord (7.1.3.4) - activemodel (= 7.1.3.4) - activesupport (= 7.1.3.4) - timeout (>= 0.4.0) - activestorage (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activesupport (= 7.1.3.4) + activemodel (7.0.8) + activesupport (= 7.0.8) + activerecord (7.0.8) + activemodel (= 7.0.8) + activesupport (= 7.0.8) + activestorage (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activesupport (= 7.0.8) marcel (~> 1.0) - activesupport (7.1.3.4) - base64 - bigdecimal + mini_mime (>= 1.1.0) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb i18n (>= 1.6, < 2) minitest (>= 5.1) - mutex_m tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - afm (0.2.2) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) @@ -100,14 +89,14 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - better_html (2.1.1) + better_html (2.0.2) actionview (>= 6.0) activesupport (>= 6.0) ast (~> 2.0) erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.8) + bigdecimal (3.1.6) bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) @@ -115,12 +104,12 @@ GEM autoprefixer-rails (>= 9.1.0) popper_js (>= 2.11.6, < 3) sassc-rails (>= 2.0.0) - brakeman (6.2.1) + brakeman (6.1.2) racc - bugsnag (6.27.1) + bugsnag (6.26.4) concurrent-ruby (~> 1.0) - builder (3.3.0) - bullet (7.2.0) + builder (3.2.4) + bullet (7.1.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) capybara (3.40.0) @@ -141,30 +130,26 @@ GEM activesupport tzinfo coderay (1.1.3) - concurrent-ruby (1.3.4) + concurrent-ruby (1.2.3) connection_pool (2.4.1) - coverband (6.1.2) + coverband (6.1.1) redis (>= 3.0) crack (1.0.0) bigdecimal rexml crass (1.0.6) - csv (3.3.0) - cuprite (0.15.1) + cuprite (0.15) capybara (~> 3.0) - ferrum (~> 0.15.0) - database_cleaner-active_record (2.2.0) + ferrum (~> 0.14.0) + database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.4) - debug (1.9.2) - irb (~> 1.10) - reline (>= 0.3.8) + date (3.3.3) debug_inspector (1.2.0) - delayed_job (4.1.12) + delayed_job (4.1.11) activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.10) + delayed_job_active_record (4.1.8) activerecord (>= 3.0, < 8.0) delayed_job (>= 3.0, < 5) delayed_job_web (1.4.4) @@ -185,11 +170,10 @@ GEM discard (1.3.0) activerecord (>= 4.2, < 8) docile (1.4.0) - dotenv (3.1.4) - dotenv-rails (3.1.4) - dotenv (= 3.1.4) + dotenv (3.1.0) + dotenv-rails (3.1.0) + dotenv (= 3.1.0) railties (>= 6.1) - drb (2.2.1) dry-core (1.0.1) concurrent-ruby (~> 1.0) zeitwerk (~> 2.6) @@ -209,23 +193,23 @@ GEM dry-inflector (~> 1.0) dry-logic (~> 1.4) zeitwerk (~> 2.6) - erb_lint (0.7.0) + erb_lint (0.5.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) rainbow - rubocop (>= 1) + rubocop smart_properties - erubi (1.13.0) - execjs (2.10.0) + erubi (1.12.0) + execjs (2.9.1) factory_bot (6.4.5) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.4.2) + faker (3.3.1) i18n (>= 1.8.11, < 2) - faraday (1.10.4) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -243,39 +227,35 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.2) + faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - ferrum (0.15) + ferrum (0.14) addressable (~> 2.5) concurrent-ruby (~> 1.1) webrick (~> 1.7) - websocket-driver (~> 0.7) - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-darwin) - ffi (1.17.0-x86_64-linux-gnu) + websocket-driver (>= 0.6, < 0.8) + ffi (1.16.3) filterrific (5.2.5) - flipper (1.3.1) + flipper (1.3.0) concurrent-ruby (< 2) flipper-active_record (1.3.0) activerecord (>= 4.2, < 8) flipper (~> 1.3.0) - flipper-ui (1.3.1) + flipper-ui (1.3.0) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.3.1) + flipper (~> 1.3.0) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) sanitize (< 7) foreman (0.88.1) formatador (1.1.0) - geocoder (1.8.3) - base64 (>= 0.1.0) - csv (>= 3.0.0) + geocoder (1.8.2) globalid (1.2.1) activesupport (>= 6.1) groupdate (6.4.0) @@ -294,35 +274,29 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - hashdiff (1.1.1) - hashery (2.1.2) + hashdiff (1.1.0) hashie (5.0.0) - httparty (0.22.0) - csv + httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.6) + i18n (1.14.4) concurrent-ruby (~> 1.0) - icalendar (2.10.2) + icalendar (2.10.1) ice_cube (~> 0.16) - ice_cube (0.17.0) + ice_cube (0.16.4) ice_nine (0.11.2) - image_processing (1.13.0) + image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) importmap-rails (2.0.1) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.7.2) - irb (1.14.1) - rdoc (>= 4.0.0) - reline (>= 0.4.2) - jbuilder (2.13.0) + jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) - json (2.7.5) - jwt (2.9.1) + json (2.7.1) + jwt (2.8.1) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -336,7 +310,7 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - knapsack_pro (7.6.2) + knapsack_pro (7.0.1) rake language_server-protocol (3.17.0.3) launchy (3.0.0) @@ -344,36 +318,40 @@ GEM childprocess (~> 5.0) letter_opener (1.10.0) launchy (>= 2.2, < 4) + libv8-node (21.7.2.0-arm64-darwin) + libv8-node (21.7.2.0-x86_64-darwin) + libv8-node (21.7.2.0-x86_64-linux) lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.23.1) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) lumberjack (1.2.9) - magic_test (0.1.0) + magic_test (0.0.9) capybara (>= 3.0) pry pry-stack_explorer - rails (>= 5.0) + rails (>= 6.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.0.2) matrix (0.4.2) - method_source (1.1.0) - mini_magick (4.13.2) + method_source (1.0.0) + mini_magick (4.11.0) mini_mime (1.1.5) - minitest (5.25.1) + mini_racer (0.12.0) + libv8-node (~> 21.7.2.0) + minitest (5.22.3) monetize (1.12.0) money (~> 6.12) money (6.16.0) @@ -383,31 +361,29 @@ GEM monetize (~> 1.9) money (~> 6.13) railties (>= 3.0) - multi_xml (0.7.1) - bigdecimal (~> 3.1) - multipart-post (2.4.1) + multi_xml (0.6.0) + multipart-post (2.4.0) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - mutex_m (0.2.0) nenv (0.3.0) net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.12) + net-imap (0.4.3) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.2) + net-protocol (0.2.1) timeout - net-smtp (0.5.0) + net-smtp (0.4.0) net-protocol - newrelic_rpm (9.13.0) - nio4r (2.7.3) - nokogiri (1.16.7-arm64-darwin) + newrelic_rpm (9.9.0) + nio4r (2.7.0) + nokogiri (1.16.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) + nokogiri (1.16.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) @@ -423,15 +399,15 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-google-oauth2 (1.2.0) - jwt (>= 2.9) + omniauth-google-oauth2 (1.1.2) + jwt (>= 2.0) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) - omniauth-rails_csrf_protection (1.0.2) + omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) omniauth (~> 2.0) orderly (0.1.1) @@ -441,18 +417,12 @@ GEM paper_trail (15.1.0) activerecord (>= 6.1) request_store (~> 1.4) - parallel (1.26.3) - parser (3.3.5.1) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) racc pdf-core (0.9.0) - pdf-reader (2.12.0) - Ascii85 (~> 1.0) - afm (~> 0.2.1) - hashery (~> 2.0) - ruby-rc4 - ttfunk - pg (1.5.7) + pg (1.5.6) popper_js (2.11.8) prawn (2.4.0) pdf-core (~> 0.9.0) @@ -471,44 +441,39 @@ GEM yard (~> 0.9.11) pry-nav (1.0.0) pry (>= 0.9.10, < 0.15) - pry-rails (0.3.11) - pry (>= 0.13.0) + pry-rails (0.3.9) + pry (>= 0.10.4) pry-remote (0.1.8) pry (~> 0.9) slop (~> 3.0) pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - psych (5.1.2) - stringio - public_suffix (6.0.1) - puma (6.4.3) + public_suffix (5.0.4) + puma (6.4.2) nio4r (~> 2.0) - racc (1.8.1) - rack (2.2.10) + racc (1.7.3) + rack (2.2.9) rack-protection (3.1.0) rack (~> 2.2, >= 2.2.4) rack-session (1.0.2) rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rackup (1.0.0) - rack (< 3) - webrick - rails (7.1.3.4) - actioncable (= 7.1.3.4) - actionmailbox (= 7.1.3.4) - actionmailer (= 7.1.3.4) - actionpack (= 7.1.3.4) - actiontext (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activemodel (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rails (7.0.8) + actioncable (= 7.0.8) + actionmailbox (= 7.0.8) + actionmailer (= 7.0.8) + actionpack (= 7.0.8) + actiontext (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activemodel (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) bundler (>= 1.15.0) - railties (= 7.1.3.4) + railties (= 7.0.8) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -525,75 +490,70 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) - irb - rackup (>= 1.0.0) + railties (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) + method_source rake (>= 12.2) - thor (~> 1.0, >= 1.2.2) - zeitwerk (~> 2.6) + thor (~> 1.0) + zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdoc (6.7.0) - psych (>= 4.0.0) - recaptcha (5.17.0) - redis (5.3.0) + recaptcha (5.16.0) + redis (5.2.0) redis-client (>= 0.22.0) - redis-client (0.22.2) + redis-client (0.22.1) connection_pool - regexp_parser (2.9.2) - reline (0.5.10) - io-console (~> 0.5) + regexp_parser (2.9.0) request_store (1.5.1) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.3.9) + rexml (3.2.6) rolify (6.0.1) rouge (4.1.2) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.1) + rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.0.1) - actionpack (>= 7.0) - activesupport (>= 7.0) - railties (>= 7.0) + rspec-rails (6.1.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.65.1) + rubocop (1.62.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) + regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.33.0) - parser (>= 3.3.1.0) - rubocop-performance (1.22.1) + rubocop-ast (1.31.2) + parser (>= 3.3.0.4) + rubocop-performance (1.20.2) rubocop (>= 1.48.1, < 2.0) - rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.1) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rails (2.24.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -601,12 +561,10 @@ GEM ruby-graphviz (1.2.5) rexml ruby-progressbar (1.13.0) - ruby-rc4 (0.1.5) - ruby-vips (2.2.2) + ruby-vips (2.1.4) ffi (~> 1.12) - logger ruby2_keywords (0.0.5) - sanitize (6.1.3) + sanitize (6.1.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) sass-rails (6.0.0) @@ -622,7 +580,7 @@ GEM shellany (0.0.1) shoulda-matchers (6.2.0) activesupport (>= 5.2.0) - simple_form (5.3.1) + simple_form (5.3.0) actionpack (>= 5.2) activemodel (>= 5.2) simplecov (0.22.0) @@ -648,35 +606,35 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - standard (1.40.0) + standard (1.35.1) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.65.0) + rubocop (~> 1.62.0) standard-custom (~> 1.0.0) - standard-performance (~> 1.4) + standard-performance (~> 1.3) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.5.0) + standard-performance (1.3.1) lint_roller (~> 1.1) - rubocop-performance (~> 1.22.0) - stimulus-rails (1.3.4) + rubocop-performance (~> 1.20.2) + stimulus-rails (1.3.3) railties (>= 6.0.0) - stringio (3.1.1) strong_migrations (1.8.0) activerecord (>= 5.2) - terser (1.2.4) + terser (1.2.2) execjs (>= 0.3.0, < 3) - thor (1.3.2) + thor (1.3.1) tilt (2.2.0) - timeout (0.4.1) + timeout (0.4.0) ttfunk (1.7.0) - turbo-rails (2.0.10) + turbo-rails (1.4.0) actionpack (>= 6.0.0) + activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.6.0) + unicode-display_width (2.5.0) uniform_notifier (1.16.0) version_gem (1.1.4) warden (1.2.9) @@ -686,25 +644,24 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.24.0) + webmock (3.23.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.2) + webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.34) - zeitwerk (2.6.18) + zeitwerk (2.6.13) PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 - arm64-darwin-24 x86_64-darwin-20 x86_64-darwin-21 x86_64-darwin-22 @@ -726,8 +683,7 @@ DEPENDENCIES clockwork coverband cuprite - database_cleaner-active_record (~> 2.2) - debug (>= 1.0.0) + database_cleaner-active_record (~> 2.1) delayed_job_active_record delayed_job_web devise (>= 4.7.1) @@ -759,6 +715,7 @@ DEPENDENCIES lograge magic_test matrix + mini_racer (~> 0.12.0) money-rails newrelic_rpm nokogiri (>= 1.10.4) @@ -767,36 +724,35 @@ DEPENDENCIES omniauth-rails_csrf_protection orderly (~> 0.1) paper_trail - pdf-reader - pg (~> 1.5.7) + pg (~> 1.5.6) prawn-rails pry-doc pry-nav pry-rails pry-remote puma - rails (= 7.1.3.4) + rails (= 7.0.8) rails-controller-testing rails-erd recaptcha - redis (~> 5.3) + redis (~> 5.2) rolify (~> 6.0) - rspec-rails (~> 7.0.1) + rspec-rails (~> 6.1.2) rubocop rubocop-performance - rubocop-rails (~> 2.25.1) + rubocop-rails (~> 2.24.1) sass-rails shoulda-matchers (~> 6.2) simple_form simplecov sprockets (~> 4.2.1) - standard (~> 1.40) + standard (~> 1.35) stimulus-rails strong_migrations (= 1.8.0) terser turbo-rails web-console - webmock (~> 3.24) + webmock (~> 3.23) BUNDLED WITH - 2.5.23 + 2.5.7 diff --git a/README.md b/README.md index 4d9cd584d0..19dc1c3636 100644 --- a/README.md +++ b/README.md @@ -54,34 +54,175 @@ Use as an Organization or Contribute as an Individual/Team to this Project: - [NGO Adoption Info](ngo.md) - information about how to use this DPG - [Skills Based Volunteering Info](sbv.md) - information about how to volunteer -## Welcome Contributors! 👋 +# Welcome Contributors! 👋 -Thanks for checking us out! Check out our [Contributing Guidelines](https://github.com/rubyforgood/human-essentials/blob/main/CONTRIBUTING.md) on how to contribute. +Thanks for checking us out! If you're new here, here are some things you should know: +- Issues tagged "Help Wanted" are self-contained and great for new contributors +- Pull Requests are reviewed within a week or so +- Ensure your build passes (`rubocop -a` is often necessary) and addresses the issue requirements +- This project relies entirely on volunteers, so please be patient with communication -## Deployment Process +### Join us on slack 💬 +You can sign up [here](https://join.slack.com/t/rubyforgood/shared_invite/zt-21pyz2ab8-H6JgQfGGI0Ab6MfNOZRIQA) and find us in #human-essentials. Many helpful members are available to answer your questions. Just ask, and someone will be there to help you! + +## Getting Started 🛠️ + +1. Install Ruby + - Install the version specified in [`.ruby-version`](.ruby-version). + - Visit the [Install Ruby on Rails](https://gorails.com/setup/osx/12-monterey) guide by GoRails for Ubuntu, Windows, and macOSX setup. ⚠️ Follow only the Installing Ruby step, as our project setup differs ⚠️ It is highly recommended you use a ruby version manager such as [rbenv](https://github.com/rbenv/rbenv), [asdf](https://asdf-vm.com/), or [rvm](https://rvm.io/). + - Verify that your Ruby installation works by running `ruby -v`. +2. Install Postgres + - Follow one of these guides: [MacOSX](https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-macos), [Ubuntu](https://www.digitalocean.com/community/tutorials/how-to-use-postgresql-with-your-ruby-on-rails-application-on-ubuntu-18-04). + - Do you develop on Windows? We'd love to hear (and for you to submit a PR explaining) how you do it. 🙏🏻 + - Create a `database.yml` file on `config/` directory with your database configurations. You can also copy the existing files called [`database.yml.example`](config/database.yml.example) and [`.env.example`](.env.example) and change the credentials. +3. Run `bin/setup` +4. Run `bin/start` and visit http://localhost:3000/ to see the human essentials page. +5. Login as a sample user with these default credentials (which also work for [staging](https://staging.humanessentials.app/)): + +
+ Super Users 🦸🏽‍♀️ + + ``` + username: superadmin@example.com + password: password! + ``` +
+ +
+ Bank Users 🏦 + + ``` + Organization Admin + Email: org_admin1@example.com + Password: password! + + User + Email: user_1@example.com + Password: password! + ``` +
+ +
+ Partner Users 👥 + + ``` + Verified Partner + Email: verified@example.com + Password: password! + + Invited Partner + Email: invited@pawneehomeless.com + Password: password! + + Unverified Partner + Email: unverified@pawneepregnancy.com + Password: password! + + Recertification Required Partner + Email: recertification_required@example.com + Password: password! + ``` +
+ +## Troubleshooting 👷🏼‍♀️ + +Please let us know by opening up an issue! We have many new contributors come through and it is likely what you experienced will happen to them as well. + +- *"My RBENV installation didn't work!"* - The rbenv repository provides a [rbenv-doctor script](https://github.com/rbenv/rbenv-installer#rbenv-doctor) to verify the installation and check if a ruby version is installed + +## Contributing Guidelines 🤝 + +Please feel free to contribute! Priority will be given to pull requests that address outstanding issues and have appropriate test coverage. Focus on issues tagged with the next milestone for higher priority. + +To contribute: +* Identify an unassigned issue +* Assign the issue to yourself to avoid duplicated efforts (or request assignment by adding a comment) +* Fork the repo if you're not a contributor yet +* Create a new branch for the issue using the format `XXX-brief-description-of-feature`, where `XXX` is the issue number +* If you create a new model run `bundle exec annotate` from the root of the app +* Create tests to validate that your work fixes the Issue (if you need help with this, please reach out!) +* Commit locally using descriptive messages that indicate the affected parts of the app +* Ensure all tests pass successfully; if any fail, fix the issues causing the failures +* Make a final commit if tests needed fixing +* Push up the branch +* Create a pull request and indicate the addressed issue in the title + +### Squashing Commits + +Consider the balance of "polluting the git log with commit messages" vs. "providing useful detail about the history of changes in the git log". If you have several smaller commits that serve a one purpose, you are encouraged to squash them into a single commit. There's no hard and fast rule here about this (for now), just use your best judgement. Please don't squash other people's commits. Everyone who contributes here deserves credit for their work! :) + +### Pull Request Merging + +At this point, someone will work with you on doing a code review. If the automated tests gives :+1: to the PR merging, we can then do any additional (staging) testing as needed. Finally if all looks good the core team will merge your code in; if your feature branch was in this main repository, the branch will be deleted after the PR is merged. Deploys are currently done about once a week! + +### In-flight Pull Requests + +Sometimes we want to get a PR up there and going so that other people can review it or provide feedback, but maybe it's incomplete. This is OK, but if you do it, please tag your PR with `in-progress` label so that we know not to review / merge it. + +### Becoming a Repo Contributor + +Users that are frequent contributors and are involved in discussion (join the slack channel! :)) may be given direct Contributor access to the Repo so they can submit Pull Requests directly instead of Forking first. + +### Stay Scoped + +Try to keep your PRs limited to one particular issue, and don't make changes that are out of scope for that issue. If you notice something that needs attention but is out of scope, please [create a new issue](https://github.com/rubyforgood/human-essentials/issues/new). +## Debugging +If starting server directly, via `rail s` or `rail console`, or built-in debugger in RubyMine, then you can use `binding.pry` to debug. Drop the pry where you want the execution to pause. + +If starting via Procfile with `bin/start`, then drop a ``binding.remote_pry`` into the line where you want execution to pause at. Then run ``pry-remote`` in the terminal to connect to it. +https://github.com/Mon-Ouie/pry-remote + + +## Testing 🧪 +### Writing Tests/Specs +- Run all the tests with `bundle exec rspec` +- Run a single test with `bundle exec rspec {path_to_test_name}_spec.rb` + +Make sure all tests run clean & green before submitting a Pull Request. If you are inexperienced in writing tests or get stuck on one, please reach out for help :). You probably don't need to write new tests when simple re-stylings are done (ie. the page may look slightly different but the Test suite is unaffected by those changes). + +*Tip: 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.* + +```ruby + it "works!" do + pending("Need to implement this") + expect(my_code).to be_valid + end +``` + +### Writing Browser/System/Feature Specs + +If you need to see a browser/system spec run in the browser, you can use the following env variable: + +``` +NOT_HEADLESS=true bundle exec rspec +``` + +We've added [magic_test](https://github.com/bullet-train-co/magic_test) which makes creating browser specs much easier. It allows you to record actions on the browser running the specs and easily paste them into the spec. You can do this by adding `magic_test` within your system spec: +```rb + it "does some browser stuff" do + magic_test + end +``` +and run the spec using this command: `MAGIC_TEST=1 NOT_HEADLESS=true bundle exec rspec ` + +**See videos of it in action [here](https://twitter.com/andrewculver/status/1366062684802846721)** + +# Deployment Process 🚀 The human-essentials & partner application should ideally be deployed on a weekly or bi-weekly schedule depending on the merged updates in the main branch. This is the process we take to deploy updates from our main branch to our servers. ### Requirements - SSH access to our servers (usually granted to core maintainers) - Login credentials to our [Mailchimp](https://mailchimp.com/) account -### Steps -#### 1. Merge main into production branch -All deploys deploy from the production branch, which keeps track of what is currently in production. - -```sh -git checkout production -git merge main -``` -#### 2. Tag & Release -1. Push a tag with the appropriate date versioning. Refer to the [releases](https://github.com/rubyforgood/human-essentials/releases) for the correct versioning. For example, if you are deploying on June 23, 2024: +### Tag & Release +1. Push a tag with the appropriate semantic versioning. Refer to the [releases](https://github.com/rubyforgood/human-essentials/releases) for the correct versioning. For example, if the last release was `2.1.0` and you're making a hotfix, use `2.1.1` ```sh - git tag 2024.06.23 - git push origin tag 2024.06.23 + git tag x.y.z + git push --tags ``` -2. Publish a release, associated to that tag pushed up in the previous step, [here](https://github.com/rubyforgood/human-essentials/releases/new). Include details about the release's updates (we use this to notify our stakeholders on updates via email). +2. Publish a release associated to that tag pushed up in the previous step [here](https://github.com/rubyforgood/human-essentials/releases/new). Include details about the release's updates (we use this to notify our stakeholders on updates via email). ### Running delayed jobs @@ -93,6 +234,10 @@ Delayed::Job.last.invoke_job You can replace the `last` query with any other query (e.g. `Delayed::Job.find(123)`). +### Additional Notes + +- Only commit the schema.rb only if you have committed anything that would change the DB schema (i.e. a migration). + # Acknowledgements Thanks to Rachel (from PDX Diaperbank) for all of her insight, support, and assistance with this application, and Sarah ( http://www.sarahkasiske.com/ ) for her wonderful design and CSS work at Ruby For Good '17! @@ -266,3 +411,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! + diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 74c027ce0e..b99f1258e3 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -188,34 +188,9 @@ select.selectpicker + .dropdown-toggle::after { background-color: #8282df } -div.low_priority_warning { +div.warning { + color: #e15563; margin: 5px; text-align: center; } -.distribution-title { - display: flex; -} - -legend.with-request { - float: none; - width: 80%; - display: inline-block; -} - -div.distribution-request-unit { - display: inline-block; - flex: 1; - text-align: right; - margin-right: 20px; - font-size: 1.5em; -} - -.li-requested { - font-size: 1.5em; - width: 100px; - min-width: 100px; - text-align: right; - margin-right: 20px; - white-space: nowrap; -} diff --git a/app/assets/stylesheets/custom.scss b/app/assets/stylesheets/custom.scss index 83ec2d042c..9a11ccd73a 100644 --- a/app/assets/stylesheets/custom.scss +++ b/app/assets/stylesheets/custom.scss @@ -92,19 +92,3 @@ .percent { text-align: right; } - -.confirm { - .message { - 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/assets/stylesheets/simple_form-bootstrap/_form_abbr.scss b/app/assets/stylesheets/simple_form-bootstrap/_form_abbr.scss index bed4f13bb2..bbca5421d7 100644 --- a/app/assets/stylesheets/simple_form-bootstrap/_form_abbr.scss +++ b/app/assets/stylesheets/simple_form-bootstrap/_form_abbr.scss @@ -1,6 +1,5 @@ // Source: http://simple-form-bootstrap.plataformatec.com.br/documentation -form abbr[title] { +abbr[title] { text-decoration: none; - cursor: inherit; -} +} \ No newline at end of file diff --git a/app/controllers/admin/account_requests_controller.rb b/app/controllers/admin/account_requests_controller.rb index 5ab55e3309..e93495a555 100644 --- a/app/controllers/admin/account_requests_controller.rb +++ b/app/controllers/admin/account_requests_controller.rb @@ -1,6 +1,4 @@ class Admin::AccountRequestsController < AdminController - before_action :set_account_request, only: [:reject, :close] - def index @open_account_requests = AccountRequest.requested.order('created_at DESC') .page(params[:open_page]).per(15) @@ -13,24 +11,12 @@ def for_rejection end def reject - @account_request.reject!(account_request_params[:rejection_reason]) + account_request = AccountRequest.find(account_request_params[:id]) + account_request.reject!(account_request_params[:rejection_reason]) redirect_to admin_account_requests_path, notice: "Account request rejected!" end - def close - @account_request.close!(account_request_params[:rejection_reason]) - redirect_to admin_account_requests_path, notice: "Account request closed!" - rescue => e - redirect_to admin_account_requests_path, alert: e.message - end - def account_request_params params.require(:account_request).permit(:id, :rejection_reason) end - - private - - def set_account_request - @account_request = AccountRequest.find(account_request_params[:id]) - end end diff --git a/app/controllers/admin/organizations_controller.rb b/app/controllers/admin/organizations_controller.rb index 33ef830d49..11313338e7 100644 --- a/app/controllers/admin/organizations_controller.rb +++ b/app/controllers/admin/organizations_controller.rb @@ -46,14 +46,13 @@ def new def create @organization = Organization.new(organization_params) - @user = User.new(user_params) if @organization.save Organization.seed_items(@organization) - UserInviteService.invite(name: user_params[:name], - email: user_params[:email], - roles: [Role::ORG_USER, Role::ORG_ADMIN], - resource: @organization) + @user = 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/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 12067427c8..db7bb10a4d 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -49,6 +49,16 @@ def create render "admin/users/new" end + def destroy + @user = User.find_by(id: params[:id]) + if @user.present? + @user.destroy + redirect_to admin_users_path, notice: "Deleted that user" + else + redirect_to admin_users_path, flash: { error: "Couldn't find that user, sorry" } + end + end + def resource_ids klass = case params[:resource_type] when "org_admin", "org_user" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e80333a9ee..70dfcc9a5e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -41,6 +41,26 @@ def current_role @role end + def organization_url_options(options = {}) + options.merge(organization_name: current_organization.to_param) + end + helper_method :organization_url_options + + # override Rails' default_url_options to ensure organization_name is added to + # each URL generated + def default_url_options(options = {}) + # Early return if the request is not authenticated and no + # current_user is defined + return options if current_user.blank? || current_role.blank? || current_role.name == Role::SUPER_ADMIN.to_s + + if current_organization.present? && !options.key?(:organization_name) + options[:organization_name] = current_organization.to_param + elsif current_role.name == Role::ORG_ADMIN.to_s + options[:organization_name] = current_user.organization.to_param + end + options + end + def dashboard_path_from_current_role return root_path if current_role.blank? @@ -49,7 +69,7 @@ def dashboard_path_from_current_role elsif current_role.name == Role::PARTNER.to_s partners_dashboard_path elsif current_user.organization - dashboard_path + dashboard_path(current_user.organization) else "/403" end diff --git a/app/controllers/audits_controller.rb b/app/controllers/audits_controller.rb index 5cb0f22d8b..c6d8e2c99d 100644 --- a/app/controllers/audits_controller.rb +++ b/app/controllers/audits_controller.rb @@ -10,7 +10,11 @@ def index end def show - @items = View::Inventory.items_for_location(@audit.storage_location) + if Event.read_events?(@audit.organization) + @items = View::Inventory.items_for_location(@audit.storage_location) + else + @inventory_items = @audit.storage_location.inventory_items + end end def edit @@ -24,7 +28,24 @@ 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 - AuditEvent.publish(@audit) + 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 @audit.finalized! redirect_to audit_path(@audit), notice: "Audit is Finalized." rescue => e diff --git a/app/controllers/concerns/importable.rb b/app/controllers/concerns/importable.rb index 488614efac..90152f41e4 100644 --- a/app/controllers/concerns/importable.rb +++ b/app/controllers/concerns/importable.rb @@ -27,12 +27,8 @@ def import_csv data = File.read(params[:file].path, encoding: "BOM|UTF-8") csv = CSV.parse(data, headers: true).reject { |row| row.to_hash.values.any?(&:nil?) } if csv.count.positive? && csv.first.headers.all? { |header| !header.nil? } - errors = resource_model.import_csv(csv, current_organization.id) - if errors.empty? - flash[:notice] = "#{resource_model_humanized} were imported successfully!" - else - flash[:error] = "The following #{resource_model_humanized} did not import successfully:\n#{errors.join("\n")}" - end + resource_model.import_csv(csv, current_organization.id) + flash[:notice] = "#{resource_model_humanized} were imported successfully!" else flash[:error] = "Check headers in file!" end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index c1af5332b2..45454f4ba9 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -3,12 +3,29 @@ class DashboardController < ApplicationController respond_to :html, :js def index - @org_stats = OrganizationStats.new(current_organization) + setup_date_range_picker + + @donations = current_organization.donations.during(helpers.selected_range) + @recent_donations = @donations.recent + @purchases = current_organization.purchases.during(helpers.selected_range) + @recent_purchases = @purchases.recent.includes(:vendor) + + distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range) + @recent_distributions = distributions.recent + + @itemized_donation_data = DonationItemizedBreakdownService.new(organization: current_organization, donation_ids: @donations.pluck(:id)).fetch + @itemized_distribution_data = DistributionItemizedBreakdownService.new(organization: current_organization, distribution_ids: distributions.pluck(:id)).fetch + @total_inventory = current_organization.total_inventory - @partners_awaiting_review = current_organization.partners.awaiting_review - @outstanding_requests = current_organization.ordered_requests.where(status: %i[pending started]).order(:created_at) - @low_inventory_report = LowInventoryQuery.call(current_organization) + @org_stats = OrganizationStats.new(current_organization) + + # calling .recent on recent donations by manufacturers will only count the last 3 donations + # which may not make sense when calculating total count using a date range + @recent_donations_from_manufacturers = current_organization.donations.during(helpers.selected_range).by_source(:manufacturer) + @top_manufacturers = current_organization.manufacturers.by_donation_count + + @distribution_data = helpers.received_distributed_data(helpers.selected_range) # passing nil here filters the announcements that didn't come from an organization @broadcast_announcements = BroadcastAnnouncement.filter_announcements(nil) diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 68a0bb39e8..9f7b48cc97 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -45,7 +45,7 @@ def index .distributions .includes(:partner, :storage_location, line_items: [:item]) .order('issued_at DESC') - .apply_filters(filter_params.except(:date_range), helpers.selected_range) + .apply_filters(filter_params, helpers.selected_range) @paginated_distributions = @distributions.page(params[:page]) @items = current_organization.items.alphabetized @item_categories = current_organization.item_categories @@ -71,22 +71,6 @@ def index end end - # This endpoint is in support of displaying a confirmation modal before a distribution is created. - # Since the modal should only be shown for a valid distribution, client side JS will invoke this - # endpoint, and if the distribution is valid, this endpoint also returns the HTML for the modal content. - # Important: The distribution model is intentionally NOT saved to the database at this point because - # the user has not yet confirmed that they want to create it. - def validate - @dist = Distribution.new(distribution_params.merge(organization: current_organization)) - @dist.line_items.combine! - if @dist.valid? - body = render_to_string(template: 'distributions/validate', formats: [:html], layout: false) - render json: {valid: true, body: body} - else - render json: {valid: false} - end - end - def create dist = Distribution.new(distribution_params.merge(organization: current_organization)) result = DistributionCreateService.new(dist, request_id).call @@ -109,17 +93,15 @@ def create # does not match any known Request @distribution.request = Request.find(request_id) end - if @distribution.line_items.size.zero? - @distribution.line_items.build - elsif request_id - @distribution.initialize_request_items - end + @distribution.line_items.build if @distribution.line_items.size.zero? @items = current_organization.items.alphabetized - @partner_list = current_organization.partners.where.not(status: 'deactivated').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? + 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 end flash_error = insufficient_error_message(result.error.message) @@ -145,11 +127,13 @@ def new @distribution.copy_from_donation(params[:donation_id], params[:storage_location_id]) end @items = current_organization.items.alphabetized - @partner_list = current_organization.partners.where.not(status: 'deactivated').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? + 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 end end @@ -166,18 +150,20 @@ def show def edit @distribution = Distribution.includes(:line_items).includes(:storage_location).find(params[:id]) - @distribution.initialize_request_items 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 - @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? - 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? + 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 end else redirect_to distributions_path, error: 'To edit a distribution, @@ -200,7 +186,6 @@ def update else 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 @storage_locations = current_organization.storage_locations.active_locations.alphabetized render :edit @@ -224,14 +209,7 @@ def itemized_breakdown # TODO: This needs a little more context. Is it JSON only? HTML? def schedule - respond_to do |format| - format.html - format.json do - start_at = params[:start].to_datetime - end_at = params[:end].to_datetime - @pick_ups = current_organization.distributions.includes(:partner).where(issued_at: start_at..end_at) - end - end + @pick_ups = current_organization.distributions end def calendar @@ -310,7 +288,7 @@ def daily_items(pick_ups) 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, :date_range) + params.require(:filters).permit(:by_item_id, :by_item_category_id, :by_partner, :by_state, :by_location) end def perform_inventory_check diff --git a/app/controllers/donations_controller.rb b/app/controllers/donations_controller.rb index 5789342677..0771597fac 100644 --- a/app/controllers/donations_controller.rb +++ b/app/controllers/donations_controller.rb @@ -2,19 +2,6 @@ class DonationsController < ApplicationController before_action :authorize_admin, only: [:destroy] - def print - @donation = Donation.find(params[:id]) - respond_to do |format| - format.any do - pdf = DonationPdf.new(current_organization, @donation) - send_data pdf.compute_and_render, - filename: format("%s %s.pdf", @donation.source, sortable_date(@donation.created_at)), - type: "application/pdf", - disposition: "inline" - end - end - end - def index setup_date_range_picker @@ -90,21 +77,14 @@ def show def update @donation = Donation.find(params[:id]) - @original_source = @donation.source ItemizableUpdateService.call(itemizable: @donation, params: donation_params, + type: :increase, event_class: DonationEvent) - flash.clear - flash[:notice] = "Donation updated!" redirect_to donations_path rescue => e flash[:alert] = "Error updating donation: #{e.message}" - load_form_collections - # calling new(donation_params) triggers a validation error if line_item quantity is invalid - @previous_input = Donation.new(donation_params.except(:line_items_attributes)) - @previous_input.line_items.build(donation_params[:line_items_attributes].values) - - render "edit", status: :conflict + render "edit" end def destroy @@ -136,14 +116,14 @@ def clean_donation_money_raised params[:donation][:money_raised] = money_raised.gsub(/[$,.]/, "") if money_raised money_raised_in_dollars = params[:donation][:money_raised_in_dollars] - params[:donation][:money_raised] = (money_raised_in_dollars.gsub(/[$,]/, "").to_d * 100).to_s if money_raised_in_dollars + params[:donation][:money_raised] = money_raised_in_dollars.gsub(/[$,]/, "").to_d * 100 if money_raised_in_dollars end def donation_params strip_unnecessary_params clean_donation_money_raised params = compact_line_items - params.require(:donation).permit(:source, :comment, :storage_location_id, :money_raised, :issued_at, :donation_site_id, :product_drive_id, :product_drive_participant_id, :manufacturer_id, line_items_attributes: %i(id item_id quantity)).merge(organization: current_organization) + params.require(:donation).permit(:source, :comment, :storage_location_id, :money_raised, :issued_at, :donation_site_id, :product_drive_id, :product_drive_participant_id, :manufacturer_id, line_items_attributes: %i(id item_id quantity _destroy)).merge(organization: current_organization) end def donation_item_params diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb new file mode 100644 index 0000000000..e1a0fc6892 --- /dev/null +++ b/app/controllers/errors_controller.rb @@ -0,0 +1,9 @@ +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/item_categories_controller.rb b/app/controllers/item_categories_controller.rb index 3d291e76ab..a131458ce1 100644 --- a/app/controllers/item_categories_controller.rb +++ b/app/controllers/item_categories_controller.rb @@ -8,7 +8,7 @@ def create @item_category.assign_attributes(item_category_params) if @item_category.save - redirect_to items_path + redirect_to items_path(organization: current_organization) else render :new end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb index e1d7e20dce..96e9578127 100644 --- a/app/controllers/items_controller.rb +++ b/app/controllers/items_controller.rb @@ -4,14 +4,14 @@ class ItemsController < ApplicationController def index @items = current_organization .items - .includes(:base_item, :kit, :line_items, :request_units, :item_category) + .includes(:base_item, :kit, :line_items) .alphabetized .class_filter(filter_params) .group('items.id') @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) + @kits = current_organization.kits.includes(line_items: :item, inventory_items: :storage_location) @storages = current_organization.storage_locations.active_locations.order(id: :asc) @include_inactive_items = params[:include_inactive_items] @@ -19,23 +19,25 @@ def index @paginated_items = @items.page(params[:page]) - @inventory = View::Inventory.new(current_organization.id) + if Event.read_events?(current_organization) + @inventory = View::Inventory.new(current_organization.id) + end @items_by_storage_collection_and_quantity = ItemsByStorageCollectionAndQuantityQuery.call(organization: current_organization, inventory: @inventory, filter_params: filter_params) respond_to do |format| format.html - format.csv { send_data Item.generate_csv_from_inventory(@items, @inventory), filename: "Items-#{Time.zone.today}.csv" } + 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 end end def create - create = if Flipper.enabled?(:enable_packs) - ItemCreateService.new(organization_id: current_organization.id, item_params: item_params, request_unit_ids:) - else - ItemCreateService.new(organization_id: current_organization.id, item_params: item_params) - end + create = ItemCreateService.new(organization_id: current_organization.id, item_params: item_params) result = create.call if result.success? @@ -46,7 +48,8 @@ def create # the provided parameters. This is required to render the page again # with the error + the invalid parameters @item = current_organization.items.new(item_params) - flash[:error] = result.error.record.errors.full_messages.to_sentence + + flash[:error] = "Something didn't work quite right -- try again?" render action: :new end end @@ -65,15 +68,20 @@ def edit def show @item = current_organization.items.find(params[:id]) - @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) + 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 @barcodes_for = current_organization.items.barcodes_for(@item) end def update @item = current_organization.items.find(params[:id]) @item.attributes = item_params + deactivated = @item.active_changed? && !@item.active if deactivated && !@item.can_deactivate? @base_items = BaseItem.without_kit.alphabetized @@ -82,7 +90,7 @@ def update return end - if update_item + if @item.save redirect_to items_path, notice: "#{@item.name} updated!" else @base_items = BaseItem.without_kit.alphabetized @@ -174,31 +182,6 @@ def item_params ) end - def request_unit_ids - params.require(:item).permit(request_unit_ids: []).fetch(:request_unit_ids, []) - end - - # We need to update both the item and the request_units together and fail together - def update_item - if Flipper.enabled?(:enable_packs) - update_item_and_request_units - else - @item.save - end - end - - def update_item_and_request_units - begin - Item.transaction do - @item.save! - @item.sync_request_units!(request_unit_ids) - end - rescue - return false - end - true - end - helper_method \ def filter_params(_parameters = nil) return {} unless params.key?(:filters) diff --git a/app/controllers/kits_controller.rb b/app/controllers/kits_controller.rb index 7ed275ca47..388f8a6b4f 100644 --- a/app/controllers/kits_controller.rb +++ b/app/controllers/kits_controller.rb @@ -1,7 +1,9 @@ class KitsController < ApplicationController def index - @kits = current_organization.kits.includes(line_items: :item).class_filter(filter_params) - @inventory = View::Inventory.new(current_organization.id) + @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 unless params[:include_inactive_items] @kits = @kits.active end @@ -24,12 +26,12 @@ def create redirect_to kits_path else flash[:error] = kit_creation.errors - .map { |error| formatted_error_message(error) } - .join(", ") - - @kit = Kit.new(kit_params) + .map { |error| formatted_error_message(error) } + .join(", ") load_form_collections - @kit.line_items.build if @kit.line_items.empty? + + @kit ||= Kit.new + @kit.line_items.build render :new end @@ -54,7 +56,11 @@ def reactivate def allocations @kit = Kit.find(params[:id]) @storage_locations = current_organization.storage_locations.active_locations - @inventory = View::Inventory.new(current_organization.id) + if Event.read_events?(current_organization) + @inventory = View::Inventory.new(current_organization.id) + else + @item_inventories = @kit.item.inventory_items + end load_form_collections end @@ -63,14 +69,21 @@ 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 - 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 + + 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}" end redirect_to allocations_kit_path(id: @kit.id) diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index b10cbce8e1..006a318a4b 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -16,7 +16,7 @@ def update @organization = current_organization if OrganizationUpdateService.update(@organization, organization_params) - redirect_to organization_path, notice: "Updated your organization!" + redirect_to organization_path(@organization), notice: "Updated your organization!" else flash[:error] = @organization.errors.full_messages.join("\n") render :edit @@ -48,7 +48,7 @@ def promote_to_org_admin resource_id: current_organization.id) redirect_to user_update_redirect_path, notice: "User has been promoted!" rescue => e - redirect_back(fallback_location: organization_path, alert: e.message) + redirect_back(fallback_location: organization_path(current_organization), alert: e.message) end end @@ -59,23 +59,24 @@ def demote_to_user RemoveRoleService.call(user_id: params[:user_id], resource_type: Role::ORG_ADMIN, resource_id: current_organization.id) - redirect_to user_update_redirect_path, notice: "User has been demoted!" + redirect_to user_update_redirect_path, notice: notice rescue => e - redirect_back(fallback_location: organization_path, alert: e.message) + redirect_back(fallback_location: organization_path(current_organization), alert: e.message) end end - def remove_user - user = User.find(params[:user_id]) + def deactivate_user + user = User.with_discarded.find_by!(id: params[:user_id]) raise ActiveRecord::RecordNotFound unless user.has_role?(Role::ORG_USER, current_organization) - begin - RemoveRoleService.call(user_id: params[:user_id], - resource_type: Role::ORG_USER, - resource_id: current_organization.id) - redirect_to user_update_redirect_path, notice: "User has been removed!" - rescue => e - redirect_back(fallback_location: organization_path, alert: e.message) - end + user.discard! + redirect_to user_update_redirect_path, notice: "User has been deactivated." + end + + def reactivate_user + user = User.with_discarded.find_by!(id: params[:user_id]) + raise ActiveRecord::RecordNotFound unless user.has_role?(Role::ORG_USER, current_organization) + user.undiscard! + redirect_to user_update_redirect_path, notice: "User has been reactivated." end private @@ -97,10 +98,7 @@ def organization_params :ndbn_member_id, :enable_child_based_requests, :enable_individual_requests, :enable_quantity_based_requests, :ytd_on_distribution_printout, :one_step_partner_invite, - :hide_value_columns_on_receipt, :hide_package_column_on_receipt, - :signature_for_distribution_pdf, - partner_form_fields: [], - request_unit_names: [] + partner_form_fields: [] ) end diff --git a/app/controllers/partner_groups_controller.rb b/app/controllers/partner_groups_controller.rb index 6d7fa5dc06..f54339d95c 100644 --- a/app/controllers/partner_groups_controller.rb +++ b/app/controllers/partner_groups_controller.rb @@ -1,9 +1,7 @@ class PartnerGroupsController < ApplicationController - before_action :set_partner_group, only: %i[edit destroy] - def new @partner_group = current_organization.partner_groups.new - set_items_categories + @item_categories = current_organization.item_categories end def create @@ -13,14 +11,13 @@ def create redirect_to partners_path + "#nav-partner-groups", notice: "Partner group added!" else flash[:error] = "Something didn't work quite right -- try again?" - set_items_categories render action: :new end end def edit @partner_group = current_organization.partner_groups.find(params[:id]) - set_items_categories + @item_categories = current_organization.item_categories end def update @@ -29,33 +26,13 @@ def update redirect_to partners_path + "#nav-partner-groups", notice: "Partner group edited!" else flash[:error] = "Something didn't work quite right -- try again?" - set_items_categories render action: :edit end end - def destroy - if @partner_group.partners.any? - redirect_to partners_path + "#nav-partner-groups", alert: "Partner Group cannot be deleted." - else - @partner_group.destroy - respond_to do |format| - format.html { redirect_to partners_path + "#nav-partner-groups", notice: "Partner Group was successfully deleted." } - end - end - end - private - def set_partner_group - @partner_group = current_organization.partner_groups.find(params[:id]) - end - def partner_group_params params.require(:partner_group).permit(:name, :send_reminders, :deadline_day, :reminder_day, item_category_ids: []) end - - def set_items_categories - @item_categories = current_organization.item_categories - end end diff --git a/app/controllers/partner_users_controller.rb b/app/controllers/partner_users_controller.rb deleted file mode 100644 index 102da47f34..0000000000 --- a/app/controllers/partner_users_controller.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_String_literal: true - -class PartnerUsersController < ApplicationController - before_action :set_partner, only: %i[index create destroy resend_invitation] - - def index - @users = @partner.users - @user = User.new(name: "") - end - - def create - @user = UserInviteService.invite( - email: user_params[:email], - name: user_params[:name], - roles: [Role::PARTNER], - resource: @partner - ) - if @user.valid? - redirect_back(fallback_location: "/", - notice: "#{@user.name} has been invited. Invitation email sent to #{@user.email}") - else - flash[:alert] = "Invitation failed. Check the form for errors." - @users = @partner.users - render :index - end - end - - def destroy - user = User.find(params[:id]) - - if user.remove_role(Role::PARTNER, @partner) - redirect_back(fallback_location: "/", notice: "Access to #{@partner.name} has been revoked for #{user.display_name}.") - else - redirect_back(fallback_location: "/", alert: "Invitation failed. Check the form for errors.") - end - end - - def resend_invitation - user = User.find(params[:id]) - - if user.invitation_accepted_at.nil? - user.invite! - else - user.errors.add(:base, "User has already accepted invitation.") - end - - if user.errors.none? - redirect_back(fallback_location: "/", notice: "Invitation email sent to #{user.email}") - else - redirect_back(fallback_location: "/", alert: user.errors.full_messages.to_sentence) - end - end - - def reset_password - user = User.find(params[:id]) - - user.send_reset_password_instructions - redirect_back(fallback_location: root_path, notice: "Password e-mail sent!") - end - - private - - def set_partner - @partner = Partner.find(params[:partner_id]) - end - - def user_params - params.require(:user).permit(:email, :name) - end -end diff --git a/app/controllers/partners/base_controller.rb b/app/controllers/partners/base_controller.rb index 37435df4a6..0b217375d1 100644 --- a/app/controllers/partners/base_controller.rb +++ b/app/controllers/partners/base_controller.rb @@ -2,23 +2,12 @@ module Partners class BaseController < ApplicationController layout 'partners/application' - before_action :require_partner - private def redirect_to_root redirect_to root_path end - def require_partner - unless current_partner - respond_to do |format| - format.html { redirect_to dashboard_path, flash: {error: "Logged in user is not set up as a 'partner'."} } - format.json { render body: nil, status: :forbidden } - end - end - end - def verify_partner_is_active if current_partner.deactivated? flash[:alert] = 'Your account has been disabled, contact the organization via their email to reactivate' diff --git a/app/controllers/partners/children_controller.rb b/app/controllers/partners/children_controller.rb index 378f6abf2a..b0e6047557 100644 --- a/app/controllers/partners/children_controller.rb +++ b/app/controllers/partners/children_controller.rb @@ -6,7 +6,7 @@ class ChildrenController < BaseController def index @filterrific = initialize_filterrific( current_partner.children - .includes(:family, :requested_items) + .includes(:family) .order(sort_order), params[:filterrific] ) || return @@ -82,11 +82,11 @@ def child_params :first_name, :gender, :health_insurance, + :item_needed_diaperid, :last_name, :race, :archived, - child_lives_with: [], - requested_item_ids: [] + child_lives_with: [] ) end diff --git a/app/controllers/partners/dashboards_controller.rb b/app/controllers/partners/dashboards_controller.rb index bc623be0e5..0483ac0f6b 100644 --- a/app/controllers/partners/dashboards_controller.rb +++ b/app/controllers/partners/dashboards_controller.rb @@ -24,7 +24,9 @@ def show @families = @partner.families @children = @partner.children - @inventory = View::Inventory.new(@partner.organization_id) + if Event.read_events?(@partner.organization) + @inventory = View::Inventory.new(@partner.organization_id) + end @broadcast_announcements = BroadcastAnnouncement.filter_announcements(@parent_org) end diff --git a/app/controllers/partners/family_requests_controller.rb b/app/controllers/partners/family_requests_controller.rb index 6f4f94b34c..ff91b4b9bc 100644 --- a/app/controllers/partners/family_requests_controller.rb +++ b/app/controllers/partners/family_requests_controller.rb @@ -14,7 +14,21 @@ def new end def create - family_requests_attributes = build_family_requests_attributes(params) + children_ids = [] + + params.each do |key, _| + is_child, id = key.split('-') + if is_child == 'child' + children_ids << id + end + end + + children = current_partner.children.active.where(id: children_ids).where.not(item_needed_diaperid: [nil, 0]) + + children_grouped_by_item_id = children.group_by(&:item_needed_diaperid) + family_requests_attributes = children_grouped_by_item_id.map do |item_id, item_requested_children| + { item_id: item_id, person_count: item_requested_children.size, children: item_requested_children } + end create_service = Partners::FamilyRequestCreateService.new( partner_user_id: current_user.id, @@ -30,43 +44,5 @@ def create redirect_to new_partners_family_request_path, error: "Request failed! #{create_service.errors.map { |error| error.message.to_s }}}" end end - - def validate - family_requests_attributes = build_family_requests_attributes(params) - - @partner_request = Partners::FamilyRequestCreateService.new( - partner_user_id: current_user.id, - family_requests_attributes: family_requests_attributes, - for_families: true - ).initialize_only - if @partner_request.valid? - @total_items = @partner_request.total_items - @quota_exceeded = current_partner.quota_exceeded?(@total_items) - body = render_to_string(template: 'partners/requests/validate', formats: [:html], layout: false) - render json: {valid: true, body: body} - else - render json: {valid: false} - end - end - - private - - def build_family_requests_attributes(params) - children_ids = [] - - params.each do |key, _| - is_child, id = key.split('-') - if is_child == 'child' - children_ids << id - end - end - - children = current_partner.children.where(id: children_ids).joins(:requested_items).select('children.*', :item_id) - - children_grouped_by_item_id = children.group_by(&:item_id) - children_grouped_by_item_id.map do |item_id, item_requested_children| - { item_id: item_id, person_count: item_requested_children.size, children: item_requested_children } - end - end end end diff --git a/app/controllers/partners/individuals_requests_controller.rb b/app/controllers/partners/individuals_requests_controller.rb index 9432bdc16f..dff8560914 100644 --- a/app/controllers/partners/individuals_requests_controller.rb +++ b/app/controllers/partners/individuals_requests_controller.rb @@ -32,22 +32,6 @@ def create end end - def validate - @partner_request = Partners::FamilyRequestCreateService.new( - partner_user_id: current_user.id, - comments: individuals_request_params[:comments], - family_requests_attributes: individuals_request_params[:items_attributes]&.values - ).initialize_only - if @partner_request.valid? - @total_items = @partner_request.total_items - @quota_exceeded = current_partner.quota_exceeded?(@total_items) - body = render_to_string(template: 'partners/requests/validate', formats: [:html], layout: false) - render json: {valid: true, body: body} - else - render json: {valid: false} - end - end - private def individuals_request_params diff --git a/app/controllers/partners/profiles_controller.rb b/app/controllers/partners/profiles_controller.rb index e92085c037..d6508f7fa4 100644 --- a/app/controllers/partners/profiles_controller.rb +++ b/app/controllers/partners/profiles_controller.rb @@ -5,13 +5,6 @@ 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 @@ -19,24 +12,10 @@ def update result = PartnerProfileUpdateService.new(current_partner, partner_params, profile_params).call if result.success? flash[:success] = "Details were successfully updated." - 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 + redirect_to partners_profile_path else - 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 + flash[:error] = "There is a problem. Try again: %s" % result.error + render :edit end end diff --git a/app/controllers/partners/requests_controller.rb b/app/controllers/partners/requests_controller.rb index 2c01b28991..eec3c8cf4a 100644 --- a/app/controllers/partners/requests_controller.rb +++ b/app/controllers/partners/requests_controller.rb @@ -11,7 +11,7 @@ def new @partner_request = ::Request.new @partner_request.item_requests.build - fetch_items + @requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.id).call end def show @@ -33,7 +33,7 @@ def create @partner_request = create_service.partner_request @errors = create_service.errors - fetch_items + @requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.id).call Rails.logger.info("[Request Creation Failure] partner_user_id=#{current_user.id} reason=#{@errors.full_messages}") @@ -41,37 +41,10 @@ def create end end - def validate - @partner_request = Partners::RequestCreateService.new( - partner_user_id: current_user.id, - comments: partner_request_params[:comments], - item_requests_attributes: partner_request_params[:item_requests_attributes]&.values || [] - ).initialize_only - - if @partner_request.valid? - @total_items = @partner_request.total_items - @quota_exceeded = current_partner.quota_exceeded?(@total_items) - body = render_to_string(template: 'partners/requests/validate', formats: [:html], layout: false) - render json: {valid: true, body: body} - else - render json: {valid: false} - end - end - private def partner_request_params - params.require(:request).permit(:comments, item_requests_attributes: [:item_id, :quantity, :request_unit]) - end - - def fetch_items - @requestable_items = PartnerFetchRequestableItemsService.new(partner_id: current_partner.id).call - if Flipper.enabled?(:enable_packs) - # hash of (item ID => hash of (request unit name => request unit plural name)) - @item_units = Item.where(id: @requestable_items.to_h.values).to_h do |i| - [i.id, i.request_units.to_h { |u| [u.name, u.name.pluralize] }] - end - end + params.require(:request).permit(:comments, item_requests_attributes: [:item_id, :quantity]) end end end diff --git a/app/controllers/partners/users_controller.rb b/app/controllers/partners/users_controller.rb index 57693186b6..bb4a4eb5bf 100644 --- a/app/controllers/partners/users_controller.rb +++ b/app/controllers/partners/users_controller.rb @@ -23,7 +23,6 @@ def update end end - # partner user creation def create user = UserInviteService.invite(name: user_params[:name], email: user_params[:email], diff --git a/app/controllers/partners_controller.rb b/app/controllers/partners_controller.rb index 53ce5e5a70..d77f86b5ba 100644 --- a/app/controllers/partners_controller.rb +++ b/app/controllers/partners_controller.rb @@ -129,6 +129,18 @@ def invite end end + def invite_partner_user + partner = current_organization.partners.find(params[:partner]) + UserInviteService.invite(name: params[:name], + email: params[:email], + roles: [Role::PARTNER], + resource: partner) + + redirect_to partner_path(partner), notice: "We have invited #{params[:email]} to #{partner.name}!" + rescue StandardError => e + redirect_to partner_path(partner), error: "Failed to invite #{params[:email]} to #{partner.name} due to: #{e.message}" + end + def recertify_partner @partner = current_organization.partners.find(params[:id]) diff --git a/app/controllers/product_drive_participants_controller.rb b/app/controllers/product_drive_participants_controller.rb index 59a58b185e..bf1d3f5374 100644 --- a/app/controllers/product_drive_participants_controller.rb +++ b/app/controllers/product_drive_participants_controller.rb @@ -60,7 +60,7 @@ def update def product_drive_participant_params params.require(:product_drive_participant) - .permit(:contact_name, :phone, :email, :business_name, :address, :comment) + .permit(:contact_name, :phone, :email, :business_name, :address) end helper_method \ diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 4f424f2a3a..7e57e24708 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,15 +1,9 @@ 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 @@ -17,25 +11,10 @@ def update @partner = current_organization.partners.find(params[:id]) result = PartnerProfileUpdateService.new(@partner, edit_partner_params, edit_profile_params).call if result.success? - 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 + redirect_to partner_path(@partner) + "#partner-information", notice: "#{@partner.name} updated!" else - 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 + flash[:error] = "Something didn't work quite right -- try again? %s " % result.error + render action: :edit end end diff --git a/app/controllers/purchases_controller.rb b/app/controllers/purchases_controller.rb index b11b8cb2ed..849cc61973 100644 --- a/app/controllers/purchases_controller.rb +++ b/app/controllers/purchases_controller.rb @@ -5,7 +5,7 @@ class PurchasesController < ApplicationController def index setup_date_range_picker @purchases = current_organization.purchases - .includes(:storage_location, :vendor, line_items: [:item]) + .includes(:line_items, :storage_location) .order(created_at: :desc) .class_filter(filter_params) .during(helpers.selected_range) @@ -68,6 +68,7 @@ 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 diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb deleted file mode 100644 index 9a86ae5659..0000000000 --- a/app/controllers/reports_controller.rb +++ /dev/null @@ -1,63 +0,0 @@ -class ReportsController < ApplicationController - before_action :setup_date_range_picker - - def donations_summary - @donations = current_organization.donations.during(helpers.selected_range) - @recent_donations = @donations.recent - end - - def manufacturer_donations_summary - @recent_donations_from_manufacturers = current_organization.donations.during(helpers.selected_range).by_source(:manufacturer) - @top_manufacturers = current_organization.manufacturers.by_donation_count(10, helpers.selected_range) - end - - def purchases_summary - @summary_struct = Purchase.organization_summary_by_dates(current_organization, helpers.selected_range) - end - - def product_drives_summary - @donations = current_organization.donations.during(helpers.selected_range) - @recent_donations = @donations.recent - end - - def itemized_donations - @donations = current_organization.donations.during(helpers.selected_range) - @itemized_donation_data = DonationItemizedBreakdownService.new(organization: current_organization, donation_ids: @donations.pluck(:id)).fetch - end - - def itemized_distributions - distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range) - @itemized_distribution_data = DistributionItemizedBreakdownService.new(organization: current_organization, distribution_ids: distributions.pluck(:id)).fetch - end - - def distributions_summary - distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range) - @recent_distributions = distributions.recent - end - - def activity_graph - @distribution_data = received_distributed_data(helpers.selected_range) - end - - private - - def total_purchased_unformatted(range = selected_range) - LineItem.active.where(itemizable: current_organization.purchases.during(range)).sum(:quantity) - end - - def total_distributed_unformatted(range = selected_range) - LineItem.active.where(itemizable: current_organization.distributions.during(range)).sum(:quantity) - end - - def total_received_donations_unformatted(range = selected_range) - LineItem.active.where(itemizable: current_organization.donations.during(range)).sum(:quantity) - end - - def received_distributed_data(range = selected_range) - { - "Received donations" => total_received_donations_unformatted(range), - "Purchased" => total_purchased_unformatted(range), - "Distributed" => total_distributed_unformatted(range) - } - end -end diff --git a/app/controllers/requests/cancelation_controller.rb b/app/controllers/requests/cancelation_controller.rb index b32311c029..1602e728ce 100644 --- a/app/controllers/requests/cancelation_controller.rb +++ b/app/controllers/requests/cancelation_controller.rb @@ -15,7 +15,7 @@ def create else errors = svc.errors.full_messages.join(", ") flash[:error] = "Request #{params[:request_id]} could not be removed because #{errors}" - redirect_to new_request_cancelation_path(request_id: params[:request_id]) + redirect_to new_request_cancelation_path(organization: @organization, request_id: params[:request_id]) end end diff --git a/app/controllers/requests_controller.rb b/app/controllers/requests_controller.rb index db073afc15..d44f022e3d 100644 --- a/app/controllers/requests_controller.rb +++ b/app/controllers/requests_controller.rb @@ -8,7 +8,7 @@ def index .undiscarded .during(helpers.selected_range) .class_filter(filter_params) - @unfulfilled_requests_count = current_organization.requests.where(status: [:pending, :started]).count + @paginated_requests = @requests.page(params[:page]) @calculate_product_totals = RequestsTotalItemsService.new(requests: @requests).calculate @items = current_organization.items.alphabetized @@ -40,30 +40,16 @@ def start redirect_to new_distribution_path(request_id: request.id) end - def print_unfulfilled - requests = current_organization - .requests - .includes(:item_requests, partner: [:profile]) - .where(status: [:pending, :started]) - .order(created_at: :desc) - - respond_to do |format| - format.any do - pdf = PicklistsPdf.new(current_organization, requests) - send_data pdf.compute_and_render, - filename: format("Picklists_%s.pdf", Time.current.to_fs(:long)), - type: "application/pdf", - disposition: "inline" - end - end - end - private def load_items return unless @request.request_items - inventory = View::Inventory.new(@request.organization_id) + inventory = nil + if Event.read_events?(@request.organization) + inventory = View::Inventory.new(@request.organization_id) + end + @request.request_items.map { |json| RequestItem.from_json(json, @request, inventory) } end diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 2fb3a6cc64..4cafad3bf3 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -9,12 +9,15 @@ def quantity end def index - @inventory = View::Inventory.new(current_organization.id) + if Event.read_events?(current_organization) + @inventory = View::Inventory.new(current_organization.id) + end + @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 filter_params[:containing].present? + if @inventory && 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 @@ -30,7 +33,21 @@ def index respond_to do |format| format.html format.csv do - send_data StorageLocation.generate_csv_from_inventory(@storage_locations, @inventory), filename: "StorageLocations-#{Time.zone.today}.csv" + 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 end end end @@ -61,14 +78,8 @@ 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 View::Inventory.within_snapshot?(current_organization.id, params[:version_date]) + if Event.read_events?(current_organization) @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| @@ -88,9 +99,6 @@ def import_inventory flash[:notice] = "Inventory imported successfully!" redirect_back(fallback_location: storage_locations_path) end - rescue Errors::InventoryAlreadyHasItems => e - flash[:error] = e.message - redirect_back(fallback_location: storage_locations_path(organization_id: current_organization)) end def update @@ -135,10 +143,21 @@ def destroy end def inventory - @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 } + 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" + respond_to :json end end diff --git a/app/events/audit_event.rb b/app/events/audit_event.rb index aab873ada2..01e0232433 100644 --- a/app/events/audit_event.rb +++ b/app/events/audit_event.rb @@ -1,5 +1,5 @@ class AuditEvent < Event - serialize :data, coder: EventTypes::StructCoder.new(EventTypes::AuditPayload) + serialize :data, EventTypes::StructCoder.new(EventTypes::AuditPayload) # @param audit [Audit] def self.publish(audit) diff --git a/app/events/event_differ.rb b/app/events/event_differ.rb new file mode 100644 index 0000000000..338d0f3c47 --- /dev/null +++ b/app/events/event_differ.rb @@ -0,0 +1,104 @@ +module Types + include Dry.Types() +end + +module EventDiffer + # Used to indicate that a storage location exists in one source but not the other. + class LocationDiff < Dry::Struct + attribute :storage_location_id, Types::Integer + attribute :database, Types::Bool + attribute :aggregate, Types::Bool + + # @param options [Object] + # @return [Hash] + def as_json(options = nil) + super.merge(type: "location") + end + end + + # Used to indicate that the quantity of an item in one source doesn't match the other. + class ItemDiff < Dry::Struct + attribute :storage_location_id, Types::Integer + attribute :item_id, Types::Integer + attribute :database, Types::Integer + attribute :aggregate, Types::Integer + + # @param options [Object] + # @return [Hash] + def as_json(options = nil) + super.merge(type: "item") + end + end + + class << self + # @param locations [Array] + # @param inventory [EventTypes::Inventory] + # @return [Array] + def check_location_ids(locations, inventory) + db_ids = locations.map(&:id) + inventory_ids = inventory.storage_locations.keys + diffs = [] + (db_ids - inventory_ids).each do |id| + diffs.push(LocationDiff.new(storage_location_id: id, database: true, aggregate: false)) + end + (inventory_ids - db_ids).each do |id| + diffs.push(LocationDiff.new(storage_location_id: id, database: false, aggregate: true)) + end + diffs + end + + # @param inventory_loc [EventTypes::EventStorageLocation] + # @param db_loc [StorageLocation] + # @return [Array] + def check_items(inventory_loc, db_loc) + diffs = [] + diffs += check_item_ids(inventory_loc, db_loc) + db_loc.inventory_items.each do |db_item| + inventory_item = inventory_loc.items[db_item.item_id] + next if inventory_item.nil? + + if inventory_item.quantity != db_item.quantity + diffs.push(ItemDiff.new(item_id: db_item.item_id, + storage_location_id: db_loc.id, + database: db_item.quantity, + aggregate: inventory_item.quantity)) + end + end + diffs + end + + # @param inventory_loc [EventTypes::EventStorageLocation] + # @param db_loc [StorageLocation] + # @return [Array] + def check_item_ids(inventory_loc, db_loc) + inventory_ids = inventory_loc.items.keys + db_ids = db_loc.inventory_items.map(&:item_id) + diffs = [] + (db_ids - inventory_ids).each do |id| + item = db_loc.inventory_items.find { |f| f.item_id == id } + diffs.push(ItemDiff.new(item_id: id, storage_location_id: db_loc.id, database: item&.quantity, aggregate: 0)) + end + (inventory_ids - db_ids).each do |id| + item = inventory_loc.items[id] + diffs.push(ItemDiff.new(item_id: id, storage_location_id: db_loc.id, database: 0, aggregate: item.quantity)) + end + diffs + end + + # @param inventory [EventTypes::Inventory] + # @return [Array] + def check_difference(inventory) + diffs = [] + org = Organization.find(inventory.organization_id) + locations = org.storage_locations.to_a + diffs += check_location_ids(locations, inventory) + locations.each do |db_loc| + inventory_loc = inventory.storage_locations[db_loc.id] + next if inventory_loc.nil? + + diffs += check_items(inventory_loc, db_loc) + end + diffs + end + end +end diff --git a/app/events/event_types/event_line_item.rb b/app/events/event_types/event_line_item.rb index e1d947c195..9dc1f8769c 100644 --- a/app/events/event_types/event_line_item.rb +++ b/app/events/event_types/event_line_item.rb @@ -19,17 +19,6 @@ 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/inventory_aggregate.rb b/app/events/inventory_aggregate.rb index ef1fe6f5cb..278e7f22c6 100644 --- a/app/events/inventory_aggregate.rb +++ b/app/events/inventory_aggregate.rb @@ -12,8 +12,6 @@ def on(*event_types, &block) # @param event_time [DateTime] # @param validate [Boolean] # @return [EventTypes::Inventory] - # This method can take a block so that you can build up the history of a particular item over - # time, for instance def inventory_for(organization_id, event_time: nil, validate: false) last_snapshot = Event.most_recent_snapshot(organization_id) @@ -34,16 +32,9 @@ def inventory_for(organization_id, event_time: nil, validate: false) event_hash = {} events.group_by(&:group_id).each do |_, event_batch| last_grouped_event = event_batch.max_by(&:updated_at) - # don't do grouping for UpdateExistingEvents - if event_batch.any? { |e| e.is_a?(UpdateExistingEvent) } - handle(last_grouped_event, inventory, validate: validate) - yield last_grouped_event, inventory if block_given? - next - end - previous_event = event_hash[[last_grouped_event.eventable_type, last_grouped_event.eventable_id]] - event_hash[[last_grouped_event.eventable_type, last_grouped_event.eventable_id]] = last_grouped_event + previous_event = event_hash[last_grouped_event.eventable] + event_hash[last_grouped_event.eventable] = last_grouped_event handle(last_grouped_event, inventory, validate: validate, previous_event: previous_event) - yield last_grouped_event, inventory if block_given? end inventory end @@ -113,8 +104,7 @@ def handle_audit_event(payload, inventory) # diff previous event on DonationEvent, DistributionEvent, AdjustmentEvent, PurchaseEvent, TransferEvent, DistributionDestroyEvent, DonationDestroyEvent, - PurchaseDestroyEvent, TransferDestroyEvent, - UpdateExistingEvent do |event, inventory, validate: false, previous_event: nil| + PurchaseDestroyEvent, TransferDestroyEvent do |event, inventory, validate: false, previous_event: nil| handle_inventory_event(event.data, inventory, validate: validate, previous_event: previous_event) rescue InventoryError => e e.event = event diff --git a/app/events/kit_deallocate_event.rb b/app/events/kit_deallocate_event.rb index dd0724f0b6..38100bb86a 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, + to_storage_location: storage_location.id, 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, + from_storage_location: storage_location.id, to_storage_location: nil )) items diff --git a/app/events/snapshot_event.rb b/app/events/snapshot_event.rb index ae277bea5e..fb5f6053b0 100644 --- a/app/events/snapshot_event.rb +++ b/app/events/snapshot_event.rb @@ -1,8 +1,26 @@ class SnapshotEvent < Event - serialize :data, coder: EventTypes::StructCoder.new(EventTypes::Inventory) + serialize :data, EventTypes::StructCoder.new(EventTypes::Inventory) # @param organization [Organization] - def self.publish(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) inventory = InventoryAggregate.inventory_for(organization.id) create( eventable: organization, @@ -11,4 +29,18 @@ def self.publish(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 deleted file mode 100644 index 90133e2e39..0000000000 --- a/app/events/update_existing_event.rb +++ /dev/null @@ -1,63 +0,0 @@ -class UpdateExistingEvent < Event - class << self - # @param line_items [Array] - # @param storage_location [StorageLocation] - # @param direction [Symbol] - # @return [Hash] - def item_quantities(line_items, storage_location, direction) - line_items.to_h do |line_item| - opts = (direction == :from) ? - {from: storage_location.id} : - {to: storage_location.id} - [line_item.item_id, EventTypes::EventLineItem.from_line_item(line_item, **opts)] - end - end - - # @param previous [Hash] - # @param current [Hash] - # @return [Array] - def diff(previous, current) - previous.each do |id, event_item| - previous[id] = if current[id] - event_item.new(quantity: current[id].quantity - event_item.quantity) - else - event_item.new(quantity: -event_item.quantity) - end - end - all_items = previous.values - (current.keys - previous.keys).each do |id| - all_items.push(current[id]) # it's been added - end - all_items - end - - # @param itemizable [Itemizable] - # @return [Symbol] - def direction(itemizable) - itemizable.is_a?(Distribution) ? :from : :to - end - - # @param itemizable [Itemizable] - # @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, original_storage_location, dir) - current_items = item_quantities(itemizable.line_items, itemizable.storage_location, dir) - 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}", - organization_id: itemizable.organization_id, - event_time: Time.zone.now, - data: EventTypes::InventoryPayload.new( - items: diff_items - ) - ) - end - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 6f7075f680..cb0a15ca7f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,12 +14,12 @@ def default_title_content end end - def active_class(controller_action_names) - (controller_action_names.include?(params[:controller]) || controller_action_names.include?("#{params[:controller]}/#{params[:action]}")) ? 'active' : '' + def active_class(name) + name.include?(controller_path) ? "active" : controller_path end - def menu_open?(controller_action_names) - (controller_action_names.include?(params[:controller]) || controller_action_names.include?("#{params[:controller]}/#{params[:action]}")) ? 'menu-open' : '' + def menu_open?(name) + name.include?(controller_path) ? 'menu-open' : '' end def can_administrate? diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 4a81cb3f11..0fd66cff3b 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,9 +1,37 @@ # Encapsulates methods used on the Dashboard that need some business logic module DashboardHelper + def received_distributed_data(range = selected_range) + { + "Received donations" => total_received_donations_unformatted(range), + "Purchased" => total_purchased_unformatted(range), + "Distributed" => total_distributed_unformatted(range) + } + end + def total_on_hand(total = nil) number_with_delimiter(total || "-1") end + def total_received_money_donations(range = selected_range) + current_organization.donations.during(range).sum { |d| d.money_raised || 0 } + end + + def total_received_money_donations_from_product_drives(range: selected_range) + current_organization.donations.by_source(:product_drive).during(range).sum { |d| d.money_raised || 0 } + end + + def total_received_donations(range = selected_range) + number_with_delimiter total_received_donations_unformatted(range) + end + + def total_received_from_product_drives(range = selected_range) + number_with_delimiter total_received_from_product_drives_unformatted(range) + end + + def total_purchased(range = selected_range) + number_with_delimiter total_purchased_unformatted(range) + end + def total_distributed(range = selected_range) number_with_delimiter total_distributed_unformatted(range) end @@ -18,6 +46,18 @@ def recently_added_user_display_text(user) private + def total_received_donations_unformatted(range = selected_range) + LineItem.active.where(itemizable: current_organization.donations.during(range)).sum(:quantity) + end + + def total_received_from_product_drives_unformatted(range = selected_range) + LineItem.active.where(itemizable: current_organization.donations.by_source(:product_drive).during(range)).sum(:quantity) + end + + def total_purchased_unformatted(range = selected_range) + LineItem.active.where(itemizable: current_organization.purchases.during(range)).sum(:quantity) + end + def total_distributed_unformatted(range = selected_range) LineItem.active.where(itemizable: current_organization.distributions.during(range)).sum(:quantity) end diff --git a/app/helpers/date_range_helper.rb b/app/helpers/date_range_helper.rb index 8d3db3a7a5..3f00f35db9 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 || default_date + params.dig(:filters, :date_range).presence || this_year end def date_range_label @@ -23,10 +23,8 @@ def date_range_label end end - 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")}" + def this_year + "January 1, #{Time.zone.today.year} - December 31, #{Time.zone.today.year}" end def selected_interval diff --git a/app/helpers/distribution_helper.rb b/app/helpers/distribution_helper.rb index c1fc027ee0..53b9d8801f 100644 --- a/app/helpers/distribution_helper.rb +++ b/app/helpers/distribution_helper.rb @@ -16,7 +16,7 @@ def pickup_date def hashed_calendar_path crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31]) - calendar_distributions_url(hash: crypt.encrypt_and_sign(current_organization.id)) + distributions_calendar_url(hash: crypt.encrypt_and_sign(current_organization.id)) end def quantity_by_item_id(distribution, item_id) diff --git a/app/helpers/donations_helper.rb b/app/helpers/donations_helper.rb deleted file mode 100644 index ed07d514ea..0000000000 --- a/app/helpers/donations_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# Encapsulates business logic related to displaying Donations -module DonationsHelper - def total_received_donations(range = selected_range) - number_with_delimiter total_received_donations_unformatted(range) - end - - def total_received_money_donations(range = selected_range) - current_organization.donations.during(range).sum { |d| d.money_raised || 0 } - end - - def total_received_money_donations_from_product_drives(range: selected_range) - current_organization.donations.by_source(:product_drive).during(range).sum { |d| d.money_raised || 0 } - end - - def total_received_from_product_drives(range = selected_range) - number_with_delimiter total_received_from_product_drives_unformatted(range) - end - - private - - def total_received_donations_unformatted(range = selected_range) - LineItem.active.where(itemizable: current_organization.donations.during(range)).sum(:quantity) - end - - def total_received_from_product_drives_unformatted(range = selected_range) - LineItem.active.where(itemizable: current_organization.donations.by_source(:product_drive).during(range)).sum(:quantity) - end -end diff --git a/app/helpers/historical_trends_helper.rb b/app/helpers/historical_trends_helper.rb index e7ab4533f7..29091194c5 100644 --- a/app/helpers/historical_trends_helper.rb +++ b/app/helpers/historical_trends_helper.rb @@ -16,18 +16,6 @@ module HistoricalTrendsHelper def last_12_months current_month = Time.zone.now.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 + MONTHS.rotate(current_month) end end diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb index 32837fce40..d3fae473d8 100644 --- a/app/helpers/items_helper.rb +++ b/app/helpers/items_helper.rb @@ -14,9 +14,4 @@ def dollar_value(value) def cents_to_dollar(value_in_cents) value_in_cents.to_f / 100 end - - def selected_item_request_units(item) - item_request_unit_names = item.persisted? ? item.request_units.pluck(:name) : [] - current_organization.request_units.select { |unit| item_request_unit_names.include?(unit.name) }.pluck(:id) - end end diff --git a/app/helpers/partners_helper.rb b/app/helpers/partners_helper.rb index 74974649f3..6a8e8b87b3 100644 --- a/app/helpers/partners_helper.rb +++ b/app/helpers/partners_helper.rb @@ -1,12 +1,5 @@ # Encapsulates methods that need some business logic module PartnersHelper - def display_requested_items(partner, child) - ids = child.requested_item_ids - ids.map do |item_id| - partner.organization.item_id_to_display_string_map[item_id] - end.join(', ') - end - def show_header_column_class(partner, additional_classes: "") if partner.quota.present? "col-sm-3 col-3 #{additional_classes}" @@ -27,20 +20,6 @@ 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/javascript/application.js b/app/javascript/application.js index c94704d50d..34a187aa12 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -90,8 +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()], + 'All Time': [today.minus({ 'years': 100}).toJSDate(), today.toJSDate()], 'Today': [today.toJSDate(), today.toJSDate()], 'Yesterday': [today.minus({'days': 1}).toJSDate(), today.minus({'days': 1}).toJSDate()], 'Last 7 Days': [today.minus({'days': 6}).toJSDate(), today.toJSDate()], diff --git a/app/javascript/controllers/accordion_controller.js b/app/javascript/controllers/accordion_controller.js deleted file mode 100644 index def5ebeaa5..0000000000 --- a/app/javascript/controllers/accordion_controller.js +++ /dev/null @@ -1,19 +0,0 @@ -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/javascript/controllers/confirmation_controller.js b/app/javascript/controllers/confirmation_controller.js deleted file mode 100644 index bb04894b7f..0000000000 --- a/app/javascript/controllers/confirmation_controller.js +++ /dev/null @@ -1,111 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -/** - * Connects to data-controller="confirmation" - * Displays a confirmation modal with the details of the form that user just submitted. - * Launched when the user clicks Save from the form. - - * First runs a "pre-check" on the form data to a validation endpoint, - * which is specified in the controller's `preCheckPathValue` property. - * If the pre-check passes, it shows the modal. Because the confirmation modal should only be shown - * when the form data can pass initial validation. - * If the pre-check fails, it submits the form to the server for full validation and render with the errors. - * - * The pre-check validation endpoint also returns the html body to display in the modal if validation passes. - - * If the user clicks the "Yes..." button from the modal, it submits the form. - * If the user clicks the "No..." button from the modal, it closes and user remains on the same url. - */ -export default class extends Controller { - static targets = [ - "modal", - "form" - ] - - static values = { - preCheckPath: String - } - - openModal(event) { - event.preventDefault(); - - const formData = new FormData(this.formTarget); - const formObject = this.buildNestedObject(formData); - - fetch(this.preCheckPathValue, { - method: "POST", - headers: { - "X-CSRF-Token": this.getMetaToken(), - "X-Requested-With": "XMLHttpRequest", - "Content-Type": "application/json", - "Accept": "application/json" - }, - body: JSON.stringify(formObject), - credentials: "same-origin" - }) - .then((response) => response.json()) - .then((data) => { - if (data.valid) { - this.modalTarget.innerHTML = data.body; - $(this.modalTarget).modal("show"); - } else { - this.formTarget.requestSubmit(); - } - }) - .catch((error) => { - // Something went wrong in communication to server validation endpoint - // In this case, just submit the form as if the user had clicked Save. - // NICE TO HAVE: Send to bugsnag but need to install/configure https://www.npmjs.com/package/@bugsnag/js - console.log(`=== ConfirmationController ERROR ${error}`); - this.formTarget.requestSubmit(); - }); - } - - getMetaToken() { - const metaTokenElement = document.querySelector("meta[name='csrf-token']"); - return metaTokenElement - ? metaTokenElement.content - : "default_test_csrf_token"; - } - - // Prepare the form data for submission as expected by Rails, excluding - // the form level authenticity token because that is specific to creation. - // This controller needs to submit a validation only request. - buildNestedObject(formData) { - let formObject = {}; - for (let [key, value] of formData.entries()) { - if (key === "authenticity_token") { - continue; - } - - const keys = key.split(/[\[\]]+/).filter((k) => k); - keys.reduce((obj, k, i) => { - if (i === keys.length - 1) { - obj[k] = value; - } else { - obj[k] = obj[k] || {}; - } - return obj[k]; - }, formObject); - } - - return formObject; - } - - debugFormData() { - const formData = new FormData(this.formTarget); - let formDataString = "=== ConfirmationController FormData:\n"; - for (const [key, value] of formData.entries()) { - formDataString += `${key}: ${value}\n`; - } - console.log(formDataString); - } - - submitForm() { - $(this.modalTarget).find('#modalClose').prop('disabled', true); - $(this.modalTarget).find('#modalYes').prop('disabled', true); - $(this.modalTarget).find('#modalNo').prop('disabled', true); - $(this.modalTarget).modal("hide"); - this.formTarget.requestSubmit(); - } -} diff --git a/app/javascript/controllers/form_input_controller.js b/app/javascript/controllers/form_input_controller.js index 2f5b3035bc..35047849c8 100644 --- a/app/javascript/controllers/form_input_controller.js +++ b/app/javascript/controllers/form_input_controller.js @@ -27,7 +27,6 @@ export default class extends Controller { detail: dest.lastElementChild, }); - dest.lastElementChild.scrollIntoView(); dest.dispatchEvent(afterInsert); } diff --git a/app/javascript/controllers/item_units_controller.js b/app/javascript/controllers/item_units_controller.js deleted file mode 100644 index c647c9e876..0000000000 --- a/app/javascript/controllers/item_units_controller.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -// Connects to data-controller="form-input" -export default class extends Controller { - static targets = ["itemSelect", "requestSelect"] - static values = { - // hash of (item ID => hash of (request unit name => request unit plural name)) - "itemUnits": Object - } - - addOption(val, text) { - let option = document.createElement("option"); - option.value = val; - option.text = text; - this.requestSelectTarget.appendChild(option); - } - - clearOptions() { - while (this.requestSelectTarget.options.length > 0) { - this.requestSelectTarget.remove(this.requestSelectTarget.options[0]) - } - } - - connect() { - this.itemSelected(); - } - - itemSelected() { - if (!this.hasRequestSelectTarget) { - return; - } - let option = this.itemSelectTarget.options[this.itemSelectTarget.selectedIndex] - let units = this.itemUnitsValue[option.value] - if (!units || Object.keys(units).length === 0) { - this.requestSelectTarget.style.display = 'none'; - this.requestSelectTarget.selectedIndex = -1; - } - else { - this.requestSelectTarget.style.display = 'inline'; - this.clearOptions() - this.addOption('-1', 'Please select a unit') - this.addOption('', 'Units') - for (const [index, [name, displayName]] of Object.entries(Object.entries(units))) { - this.addOption(name, displayName) - } - } - } - -} diff --git a/app/javascript/controllers/password_visibility_controller.js b/app/javascript/controllers/password_visibility_controller.js deleted file mode 100644 index 2df6a93f03..0000000000 --- a/app/javascript/controllers/password_visibility_controller.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -export default class extends Controller { - static targets = ["password", "icon"]; - - toggle() { - const isPasswordVisible = this.passwordTarget.type === "text"; - - this.passwordTarget.type = isPasswordVisible ? "password" : "text"; - this.iconTarget.classList.toggle("fa-eye", !isPasswordVisible); - this.iconTarget.classList.toggle("fa-eye-slash", isPasswordVisible); - } -} diff --git a/app/javascript/controllers/select2_controller.js b/app/javascript/controllers/select2_controller.js index f1dc82e8b8..e15acc3fcc 100644 --- a/app/javascript/controllers/select2_controller.js +++ b/app/javascript/controllers/select2_controller.js @@ -5,29 +5,17 @@ import "select2" export default class extends Controller { static values = { config: { type: Object, default: {} }, - hideDropdown: { type: Boolean, default: false } - }; + } connect() { - const select2 = $(this.element).select2(this.configValue); - - if (this.hideDropdownValue) { - select2.on('select2:open', function (e) { - $('.select2-container--open .select2-dropdown--below').css('display','none'); - }); - } + $(this.element).select2(this.configValue); /** * This is a workaround to auto focus on the select2 input when it is opened. */ $(this.element).on('select2:open', function (e) { - let select2Instance = $(e.target).data('select2'); - if (select2Instance) { - let searchField = select2Instance.dropdown.$search || select2Instance.selection.$search; - if (searchField) { - searchField.focus(); - } - } - }); + $(".select2-search__field")[0].focus(); + }) } + } diff --git a/app/javascript/utils/barcode_items.js b/app/javascript/utils/barcode_items.js index eb75d686bc..8474a047e7 100644 --- a/app/javascript/utils/barcode_items.js +++ b/app/javascript/utils/barcode_items.js @@ -12,7 +12,7 @@ $(document).ready(function() { */ function capture_entry(event) { if (event.which == '10' || event.which == '13') { - barcode_item_lookup(event.target.value, event.target); + barcode_item_lookup(event.target.value, $(event.target).data("organization-id"),event.target); event.preventDefault(); } } @@ -21,11 +21,12 @@ $(document).ready(function() { barcode_item_lookup @brief Invokes an ajax lookup of a provided barcode value @param value : the barcode + @param organization_id : passed in as a param. This constrains the lookup @param src : the DOM source, so we can callback to it. */ - function barcode_item_lookup(value, src) { + function barcode_item_lookup(value, organization_id,src) { // Hardcoding magic URLs isn't ideal but it works for now - $.getJSON("/barcode_items/find.json?barcode_item[value]=" + value, {}, function(data) { + $.getJSON("/" + organization_id + "/barcode_items/find.json?barcode_item[value]=" + value, {}, function(data) { // Preserve this for reference of where we came from. data['src'] = src; data['value'] = value; diff --git a/app/javascript/utils/distributions_and_transfers.js b/app/javascript/utils/distributions_and_transfers.js index 24082e5c69..2871accb61 100644 --- a/app/javascript/utils/distributions_and_transfers.js +++ b/app/javascript/utils/distributions_and_transfers.js @@ -66,7 +66,6 @@ $(function() { $(document).on("change", "select.storage-location-source", function() { const default_item = $(".line-item-fields select"); - control = $("select.storage-location-source"); if (storage_location_required && !control.val()) { $("#__add_line_item").addClass("disabled"); } diff --git a/app/jobs/backup_db_rds.rb b/app/jobs/backup_db_rds.rb deleted file mode 100644 index d5eb004586..0000000000 --- a/app/jobs/backup_db_rds.rb +++ /dev/null @@ -1,24 +0,0 @@ -# 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 2006d2931c..32cd255586 100644 --- a/app/jobs/reminder_deadline_job.rb +++ b/app/jobs/reminder_deadline_job.rb @@ -7,7 +7,6 @@ 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/mailers/distribution_mailer.rb b/app/mailers/distribution_mailer.rb index 8ef47f4a11..c84dadf90c 100644 --- a/app/mailers/distribution_mailer.rb +++ b/app/mailers/distribution_mailer.rb @@ -27,11 +27,7 @@ def partner_mailer(current_organization, distribution, subject, distribution_cha pdf = DistributionPdf.new(current_organization, @distribution).compute_and_render attachments[format("%s %s.pdf", @partner.name, @distribution.created_at.strftime("%Y-%m-%d"))] = pdf cc = [@partner.email] - if distribution.pick_up? && @partner.profile&.pick_up_email - pick_up_emails = @partner.profile.split_pick_up_emails - cc.push(pick_up_emails) - end - cc.flatten! + cc.push(@partner.profile&.pick_up_email) if distribution.pick_up? cc.compact! cc.uniq! diff --git a/app/mailers/requests_confirmation_mailer.rb b/app/mailers/requests_confirmation_mailer.rb index 71e5fb4758..7d520b69af 100644 --- a/app/mailers/requests_confirmation_mailer.rb +++ b/app/mailers/requests_confirmation_mailer.rb @@ -4,12 +4,12 @@ def confirmation_email(request) @partner = request.partner @request_items = fetch_items(request) requestee_email = request.user_email + mail(to: requestee_email, cc: @partner.email, subject: "#{@organization.name} - Requests Confirmation") end private - # TODO: remove the need to de-duplicate items in the request def fetch_items(request) combined = combined_items(request) item_ids = combined&.map { |item| item['item_id'] } @@ -21,10 +21,10 @@ def fetch_items(request) def combined_items(request) return [] if request.request_items.size == 0 # convert items into a hash of (id => list of items with that ID) - grouped = request.request_items.group_by { |i| [i['item_id'], i['request_unit']] } + grouped = request.request_items.group_by { |i| i['item_id'] } # convert hash into an array of items with combined quantities - grouped.map do |id_unit, items| - { 'item_id' => id_unit.first, 'quantity' => items.map { |i| i['quantity'] }.sum, "unit" => id_unit.last } + grouped.map do |id, items| + { 'item_id' => id, 'quantity' => items.map { |i| i['quantity'] }.sum } end end end diff --git a/app/models/account_request.rb b/app/models/account_request.rb index c68fe75559..6845565671 100644 --- a/app/models/account_request.rb +++ b/app/models/account_request.rb @@ -21,7 +21,6 @@ class AccountRequest < ApplicationRecord validates :email, presence: true, uniqueness: true validates :request_details, presence: true, length: { minimum: 50 } validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } - validates :organization_website, format: { with: URI::DEFAULT_PARSER.make_regexp, message: "should look like 'https://www.example.com'" }, allow_blank: true validate :email_not_already_used_by_organization validate :email_not_already_used_by_user @@ -30,10 +29,10 @@ class AccountRequest < ApplicationRecord has_one :organization, dependent: :nullify - enum status: %w[started user_confirmed admin_approved rejected admin_closed].map { |v| [v, v] }.to_h + enum status: %w[started user_confirmed admin_approved rejected].map { |v| [v, v] }.to_h scope :requested, -> { where(status: %w[started user_confirmed]) } - scope :closed, -> { where(status: %w[admin_approved rejected admin_closed]) } + scope :closed, -> { where(status: %w[admin_approved rejected]) } def self.get_by_identity_token(identity_token) decrypted_token = JWT.decode(identity_token, Rails.application.secret_key_base, true, { algorithm: 'HS256' }) @@ -62,11 +61,6 @@ def processed? organization.present? end - # @return [Boolean] - def can_be_closed? - started? || user_confirmed? - end - def confirm! update!(confirmed_at: Time.current, status: 'user_confirmed') AccountRequestMailer.approval_request(account_request_id: id).deliver_later @@ -78,12 +72,6 @@ def reject!(reason) AccountRequestMailer.rejection(account_request_id: id).deliver_later end - # @param reason [String] - def close!(reason) - raise 'Cannot be closed from this state' unless can_be_closed? - update!(status: 'admin_closed', rejection_reason: reason) - end - private def email_not_already_used_by_organization diff --git a/app/models/audit.rb b/app/models/audit.rb index 7f490e0675..ab0634915f 100644 --- a/app/models/audit.rb +++ b/app/models/audit.rb @@ -28,6 +28,7 @@ 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 @@ -40,7 +41,7 @@ def self.finalized_since?(itemizable, *location_ids) item_ids = itemizable.line_items.pluck(:item_id) where(status: "finalized") .where(storage_location_id: location_ids) - .where(updated_at: itemizable.created_at..) + .where(created_at: itemizable.created_at..) .joins(:line_items) .where(line_items: {item_id: item_ids}) .exists? diff --git a/app/models/concerns/issued_at.rb b/app/models/concerns/issued_at.rb index 3abfa79c53..35045a6959 100644 --- a/app/models/concerns/issued_at.rb +++ b/app/models/concerns/issued_at.rb @@ -10,7 +10,6 @@ module IssuedAt scope :by_issued_at, ->(issued_at) { where(issued_at: issued_at.beginning_of_month..issued_at.end_of_month) } scope :for_year, ->(year) { where("extract(year from issued_at) = ?", year) } validate :issued_at_cannot_be_before_2000 - validate :issued_at_cannot_be_further_than_1_year end private @@ -24,10 +23,4 @@ def issued_at_cannot_be_before_2000 errors.add(:issued_at, "Cannot be before 2000") end end - - def issued_at_cannot_be_further_than_1_year - if issued_at.present? && issued_at > DateTime.now.next_year - errors.add(:issued_at, "cannot be more than 1 year in the future") - end - end end diff --git a/app/models/concerns/itemizable.rb b/app/models/concerns/itemizable.rb index 29ebcfabee..bd2155ce33 100644 --- a/app/models/concerns/itemizable.rb +++ b/app/models/concerns/itemizable.rb @@ -119,6 +119,14 @@ def line_item_values end end + def to_a + return line_item_values unless Flipper.enabled?(:deprecate_to_a) + + Rails.logger.warn "Called #to_a on an Itemizable #{inspect}." + Rails.logger.warn caller.join("\n") + raise StandardError, "Calling to_a on an Itemizable is deprecated. Use #line_item_values instead." + end + private # From Controller parameters @@ -138,4 +146,20 @@ 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/concerns/provideable.rb b/app/models/concerns/provideable.rb index 56a64620c1..478a3869bb 100644 --- a/app/models/concerns/provideable.rb +++ b/app/models/concerns/provideable.rb @@ -8,6 +8,9 @@ module Provideable included do belongs_to :organization # Automatically validates presence as of Rails 5 + validates :contact_name, presence: { message: "Must provide a name or a business name" }, if: proc { |ddp| ddp.business_name.blank? } + validates :business_name, presence: { message: "Must provide a name or a business name" }, if: proc { |ddp| ddp.contact_name.blank? } + scope :for_csv_export, ->(organization, *) { where(organization: organization).order(:business_name) } @@ -19,7 +22,6 @@ def self.import_csv(csv, organization) loc.save! end - [] end def self.csv_export_headers diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 43243a32d5..f8d591e25b 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -39,6 +39,7 @@ 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? @@ -101,31 +102,6 @@ def copy_from_donation(donation_id, storage_location_id) self.storage_location = StorageLocation.find(storage_location_id) if storage_location_id end - # This is meant for the Edit page - we will be adding any request items that aren't in the - # distribution for whatever reason, with zero quantity. - def initialize_request_items - return if request.nil? - - item_ids = Set.new - line_items.each do |line_item| - item_request = request.item_requests.find { |r| r.item_id == line_item.item_id } - if item_request - item_ids.add(item_request) - line_item.requested_item = item_request - end - end - - request.item_requests.each do |item_request| - next if item_ids.include?(item_request) - - line_items.new( - requested_item: item_request, - quantity: 0, - item_id: item_request.item_id - ) - end - end - def copy_from_request(request_id) request = Request.find(request_id) self.request = request @@ -134,12 +110,12 @@ def copy_from_request(request_id) self.agency_rep = request.partner_user&.formatted_email self.comment = request.comments self.issued_at = Time.zone.today + 1.day - request.item_requests.each do |item_request| + request.request_items.each do |item| line_items.new( - requested_item: item_request, - # if there is a custom unit, don't prefill with the quantity - they have to enter it - quantity: item_request.request_unit.present? ? nil : item_request.quantity, - item_id: item_request.item_id + quantity: item["quantity"], + item: Item.eager_load(:base_item).find_by(organization: request.organization, id: item["item_id"]), + itemizable_id: request.id, + itemizable_type: "Distribution" ) end end diff --git a/app/models/donation.rb b/app/models/donation.rb index 6fb1466d78..2100b738af 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -136,10 +136,6 @@ def storage_view storage_location.nil? ? "N/A" : storage_location.name end - def in_kind_value_money - Money.new(value_per_itemizable) - end - private def combine_duplicates diff --git a/app/models/donation_site.rb b/app/models/donation_site.rb index 99ddd71d97..95195b5d1a 100644 --- a/app/models/donation_site.rb +++ b/app/models/donation_site.rb @@ -46,7 +46,6 @@ def self.import_csv(csv, organization) loc.organization_id = organization loc.save! end - [] end def self.csv_export_headers diff --git a/app/models/errors.rb b/app/models/errors.rb index 1d9cc89cad..ef9e3c3762 100644 --- a/app/models/errors.rb +++ b/app/models/errors.rb @@ -54,10 +54,4 @@ def message "KitAllocation not found for given kit" end end - - class InventoryAlreadyHasItems < StandardError - def message - "Could not complete action: inventory already has items stored" - end - end end diff --git a/app/models/event.rb b/app/models/event.rb index a67f972666..23b14cc19c 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -29,7 +29,7 @@ class Event < ApplicationRecord .where("type = 'SnapshotEvent' OR (item->>'from_storage_location')=? OR (item->>'to_storage_location')=?", loc_id, loc_id) } - serialize :data, coder: EventTypes::StructCoder.new(EventTypes::InventoryPayload) + serialize :data, EventTypes::StructCoder.new(EventTypes::InventoryPayload) belongs_to :eventable, polymorphic: true belongs_to :user, optional: true @@ -74,7 +74,13 @@ 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}" @@ -86,4 +92,16 @@ def validate_inventory end raise e end + + after_create_commit do + inventory = InventoryAggregate.inventory_for(organization_id) + diffs = EventDiffer.check_difference(inventory) + if diffs.any? + InventoryDiscrepancy.create!( + event_id: id, + organization_id: organization_id, + diff: diffs + ) + end + end end diff --git a/app/models/inventory_discrepancy.rb b/app/models/inventory_discrepancy.rb new file mode 100644 index 0000000000..338a324ae1 --- /dev/null +++ b/app/models/inventory_discrepancy.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: inventory_discrepancies +# +# id :bigint not null, primary key +# diff :json +# created_at :datetime not null +# updated_at :datetime not null +# event_id :bigint not null +# organization_id :bigint not null +# +class InventoryDiscrepancy < ApplicationRecord + belongs_to :event + belongs_to :organization +end diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb index 1c46a2bda2..f038656ad6 100644 --- a/app/models/inventory_item.rb +++ b/app/models/inventory_item.rb @@ -22,14 +22,23 @@ 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 + { 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 end end diff --git a/app/models/item.rb b/app/models/item.rb index 6151ecedcd..fd818bcdb6 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -34,7 +34,7 @@ class Item < ApplicationRecord belongs_to :kit, optional: true belongs_to :item_category, optional: true - validates :name, uniqueness: { scope: :organization, case_sensitive: false, message: "- An item with that name already exists (could be an inactive item)" } + validates :name, uniqueness: { scope: :organization } validates :name, presence: true validates :organization, presence: true validates :distribution_quantity, numericality: { greater_than: 0 }, allow_blank: true @@ -44,9 +44,9 @@ 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) } @@ -66,44 +66,32 @@ class Item < ApplicationRecord .alphabetized } - # Scopes - explanation of business rules for filtering scopes as of 20240527. This was a mess, but is much better now. - # 1/ Disposable. Disposables are only the disposable diapers for children. So we deliberately exclude adult and cloth - # 2/ Cloth. Cloth diapers for children. Exclude adult cloth. Cloth training pants also go here. - # 3/ Adult incontinence. Items for adult incontinence -- diapers, ai pads, but not adult wipes. - # 4/ Period supplies. All things with 'menstrual in the category' - # 5/ Other -- Miscellaneous, and wipes - # Known holes and ambiguities as of 20240527. Working on these with the business - # 1/ Liners. We are adding a new item for AI liners, and renaming the current liners to be specficially for periods, - # having confirmed with the business that the majority of liners are for menstrual use. - # However, there is a product which can be used for either, so we are still sussing out what to do about that. - scope :disposable, -> { joins(:base_item) .where("lower(base_items.category) LIKE '%diaper%'") .where.not("lower(base_items.category) LIKE '%cloth%' OR lower(base_items.name) LIKE '%cloth%'") - .where.not("lower(base_items.category) LIKE '%adult%'") } scope :cloth_diapers, -> { joins(:base_item) - .where("lower(base_items.category) LIKE '%cloth%'") - .or(where("base_items.category = 'Training Pants'")) - .where.not("lower(base_items.category) LIKE '%adult%'") + .where("lower(base_items.category) LIKE '%cloth%' OR lower(base_items.name) LIKE '%cloth%'") } scope :adult_incontinence, -> { joins(:base_item) - .where("lower(base_items.category) LIKE '%adult%' AND lower(base_items.category) NOT LIKE '%wipes%'") + .where(items: { partner_key: %w(adult_incontinence underpads liners) }) + .or(where("items.partner_key LIKE '%adult%' AND items.partner_key NOT LIKE '%cloth%'")) } scope :period_supplies, -> { joins(:base_item) - .where("lower(base_items.category) LIKE '%menstrual%'") + .where(items: { partner_key: %w(tampons pads) }) + .or(where("base_items.category = 'Period Supplies'")) } scope :other_categories, -> { joins(:base_item) - .where("lower(base_items.category) LIKE '%wipes%'") + .where(items: { partner_key: %w(cloth_training_pants wipes adult_wipes) }) .or(where("base_items.category = 'Miscellaneous'")) } @@ -113,6 +101,10 @@ 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 @@ -123,37 +115,35 @@ def self.reactivate(item_ids) end def has_inventory?(inventory = nil) - inventory&.quantity_for(item_id: id)&.positive? - end - - def in_request? - Request.by_request_item_id(id).exists? - end - - def is_in_kit?(kits = nil) - if kits - kits.any? { |k| k.line_items.map(&:item_id).include?(id) } + if inventory + inventory.quantity_for(item_id: id).positive? else - organization.kits - .active - .joins(:line_items) - .where(line_items: { item_id: id}).any? + inventory_items.where("quantity > 0").any? end end - def can_delete?(inventory = nil, kits = nil) - can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? + def is_in_kit? + organization.kits + .active + .joins(:line_items) + .where(line_items: { item_id: id}).any? + end + + def can_delete?(inventory = nil) + can_deactivate_or_delete?(inventory) && line_items.none? && !barcode_count&.positive? end # @return [Boolean] - def can_deactivate_or_delete?(inventory = nil, kits = nil) - inventory ||= View::Inventory.new(organization_id) + def can_deactivate_or_delete?(inventory = nil) + if inventory.nil? && Event.read_events?(organization) + inventory = View::Inventory.new(organization_id) + end # 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 # for an inactive item - which we said above we don't want to allow. - !has_inventory?(inventory) && !is_in_kit?(kits) + !has_inventory?(inventory) && !is_in_kit? end def validate_destroy @@ -200,6 +190,16 @@ 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] @@ -218,11 +218,8 @@ def default_quantity distribution_quantity || 50 end - def sync_request_units!(unit_ids) - request_units.clear - organization.request_units.where(id: unit_ids).pluck(:name).each do |name| - request_units.create!(name:) - end + def inventory_item_at(storage_location_id) + inventory_items.find_by(storage_location_id: storage_location_id) end private diff --git a/app/models/item_unit.rb b/app/models/item_unit.rb deleted file mode 100644 index d9453394d2..0000000000 --- a/app/models/item_unit.rb +++ /dev/null @@ -1,20 +0,0 @@ -# == Schema Information -# -# Table name: item_units -# -# id :bigint not null, primary key -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# item_id :bigint -# -class ItemUnit < ApplicationRecord - belongs_to :item - - validate do - names = item.organization.request_units.map(&:name) - unless names.include?(name) - errors.add(:name, "is not supported by the organization") - end - end -end diff --git a/app/models/kit.rb b/app/models/kit.rb index 363407266f..759ae7451c 100644 --- a/app/models/kit.rb +++ b/app/models/kit.rb @@ -19,6 +19,7 @@ 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) } @@ -33,9 +34,12 @@ class Kit < ApplicationRecord # @param inventory [View::Inventory] # @return [Boolean] - def can_deactivate?(inventory = nil) - inventory ||= View::Inventory.new(organization_id) - inventory.quantity_for(item_id: item.id).zero? + def can_deactivate?(inventory) + if inventory + inventory.quantity_for(item_id: item.id).zero? + else + inventory_items.where('quantity > 0').none? + end end def deactivate diff --git a/app/models/line_item.rb b/app/models/line_item.rb index 8cdcb79b7c..ab2025a66a 100644 --- a/app/models/line_item.rb +++ b/app/models/line_item.rb @@ -21,22 +21,9 @@ class LineItem < ApplicationRecord belongs_to :item validates :item_id, presence: true - validates :quantity, numericality: { only_integer: true, message: "is not a number. Note: commas are not allowed" } - validate :quantity_must_be_a_number_within_range + validates :quantity, numericality: { only_integer: true, less_than: MAX_INT, greater_than: MIN_INT } scope :active, -> { joins(:item).where(items: { active: true }) } delegate :name, to: :item - - # Used in a distribution that was initialized from a request. The `item_request` will be - # populated here. - attr_accessor :requested_item - - def quantity_must_be_a_number_within_range - if quantity && quantity > MAX_INT - errors.add(:quantity, "must be less than #{MAX_INT}") - elsif quantity && quantity < MIN_INT - errors.add(:quantity, "must be greater than #{MIN_INT}") - end - end end diff --git a/app/models/manufacturer.rb b/app/models/manufacturer.rb index 21034cc1eb..3a274fefa8 100644 --- a/app/models/manufacturer.rb +++ b/app/models/manufacturer.rb @@ -26,15 +26,10 @@ def volume donations.joins(:line_items).sum(:quantity) end - def self.by_donation_count(count = 10, date_range = nil) - # selects manufacturers that have donation qty > 0 in the provided date range + def self.by_donation_count(count = 10) + # selects manufacturers that have donation qty > 0 # and sorts them by highest volume of donation - joins(donations: :line_items).where(donations: { issued_at: date_range }) - .select('manufacturers.*, sum(line_items.quantity) as donation_count') - .group('manufacturers.id') - .having('sum(line_items.quantity) > 0') - .order('donation_count DESC') - .limit(count) + select { |m| m.volume.positive? }.sort.reverse.first(count) end private diff --git a/app/models/organization.rb b/app/models/organization.rb index 06279d7db2..b7f577d6e7 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -11,8 +11,6 @@ # enable_child_based_requests :boolean default(TRUE), not null # enable_individual_requests :boolean default(TRUE), not null # enable_quantity_based_requests :boolean default(TRUE), not null -# hide_package_column_on_receipt :boolean default(FALSE) -# hide_value_columns_on_receipt :boolean default(FALSE) # intake_location :integer # invitation_text :text # latitude :float @@ -23,7 +21,6 @@ # reminder_day :integer # repackage_essentials :boolean default(FALSE), not null # short_name :string -# signature_for_distribution_pdf :boolean default(FALSE) # state :string # street :string # url :string @@ -73,7 +70,6 @@ class Organization < ApplicationRecord has_many :transfers has_many :users, -> { distinct }, through: :roles has_many :vendors - has_many :request_units, class_name: 'Unit' end has_many :items, dependent: :destroy do @@ -135,7 +131,7 @@ def flipper_id has_one_attached :logo - accepts_nested_attributes_for :users, :account_request, :request_units + accepts_nested_attributes_for :users, :account_request include Geocodable @@ -193,7 +189,11 @@ def address_inline end def total_inventory - View::Inventory.total_inventory(id) + if Event.read_events?(self) + View::Inventory.total_inventory(id) + else + inventory_items.sum(:quantity) || 0 + end end def self.seed_items(organization = Organization.all) @@ -212,7 +212,7 @@ def seed_items(item_collection) rescue ActiveRecord::RecordInvalid => e Rails.logger.info "[SEED] Duplicate item! #{e.record.name}" existing_item = items.find_by(name: e.record.name) - if e.to_s.match(/already exists/).present? && existing_item.other? + if e.to_s.match(/been taken/).present? && existing_item.other? Rails.logger.info "Changing Item##{existing_item.id} from Other to #{e.record.partner_key}" existing_item.update(partner_key: e.record.partner_key) existing_item.reload diff --git a/app/models/organization_stats.rb b/app/models/organization_stats.rb index f314e36201..726a988c5d 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 - inventory = View::Inventory.new(current_organization.id) - storage_locations.select { |loc| inventory.quantity_for(storage_location: loc.id).positive? } + 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 } end private diff --git a/app/models/partner.rb b/app/models/partner.rb index 2e7b5ab77b..455428869c 100644 --- a/app/models/partner.rb +++ b/app/models/partner.rb @@ -48,9 +48,10 @@ class Partner < ApplicationRecord validates :organization, presence: true validates :name, presence: true, uniqueness: { scope: :organization } - validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :email, presence: true, uniqueness: { case_sensitive: false }, + format: { with: URI::MailTo::EMAIL_REGEXP, on: :create } - validates :quota, numericality: { greater_than_or_equal_to: 0 }, allow_blank: true + validates :quota, numericality: true, allow_blank: true validate :correct_document_mime_type @@ -157,7 +158,6 @@ def approvable? # better to extract this outside of the model def self.import_csv(csv, organization_id) - errors = [] organization = Organization.find(organization_id) csv.each do |row| @@ -165,23 +165,13 @@ def self.import_csv(csv, organization_id) svc = PartnerCreateService.new(organization: organization, partner_attrs: hash_rows) svc.call - if svc.errors.present? - errors << "#{svc.partner.name}: #{svc.partner.errors.full_messages.to_sentence}" - end end - errors end def self.csv_export_headers [ "Agency Name", "Agency Email", - "Agency Address", - "Agency City", - "Agency State", - "Agency Zip Code", - "Agency Website", - "Agency Type", "Contact Name", "Contact Phone", "Contact Email", @@ -193,12 +183,6 @@ def csv_export_attributes [ name, email, - agency_info[:address], - agency_info[:city], - agency_info[:state], - agency_info[:zip_code], - agency_info[:website], - agency_info[:agency_type], contact_person[:name], contact_person[:phone], contact_person[:email], @@ -219,21 +203,6 @@ def contact_person } end - def agency_info - return @agency_info if @agency_info - - return {} if profile.blank? - - @agency_info = { - address: [profile.address1, profile.address2].select(&:present?).join(', '), - city: profile.city, - state: profile.state, - zip_code: profile.zip_code, - website: profile.website, - agency_type: (profile.agency_type == AGENCY_TYPES["OTHER"]) ? "#{AGENCY_TYPES["OTHER"]}: #{profile.other_agency_type}" : profile.agency_type - } - end - def partials_to_show organization.partner_form_fields.presence || ALL_PARTIALS end @@ -254,10 +223,6 @@ def impact_metrics } end - def quota_exceeded?(total) - quota.present? && total > quota - end - private def families_served_count diff --git a/app/models/partners/child.rb b/app/models/partners/child.rb index 3cc260ebdd..1421bd47a1 100644 --- a/app/models/partners/child.rb +++ b/app/models/partners/child.rb @@ -22,10 +22,9 @@ module Partners class Child < Base has_paper_trail - serialize :child_lives_with, type: Array + serialize :child_lives_with, Array belongs_to :family has_many :child_item_requests, dependent: :destroy - has_and_belongs_to_many :requested_items, class_name: 'Item' include Filterable include Exportable @@ -89,7 +88,7 @@ def display_name def self.csv_export_headers %w[ id first_name last_name date_of_birth gender child_lives_with race agency_child_id - health_insurance comments created_at updated_at guardian_last_name guardian_first_name requested_items active archived + health_insurance comments created_at updated_at family_id item_needed_diaperid active archived ].freeze end @@ -107,9 +106,8 @@ def csv_export_attributes comments, created_at, updated_at, - family.guardian_last_name, - family.guardian_first_name, - requested_items.map(&:name).join(", "), + family_id, + item_needed_diaperid, active, archived ] diff --git a/app/models/partners/family.rb b/app/models/partners/family.rb index 11d73da765..52413ff24c 100644 --- a/app/models/partners/family.rb +++ b/app/models/partners/family.rb @@ -31,7 +31,7 @@ class Family < Base belongs_to :partner, class_name: '::Partner' has_many :children, dependent: :destroy has_many :authorized_family_members, dependent: :destroy - serialize :sources_of_income, type: Array + serialize :sources_of_income, Array validates :guardian_first_name, :guardian_last_name, :guardian_zip_code, presence: true include Filterable diff --git a/app/models/partners/item_request.rb b/app/models/partners/item_request.rb index d10fc2e920..0232589b43 100644 --- a/app/models/partners/item_request.rb +++ b/app/models/partners/item_request.rb @@ -6,7 +6,6 @@ # name :string # partner_key :string # quantity :string -# request_unit :string # created_at :datetime not null # updated_at :datetime not null # item_id :integer @@ -17,7 +16,6 @@ module Partners class ItemRequest < Base has_paper_trail belongs_to :request, class_name: '::Request', foreign_key: :partner_request_id, inverse_of: :item_requests - belongs_to :item has_many :child_item_requests, dependent: :destroy has_many :children, through: :child_item_requests @@ -25,25 +23,5 @@ class ItemRequest < Base validates :quantity, numericality: { only_integer: true, greater_than_or_equal_to: 1 } validates :name, presence: true validates :partner_key, presence: true - validate :request_unit_is_supported - - def request_unit_is_supported - return if request_unit.blank? - - names = item.request_units.map(&:name) - unless names.include?(request_unit) - 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 3fd9fcb397..296204397b 100644 --- a/app/models/partners/profile.rb +++ b/app/models/partners/profile.rb @@ -98,7 +98,6 @@ class Profile < Base validate :client_share_is_0_or_100 validate :has_at_least_one_request_setting - validate :pick_up_email_addresses self.ignored_columns = %w[ evidence_based_description @@ -115,14 +114,7 @@ class Profile < Base ] def client_share_total - # client_share could be nil - served_areas.map(&:client_share).compact.sum - end - - def split_pick_up_emails - return nil if pick_up_email.nil? - - pick_up_email.split(/,|\s+/).compact_blank + served_areas.sum(&:client_share) end private @@ -142,40 +134,13 @@ def client_share_is_0_or_100 # their allocation actually is total = client_share_total if total != 0 && total != 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 + errors.add(:base, "Total client share must be 0 or 100") end end def has_at_least_one_request_setting if !(enable_child_based_requests || enable_individual_requests || enable_quantity_based_requests) - 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 - - def pick_up_email_addresses - # pick_up_email is a string of comma-separated emails, check specs for details - return if pick_up_email.nil? - - emails = split_pick_up_emails - if emails.size > 3 - errors.add(:pick_up_email, "can't have more than three email addresses") - nil - end - if emails.uniq.size != emails.size - errors.add(:pick_up_email, "should not have repeated email addresses") - end - emails.each do |e| - errors.add(:pick_up_email, "is invalid") unless e.match? URI::MailTo::EMAIL_REGEXP + 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 00bb758c26..5fa2ea92a5 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, message: "Client share must be between 1 and 100 inclusive"} + validates :client_share, inclusion: {in: 1..100} end end diff --git a/app/models/product_drive_participant.rb b/app/models/product_drive_participant.rb index a2863fe8ed..150209f846 100644 --- a/app/models/product_drive_participant.rb +++ b/app/models/product_drive_participant.rb @@ -25,9 +25,6 @@ class ProductDriveParticipant < ApplicationRecord validates :phone, presence: { message: "Must provide a phone or an e-mail" }, if: proc { |pdp| pdp.email.blank? } validates :email, presence: { message: "Must provide a phone or an e-mail" }, if: proc { |pdp| pdp.phone.blank? } - validates :contact_name, presence: { message: "Must provide a name or a business name" }, if: proc { |pdp| pdp.business_name.blank? } - validates :business_name, presence: { message: "Must provide a name or a business name" }, if: proc { |pdp| pdp.contact_name.blank? } - validates :comment, length: { maximum: 500 } scope :alphabetized, -> { order(:contact_name) } diff --git a/app/models/purchase.rb b/app/models/purchase.rb index 734d36fff7..4cfa2bb0fd 100644 --- a/app/models/purchase.rb +++ b/app/models/purchase.rb @@ -81,20 +81,6 @@ 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 799d2f5331..c6963aeeb3 100644 --- a/app/models/request.rb +++ b/app/models/request.rb @@ -33,10 +33,7 @@ class Request < ApplicationRecord enum status: { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, _prefix: true validates :distribution_id, uniqueness: true, allow_nil: true - validate :item_requests_uniqueness_by_item_id - validate :not_completely_empty - - after_validation :sanitize_items_data + before_save :sanitize_items_data include Filterable # add request item scope to allow filtering distributions by request item @@ -62,13 +59,6 @@ def user_email private - def item_requests_uniqueness_by_item_id - item_ids = item_requests.map(&:item_id) - if item_ids.uniq.length != item_ids.length - errors.add(:item_requests, "should have unique item_ids") - end - end - def sanitize_items_data return unless request_items && request_items_changed? @@ -76,10 +66,4 @@ def sanitize_items_data item.merge("item_id" => item["item_id"]&.to_i, "quantity" => item["quantity"]&.to_i) end end - - def not_completely_empty - if comments.blank? && item_requests.blank? - errors.add(:base, "completely empty request") - end - end end diff --git a/app/models/request_item.rb b/app/models/request_item.rb index e4f180d01f..a843f293d2 100644 --- a/app/models/request_item.rb +++ b/app/models/request_item.rb @@ -1,5 +1,5 @@ class RequestItem - attr_accessor :item, :quantity, :unit, :on_hand, :on_hand_for_location + attr_accessor :item, :quantity, :on_hand, :on_hand_for_location include ItemQuantity def self.from_json(json, request, inventory = nil) @@ -9,20 +9,21 @@ def self.from_json(json, request, inventory = nil) item = Item.find(json['item_id']) quantity = json['quantity'] - unit = request.item_requests.find { |item_request| item_request.item_id == item.id }&.request_unit 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') + new(item, quantity, on_hand, on_hand_for_location&.positive? ? on_hand_for_location : 'N/A') end delegate :name, to: :item - def initialize(item, quantity, unit, on_hand, on_hand_for_location) + def initialize(item, quantity, on_hand, on_hand_for_location) @item = item @quantity = quantity - @unit = unit @on_hand = on_hand @on_hand_for_location = on_hand_for_location end diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 6ad65c7fa9..2d562ec894 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -32,6 +32,7 @@ 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, @@ -52,6 +53,12 @@ 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) } @@ -59,36 +66,41 @@ class StorageLocation < ApplicationRecord # @param organization [Organization] # @param inventory [View::Inventory] def self.items_inventoried(organization, inventory = nil) - 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) } + 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 end - # @return [Array] - def items - View::Inventory.items_for_location(self).map(&:db_item) + def item_total(item_id) + inventory_items.where(item_id: item_id).pick(:quantity) || 0 end - # @return [Integer] def size - View::Inventory.items_for_location(self).map(&:quantity).sum + inventory_items.sum(:quantity) end - # @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) + def total_active_inventory_count + active_inventory_items + .select('items.quantity') + .sum(:quantity) end - # @param inventory [View::Inventory] - # @return [Integer] def inventory_total_value_in_dollars(inventory = nil) - inventory ||= View::Inventory.new(organization_id) - inventory&.total_value_in_dollars(storage_location: id) + 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 end def to_csv @@ -109,7 +121,6 @@ def self.import_csv(csv, organization) loc.organization_id = organization loc.save! end - [] end # NOTE: We should generalize this elsewhere -- Importable concern? @@ -121,9 +132,6 @@ def self.import_csv(csv, organization) # @param loc [Integer] StorageLocation ID # @return [void] def self.import_inventory(filename, org, loc) - storage_location = StorageLocation.find(loc.to_i) - raise Errors::InventoryAlreadyHasItems unless storage_location.empty_inventory? - current_org = Organization.find(org) adjustment = current_org.adjustments.new(storage_location_id: loc.to_i, user_id: User.with_role(Role::ORG_ADMIN, current_org).first&.id, @@ -136,6 +144,77 @@ 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") @@ -147,6 +226,13 @@ 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] @@ -167,7 +253,17 @@ def self.generate_csv_from_inventory(storage_locations, inventory) end def empty_inventory? - inventory = View::Inventory.new(organization_id) - inventory.quantity_for(storage_location: id).zero? + 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 }) end end diff --git a/app/models/transfer.rb b/app/models/transfer.rb index 3945ac776f..f6f025f3a7 100644 --- a/app/models/transfer.rb +++ b/app/models/transfer.rb @@ -38,6 +38,7 @@ def self.storage_locations_transferred_from_in(organization) 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 @@ -88,7 +89,11 @@ def from_storage_quantities end def insufficient_items - inventory = View::Inventory.new(organization_id) - line_items.select { |i| i.quantity > inventory.quantity_for(item_id: i.item_id) } + 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 end end diff --git a/app/models/unit.rb b/app/models/unit.rb deleted file mode 100644 index cfe4731589..0000000000 --- a/app/models/unit.rb +++ /dev/null @@ -1,14 +0,0 @@ -# == Schema Information -# -# Table name: units -# -# id :bigint not null, primary key -# name :string not null -# created_at :datetime not null -# updated_at :datetime not null -# organization_id :bigint -# -class Unit < ApplicationRecord - belongs_to :organization - validates :name, uniqueness: {scope: :organization} -end diff --git a/app/models/user.rb b/app/models/user.rb index 5a47d45fdb..52e16fcc63 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -120,10 +120,6 @@ def kind "normal" end - def is_admin?(org) - has_role?(Role::ORG_ADMIN, org) || has_role?(Role::SUPER_ADMIN) - end - def switchable_roles all_roles = roles.to_a.group_by(&:resource_id) all_roles.values.each do |role_list| diff --git a/app/models/vendor.rb b/app/models/vendor.rb index c54fc6b36a..ac264db394 100644 --- a/app/models/vendor.rb +++ b/app/models/vendor.rb @@ -23,8 +23,6 @@ class Vendor < ApplicationRecord has_many :purchases, inverse_of: :vendor, dependent: :destroy - validates :business_name, presence: true - scope :alphabetized, -> { order(:business_name) } def volume diff --git a/app/models/view/inventory.rb b/app/models/view/inventory.rb index 08a209fe7e..ea291eae09 100644 --- a/app/models/view/inventory.rb +++ b/app/models/view/inventory.rb @@ -13,31 +13,6 @@ class ViewInventoryItem < EventTypes::EventItem attr_accessor :inventory, :organization_id delegate :storage_locations, to: :inventory - # @param event_time [ActiveSupport::TimeWithZone] - # @return [Boolean] - def self.within_snapshot?(organization_id, event_time) - return true if event_time.blank? - - event = SnapshotEvent.where(organization_id: organization_id).first - event && event.created_at < event_time - end - - # @param organization_id [Integer] - # @param storage_location_id [Integer] - # @param event_time [ActiveSupport::TimeWithZone] - # @return [Array] - def self.legacy_inventory_for_storage_location(organization_id, storage_location_id, event_time) - items = Organization.find(organization_id).inventory_items.where(storage_location_id: storage_location_id) - items.map do |item| - ViewInventoryItem.new( - item_id: item.item_id, - quantity: item.paper_trail.version_at(event_time)&.quantity || 0, - storage_location_id: storage_location_id, - db_item: item.item - ) - end - end - # @param organization_id [Integer] # @param event_time [DateTime] def initialize(organization_id, event_time: nil) diff --git a/app/pdfs/distribution_pdf.rb b/app/pdfs/distribution_pdf.rb index 5942348593..f51beff39b 100644 --- a/app/pdfs/distribution_pdf.rb +++ b/app/pdfs/distribution_pdf.rb @@ -19,115 +19,103 @@ def compute_and_render Organization::DIAPER_APP_LOGO end - footer_height = 35 + image logo_image, fit: [250, 85] - # Bounding box containing non-footer elements - bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do - image logo_image, fit: [250, 85] + bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do + text @organization.name, align: :right + text @organization.address, align: :right + text @organization.email, align: :right + end - bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do - text @organization.name, align: :right - text @organization.address, align: :right - text @organization.email, align: :right - end + text "Issued to:", style: :bold + font_size 12 + text @distribution.partner.name + move_up 24 - text "Issued to:", style: :bold - font_size 12 - text @distribution.partner.name - move_up 24 + text "Partner Primary Contact:", style: :bold, align: :right + font_size 12 + text @distribution.partner.profile.primary_contact_name, align: :right + font_size 10 + text @distribution.partner.profile.primary_contact_email, align: :right + text @distribution.partner.profile.primary_contact_phone, align: :right + move_down 10 - text "Partner Primary Contact:", style: :bold, align: :right + if %w(shipped delivered).include?(@distribution.delivery_method) + move_up 10 + text "Delivery address:", style: :bold + font_size 10 + text @distribution.partner.profile.address1 + text @distribution.partner.profile.address2 + text @distribution.partner.profile.city + text @distribution.partner.profile.state + text @distribution.partner.profile.zip_code + move_up 40 + + text "Issued on:", style: :bold, align: :right font_size 12 - text @distribution.partner.profile.primary_contact_name, align: :right + text @distribution.distributed_at, align: :right font_size 10 - text @distribution.partner.profile.primary_contact_email, align: :right - text @distribution.partner.profile.primary_contact_phone, align: :right - move_down 10 - - if %w(shipped delivered).include?(@distribution.delivery_method) - move_up 10 - text "Delivery address:", style: :bold - font_size 10 - text @distribution.partner.profile.address1 - text @distribution.partner.profile.address2 - text @distribution.partner.profile.city - text @distribution.partner.profile.state - text @distribution.partner.profile.zip_code - move_up 40 - - text "Issued on:", style: :bold, align: :right - font_size 12 - text @distribution.distributed_at, align: :right - font_size 10 - move_down 30 - else - text "Issued on:", style: :bold - font_size 12 - text @distribution.distributed_at - font_size 10 - end - - if @organization.ytd_on_distribution_printout - move_up 22 - text "Items Received Year-to-Date:", style: :bold, align: :right - font_size 12 - text @distribution.partner.quantity_year_to_date.to_s, align: :right - font_size 10 - end - - move_down 10 - text "Comments:", style: :bold + move_down 30 + else + text "Issued on:", style: :bold font_size 12 - text @distribution.comment - - move_down 20 - - data = @distribution.request ? request_data : non_request_data - has_request = @distribution.request.present? - - hide_columns(data) - hidden_columns_length = column_names_to_hide.length - - font_size 11 - - # Line item table - table(data) do - self.header = true - self.cell_style = { - padding: has_request ? [5, 10, 5, 10] : [5, 20, 5, 20] - } - self.row_colors = %w(dddddd ffffff) - - cells.borders = [] - - # Header row - row(0).borders = [:bottom] - row(0).border_width = 2 - row(0).font_style = :bold - row(0).size = has_request ? 8 : 9 - row(0).column(1..-1).borders = %i(bottom left) - - # Total Items footer row - row(-1).borders = [:top] - row(-1).font_style = :bold - row(-1).column(1..-1).borders = %i(top left) - row(-1).column(1..-1).border_left_color = "aaaaaa" + text @distribution.distributed_at + font_size 10 + end - # Footer spacing row - row(-2).borders = [:top] - row(-2).padding = [2, 0, 2, 0] + if @organization.ytd_on_distribution_printout + move_up 22 + text "Items Received Year-to-Date:", style: :bold, align: :right + font_size 12 + text @distribution.partner.quantity_year_to_date.to_s, align: :right + font_size 10 + end - column(0).width = 190 + (hidden_columns_length * 60) + move_down 10 + text "Comments:", style: :bold + font_size 12 + text @distribution.comment - # Quantity column - column(1..-1).row(1..-3).borders = [:left] - column(1..-1).row(1..-3).border_left_color = "aaaaaa" - column(-1).row(-1).borders = [:left, :bottom] - end + move_down 20 - if @organization.signature_for_distribution_pdf - insert_signature_fields - end + data = @distribution.request ? request_data : non_request_data + has_request = @distribution.request.present? + + font_size 11 + # Line item table + table(data) do + self.header = true + self.cell_style = { + padding: has_request ? [5, 10, 5, 10] : [5, 20, 5, 20] + } + self.row_colors = %w(dddddd ffffff) + + cells.borders = [] + + # Header row + row(0).borders = [:bottom] + row(0).border_width = 2 + row(0).font_style = :bold + row(0).size = has_request ? 8 : 9 + row(0).column(1..-1).borders = %i(bottom left) + + # Total Items footer row + row(-1).borders = [:top] + row(-1).font_style = :bold + row(-1).column(1..-1).borders = %i(top left) + row(-1).column(1..-1).border_left_color = "aaaaaa" + + # Footer spacing row + row(-2).borders = [:top] + row(-2).padding = [2, 0, 2, 0] + + column(0).width = 190 + + # Quantity column + column(1..-1).row(1..-3).borders = [:left] + column(1..-1).row(1..-3).border_left_color = "aaaaaa" + column(1).style align: :right + column(-1).row(-1).borders = [:left, :bottom] end number_pages "Page of ", @@ -137,7 +125,7 @@ def compute_and_render repeat :all do # Page footer - bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do + bounding_box [bounds.left, bounds.bottom + 35], width: bounds.width do stroke_bounds font "OpenSans" font_size 9 @@ -164,7 +152,9 @@ def request_data "Packages"]] inventory = nil - inventory = View::Inventory.new(@distribution.organization_id) + if Event.read_events?(@distribution.organization) + inventory = View::Inventory.new(@distribution.organization_id) + end request_items = @distribution.request.request_items.map do |request_item| RequestItem.from_json(request_item, @distribution.request, inventory) end @@ -175,20 +165,20 @@ def request_data end data += line_items.map do |c| - request_item = request_items.find { |i| i.item&.id == c.item_id } + request_item = request_items.find { |i| i.item.id == c.item_id } [c.item.name, - request_display_qty(request_item), + request_item&.quantity || "", c.quantity, dollar_value(c.item.value_in_cents), dollar_value(c.value_per_line_item), c.package_count] end - data += requested_not_received.sort_by(&:name).map do |request_item| - [request_item.item.name, - request_display_qty(request_item), + data += requested_not_received.sort_by(&:name).map do |c| + [c.item.name, + c.quantity, "", - dollar_value(request_item.item.value_in_cents), + dollar_value(c.item.value_in_cents), nil, nil] end @@ -222,63 +212,4 @@ def non_request_data @distribution.line_items.total, ""]] end - - def hide_columns(data) - column_names_to_hide.each do |col_name| - col_index = data.first.find_index(col_name) - data.each { |line| line.delete_at(col_index) } if col_index.present? - end - end - - private - - def column_names_to_hide - in_kind_column_name = @distribution.request.present? ? "In-Kind Value Received" : "In-Kind Value" - columns_to_hide = [] - columns_to_hide.push("Value/item", in_kind_column_name) if @organization.hide_value_columns_on_receipt - columns_to_hide.push("Packages") if @organization.hide_package_column_on_receipt - columns_to_hide - end - - def insert_signature_fields - minimum_space_required = 165 - # Signatures lines shouldn't be separated by a page break, so if we can't fit both lines together we make a new page - if cursor < minimum_space_required - start_new_page - else - move_down 20 - end - - signature_lines_for "Received By:" - - move_down 20 - signature_lines_for "Delivered By:" - end - - def signature_lines_for(label) - # Calculate half the screen width and the gap between the two signature lines - half_width = bounds.width / 2 - gap = 20 - left_end = half_width - (gap / 2) - right_start = half_width + (gap / 2) - - text label, style: :bold - move_down 30 - stroke do - horizontal_line 0, left_end - horizontal_line right_start, bounds.width - end - - move_down 10 - draw_text "(Print Name)", at: [0, cursor] - draw_text "(Signature and Date)", at: [right_start, cursor] - end - - def request_display_qty(request_item) - if Flipper.enabled?(:enable_packs) && request_item&.unit - "#{request_item.quantity} #{request_item.unit.pluralize(request_item.quantity)}" - else - request_item&.quantity || "" - end - end end diff --git a/app/pdfs/donation_pdf.rb b/app/pdfs/donation_pdf.rb deleted file mode 100644 index 773e1b9095..0000000000 --- a/app/pdfs/donation_pdf.rb +++ /dev/null @@ -1,195 +0,0 @@ -# Configures a Prawn PDF template for generating Donation receipts -class DonationPdf - include Prawn::View - include ItemsHelper - - class DonorInfo - attr_reader :name, :address, :email - - def initialize(donation) - if donation.nil? - raise "Must pass a Donation object" - end - case donation.source - when Donation::SOURCES[:donation_site] - @name = donation.donation_site.name - @address = donation.donation_site.address - @email = donation.donation_site.email - when Donation::SOURCES[:manufacturer] - @name = donation.manufacturer.name - @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 - when Donation::SOURCES[:misc] - @name = "Misc. Donation" - @address = nil - @email = nil - end - end - end - - def initialize(organization, donation) - @donation = Donation.includes(line_items: [:item]).find_by(id: donation.id) - @organization = organization - @donor = DonorInfo.new(@donation) - end - - def compute_and_render - font_families["OpenSans"] = PrawnRails.config["font_families"][:OpenSans] - font "OpenSans" - font_size 10 - - logo_image = if @organization.logo.attached? - StringIO.open(@organization.logo.download) - else - Organization::DIAPER_APP_LOGO - end - - footer_height = 35 - - # Bounding box containing non-footer elements - bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do - image logo_image, fit: [250, 85] - - bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do - text @organization.name, align: :right - text @organization.address, align: :right - text @organization.email, align: :right - end - - font_size 12 - text "Issued on:", style: :bold - text @donation.issued_at.to_fs(:distribution_date) - move_up 24 - - font_size 12 - text "Donation from:", style: :bold, align: :right - font_size 10 - text @donor.name, align: :right - text @donor.address, align: :right - text @donor.email, align: :right - move_down 10 - # Get some additional vertical distance in left column if all donor info is nil - if @donor.name.nil? && @donor.address.nil? && @donor.email.nil? - move_down 10 - end - - font_size 12 - money_raised = "$0.00" - if @donation.money_raised && @donation.money_raised > 0 - money_raised = dollar_value(@donation.money_raised) - end - text "Money Raised In Dollars: #{money_raised}", inline_format: true - - move_down 10 - font_size 12 - text "Comments:", style: :bold - text @donation.comment - - move_down 20 - - data = donation_data - - hide_columns(data) - hidden_columns_length = column_names_to_hide.length - - font_size 11 - - # Line item table - table(data) do - self.header = true - self.cell_style = { - padding: [5, 20, 5, 20] - } - self.row_colors = %w[dddddd ffffff] - - cells.borders = [] - - # Header row - row(0).borders = [:bottom] - row(0).border_width = 2 - row(0).font_style = :bold - row(0).size = 9 - row(0).column(1..-1).borders = %i[bottom left] - - # Total Items footer row - row(-1).borders = [:top] - row(-1).font_style = :bold - row(-1).column(1..-1).borders = %i[top left] - row(-1).column(1..-1).border_left_color = "aaaaaa" - - # Footer spacing row - row(-2).borders = [:top] - row(-2).padding = [2, 0, 2, 0] - - column(0).width = 190 + (hidden_columns_length * 60) - - # Quantity column - column(1..-1).row(1..-3).borders = [:left] - column(1..-1).row(1..-3).border_left_color = "aaaaaa" - column(1).style align: :right - end - end - - number_pages "Page of ", - start_count_at: 1, - at: [bounds.right - 130, 22], - align: :right - - repeat :all do - # Page footer - bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do - stroke_bounds - font "OpenSans" - font_size 9 - stroke_horizontal_rule - move_down(5) - - logo_offset = (bounds.width - 190) / 2 - bounding_box([logo_offset, 0], width: 190, height: 33) do - text "Lovingly created with", valign: :center - image Organization::DIAPER_APP_LOGO, width: 75, vposition: :center, position: :right - end - end - end - - render - end - - def donation_data - data = [["Items Received", - "Value/item", - "In-Kind Value", - "Quantity"]] - data += @donation.line_items.sorted.map do |c| - [c.item.name, - dollar_value(c.item.value_in_cents), - dollar_value(c.value_per_line_item), - c.quantity] - end - data + [["", "", "", ""], - ["Total Items Received", - "", - dollar_value(@donation.value_per_itemizable), - @donation.line_items.total]] - end - - def hide_columns(data) - column_names_to_hide.each do |col_name| - col_index = data.first.find_index(col_name) - data.each { |line| line.delete_at(col_index) } if col_index.present? - end - end - - private - - def column_names_to_hide - in_kind_column_name = "In-Kind Value" - columns_to_hide = [] - columns_to_hide.push("Value/item", in_kind_column_name) if @organization.hide_value_columns_on_receipt - columns_to_hide - end -end diff --git a/app/pdfs/picklists_pdf.rb b/app/pdfs/picklists_pdf.rb deleted file mode 100644 index 20022c2dc0..0000000000 --- a/app/pdfs/picklists_pdf.rb +++ /dev/null @@ -1,167 +0,0 @@ -# Configures a Prawn PDF template for generating Distribution manifests -class PicklistsPdf - include Prawn::View - include ItemsHelper - - def initialize(organization, requests) - @requests = requests - @organization = organization - end - - def compute_and_render - font_families["OpenSans"] = PrawnRails.config["font_families"][:OpenSans] - font "OpenSans" - font_size 10 - footer_height = 35 - - @requests.each do |request| - logo_image = if @organization.logo.attached? - StringIO.open(@organization.logo.download) - else - Organization::DIAPER_APP_LOGO - end - - # Bounding box containing non-footer elements - bounding_box [bounds.left, bounds.top], width: bounds.width, height: bounds.height - footer_height do - image logo_image, fit: [250, 85] - - bounding_box [bounds.right - 225, bounds.top], width: 225, height: 85 do - text @organization.name, align: :right - text @organization.address, align: :right - text @organization.email, align: :right - end - - text "Requested by:", style: :bold - font_size 12 - text request.partner.name - move_up 24 - - text "Partner Primary Contact:", style: :bold, align: :right - font_size 12 - text request.partner.profile.primary_contact_name, align: :right - font_size 10 - text request.partner.profile.primary_contact_email, align: :right - text request.partner.profile.primary_contact_phone, align: :right - move_down 10 - - if request.partner.profile.pick_up_name.present? - move_up 10 - text "Partner Pickup Person:", style: :bold - font_size 12 - text request.partner.profile.pick_up_name - font_size 10 - text request.partner.profile.pick_up_email - text request.partner.profile.pick_up_phone - move_up 24 - - text "Requested on:", style: :bold, align: :right - font_size 12 - text request.created_at.to_fs(:date_picker), align: :right - font_size 10 - move_down 30 - else - text "Requested on:", style: :bold - font_size 12 - text request.created_at.to_fs(:date_picker) - font_size 10 - end - - if @organization.ytd_on_distribution_printout - move_up 22 - text "Items Received Year-to-Date:", style: :bold, align: :right - font_size 12 - text request.partner.quantity_year_to_date.to_s, align: :right - font_size 10 - end - - move_down 10 - text "Comments:", style: :bold - font_size 12 - text request.comments - - move_down 20 - - line_items = request.item_requests - data = has_custom_units?(line_items) ? data_with_units(line_items) : data_no_units(line_items) - - font_size 11 - - # Line item table - table(data, width: bounds.width, column_widths: {1 => 65, -2 => 35}) do - self.header = true - self.cell_style = {padding: [5, 10, 5, 10]} - self.row_colors = %w[dddddd ffffff] - - cells.borders = [] - - # Header row - row(0).borders = [:bottom] - row(0).border_width = 2 - row(0).font_style = :bold - row(0).size = 10 - row(0).column(1..-1).borders = %i[bottom left] - end - end - - start_new_page unless request == @requests.last - end - - repeat :all do - # Page footer - bounding_box [bounds.left, bounds.bottom + footer_height], width: bounds.width do - stroke_bounds - font "OpenSans" - font_size 9 - stroke_horizontal_rule - move_down 5 - - logo_offset = (bounds.width - 190) / 2 - bounding_box([logo_offset, 0], width: 190, height: 33) do - text "Lovingly created with", valign: :center - image Organization::DIAPER_APP_LOGO, width: 75, vposition: :center, position: :right - end - end - end - - number_pages "Page of ", - start_count_at: 1, - at: [bounds.right - 130, 22], - align: :right - - render - end - - def has_custom_units?(line_items) - Flipper.enabled?(:enable_packs) && line_items.any? { |line_item| line_item.request_unit } - end - - def data_with_units(line_items) - data = [["Items Requested", - "Quantity", - "Unit (if applicable)", - "[X]", - "Differences / Comments"]] - - data + line_items.map do |line_item| - [line_item.name, - line_item.quantity, - line_item.request_unit&.capitalize&.pluralize(line_item.quantity), - "[ ]", - ""] - end - end - - def data_no_units(line_items) - data = [["Items Requested", - "Quantity", - "[X]", - "Differences / Comments"]] - - data + line_items.map do |line_item| - [line_item.name, - line_item.quantity, - "[ ]", - ""] - end - end -end diff --git a/app/queries/items_by_storage_collection_and_quantity_query.rb b/app/queries/items_by_storage_collection_and_quantity_query.rb index 664c690d4b..cf252dbd3c 100644 --- a/app/queries/items_by_storage_collection_and_quantity_query.rb +++ b/app/queries/items_by_storage_collection_and_quantity_query.rb @@ -2,29 +2,59 @@ # 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:) - 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) + 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 } 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) + + if row.storage_id + items_by_storage_collection_and_quantity[row.id][:locations] << { + id: row.storage_id, + name: row.storage_name, + quantity: row.quantity } - ] + 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 new file mode 100644 index 0000000000..589c586f33 --- /dev/null +++ b/app/queries/items_by_storage_collection_query.rb @@ -0,0 +1,36 @@ +# 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 deleted file mode 100644 index 5deee91d7a..0000000000 --- a/app/queries/low_inventory_query.rb +++ /dev/null @@ -1,22 +0,0 @@ -class LowInventoryQuery - def self.call(organization) - 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 - 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 1b16f3c8d6..a068e71fda 100644 --- a/app/services/adjustment_create_service.rb +++ b/app/services/adjustment_create_service.rb @@ -22,6 +22,11 @@ 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 @@ -39,6 +44,13 @@ 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 new file mode 100644 index 0000000000..9451010d28 --- /dev/null +++ b/app/services/allocate_kit_inventory_service.rb @@ -0,0 +1,98 @@ +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 new file mode 100644 index 0000000000..a394556a09 --- /dev/null +++ b/app/services/deallocate_kit_inventory_service.rb @@ -0,0 +1,106 @@ +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 7e7dae2f5e..d98a848a0a 100644 --- a/app/services/distribution_create_service.rb +++ b/app/services/distribution_create_service.rb @@ -14,6 +14,7 @@ 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 ca12dd073b..f788bd4bdb 100644 --- a/app/services/distribution_destroy_service.rb +++ b/app/services/distribution_destroy_service.rb @@ -7,6 +7,7 @@ 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 44c4caadf7..ecd8a03a97 100644 --- a/app/services/distribution_itemized_breakdown_service.rb +++ b/app/services/distribution_itemized_breakdown_service.rb @@ -18,7 +18,10 @@ def initialize(organization:, distribution_ids:) # # @return [Array] def fetch - inventory = View::Inventory.new(@organization.id) + inventory = nil + if Event.read_events?(@organization) + inventory = View::Inventory.new(@organization.id) + end current_onhand = current_onhand_quantities(inventory) current_min_onhand = current_onhand_minimums(inventory) items_distributed = fetch_items_distributed @@ -59,11 +62,19 @@ def distributions end def current_onhand_quantities(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } + 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 end def current_onhand_minimums(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.map(&:on_hand_minimum_quantity).max] } + 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 end def fetch_items_distributed diff --git a/app/services/distribution_update_service.rb b/app/services/distribution_update_service.rb index 03b8049da7..5889d2a622 100644 --- a/app/services/distribution_update_service.rb +++ b/app/services/distribution_update_service.rb @@ -9,13 +9,11 @@ def call perform_distribution_service do @old_issued_at = distribution.issued_at @old_delivery_method = distribution.delivery_method - @params[:line_items_attributes]&.delete_if { |_, a| a[:quantity].to_i.zero? } - - # remove line_items with zero quantity 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 c349fc330b..fb782ef7d8 100644 --- a/app/services/donation_create_service.rb +++ b/app/services/donation_create_service.rb @@ -5,6 +5,7 @@ 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 47cb88fa58..b95c01bae9 100644 --- a/app/services/donation_destroy_service.rb +++ b/app/services/donation_destroy_service.rb @@ -10,6 +10,7 @@ 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 9a38e245ce..e909d3b0ae 100644 --- a/app/services/donation_itemized_breakdown_service.rb +++ b/app/services/donation_itemized_breakdown_service.rb @@ -13,7 +13,10 @@ def initialize(organization:, donation_ids:) end def fetch - inventory = View::Inventory.new(@organization.id) + inventory = nil + if Event.read_events?(@organization) + inventory = View::Inventory.new(@organization.id) + end items_donated = fetch_items_donated current_onhand = current_onhand_quantities(inventory) @@ -38,7 +41,11 @@ def donations end def current_onhand_quantities(inventory) - inventory.all_items.group_by(&:name).to_h { |k, v| [k, v.sum(&:quantity)] } + 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 end def fetch_items_donated diff --git a/app/services/exports/export_distributions_csv_service.rb b/app/services/exports/export_distributions_csv_service.rb index 70099d6c37..a0f6569409 100644 --- a/app/services/exports/export_distributions_csv_service.rb +++ b/app/services/exports/export_distributions_csv_service.rb @@ -63,11 +63,8 @@ def base_table "Partner" => ->(distribution) { distribution.partner.name }, - "Initial Allocation" => ->(distribution) { - distribution.created_at.strftime("%m/%d/%Y") - }, - "Scheduled for" => ->(distribution) { - (distribution.issued_at.presence || distribution.created_at).strftime("%m/%d/%Y") + "Date of Distribution" => ->(distribution) { + distribution.issued_at.strftime("%m/%d/%Y") }, "Source Inventory" => ->(distribution) { distribution.storage_location.name @@ -90,7 +87,7 @@ def base_table "Shipping Cost" => ->(distribution) { distribution_shipping_cost(distribution.shipping_cost) }, - "Status" => ->(distribution) { + "State" => ->(distribution) { distribution.state }, "Agency Representative" => ->(distribution) { @@ -118,7 +115,7 @@ def base_headers def item_headers return @item_headers if @item_headers - @item_headers = @organization.items.select("DISTINCT ON (LOWER(name)) items.name").order("LOWER(name) ASC").map(&:name) + @item_headers = @organization.items.order(:created_at).distinct.select([:created_at, :name]).map(&:name) end def build_row_data(distribution) diff --git a/app/services/exports/export_donations_csv_service.rb b/app/services/exports/export_donations_csv_service.rb index 6e600ea09b..254ebcd196 100644 --- a/app/services/exports/export_donations_csv_service.rb +++ b/app/services/exports/export_donations_csv_service.rb @@ -80,9 +80,6 @@ def base_table "Variety of Items" => ->(donation) { donation.line_items.map(&:name).uniq.size }, - "In-Kind Value" => ->(donation) { - donation.in_kind_value_money - }, "Comments" => ->(donation) { donation.comment } diff --git a/app/services/exports/export_request_service.rb b/app/services/exports/export_request_service.rb index 94501fae1c..e7bfba43af 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, {item_requests: :item}) + @requests = requests.includes(:partner) end def generate_csv @@ -61,25 +61,7 @@ def item_headers end def compute_item_headers - # 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 + item_names = items.pluck(:name) # Adding this to handle cases in which a requested item # has been deleted. Normally this wouldn't be neccessary, @@ -93,20 +75,38 @@ def build_row_data(request) row += Array.new(item_headers.size, 0) - request.item_requests.each do |item_request| - item_name = item_request.name_with_unit(0) || DELETED_ITEMS_COLUMN_HEADER + request.request_items.each do |request_item| + item_name = fetch_item_name(request_item['item_id']) || DELETED_ITEMS_COLUMN_HEADER item_column_idx = headers_with_indexes[item_name] - row[item_column_idx] ||= 0 - row[item_column_idx] += item_request.quantity.to_i + + 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'] end row end - 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 + 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) end end end diff --git a/app/services/historical_trend_service.rb b/app/services/historical_trend_service.rb index 5f0560804c..f8615a625b 100644 --- a/app/services/historical_trend_service.rb +++ b/app/services/historical_trend_service.rb @@ -4,36 +4,26 @@ 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 - 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) + # 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) - array_of_items = [] + month_offset = [*1..12].rotate(Time.zone.today.month) + default_dates = (1..12).index_with { |i| 0 } - records_for_type.each do |record| - index = record.issued_at.month - Date.current.month - 1 + items_with_line_items.each_with_object([]) do |item, array_of_items| + dates = default_dates.deep_dup - record.line_items.each do |line_item| - name = line_item.item.name - quantity = line_item.quantity - next if quantity.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 + 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 - end - array_of_items.sort_by { |item| item[:name] } + array_of_items << {name: item.name, data: dates.values, visible: false} unless dates.values.sum.zero? + end end end diff --git a/app/services/inventory_check_service.rb b/app/services/inventory_check_service.rb index 8f331887cb..9a149df76a 100644 --- a/app/services/inventory_check_service.rb +++ b/app/services/inventory_check_service.rb @@ -8,7 +8,10 @@ def initialize(distribution) end def call - @inventory = View::Inventory.new(@distribution.organization_id) + @inventory = nil + if Event.read_events?(@distribution.organization) + @inventory = View::Inventory.new(@distribution.organization_id) + end unless items_below_minimum_quantity.empty? set_error end @@ -34,8 +37,13 @@ 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| - 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) + 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 end.map(&:item_id) @items_below_minimum_quantity = Item.find(item_ids) @@ -48,8 +56,13 @@ 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| - 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) + 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 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 60126ad803..c5a041095b 100644 --- a/app/services/item_create_service.rb +++ b/app/services/item_create_service.rb @@ -1,15 +1,22 @@ class ItemCreateService - def initialize(organization_id:, item_params:, request_unit_ids: []) + def initialize(organization_id:, item_params:) @organization_id = organization_id - @request_unit_ids = request_unit_ids @item_params = item_params end def call new_item = organization.items.new(item_params) - new_item.save! - if Flipper.enabled?(:enable_packs) - new_item.sync_request_units!(@request_unit_ids) + + organization.transaction do + new_item.save! + + organization.storage_locations.each do |sl| + InventoryItem.create!( + storage_location_id: sl.id, + item_id: new_item.id, + quantity: 0 + ) + end 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 039afabc76..519817d00c 100644 --- a/app/services/itemizable_update_service.rb +++ b/app/services/itemizable_update_service.rb @@ -1,9 +1,10 @@ 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:, params: {}, event_class: nil) - original_storage_location = itemizable.storage_location + def self.call(itemizable:, type: :increase, params: {}, event_class: nil) 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) @@ -14,50 +15,37 @@ def self.call(itemizable:, params: {}, event_class: nil) from_location = to_location = itemizable.storage_location to_location = StorageLocation.find(params[:storage_location_id]) if params[:storage_location_id] - verify_intervening_audit_on_storage_location_items(itemizable: itemizable, from_location_id: from_location.id, to_location_id: to_location.id) - - 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? - previous = itemizable.line_items.map(&:dup) - end + apply_change_method = (type == :increase) ? :increase_inventory : :decrease_inventory + undo_change_method = (type == :increase) ? :decrease_inventory : :increase_inventory line_item_attrs = Array.wrap(params[:line_items_attributes]&.values) line_item_attrs.each { |attr| attr.delete(:id) } - update_storage_location(itemizable: itemizable, params: params) - if previous - UpdateExistingEvent.publish(itemizable, previous, original_storage_location) - else - event_class&.publish(itemizable) - end + 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) + event_class&.publish(itemizable) end 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`. - def self.update_storage_location(itemizable:, params:) + # @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) # 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 - end - - # @param itemizable [Itemizable] - # @param from_location [StorageLocation] - # @param to_location [StorageLocation] - def self.verify_intervening_audit_on_storage_location_items(itemizable:, from_location_id:, to_location_id:) - return if from_location_id == to_location_id || !Audit.finalized_since?(itemizable, [from_location_id, to_location_id]) - - itemizable_type = itemizable.class.name.downcase - case itemizable_type - when "distribution" - raise "Cannot change the storage location because there has been an intervening audit of some items. " \ - "If you need to change the storage location, please reclaim this distribution and create a new distribution from the new storage location." - else - raise "Cannot change the storage location because there has been an intervening audit of some items. " \ - "If you need to change the storage location, please delete this #{itemizable_type} and create a new #{itemizable_type} with the new storage location." - end + # Apply the new changes to the storage location inventory + to_location.public_send(apply_change_method, itemizable.line_item_values) end end diff --git a/app/services/organization_update_service.rb b/app/services/organization_update_service.rb index 9ebd27e4a8..0b5b3c72c6 100644 --- a/app/services/organization_update_service.rb +++ b/app/services/organization_update_service.rb @@ -12,25 +12,12 @@ class << self def update(organization, params) return false unless valid?(organization, params) - org_params = params.dup - - if org_params.has_key?("partner_form_fields") - org_params["partner_form_fields"] = org_params["partner_form_fields"].reject(&:blank?) - end - - if Flipper.enabled?(:enable_packs) && org_params[:request_unit_names] - # Find or create units for the organization - request_unit_ids = org_params[:request_unit_names].reject(&:blank?).map do |request_unit_name| - Unit.find_or_create_by(organization: organization, name: request_unit_name).id - end - org_params.delete(:request_unit_names) - org_params[:request_unit_ids] = request_unit_ids + if params.has_key?("partner_form_fields") + params["partner_form_fields"].delete_if { |field| field == "" } end - - result = organization.update(org_params) - + result = organization.update(params) return false unless result - return false unless update_partner_flags(organization) + update_partner_flags(organization) true end @@ -46,9 +33,6 @@ def update_partner_flags(organization) next if organization.send(field) organization.partners.each do |partner| partner.profile.update!(field => organization.send(field)) - rescue ActiveRecord::RecordInvalid => e - organization.errors.add(:base, "Profile for partner '#{e.record.partner.name}' had error(s) preventing the organization from being saved. #{e.message}") - return false end end end diff --git a/app/services/partner_create_service.rb b/app/services/partner_create_service.rb index 6b81bfae59..0028155e32 100644 --- a/app/services/partner_create_service.rb +++ b/app/services/partner_create_service.rb @@ -11,27 +11,27 @@ def initialize(organization:, partner_attrs:) def call @partner = organization.partners.build(partner_attrs) - if @partner.valid? - ActiveRecord::Base.transaction do - @partner.save! - - Partners::Profile.create!({ - partner_id: @partner.id, - name: @partner.name, - enable_child_based_requests: organization.enable_child_based_requests, - enable_individual_requests: organization.enable_individual_requests, - enable_quantity_based_requests: organization.enable_quantity_based_requests - }) - rescue StandardError => e - errors.add(:base, e.message) - raise ActiveRecord::Rollback - end - else + unless @partner.valid? @partner.errors.each do |error| errors.add(error.attribute, error.message) end end + ActiveRecord::Base.transaction do + @partner.save! + + Partners::Profile.create!({ + partner_id: @partner.id, + name: @partner.name, + enable_child_based_requests: organization.enable_child_based_requests, + enable_individual_requests: organization.enable_individual_requests, + enable_quantity_based_requests: organization.enable_quantity_based_requests + }) + rescue StandardError => e + errors.add(:base, e.message) + raise ActiveRecord::Rollback + end + self end diff --git a/app/services/partner_profile_update_service.rb b/app/services/partner_profile_update_service.rb index 7098599b80..3fa2b6bbf7 100644 --- a/app/services/partner_profile_update_service.rb +++ b/app/services/partner_profile_update_service.rb @@ -18,8 +18,6 @@ def call @profile.served_areas.destroy_all @profile.attributes = @profile_params @profile.save!(context: :edit) - else - @error = "Partner '#{@partner.name}' had error(s) preventing the profile from being updated: #{@partner.errors.full_messages.join(", ")}" end end end diff --git a/app/services/partners/family_request_create_service.rb b/app/services/partners/family_request_create_service.rb index e585da9a13..e181d3e2f7 100644 --- a/app/services/partners/family_request_create_service.rb +++ b/app/services/partners/family_request_create_service.rb @@ -39,15 +39,6 @@ def call self end - def initialize_only - Partners::RequestCreateService.new( - partner_user_id: partner_user_id, - comments: comments, - for_families: @for_families, - item_requests_attributes: item_requests_attributes - ).initialize_only - end - private def valid? diff --git a/app/services/partners/request_create_service.rb b/app/services/partners/request_create_service.rb index a9875fbf2c..e5c4058083 100644 --- a/app/services/partners/request_create_service.rb +++ b/app/services/partners/request_create_service.rb @@ -26,6 +26,10 @@ def call end end + if @partner_request.comments.blank? && @partner_request.item_requests.blank? + errors.add(:base, 'completely empty request') + end + return self if errors.present? Request.transaction do @@ -40,16 +44,6 @@ def call self end - def initialize_only - partner_request = ::Request.new(partner_id: partner.id, - organization_id: organization_id, - comments: comments, - partner_user_id: partner_user_id) - partner_request = populate_item_request(partner_request) - partner_request.assign_attributes(additional_attrs) - partner_request - end - private attr_reader :partner_user_id, :comments, :item_requests_attributes, :additional_attrs @@ -60,38 +54,16 @@ def populate_item_request(partner_request) attrs['item_id'].blank? && attrs['quantity'].blank? end - items = {} - - formatted_line_items.each do |input_item| - pre_existing_entry = items[input_item['item_id']] - if pre_existing_entry - pre_existing_entry.quantity = (pre_existing_entry.quantity.to_i + input_item['quantity'].to_i).to_s - # NOTE: When this code was written (and maybe it's still the - # case as you read it!), the FamilyRequestsController does a - # ton of calculation to translate children to item quantities. - # If that logic is incorrect, there's not much we can do here - # to fix things. Could make sense to move more of that logic - # into one of the service objects that instantiate the Request - # object (either this one or the FamilyRequestCreateService). - pre_existing_entry.children = (pre_existing_entry.children + (input_item['children'] || [])).uniq - else - if input_item['request_unit'].to_s == '-1' # nothing selected - errors.add(:base, "Please select a unit for #{Item.find(input_item["item_id"]).name}") - next - end - items[input_item['item_id']] = Partners::ItemRequest.new( - item_id: input_item['item_id'], - request_unit: input_item['request_unit'], - quantity: input_item['quantity'], - children: input_item['children'] || [], # will create ChildItemRequests if there are any - name: fetch_organization_item_name(input_item['item_id']), - partner_key: fetch_organization_partner_key(input_item['item_id']) - ) - end + item_requests = formatted_line_items.map do |ira| + Partners::ItemRequest.new( + item_id: ira['item_id'], + quantity: ira['quantity'], + children: ira['children'] || [], # will create ChildItemRequests if there are any + name: fetch_organization_item_name(ira['item_id']), + partner_key: fetch_organization_partner_key(ira['item_id']) + ) end - item_requests = items.values - partner_request.item_requests << item_requests partner_request.request_items = partner_request.item_requests.map do |ir| diff --git a/app/services/partners/section_error_service.rb b/app/services/partners/section_error_service.rb deleted file mode 100644 index 5a6ff006b2..0000000000 --- a/app/services/partners/section_error_service.rb +++ /dev/null @@ -1,30 +0,0 @@ -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 969d832bc3..16c1718bb5 100644 --- a/app/services/purchase_create_service.rb +++ b/app/services/purchase_create_service.rb @@ -5,6 +5,7 @@ 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 6e84e90307..c32ca02921 100644 --- a/app/services/purchase_destroy_service.rb +++ b/app/services/purchase_destroy_service.rb @@ -2,6 +2,7 @@ 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.rb b/app/services/reports.rb index 6af011bfba..507b4e8ece 100644 --- a/app/services/reports.rb +++ b/app/services/reports.rb @@ -5,7 +5,7 @@ class << self # @return [Array] def all_reports(year:, organization:) [ - Reports::DiaperReportService.new(year: year, organization: organization).report, + Reports::AcquisitionReportService.new(year: year, organization: organization).report, Reports::WarehouseReportService.new(year: year, organization: organization).report, Reports::AdultIncontinenceReportService.new(year: year, organization: organization).report, Reports::PeriodSupplyReportService.new(year: year, organization: organization).report, diff --git a/app/services/reports/diaper_report_service.rb b/app/services/reports/acquisition_report_service.rb similarity index 57% rename from app/services/reports/diaper_report_service.rb rename to app/services/reports/acquisition_report_service.rb index 8c740807f1..71977af338 100644 --- a/app/services/reports/diaper_report_service.rb +++ b/app/services/reports/acquisition_report_service.rb @@ -1,5 +1,5 @@ module Reports - class DiaperReportService + class AcquisitionReportService include ActionView::Helpers::NumberHelper attr_reader :year, :organization @@ -12,39 +12,37 @@ def initialize(year:, organization:) # @return [Hash] def report - @report ||= {name: "Diapers", - entries: { - "Disposable diapers distributed" => number_with_delimiter(total_disposable_diapers_distributed), - "Cloth diapers distributed" => number_with_delimiter(distributed_cloth_diapers), - "Average monthly disposable diapers distributed" => number_with_delimiter(monthly_disposable_diapers), - "Total product drives" => annual_drives.count, - "Disposable diapers collected from drives" => number_with_delimiter(disposable_diapers_from_drives), - "Cloth diapers collected from drives" => number_with_delimiter(cloth_diapers_from_drives), - "Money raised from product drives" => number_to_currency(money_from_drives), - "Total product drives (virtual)" => virtual_product_drives.count, - "Money raised from product drives (virtual)" => number_to_currency(money_from_virtual_drives), - "Disposable diapers collected from drives (virtual)" => number_with_delimiter(disposable_diapers_from_virtual_drives), - "Cloth diapers collected from drives (virtual)" => number_with_delimiter(cloth_diapers_from_virtual_drives), - "Disposable diapers donated" => number_with_delimiter(donated_disposable_diapers), - "% disposable diapers donated" => "#{percent_disposable_donated.round}%", - "% cloth diapers donated" => "#{percent_cloth_diapers_donated.round}%", - "Disposable diapers purchased" => number_with_delimiter(purchased_loose_disposable_diapers), - "% disposable diapers purchased" => "#{percent_disposable_diapers_purchased.round}%", - "% cloth diapers purchased" => "#{percent_cloth_diapers_purchased.round}%", - "Money spent purchasing diapers" => number_to_currency(money_spent_on_diapers), - "Purchased from" => purchased_from, - "Vendors diapers purchased through" => vendors_purchased_from - }} + @report ||= { name: 'Diaper Acquisition', + entries: { + 'Disposable diapers distributed' => number_with_delimiter(total_disposable_diapers_distributed), + 'Cloth diapers distributed' => number_with_delimiter(distributed_cloth_diapers), + 'Average monthly disposable diapers distributed' => number_with_delimiter(monthly_disposable_diapers), + 'Total product drives' => annual_drives.count, + 'Disposable diapers collected from drives' => number_with_delimiter(disposable_diapers_from_drives), + 'Cloth diapers collected from drives' => number_with_delimiter(cloth_diapers_from_drives), + 'Money raised from product drives' => number_to_currency(money_from_drives), + 'Total product drives (virtual)' => virtual_product_drives.count, + 'Money raised from product drives (virtual)' => number_to_currency(money_from_virtual_drives), + 'Disposable diapers collected from drives (virtual)' => number_with_delimiter(disposable_diapers_from_virtual_drives), + 'Cloth diapers collected from drives (virtual)' => number_with_delimiter(cloth_diapers_from_virtual_drives), + '% disposable diapers donated' => "#{percent_disposable_donated.round}%", + '% cloth diapers donated' => "#{percent_cloth_diapers_donated.round}%", + '% disposable diapers purchased' => "#{percent_disposable_diapers_purchased.round}%", + '% cloth diapers purchased' => "#{percent_cloth_diapers_purchased.round}%", + 'Money spent purchasing diapers' => number_to_currency(money_spent_on_diapers), + 'Purchased from' => purchased_from, + 'Vendors diapers purchased through' => vendors_purchased_from + } } end # @return [Integer] def distributed_loose_disposable_diapers @distributed_loose_disposable_diapers ||= organization - .distributions - .for_year(year) - .joins(line_items: :item) - .merge(Item.disposable) - .sum("line_items.quantity") + .distributions + .for_year(year) + .joins(line_items: :item) + .merge(Item.disposable) + .sum('line_items.quantity') end def distributed_disposable_diapers_from_kits @@ -71,7 +69,7 @@ def distributed_disposable_diapers_from_kits result = ActiveRecord::Base.connection.execute(sanitized_sql) - result.first["sum"].to_i + result.first['sum'].to_i end def total_disposable_diapers_distributed @@ -80,11 +78,11 @@ def total_disposable_diapers_distributed def distributed_cloth_diapers @distributed_cloth_diapers ||= organization - .distributions - .for_year(year) - .joins(line_items: :item) - .merge(Item.cloth_diapers) - .sum("line_items.quantity") + .distributions + .for_year(year) + .joins(line_items: :item) + .merge(Item.cloth_diapers) + .sum('line_items.quantity') end # @return [Integer] @@ -100,12 +98,12 @@ def annual_drives # @return [Integer] def disposable_diapers_from_drives @disposable_diapers_from_drives ||= - annual_drives.joins(donations: {line_items: :item}).merge(Item.disposable).sum(:quantity) + annual_drives.joins(donations: { line_items: :item }).merge(Item.disposable).sum(:quantity) end def cloth_diapers_from_drives @cloth_diapers_from_drives ||= - annual_drives.joins(donations: {line_items: :item}).merge(Item.cloth_diapers).sum(:quantity) + annual_drives.joins(donations: { line_items: :item }).merge(Item.cloth_diapers).sum(:quantity) end # @return [Float] @@ -126,17 +124,17 @@ def money_from_virtual_drives # @return [Integer] def disposable_diapers_from_virtual_drives @disposable_diapers_from_virtual_drives ||= virtual_product_drives - .joins(donations: {line_items: :item}) - .merge(Item.disposable) - .sum(:quantity) + .joins(donations: { line_items: :item }) + .merge(Item.disposable) + .sum(:quantity) end # @return [Integer] def cloth_diapers_from_virtual_drives @cloth_diapers_from_virtual_drives ||= virtual_product_drives - .joins(donations: {line_items: :item}) - .merge(Item.cloth_diapers) - .sum(:quantity) + .joins(donations: { line_items: :item }) + .merge(Item.cloth_diapers) + .sum(:quantity) end # @return [Float] @@ -181,7 +179,7 @@ def purchased_from .distinct .pluck(:purchased_from) .compact - .join(", ") + .join(', ') end # @return [String] @@ -194,7 +192,7 @@ def vendors_purchased_from .distinct .pluck(:business_name) .compact - .join(", ") + .join(', ') end ###### HELPER METHODS ###### @@ -202,17 +200,17 @@ def vendors_purchased_from # @return [Integer] def purchased_loose_disposable_diapers @purchased_disposable_diapers ||= LineItem.joins(:item) - .merge(Item.disposable) - .where(itemizable: organization.purchases.for_year(year)) - .sum(:quantity) + .merge(Item.disposable) + .where(itemizable: organization.purchases.for_year(year)) + .sum(:quantity) end # @return [Integer] def purchased_cloth_diapers @purchased_cloth_diapers ||= LineItem.joins(:item) - .merge(Item.cloth_diapers) - .where(itemizable: organization.purchases.for_year(year)) - .sum(:quantity) + .merge(Item.cloth_diapers) + .where(itemizable: organization.purchases.for_year(year)) + .sum(:quantity) end # @return [Integer] @@ -228,17 +226,17 @@ def total_cloth_diapers_acquired # @return [Integer] def donated_disposable_diapers @donated_diapers ||= LineItem.joins(:item) - .merge(Item.disposable) - .where(itemizable: organization.donations.for_year(year)) - .sum(:quantity) + .merge(Item.disposable) + .where(itemizable: organization.donations.for_year(year)) + .sum(:quantity) end # @return [Integer] def donated_cloth_diapers @donated_cloth_diapers ||= LineItem.joins(:item) - .merge(Item.cloth_diapers) - .where(itemizable: organization.donations.for_year(year)) - .sum(:quantity) + .merge(Item.cloth_diapers) + .where(itemizable: organization.donations.for_year(year)) + .sum(:quantity) end end end diff --git a/app/services/reports/children_served_report_service.rb b/app/services/reports/children_served_report_service.rb index 1242ba29a6..b455359193 100644 --- a/app/services/reports/children_served_report_service.rb +++ b/app/services/reports/children_served_report_service.rb @@ -48,7 +48,6 @@ def disposable_diapers_from_kits_total 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]) diff --git a/app/services/reports/partner_info_report_service.rb b/app/services/reports/partner_info_report_service.rb index b27f583115..9db4d27199 100644 --- a/app/services/reports/partner_info_report_service.rb +++ b/app/services/reports/partner_info_report_service.rb @@ -37,7 +37,7 @@ def partner_agency_counts end def partner_zipcodes_serviced - partner_agency_profiles.map(&:zips_served).uniq.compact.sort.join(', ') + partner_agency_profiles.map(&:zips_served).uniq.sort.join(', ') end end end diff --git a/app/services/reports/period_supply_report_service.rb b/app/services/reports/period_supply_report_service.rb index 8827e682ad..851896558d 100644 --- a/app/services/reports/period_supply_report_service.rb +++ b/app/services/reports/period_supply_report_service.rb @@ -14,7 +14,8 @@ def initialize(year:, organization:) def report @report ||= {name: "Period Supplies", entries: { - "Period supplies distributed" => number_with_delimiter(total_distributed_period_supplies), + "Period supplies distributed" => number_with_delimiter(distributed_supplies), + "Period supplies per adult per month" => monthly_supplies&.round || 0, "Period supplies" => types_of_supplies, "% period supplies donated" => "#{percent_donated.round}%", "% period supplies bought" => "#{percent_bought.round}%", @@ -23,7 +24,7 @@ def report end # @return [Integer] - def distributed_loose_period_supplies + def distributed_supplies @distributed_supplies ||= organization .distributions .for_year(year) @@ -32,12 +33,17 @@ def distributed_loose_period_supplies .sum("line_items.quantity") end - def distributed_period_supplies_from_kits - kit_items_calculation("distributions", "Distribution") - end - - def total_distributed_period_supplies - distributed_loose_period_supplies + distributed_period_supplies_from_kits + # @return [Integer] + def monthly_supplies + # NOTE: This is asking "per adult per month" but there doesn't seem to be much difference + # in calculating per month or per any other time frame, since all it's really asking + # is the value of the `distribution_quantity` field for the items we're giving out. + organization + .distributions + .for_year(year) + .joins(line_items: :item) + .merge(Item.period_supplies) + .average("COALESCE(items.distribution_quantity, 50)") end def types_of_supplies @@ -48,14 +54,14 @@ def types_of_supplies def percent_donated return 0.0 if total_supplies.zero? - (total_donated_supplies / total_supplies.to_f) * 100 + (donated_supplies / total_supplies.to_f) * 100 end # @return [Float] def percent_bought return 0.0 if total_supplies.zero? - (total_purchased_supplies / total_supplies.to_f) * 100 + (purchased_supplies / total_supplies.to_f) * 100 end # @return [String] @@ -66,67 +72,24 @@ def money_spent_on_supplies ###### HELPER METHODS ###### # @return [Integer] - def total_purchased_supplies + def purchased_supplies @purchased_supplies ||= LineItem.joins(:item) .merge(Item.period_supplies) .where(itemizable: organization.purchases.for_year(year)) .sum(:quantity) - - @purchased_supplies + purchased_supplies_from_kits - end - - def purchased_supplies_from_kits - kit_items_calculation("purchases", "Purchase") end # @return [Integer] def total_supplies - @total_supplies ||= total_purchased_supplies + total_donated_supplies + @total_supplies ||= purchased_supplies + donated_supplies end # @return [Integer] - def total_donated_supplies - loose_donated_supplies = LineItem.joins(:item) + def donated_supplies + @donated_supplies ||= LineItem.joins(:item) .merge(Item.period_supplies) .where(itemizable: organization.donations.for_year(year)) .sum(:quantity) - - loose_donated_supplies + donated_supplies_from_kits - end - - def donated_supplies_from_kits - kit_items_calculation("donations", "Donation") - end - - private - - def kit_items_calculation(itemizable_type, string_itemizable_type) - organization_id = @organization.id - year = @year - - # Sanitize and validate inputs - itemizable_type = ActiveRecord::Base.connection.quote_table_name(itemizable_type) - string_itemizable_type = ActiveRecord::Base.connection.quote(string_itemizable_type) - - sql_query = <<-SQL - SELECT SUM(line_items.quantity * kit_line_items.quantity) - FROM #{itemizable_type} - INNER JOIN line_items ON line_items.itemizable_type = #{string_itemizable_type} AND line_items.itemizable_id = #{itemizable_type}.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 #{itemizable_type}.organization_id = ? - AND EXTRACT(year FROM issued_at) = ? - AND LOWER(base_items.category) LIKE '%menstrual supplies%' - AND NOT (LOWER(base_items.category) LIKE '%diaper%' OR LOWER(base_items.name) LIKE '%cloth%') - AND kit_line_items.itemizable_type = 'Kit'; - 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 end end diff --git a/app/services/reports/summary_report_service.rb b/app/services/reports/summary_report_service.rb index fb5c20634a..4052ebc093 100644 --- a/app/services/reports/summary_report_service.rb +++ b/app/services/reports/summary_report_service.rb @@ -16,7 +16,7 @@ def report entries: { '% difference in yearly donations' => percent_donations, '% difference in total money donated' => percent_money, - '% difference in disposable diaper donations' => percent_diapers + '% difference in diaper donations' => percent_diapers } } end diff --git a/app/services/requests_total_items_service.rb b/app/services/requests_total_items_service.rb index 1b7ea3c7f6..dd5e673cb5 100644 --- a/app/services/requests_total_items_service.rb +++ b/app/services/requests_total_items_service.rb @@ -1,24 +1,44 @@ class RequestsTotalItemsService def initialize(requests:) - @requests = requests.includes(item_requests: {item: :request_units}) + @requests = requests end def calculate return unless requests - totals = Hash.new(0) - item_requests.each do |item_request| - totals[item_request.name_with_unit] += item_request.quantity.to_i + request_items_array = [] + + request_items.each do |items| + items.each do |json| + request_items_array << [item_name(json['item_id']), json['quantity']] + end end - totals + request_items_array.inject({}) do |item, (quantity, total)| + item[quantity] ||= 0 + item[quantity] += total.to_i + item + end end private attr_accessor :requests - def item_requests - @item_requests ||= requests.flat_map(&:item_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*' end end diff --git a/app/services/storage_location_deactivate_service.rb b/app/services/storage_location_deactivate_service.rb index 5dd8615106..60fb00b87c 100644 --- a/app/services/storage_location_deactivate_service.rb +++ b/app/services/storage_location_deactivate_service.rb @@ -16,7 +16,11 @@ def call private def valid? - inventory = View::Inventory.new(@storage_location.organization_id) - inventory.quantity_for(storage_location: @storage_location.id) <= 0 + 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 end end diff --git a/app/services/transfer_create_service.rb b/app/services/transfer_create_service.rb index 266a095f72..c4cb5cc12c 100644 --- a/app/services/transfer_create_service.rb +++ b/app/services/transfer_create_service.rb @@ -4,10 +4,12 @@ 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 - raise StandardError.new(transfer.errors.full_messages.join(", ")) + raise StandardError.new(transfer.errors.full_messages.join("
")) end end end diff --git a/app/services/transfer_destroy_service.rb b/app/services/transfer_destroy_service.rb index cbeaaa4def..70e562eb71 100644 --- a/app/services/transfer_destroy_service.rb +++ b/app/services/transfer_destroy_service.rb @@ -9,6 +9,7 @@ def call end transfer.transaction do + revert_inventory_transfer! TransferDestroyEvent.publish(transfer) transfer.destroy! end @@ -25,4 +26,9 @@ 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/account_requests/new.html.erb b/app/views/account_requests/new.html.erb index 63292f501a..2125b21a3e 100644 --- a/app/views/account_requests/new.html.erb +++ b/app/views/account_requests/new.html.erb @@ -75,7 +75,7 @@
- <%= f.input :organization_website, placeholder: "https://www.example.com" %> + <%= f.input :organization_website %>
diff --git a/app/views/adjustments/index.html.erb b/app/views/adjustments/index.html.erb index 4027d37bbe..be068c3c45 100644 --- a/app/views/adjustments/index.html.erb +++ b/app/views/adjustments/index.html.erb @@ -54,7 +54,7 @@ text: "Export Adjustments" ) if @adjustments.any? %> - <%= new_button_to new_adjustment_path, {text: "New Adjustment"} %> + <%= new_button_to new_adjustment_path(organization_name: current_organization), {text: "New Adjustment"} %>
<% end # form %> @@ -76,7 +76,7 @@ - + diff --git a/app/views/admin/account_requests/_open_account_request.html.erb b/app/views/admin/account_requests/_open_account_request.html.erb index 4f05036f0a..d06023658d 100644 --- a/app/views/admin/account_requests/_open_account_request.html.erb +++ b/app/views/admin/account_requests/_open_account_request.html.erb @@ -13,10 +13,5 @@ - - + data: { request_id: open_account_request.id }) %> diff --git a/app/views/admin/account_requests/_rejection_modal.html.erb b/app/views/admin/account_requests/_rejection_modal.html.erb index 4dc350165f..1a4d198d38 100644 --- a/app/views/admin/account_requests/_rejection_modal.html.erb +++ b/app/views/admin/account_requests/_rejection_modal.html.erb @@ -1,18 +1,19 @@ - - + <% end %> diff --git a/app/views/admin/dashboard.html.erb b/app/views/admin/dashboard.html.erb index 886000b65b..79dd1b6178 100644 --- a/app/views/admin/dashboard.html.erb +++ b/app/views/admin/dashboard.html.erb @@ -61,7 +61,7 @@
- +
Total Organizations <%= @organization_count %> diff --git a/app/views/admin/organizations/_list.html.erb b/app/views/admin/organizations/_list.html.erb index 6813b48aec..40c3b79d3d 100644 --- a/app/views/admin/organizations/_list.html.erb +++ b/app/views/admin/organizations/_list.html.erb @@ -1,32 +1,27 @@ -
-
CreatedCreated Organization Storage location Comment<%= js_button(text: 'Reject', icon: 'ban', class: 'reject-button', - data: { request_id: open_account_request.id, modal: 'reject' }) %><%= js_button(text: 'Close (Admin)', - icon: 'times', - class: 'reject-button', - data: { request_id: open_account_request.id, modal: 'close' }) %>
<%= item.name %><%= item.organization.name %> (id: <%= item.organization.id %>)<%= link_to item.organization.name, organization_path(item.organization) %>
- - - - - +
OrganizationContact E-mailAdded On
+ + + + + - - - + + + - - <% @organizations.each do |organization| %> - - - - + + <% @organizations.each do |organization| %> + + + + - - - <% end %> - -
OrganizationContact E-mailAdded On Last Distribution DateActions
Actions
<%= organization.name %><%= link_to organization.email, "mailto:#{organization.email}" %><%= organization.created_at.strftime("%F") %>
<%= organization.name %><%= link_to organization.email, "mailto:#{organization.email}" %><%= organization.created_at.strftime("%F") %> <%= organization.display_last_distribution_date %> - <%= view_button_to admin_organization_path(organization.id) %> - <%= edit_button_to edit_admin_organization_path(organization.id) %> - <%= delete_button_to(admin_organization_path(organization.id), { confirm: confirm_delete_msg(organization.name) }) unless (Organization.count <= 1) %> -
- - + + <%= view_button_to admin_organization_path(organization.id) %> + <%= edit_button_to edit_admin_organization_path(organization.id) %> + <%= delete_button_to(admin_organization_path(organization.id), { confirm: confirm_delete_msg(organization.name) }) unless (Organization.count <= 1) %> + + + <% end %> + + diff --git a/app/views/admin/organizations/index.html.erb b/app/views/admin/organizations/index.html.erb index e8d1fcb62a..fd4f388705 100644 --- a/app/views/admin/organizations/index.html.erb +++ b/app/views/admin/organizations/index.html.erb @@ -44,8 +44,10 @@ <% end %> -
- <%= render( partial: 'list', locals: { organizations: @organizations }) %> +
+
+ <%= render( partial: 'list', locals: { organizations: @organizations }) %> +
diff --git a/app/views/admin/users/_list.html.erb b/app/views/admin/users/_list.html.erb index a2e51ccb17..8b47c161a2 100644 --- a/app/views/admin/users/_list.html.erb +++ b/app/views/admin/users/_list.html.erb @@ -15,6 +15,7 @@ <%= user.email %> <%= edit_button_to edit_admin_user_path(user) %> + <%= delete_button_to(admin_user_path(user), {confirm: "Are you sure you want to permanently remove this user?"}) unless user == current_user %> <% end %> diff --git a/app/views/admin/users/_roles.html.erb b/app/views/admin/users/_roles.html.erb index 1d1d843da6..21cc9b024b 100644 --- a/app/views/admin/users/_roles.html.erb +++ b/app/views/admin/users/_roles.html.erb @@ -24,7 +24,7 @@ <% user.roles.each do |role| %> <%= role.title %> - <%= role.resource.name %> (id: <%= role.resource.id %>) + <%= link_to role.resource.name, role.resource %> <%= delete_button_to admin_user_remove_role_path(user, role_id: role.id), confirm: "Are you sure you want to remove this role?" %> diff --git a/app/views/audits/_form.html.erb b/app/views/audits/_form.html.erb index d9014cae5f..9d7f4c733c 100644 --- a/app/views/audits/_form.html.erb +++ b/app/views/audits/_form.html.erb @@ -22,7 +22,7 @@ diff --git a/app/views/audits/index.html.erb b/app/views/audits/index.html.erb index 9bb22d2683..55195842b4 100644 --- a/app/views/audits/index.html.erb +++ b/app/views/audits/index.html.erb @@ -47,7 +47,7 @@ <%= clear_filter_button %>
- <%= new_button_to new_audit_path, {text: "New Audit"} %> + <%= new_button_to new_audit_path(organization_name: current_organization), {text: "New Audit"} %>
<% end # form %> diff --git a/app/views/audits/new.html.erb b/app/views/audits/new.html.erb index 25d72693a5..fbe0a75338 100644 --- a/app/views/audits/new.html.erb +++ b/app/views/audits/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/audits/show.html.erb b/app/views/audits/show.html.erb index 1563f47519..f5a04a1ec2 100644 --- a/app/views/audits/show.html.erb +++ b/app/views/audits/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + @@ -50,13 +50,24 @@ - <% @items.each do |inventory_item| %> - <% line_item = @audit.line_items.find { |i| i.item_id == inventory_item.item_id } %> - "> - <%= inventory_item.name %> - <%= line_item&.quantity&.abs || "Not Audited" %> - <%= inventory_item.quantity %> - + <% if @items %> + <% @items.each do |inventory_item| %> + <% line_item = @audit.line_items.find { |i| i.item_id == inventory_item.item_id } %> + "> + <%= inventory_item.name %> + <%= line_item&.quantity&.abs || "Not Audited" %> + <%= inventory_item.quantity %> + + <% end %> + <% else %> + <% @inventory_items.each do |inventory_item| %> + <% line_item = @audit.line_items.find_by(item: inventory_item.item) %> + "> + <%= inventory_item.item.name %> + <%= line_item&.quantity&.abs || "Not Audited" %> + <%= inventory_item.quantity %> + + <% end %> <% end %> diff --git a/app/views/barcode_items/_barcode_item_lookup.html.erb b/app/views/barcode_items/_barcode_item_lookup.html.erb index 73171d7821..27e81092d2 100644 --- a/app/views/barcode_items/_barcode_item_lookup.html.erb +++ b/app/views/barcode_items/_barcode_item_lookup.html.erb @@ -1,2 +1,2 @@ <%# Increment a counter on how many we've seen so that we can create a unique ID -- really only necessary for test hooks %> -" class="__barcode_item_lookup form-control" value="" placeholder="Barcode Entry" autocomplete="on"> +" class="__barcode_item_lookup form-control" value="" placeholder="Barcode Entry" data-organization-id="<%= params[:organization_name] %>" autocomplete="on"> diff --git a/app/views/barcode_items/create.js.erb b/app/views/barcode_items/create.js.erb index 3d47d8c8fd..b8690025d0 100644 --- a/app/views/barcode_items/create.js.erb +++ b/app/views/barcode_items/create.js.erb @@ -12,7 +12,10 @@ $('#trigger-field-id').val(''); // Notify the user toastr.success("Barcode Added to Inventory"); -// Trigger the capture_entry/barcode_item_lookup functions with a return keypress -// which fills in the item name and quantity, adds new item, and changes focus -return_keypress = $.Event( "keypress", { which: 13 } ) -$("input.__barcode_item_lookup").last().trigger(return_keypress); +// Locate the row where the barcode was entered from +line_item = $('#' + source_field).closest('.nested-fields'); +// Set the values in the form. This is replicating some logic from barcode_items.js.erb +$(line_item).find('[data-quantity]').val(quantity); +$(line_item).find('[value="' + item_id + '"]').attr("selected", true); +$('#__add_line_item').trigger('click'); +$("input.__barcode_item_lookup").last().focus(); diff --git a/app/views/barcode_items/edit.html.erb b/app/views/barcode_items/edit.html.erb index e4f912e866..e378c6a845 100644 --- a/app/views/barcode_items/edit.html.erb +++ b/app/views/barcode_items/edit.html.erb @@ -13,7 +13,7 @@ Home <% end %> - + diff --git a/app/views/barcode_items/index.html.erb b/app/views/barcode_items/index.html.erb index fa8d97d260..42e95ce068 100644 --- a/app/views/barcode_items/index.html.erb +++ b/app/views/barcode_items/index.html.erb @@ -48,7 +48,7 @@ <%= download_button_to(barcode_items_path(format: :csv, filters: filter_params.merge(date_range: date_range_params)), {text: "Export Barcode Items", size: "md"}) if @barcode_items.any? %> <%= download_button_to(font_barcode_items_path, {text: "Download Barcode Font"}) %> - <%= new_button_to new_barcode_item_path, {text: "New Barcode"} %> + <%= new_button_to new_barcode_item_path(organization_name: current_organization), {text: "New Barcode"} %> <% end # form %> diff --git a/app/views/barcode_items/new.html.erb b/app/views/barcode_items/new.html.erb index 22c3fd19ee..79d43de9bd 100644 --- a/app/views/barcode_items/new.html.erb +++ b/app/views/barcode_items/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/barcode_items/show.html.erb b/app/views/barcode_items/show.html.erb index c8facef031..af98ea1010 100644 --- a/app/views/barcode_items/show.html.erb +++ b/app/views/barcode_items/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/broadcast_announcements/edit.html.erb b/app/views/broadcast_announcements/edit.html.erb index a670857b3a..3a2c59f28a 100644 --- a/app/views/broadcast_announcements/edit.html.erb +++ b/app/views/broadcast_announcements/edit.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/broadcast_announcements/index.html.erb b/app/views/broadcast_announcements/index.html.erb index f80732a3fd..4114c6a691 100644 --- a/app/views/broadcast_announcements/index.html.erb +++ b/app/views/broadcast_announcements/index.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + @@ -31,7 +31,7 @@
- <%= new_button_to new_broadcast_announcement_path, {text: "New Announcement"} %> + <%= new_button_to new_broadcast_announcement_path(organization_name: current_organization), {text: "New Announcement"} %>
diff --git a/app/views/broadcast_announcements/new.html.erb b/app/views/broadcast_announcements/new.html.erb index 91ba199da3..538632cb6e 100644 --- a/app/views/broadcast_announcements/new.html.erb +++ b/app/views/broadcast_announcements/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/dashboard/_announcements.html.erb b/app/views/dashboard/_announcements.html.erb deleted file mode 100644 index 85d14b6349..0000000000 --- a/app/views/dashboard/_announcements.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -<% if @broadcast_announcements.any? %> -
-
-

Announcements - from Human Essentials

-
- -
-
-
-
    - <% @broadcast_announcements.each do |announcement| %> -
  • - <%= if announcement.created_at.strftime("%Y") == DateTime.now.strftime("%Y") - announcement.created_at.strftime("%B %d") - else - announcement.created_at.strftime("%B %d %Y") - end %> -
    - <%= announcement.message %> - <% unless announcement.link == '' %> -
    - more info - <% end %> -
  • - <% end %> -
-
-
-<% end %> diff --git a/app/views/reports/_distribution.html.erb b/app/views/dashboard/_distribution.html.erb similarity index 100% rename from app/views/reports/_distribution.html.erb rename to app/views/dashboard/_distribution.html.erb diff --git a/app/views/reports/_donation.html.erb b/app/views/dashboard/_donation.html.erb similarity index 100% rename from app/views/reports/_donation.html.erb rename to app/views/dashboard/_donation.html.erb diff --git a/app/views/dashboard/_getting_started_prompt.html.erb b/app/views/dashboard/_getting_started_prompt.html.erb index 6872072ada..726f5b60a7 100644 --- a/app/views/dashboard/_getting_started_prompt.html.erb +++ b/app/views/dashboard/_getting_started_prompt.html.erb @@ -3,151 +3,127 @@ <% location_criteria_met = org_stats.storage_locations_added > 0 %> <% donation_criteria_met = org_stats.donation_sites_added > 0 %> <% inventory_criteria_met = org_stats.locations_with_inventory.length > 0 %> -<% criterias = [ location_criteria_met, partner_criteria_met, donation_criteria_met, inventory_criteria_met ] %> +<% criterias = [ partner_criteria_met, location_criteria_met, donation_criteria_met, inventory_criteria_met ] %> <% current_step = criterias.find_index(false) %> <% if current_step.present? %> +
+
+
+

+ Just Starting? + Here are some things you may need to set up: +

+
- <%= render( - "shared/card", - id: "summary", - header: "Getting Started", - type: :plain, - ) do %> -
-
-
-
-

- Just Starting? - Here are some things you may need to set up: -

-
- -
-
- <%= render partial: "getting_started_progress_stepper", locals: { - current_step: current_step, - criterias: criterias, - lines: ["line-right", "line-right line-left", "line-right line-left", "line-left"], - step_labels: ["Storage Locations", "Partner Agencies", "Donation Sites", "Inventory"] - } %> -
+
+
+ <%= render partial: "getting_started_progress_stepper", locals: { + current_step: current_step, + criterias: criterias, + lines: ["line-right", "line-right line-left", "line-right line-left", "line-left"], + step_labels: ["Partner Agencies", "Storage Locations", "Donation Sites", "Inventory"] + } %> +
-
-
-
-
- <% if location_criteria_met %> - - <% else %> - - <% end %> -

<%= pluralize(org_stats.storage_locations_added, 'Storage Location') %> Added

-
+
+
+
+
+ <% if partner_criteria_met %> + + <% else %> + + <% end %> +

<%= pluralize(org_stats.partners_added, 'Partner Agency') %> Added

+
-

- Add details for all Storage Locations you use for your inventory. -

+

+ To start building your community in Human Essentials, import a list of your current partner agencies or add them individually. +

-
- <% location_link_text = location_criteria_met ? "Add More Storage Locations" : "Add a Storage Location" %> - <%= new_button_to new_storage_location_path, { text: location_link_text, size: "md" } %> -
-
+
+ <% partner_link_text = partner_criteria_met ? "Add More Partners" : "Add a Partner" %> + <%= new_button_to new_partner_path, { text: partner_link_text, size: "md" } %>
+
+
-
-
-
-
- <% if partner_criteria_met %> - - <% else %> - - <% end %> -

<%= pluralize(org_stats.partners_added, 'Partner Agency') %> Added

-
+
+
+
+
+ <% if location_criteria_met %> + + <% else %> + + <% end %> +

<%= pluralize(org_stats.storage_locations_added, 'Storage Location') %> Added

+
-

- To start building your community in Human Essentials, import a list of your current partner agencies or add them individually. -

+

+ Add details for all Storage Locations you use for your inventory. +

-
- <%= modal_button_to("#csvImportModal", { text: "Import Partners", icon: "upload", size: "md" }) %> - <% partner_link_text = partner_criteria_met ? "Add More Partners" : "Add a Partner" %> - <%= new_button_to new_partner_path, { text: partner_link_text, size: "md" } %> -
-
+
+ <% location_link_text = location_criteria_met ? "Add More Storage Locations" : "Add a Storage Location" %> + <%= new_button_to new_storage_location_path, { text: location_link_text, size: "md" } %>
+
+
-
-
-
-
- <% if donation_criteria_met %> - - <% else %> - - <% end %> -

<%= pluralize(org_stats.donation_sites_added, 'Donation Site') %> Added

-
+
+
+
+
+ <% if donation_criteria_met %> + + <% else %> + + <% end %> +

<%= pluralize(org_stats.donation_sites_added, 'Donation Site') %> Added

+
-

- Add any community sites (including your primary storage facility) that accept donations on your behalf. -

+

+ Add any community sites (including your primary storage facility) that accept donations on your behalf. +

-
- <% donation_link_text = donation_criteria_met ? "Add More Donation Sites" : "Add a Donation Site" %> - <%= new_button_to new_donation_site_path, { text: donation_link_text, size: "md" } %> -
-
+
+ <% donation_link_text = donation_criteria_met ? "Add More Donation Sites" : "Add a Donation Site" %> + <%= new_button_to new_donation_site_path, { text: donation_link_text, size: "md" } %>
+
+
-
-
-
-
- <% if inventory_criteria_met %> - - <% else %> - - <% end %> -

<%= pluralize(org_stats.locations_with_inventory.length, 'Storage Location') %> with Inventory

-
+
+
+
+
+ <% if inventory_criteria_met %> + + <% else %> + + <% end %> +

<%= pluralize(org_stats.locations_with_inventory.length, 'Storage Location') %> with Inventory

+
-
-

- As an essentials bank, you might already have inventory on hand. These items might have come from previous donations/purchases. If you have multiple sources or want to add inventory to multiple storage locations, you can create multiple donations or purchases while specifying the correct date, source, and location. -

-
+
+

+ As an essentials bank, you might already have inventory on hand. These items might have come from previous donations/purchases. If you have multiple sources or want to add inventory to multiple storage locations, you can create multiple donations or purchases while specifying the correct date, source, and location. +

+
-
- <%= new_button_to new_donation_path, { text: "Add past donation", size: "md" } %> - <%= new_button_to new_purchase_path, { text: "Add past purchase", size: "md" } %> -
-
+
+ <%= new_button_to new_donation_path, { text: "Add past donation", size: "md" } %> + <%= new_button_to new_purchase_path, { text: "Add past purchase", size: "md" } %>
- - <%= render( - layout: "shared/csv_import_modal", - locals: { - import_type: "Partners", - csv_template_url: "/partners_template.csv", - csv_import_url: import_csv_partners_path, - }, - ) do %> -
  • Open the CSV file with Excel or your favourite spreadsheet program.
  • -
  • Delete the sample data and enter your partner agency names and addresses in the appropriate columns.
  • -
  • Save the file as a CSV file.
  • - <% end %> - <% end %> +
    <% end %> diff --git a/app/views/dashboard/_itemized_distributions_partial.html.erb b/app/views/dashboard/_itemized_distributions_partial.html.erb new file mode 100644 index 0000000000..07fed7e051 --- /dev/null +++ b/app/views/dashboard/_itemized_distributions_partial.html.erb @@ -0,0 +1,21 @@ +
    + + + + + + + + <%# Ordering from highest distributed to lowest %> + <% (local_assigns[:itemized_breakdown] || []).each do |item| %> + + + + + + + <% end %> + +
       ItemTotal DistributedTotal On Hand
       <%= item[:name] %><%= item[:distributed] %> + <%= item[:current_onhand] || "Unknown" %> +
    diff --git a/app/views/dashboard/_itemized_donations_partial.html.erb b/app/views/dashboard/_itemized_donations_partial.html.erb new file mode 100644 index 0000000000..fec34aff19 --- /dev/null +++ b/app/views/dashboard/_itemized_donations_partial.html.erb @@ -0,0 +1,19 @@ + + + + + + + + + <%# Ordering from highest distributed to lowest %> + <% (local_assigns[:itemized_breakdown] || []).each do |item| %> + + + + + + + <% end %> + +
       ItemTotal DonatedTotal On Hand
       <%= item[:name] %><%= item[:donated] %><%= item[:current_onhand] || "Unknown" %>
    diff --git a/app/views/dashboard/_low_inventory_report.html.erb b/app/views/dashboard/_low_inventory_report.html.erb deleted file mode 100644 index 9563eaed98..0000000000 --- a/app/views/dashboard/_low_inventory_report.html.erb +++ /dev/null @@ -1,37 +0,0 @@ -<%= render( - "shared/card", - id: "low_inventory", - gradient: "info", - title: "Bank-wide Low inventory" -) do %> - <% if @low_inventory_report.count == 0 %> -

    Inventory is at recommended levels (minimum and recommended levels can be set on each item)

    - <% else %> - - - - - - - - - - - <% @low_inventory_report.each do |inventory_item| %> - - - - - - - <% end %> - -
    Item NameQuantityMinimum QuantityRecommended Quantity
    <%= inventory_item["name"] %> - <% if inventory_item["total_quantity"] < inventory_item["on_hand_minimum_quantity"] %> - <%= inventory_item["total_quantity"] %> - <% else %> - <%= inventory_item["total_quantity"] %> - <% end %> - <%= inventory_item["on_hand_minimum_quantity"] %><%= inventory_item["on_hand_recommended_quantity"] %>
    - <% end %> -<% end %> diff --git a/app/views/dashboard/_manufacturer.html.erb b/app/views/dashboard/_manufacturer.html.erb new file mode 100644 index 0000000000..67fe3ad341 --- /dev/null +++ b/app/views/dashboard/_manufacturer.html.erb @@ -0,0 +1,5 @@ +
    + <%= link_to manufacturer do %> + <%= manufacturer.name %> (<%= number_with_delimiter(manufacturer.volume) %>) + <% end %> +
    diff --git a/app/views/dashboard/_outstanding_requests.html.erb b/app/views/dashboard/_outstanding_requests.html.erb deleted file mode 100644 index 3ea3f23e01..0000000000 --- a/app/views/dashboard/_outstanding_requests.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<%= - render( - "shared/card", - id: "outstanding", - gradient: "warning", - title: "Outstanding Requests", - footer: link_to("View all requests", requests_path), - footer_options: { class: "text-center" }, - ) do -%> - <% if @outstanding_requests.empty? %> - No outstanding requests! - <% else %> - - - - - - - - - - - <% @outstanding_requests.take(25).each do |item| %> - - - - - - - <% end %> - -
    DatePartnerRequestorComments
    <%= link_to item.created_at.strftime("%m/%d/%Y"), item %><%= item.partner.name %><%= item.partner_user&.formatted_email %><%= item.comments %>
    - <% end %> -<% end %> diff --git a/app/views/dashboard/_partner_approvals.html.erb b/app/views/dashboard/_partner_approvals.html.erb deleted file mode 100644 index 7c82b41395..0000000000 --- a/app/views/dashboard/_partner_approvals.html.erb +++ /dev/null @@ -1,34 +0,0 @@ -<%= render( - "shared/card", - id: "partner_approvals", - title: "Partner Approvals", - class: "wide-table", - type: @partners_awaiting_review.blank? ? :box : :table -) do %> - <% if @partners_awaiting_review.present? %> - - - - - - - - - - - <% @partners_awaiting_review.each do |partner| %> - - - - - - - - <% end %> -
    Partner NamePrimary Contact NamePrimary Contact EmailReview RequestedAction
    <%= partner.name %><%= partner.profile.primary_contact_name %><%= partner.profile.primary_contact_email %><%= partner.updated_at.strftime("%B %d %Y") %> - <%= view_button_to partner_path(partner) + "#partner-information", { text: "Review Application", icon: "check", type: "warning", class: 'badge' } %> -
    - <% else %> - No partners waiting for approval - <% end %> -<% end %> diff --git a/app/views/dashboard/_product_drive.html.erb b/app/views/dashboard/_product_drive.html.erb new file mode 100644 index 0000000000..6bdb592bdb --- /dev/null +++ b/app/views/dashboard/_product_drive.html.erb @@ -0,0 +1,5 @@ +
    + <%= link_to donation do %> + <%= number_with_delimiter donation.line_items.total %> from <%= donation.product_drive&.name || donation.product_drive_participant.business_name %> + <% end %> +
    diff --git a/app/views/reports/_purchase.html.erb b/app/views/dashboard/_purchase.html.erb similarity index 98% rename from app/views/reports/_purchase.html.erb rename to app/views/dashboard/_purchase.html.erb index dfa4b79ddb..bb13b180f7 100644 --- a/app/views/reports/_purchase.html.erb +++ b/app/views/dashboard/_purchase.html.erb @@ -1,4 +1,4 @@ - +
    <%= link_to purchase do %> <%= number_with_delimiter purchase.line_items.total %> items from <%= purchase.purchased_from_view %> diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 148f601ace..07988d141d 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -19,21 +19,368 @@
    - <%= render partial: "getting_started_prompt", locals: {org_stats: @org_stats} %> +
    +
    +
    <%= current_organization.name %>
    +
    + + +
    +
    + +
    +
    + <%= render partial: "getting_started_prompt", locals: {org_stats: @org_stats} %> + +
    + <%== display_logo_or_name %> +
    + +
    + +
    + +
    + +
    + +
    -
    -
    +
    + <% if @broadcast_announcements.any? %> +
    +
    +

    Announcements + from Human Essentials

    +
    + +
    +
    +
    +
      + <% @broadcast_announcements.each do |announcement| %> +
    • + <%= if announcement.created_at.strftime("%Y") == DateTime.now.strftime("%Y") + announcement.created_at.strftime("%B %d") + else + announcement.created_at.strftime("%B %d %Y") + end %> +
      + <%= announcement.message %> + <% unless announcement.link == '' %> +
      + more info + <% end %> +
    • + <% end %> +
    +
    +
    + <% end %> + +
    +
    +

    Distributions + <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    +
    + <%= new_button_to new_distribution_path, {text: "New Distribution"} %> + <%= print_button_to distributions_by_county_report_path(filters: { date_range: date_range_params }), {text: "Distributions by County", size: "md"} %> + +

    + + <%= total_distributed %> + + items distributed <%= @selected_date_range_label %> +

    +

    (<%= future_distributed %> items scheduled for future distribution)

    +
    +

    Recent distributions

    + <%= render partial: "distribution", collection: @recent_distributions, as: :distribution %> +
    +
    +
    +
    + +
    + +
    +
    +

    Itemized Distributions + <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    + <%= download_button_to(itemized_breakdown_distributions_path(format: :csv, filters: { date_range: date_range_params }), {text: "Export To CSV"}) %> +
    + + <%= render partial: "itemized_distributions_partial", locals: { itemized_breakdown: @itemized_distribution_data } %> +
    +
    + +
    +
    +

    Activity + <%= @selected_date_range %> +

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + <% + activity_chart_config = { + chart: { + type: "bar" + }, + title: "", + xAxis: { + categories: @distribution_data.keys, + title: { + text: nil + } + }, + yAxis: { + title: { + text: nil + } + }, + legend: { + enabled: false + }, + series: [ + { + data: @distribution_data.values + } + ] + }.to_json + %> +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    Donations (All Sources) + <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    +
    + <%= new_button_to new_donation_path, {text: "New Donation"} %> +

    + + <%= total_received_donations %> + + items + received <%= @selected_date_range_label %>

    +

    <%= dollar_presentation(total_received_money_donations) %> + raised <%= @selected_date_range_label %>

    +
    +

    Recent Donations

    + <%= render partial: "donation", collection: @recent_donations, as: :donation %> +
    +
    +
    +
    + +
    + +
    +
    +

    Product Drives <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    +
    +

    + + <%= total_received_from_product_drives %> + items + received <%= @selected_date_range_label %>

    +

    + + <%= dollar_presentation( total_received_money_donations_from_product_drives) %> + raised <%= @selected_date_range_label %>

    +
    +

    Recent Donations from Product Drives

    + <%= render partial: "product_drive", collection: @recent_donations.by_source(:product_drive), as: :donation %> +
    +
    +
    +
    + +
    + +
    +
    +

    Manufacturer Donations <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    +
    +

    + + <%= number_with_delimiter(@recent_donations_from_manufacturers.sum { |d| d.line_items.total }) %> + + items donated <%= @selected_date_range_label %> + by + + <%= pluralize(@recent_donations_from_manufacturers.group_by(&:manufacturer).count, 'Manufacturer') %> + +

    +
    +

    Top Manufacturer Donations

    + <%= render partial: "manufacturer", collection: @top_manufacturers, as: :manufacturer %> +
    +
    +
    +
    + +
    + +
    +
    +

    Purchases <%= @selected_date_range %>

    +
    + +
    +
    +
    +
    +
    + <%= new_button_to new_purchase_path, {text: "New Purchase"} %> +

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

    +
    +

    Recent purchases

    + <%= render partial: "purchase", collection: @recent_purchases, as: :purchase %> +
    +
    +
    +
    + +
    - <%= render partial: "announcements" %> - <%= render partial: "outstanding_requests" %> - <%= render partial: "partner_approvals" %> - <%= render partial: "low_inventory_report" %> +
    +
    +

    Itemized Donations + <%= @selected_date_range %>

    +
    +
    +
    +
    + <%= render partial: "itemized_donations_partial", locals: { itemized_breakdown: @itemized_donation_data } %> +
    +
    diff --git a/app/views/distributions/_distribution_row.html.erb b/app/views/distributions/_distribution_row.html.erb index 6e1c99ee7b..625ffc1838 100644 --- a/app/views/distributions/_distribution_row.html.erb +++ b/app/views/distributions/_distribution_row.html.erb @@ -1,8 +1,7 @@ > <%= distribution_row.id %> <%= distribution_row.partner.name %> - <%= distribution_row.created_at.strftime("%m/%d/%Y") %> - <%= (distribution_row.issued_at.presence || distribution_row.created_at).strftime("%m/%d/%Y") %> + <%= (distribution_row.issued_at.presence || distribution_row.created_at).strftime("%m/%d/%Y") %> <%= distribution_row.storage_location.name %> @@ -32,6 +31,6 @@ text: "Reclaim", icon: "undo", enabled: !distribution_row.has_inactive_item? } %> <% if distribution_row.has_inactive_item? %> -
    Has Inactive Items
    +
    Has Inactive Items
    <% end %> diff --git a/app/views/distributions/_distribution_total.html.erb b/app/views/distributions/_distribution_total.html.erb index 669c4251ca..37fc41304e 100644 --- a/app/views/distributions/_distribution_total.html.erb +++ b/app/views/distributions/_distribution_total.html.erb @@ -2,8 +2,6 @@ Total: - - <%= number_with_delimiter(@total_items_all_distributions, :delimiter => ',') %> (Total)
    diff --git a/app/views/distributions/_form.html.erb b/app/views/distributions/_form.html.erb index da523a8d3b..ba5e84cf84 100644 --- a/app/views/distributions/_form.html.erb +++ b/app/views/distributions/_form.html.erb @@ -1,7 +1,4 @@ -<%= simple_form_for distribution, - data: { controller: "form-input", confirmation_target: "form" }, - html: { class: "storage-location-required" }, - wrapper_mappings: { datetime: :custom_multi_select } do |f| %> +<%= simple_form_for distribution, data: { controller: "form-input" }, html: {class: "storage-location-required"}, wrapper_mappings: { datetime: :custom_multi_select } do |f| %>
    <%= f.simple_fields_for :request do |r| %> @@ -9,10 +6,9 @@ <% end %> <%= f.association :partner, - collection: @partner_list, + collection: current_organization.partners.alphabetized, label: "Partner", error: "Which partner is this distribution going to?" %> -
    <%= f.input :issued_at, as: :datetime, ampm: true, minute_step: 15, label: "Distribution date and time", html5: true, :input_html => { :value => date_place_holder&.strftime("%Y-%m-%dT%0k:%M")} %>
    @@ -33,36 +29,20 @@ <%= f.input :comment, label: "Comment" %>
    -
    - Items in this distribution - <% if distribution.request %> -
    Quantity - Total Units
    -
    Requested
    - <% end %> -
    + Items in this distribution
    - <%= render 'line_items/line_item_fields', form: f, locals: { show_request_items: true } %> + <%= render 'line_items/line_item_fields', form: f %>
    <% end %> - -<%# Confirmation modal: See confirmation_controller.js for how this gets displayed %> -<%# and app/controllers/distributions_controller.rb#validate for how it gets populated. %> - diff --git a/app/views/distributions/edit.html.erb b/app/views/distributions/edit.html.erb index a25558c548..2ccbd55a32 100644 --- a/app/views/distributions/edit.html.erb +++ b/app/views/distributions/edit.html.erb @@ -14,7 +14,7 @@ Home <% end %> - +
    diff --git a/app/views/distributions/index.html.erb b/app/views/distributions/index.html.erb index 42da475824..9760ba36a1 100644 --- a/app/views/distributions/index.html.erb +++ b/app/views/distributions/index.html.erb @@ -38,17 +38,17 @@
    <% if @items.present? %>
    - <%= filter_select(label: "Filter by Item", scope: :by_item_id, collection: @items, selected: @selected_item) %> + <%= filter_select(label: "Filter by item", scope: :by_item_id, collection: @items, selected: @selected_item) %>
    <% end %> <% if @item_categories.present? %>
    - <%= filter_select(label: "Filter by Item Category", scope: :by_item_category_id, collection: @item_categories, selected: @selected_item_category) %> + <%= filter_select(label: "Filter by item category", scope: :by_item_category_id, collection: @item_categories, selected: @selected_item_category) %>
    <% end %> <% if @partners.present? %>
    - <%= filter_select(label: "Filter by Partner", scope: :by_partner, collection: @partners, selected: @selected_partner) %> + <%= filter_select(scope: :by_partner, collection: @partners, selected: @selected_partner) %>
    <% end %> <% if @storage_locations.present? %> @@ -60,7 +60,7 @@ <%= filter_select(label: "Filter by Status", scope: :by_state, collection: @statuses, key: :last, value: :first, selected: @selected_status) %>
    - <%= label_tag "Date Range", "Date Range" %> + <%= label_tag "Date Range" %> <%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %>
    @@ -76,7 +76,7 @@ ) end %> - <%= new_button_to new_distribution_path, {text: "New Distribution"} %> + <%= new_button_to new_distribution_path(organization_name: current_organization), {text: "New Distribution"} %>
    <% end # form %> @@ -99,7 +99,6 @@ ID Partner - Initial Allocation Date of Distribution Source Inventory @@ -109,15 +108,15 @@ <% elsif filter_params[:by_item_category_id].present? %> Total in <%= @item_categories.find { |ic| ic.id == filter_params[:by_item_category_id].to_i }&.name %> <% else %> - Total Items + Total items <% end %> - Total Value - Delivery Method + Total value + Delivery method Shipping Cost Comments - Status + State Actions diff --git a/app/views/distributions/new.html.erb b/app/views/distributions/new.html.erb index f741e4823b..38fa80e41a 100644 --- a/app/views/distributions/new.html.erb +++ b/app/views/distributions/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + @@ -33,9 +33,7 @@
    -
    +
    <%= render 'form', distribution: @distribution, date_place_holder: Time.zone.now.end_of_day %>
    diff --git a/app/views/distributions/pickup_day.html.erb b/app/views/distributions/pickup_day.html.erb index 71b3ee35b9..ac32ad4e04 100644 --- a/app/views/distributions/pickup_day.html.erb +++ b/app/views/distributions/pickup_day.html.erb @@ -13,8 +13,8 @@ Home <% end %> - - + +
    diff --git a/app/views/distributions/schedule.html.erb b/app/views/distributions/schedule.html.erb index 02e5185eda..f1c6bfb0c8 100644 --- a/app/views/distributions/schedule.html.erb +++ b/app/views/distributions/schedule.html.erb @@ -16,7 +16,7 @@ Home <% end %> - + diff --git a/app/views/distributions/show.html.erb b/app/views/distributions/show.html.erb index 7cf1405c96..168ea57273 100644 --- a/app/views/distributions/show.html.erb +++ b/app/views/distributions/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + @@ -40,7 +40,7 @@ Delivery method: Shipping cost: Comments: - Status: + State: @@ -92,7 +92,7 @@ <%= print_button_to print_distribution_path(@distribution, format: :pdf), {size: "md"} %> <% if @distribution.has_inactive_item? %> - diff --git a/app/views/donation_sites/index.html.erb b/app/views/donation_sites/index.html.erb index 9f6ff112b3..68f2003feb 100644 --- a/app/views/donation_sites/index.html.erb +++ b/app/views/donation_sites/index.html.erb @@ -55,7 +55,7 @@
    <%= modal_button_to("#csvImportModal", {icon: "upload", text: "Import Donation Sites", size: "md"}) if @donation_sites.empty? %> <%= download_button_to(donation_sites_path(format: :csv, filters: filter_params.merge(date_range: date_range_params)), {text: "Export Donation Sites", size: "md"}) if @donation_sites.any? %> - <%= new_button_to new_donation_site_path, {text: "New Donation Site"} %> + <%= new_button_to new_donation_site_path(organization_name: current_organization), {text: "New Donation Site"} %>
    @@ -98,7 +98,7 @@ layout: "shared/csv_import_modal", locals: { import_type: 'Donation Sites', - csv_template_url: "/donation_sites_template.csv", + csv_template_url: "/donation_sites.csv", csv_import_url: import_csv_donation_sites_path } ) do %> diff --git a/app/views/donation_sites/new.html.erb b/app/views/donation_sites/new.html.erb index 769c860582..36f35fe018 100644 --- a/app/views/donation_sites/new.html.erb +++ b/app/views/donation_sites/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/donation_sites/show.html.erb b/app/views/donation_sites/show.html.erb index 77c3abc569..12c8cb6c12 100644 --- a/app/views/donation_sites/show.html.erb +++ b/app/views/donation_sites/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/donations/_donation_form.html.erb b/app/views/donations/_donation_form.html.erb index c7de6c4921..295be85271 100644 --- a/app/views/donations/_donation_form.html.erb +++ b/app/views/donations/_donation_form.html.erb @@ -6,8 +6,6 @@
    <%= f.input :source, collection: Donation::SOURCES.values, - selected: donation_form.source, - include_blank: true, label: "Source", error: "What effort or initiative did this donation come from?", wrapper: :input_group %> @@ -18,8 +16,6 @@
    <%= f.association :donation_site, collection: @donation_sites, - selected: donation_form.donation_site_id, - include_blank: true, label: "Donation Site", error: "Where was this donation dropped off?", wrapper: :input_group %> @@ -29,8 +25,6 @@
    <%= f.association :product_drive, collection: @product_drives, - selected: donation_form.product_drive_id, - include_blank: true, label_method: lambda { |x| "#{x.try(:name) }" }, label: "Product Drive", error: "Which product drive was this from?", @@ -39,8 +33,6 @@
    <%= f.association :product_drive_participant, collection: @product_drive_participants, - selected: donation_form.product_drive_participant_id, - include_blank: true, label_method: lambda { |x| "#{x.try(:business_name) }" }, label: "Product Drive Participant", error: "Which product drive participant was this from?", @@ -52,8 +44,7 @@
    <%= f.association :manufacturer, collection: @manufacturers, - selected: donation_form.manufacturer_id, - include_blank: true, + prompt: "Choose one...", label_method: lambda { |x| "#{x.try(:name) }" }, label: "Manufacturer", error: "Which Manufacturer was this from?", @@ -67,7 +58,7 @@ collection: @storage_locations, label: "Storage Location", error: "Where is it being stored?", - selected: donation_form.storage_location&.id || current_organization.intake_location, + selected: f.object.storage_location&.id || current_organization.intake_location, include_blank: true, wrapper: :input_group %>
    @@ -77,18 +68,15 @@
    <%= f.input :money_raised_in_dollars, as: :money_raised_in_dollars, wrapper: :input_group do %> - <%= f.input_field :money_raised_in_dollars, - class: "form-control", - value: donation_form.money_raised_in_dollars %> + <%= f.input_field :money_raised_in_dollars, class: "form-control" %> <% end %>
    - <%= f.input :comment, wrapper: :input_group do %> - <%= f.text_area :comment, value: donation_form.comment, cols: 90, rows: 2, class: "text optional form-control" %> - <% end %> + <%= f.input :comment, + wrapper: :input_group %>
    @@ -98,8 +86,7 @@ label: "Issued on", as: :date, html5: true, - wrapper: :input_group, - input_html: { value: donation_form.issued_at } %> + wrapper: :input_group %>
    @@ -107,7 +94,7 @@ Items in this donation
    - <%= render 'line_items/line_item_fields', form: f, object: donation_form.line_items %> + <%= render 'line_items/line_item_fields', form: f %>
    diff --git a/app/views/donations/show.html.erb b/app/views/donations/show.html.erb index b13ed109b2..4a89e1ee25 100644 --- a/app/views/donations/show.html.erb +++ b/app/views/donations/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - +
    @@ -75,19 +75,13 @@ enabled: !@donation.has_inactive_item?, size: "md" } %> <%= new_button_to new_distribution_path(donation_id: @donation.id, storage_location_id: @donation.storage_location_id), { text: "Start a new Distribution" } %> - <% if current_user.has_role?(Role::ORG_ADMIN, current_organization) %> - <%= delete_button_to donation_path(@donation), { - size: "md", - enabled: !@donation.has_inactive_item?, - confirm: "Are you sure you want to permanently remove this donation?" } %> - <% end %> + <%= delete_button_to donation_path(@donation), { size: "md", confirm: "Are you sure you want to permanently remove this donation?" } if current_user.has_role?(Role::ORG_ADMIN, current_organization) %> <% if @donation.has_inactive_item? %> -
    - You can only delete or correct donations where all the items are active. - If you need to delete this donation or make a correction, please make the following items active: <%= @donation.inactive_items.map(&:name).join(", ") %> +
    + You can only correct donations where all the items are active. + If you need to make a correction, please make the following items active: <%= @donation.inactive_items.map(&:name).join(", ") %>
    <% end %> - <%= print_button_to print_donation_path(@donation, format: :pdf), { size: "md" } %>
    diff --git a/app/views/errors/insufficient.html.erb b/app/views/errors/insufficient.html.erb new file mode 100644 index 0000000000..85f844732c --- /dev/null +++ b/app/views/errors/insufficient.html.erb @@ -0,0 +1,3 @@ +

    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 new file mode 100644 index 0000000000..bfedf09242 --- /dev/null +++ b/app/views/errors/internal_server_error.html.erb @@ -0,0 +1,28 @@ +
    +
    +
    +
    +

    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 new file mode 100644 index 0000000000..12c3302ee4 --- /dev/null +++ b/app/views/errors/not_found.html.erb @@ -0,0 +1,31 @@ +
    +
    +
    +
    +

    404 Error Page

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

    404

    +
    +
    +

    Oops! Page not found.

    + +

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

    + +
    +
    +
    diff --git a/app/views/events/_event_row.html.erb b/app/views/events/_event_row.html.erb index bb11e99e91..59d4c4ff3f 100644 --- a/app/views/events/_event_row.html.erb +++ b/app/views/events/_event_row.html.erb @@ -11,7 +11,7 @@ <%= link_to event.eventable_id, event.eventable %> <%= link_to( - events_path(eventable_type: event.eventable_type, eventable_id: event.eventable_id), + events_path(current_organization, eventable_type: event.eventable_type, eventable_id: event.eventable_id), class: 'btn btn-md') do %> <% end %> diff --git a/app/views/items/_form.html.erb b/app/views/items/_form.html.erb index 15dc7ebbb3..b1a48607c0 100644 --- a/app/views/items/_form.html.erb +++ b/app/views/items/_form.html.erb @@ -43,12 +43,6 @@ <%= f.input_field :package_size, class: "form-control" %> <% end %> - <% if Flipper.enabled?(:enable_packs) %> - <%= f.input :request_units, label: "Additional Custom Request Units" do %> - <%= f.association :request_units, as: :check_boxes, collection: current_organization.request_units, checked: selected_item_request_units(@item), label_method: :name, value_method: :id, class: "form-check-input" %> - <% end %> - <% end %> - <%= f.input :visible, label: "Item is Visible to Partners?", wrapper: :input_group do %> <%= f.check_box :visible_to_partners, {class: "input-group-text", id: "visible_to_partners"}, "true", "false" %> <% end %> diff --git a/app/views/items/_item_categories.html.erb b/app/views/items/_item_categories.html.erb index 12554cd444..87525ed450 100644 --- a/app/views/items/_item_categories.html.erb +++ b/app/views/items/_item_categories.html.erb @@ -1,7 +1,7 @@
    - <%= new_button_to new_item_category_path, {text: "New Item Category"} %> + <%= new_button_to new_item_category_path(organization_name: current_organization), {text: "New Item Category"} %>
    diff --git a/app/views/items/_item_list.html.erb b/app/views/items/_item_list.html.erb index b8ae121563..49cd464521 100644 --- a/app/views/items/_item_list.html.erb +++ b/app/views/items/_item_list.html.erb @@ -1,6 +1,6 @@
    - <%= new_button_to new_item_path, {text: "New Item"} %> + <%= new_button_to new_item_path(organization_name: current_organization), {text: "New Item"} %>
    @@ -8,18 +8,13 @@ - - - <% if Flipper.enabled?(:enable_packs) %> - <% unless current_organization.request_units.empty? %> - - <% end %> - <% end %> - + + + - <%= render partial: "items/item_row", collection: items, as: :item_row, locals: { inventory: inventory, kits: kits } %> + <%= render partial: "items/item_row", collection: items, as: :item_row, locals: { inventory: inventory } %>
    Category NameQuantity Per IndividualFair Market Value (per item)Custom Request UnitsActionsQuantity Per IndividualFair Market Value (per item)Actions
    diff --git a/app/views/items/_item_row.html.erb b/app/views/items/_item_row.html.erb index a6147c129d..668ffb1b30 100644 --- a/app/views/items/_item_row.html.erb +++ b/app/views/items/_item_row.html.erb @@ -1,23 +1,18 @@ <%= item_row.item_category && link_to(item_row.item_category&.name, item_category_path(item_row.item_category), class: 'text-blue-500') %> <%= item_row.name %> - <%= item_row.distribution_quantity %> + <%= item_row.distribution_quantity %> <%= dollar_value(item_row.value_in_cents) %> - <% if Flipper.enabled?(:enable_packs) %> - <% unless current_organization.request_units.empty? %> - <%= item_row.request_units.pluck(:name).join(', ') %> - <% end %> - <% end %> <%= view_button_to item_path(item_row) %> <%= edit_button_to edit_item_path(item_row) %> <% if item_row.active? %> - <% if item_row.can_delete?(inventory, kits) %> + <% if item_row.can_delete?(inventory) %> <%= delete_button_to item_path(item_row), text: 'Delete', confirm: confirm_delete_msg(item_row.name) %> <% else %> - <% can_deactivate = item_row.can_deactivate_or_delete?(inventory, kits) %> + <% can_deactivate = item_row.can_deactivate_or_delete?(inventory) %> <%= delete_button_to deactivate_item_path(item_row), text: 'Deactivate', enabled: can_deactivate, diff --git a/app/views/items/_items_inventory.html.erb b/app/views/items/_items_inventory.html.erb index de82d10c5f..9ec8ea5ddd 100644 --- a/app/views/items/_items_inventory.html.erb +++ b/app/views/items/_items_inventory.html.erb @@ -1,6 +1,6 @@
    - <%= new_button_to new_item_path, {text: "New Item"} %> + <%= new_button_to new_item_path(organization_name: current_organization), {text: "New Item"} %>
    diff --git a/app/views/items/_items_quantity_and_location.html.erb b/app/views/items/_items_quantity_and_location.html.erb index c2aee4fb1e..0fcf6bb2b9 100644 --- a/app/views/items/_items_quantity_and_location.html.erb +++ b/app/views/items/_items_quantity_and_location.html.erb @@ -1,6 +1,6 @@
    - <%= new_button_to new_item_path, {text: "New Item"} %> + <%= new_button_to new_item_path(organization_name: current_organization), {text: "New Item"} %>
    diff --git a/app/views/items/_kits.html.erb b/app/views/items/_kits.html.erb index 1a422e9fe8..b9081e0630 100644 --- a/app/views/items/_kits.html.erb +++ b/app/views/items/_kits.html.erb @@ -1,6 +1,6 @@
    - <%= new_button_to new_kit_path, {text: "New Kit"} %> + <%= new_button_to new_kit_path(organization_name: current_organization), {text: "New Kit"} %>
    <%= render partial: 'kits/table' %> diff --git a/app/views/items/edit.html.erb b/app/views/items/edit.html.erb index 5bcc779c3a..63475f9a23 100644 --- a/app/views/items/edit.html.erb +++ b/app/views/items/edit.html.erb @@ -13,7 +13,7 @@ Home <% end %> - +
    diff --git a/app/views/items/index.html.erb b/app/views/items/index.html.erb index 169618aadc..23d706d6e0 100644 --- a/app/views/items/index.html.erb +++ b/app/views/items/index.html.erb @@ -93,7 +93,7 @@
    - <%= render partial: 'item_list', locals: { items: @items, inventory: @inventory, kits: @kits.active } %> + <%= render partial: 'item_list', locals: { items: @items, inventory: @inventory } %> <%= render partial: 'item_categories', locals: { item_categories: @item_categories } %> <%= render partial: 'items_quantity_and_location' %> <%= render partial: 'items_inventory' %> diff --git a/app/views/items/new.html.erb b/app/views/items/new.html.erb index 5c968bc585..b22ba13ec4 100644 --- a/app/views/items/new.html.erb +++ b/app/views/items/new.html.erb @@ -14,7 +14,7 @@ Home <% end %> - +
    diff --git a/app/views/items/show.html.erb b/app/views/items/show.html.erb index 93052bd973..9ceb0ed3de 100644 --- a/app/views/items/show.html.erb +++ b/app/views/items/show.html.erb @@ -14,7 +14,7 @@ Home <% end %> - +
    @@ -30,51 +30,20 @@
    - - - - - - - - - - - - - + - - - - - - - - - - - - - - - + - - - - <% if Flipper.enabled?(:enable_packs) %> - - <% item_units = @item.request_units&.pluck("item_units.name") %> - - <% end %> + + - - + + + - +
    Base Item<%= @item.base_item.name %>
    Name<%= @item.name %>
    Category<%= @item&.item_category&.name %>
    Value Per Item<%= @item.value_in_cents || 0 %>
    Quantity per Individual<%= @item.distribution_quantity || 0 %>
    On hand minimum quantity<%= @item.on_hand_minimum_quantity || 0 %>
    On hand recommended quantity<%= @item.on_hand_recommended_quantity || 0 %>
    Distribution Quantity Package Size<%= @item.package_size || 0 %>
    Custom Units<%= item_units&.join("; ") %>
    Item is visible to partners<%= @item.visible_to_partners ? 'Yes' : 'No' %><%= @item.value_in_cents ? @item.value_in_cents : 0 %><%= @item.distribution_quantity ? @item.distribution_quantity : 0 %><%= @item.package_size ? @item.package_size : 0 %>
    diff --git a/app/views/kits/_form.html.erb b/app/views/kits/_form.html.erb index 3c1acb8320..b91b8887ac 100644 --- a/app/views/kits/_form.html.erb +++ b/app/views/kits/_form.html.erb @@ -25,9 +25,11 @@ <%= 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 61c4b1115c..09b81b4042 100644 --- a/app/views/kits/_table.html.erb +++ b/app/views/kits/_table.html.erb @@ -26,15 +26,25 @@ Quantity - <% @inventory.all_items.select { |i| i.item_id == kit.item.id}.each do |item| %> - - <%= @inventory.storage_location_name(item.storage_location_id) %> - <%= item.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 %> <% end %>
    - <%= edit_button_to allocations_kit_path(kit), { text: "Modify Allocation" } %> + <%= edit_button_to allocations_kit_path(organization_name: current_organization.id, id: kit.id), { text: "Modify Allocation" } %>
    diff --git a/app/views/kits/allocations.html.erb b/app/views/kits/allocations.html.erb index f9e775771c..72b3d1c0e6 100644 --- a/app/views/kits/allocations.html.erb +++ b/app/views/kits/allocations.html.erb @@ -14,7 +14,7 @@ Home <% end %> - + diff --git a/app/views/kits/index.html.erb b/app/views/kits/index.html.erb index 5571d91794..dd1c4b5e44 100644 --- a/app/views/kits/index.html.erb +++ b/app/views/kits/index.html.erb @@ -44,7 +44,7 @@ <%= filter_button %> <%= clear_filter_button %> - <%= new_button_to new_kit_path, { text: "New Kit" } %> + <%= new_button_to new_kit_path(organization_name: current_organization), { text: "New Kit" } %> <% end # form %> diff --git a/app/views/kits/new.html.erb b/app/views/kits/new.html.erb index d13a03006d..2158d68236 100644 --- a/app/views/kits/new.html.erb +++ b/app/views/kits/new.html.erb @@ -21,4 +21,3 @@ <%= render 'form' %> -<%= render partial: "barcode_items/barcode_modal" %> diff --git a/app/views/layouts/_lte_admin_navbar.html.erb b/app/views/layouts/_lte_admin_navbar.html.erb index 5b46468b86..63e7cc4c10 100644 --- a/app/views/layouts/_lte_admin_navbar.html.erb +++ b/app/views/layouts/_lte_admin_navbar.html.erb @@ -1,20 +1,35 @@ - + diff --git a/app/views/layouts/_lte_admin_sidebar.html.erb b/app/views/layouts/_lte_admin_sidebar.html.erb index 2679b59345..889502b3b3 100644 --- a/app/views/layouts/_lte_admin_sidebar.html.erb +++ b/app/views/layouts/_lte_admin_sidebar.html.erb @@ -1,14 +1,14 @@ - - - - - - <% end %> + <% if (current_user.organization.present?) %> + + <% end %> + diff --git a/app/views/layouts/_lte_navbar.html.erb b/app/views/layouts/_lte_navbar.html.erb index f2abf12076..606dd55b14 100644 --- a/app/views/layouts/_lte_navbar.html.erb +++ b/app/views/layouts/_lte_navbar.html.erb @@ -1,3 +1,25 @@ +<% if current_user.has_role?(Role::SUPER_ADMIN) %> + +<% end %> + @@ -13,7 +35,7 @@ remaining this week - <%= link_to_if current_organization.id.present?, "View Calendar", schedule_distributions_path, class: "dropdown-item dropdown-footer" %> + <%= link_to_if current_organization.id.present?, "View Calendar", schedule_distributions_path(organization_name: current_organization.to_param), class: "dropdown-item dropdown-footer" %> @@ -41,26 +63,31 @@ <%= current_user.display_name %> diff --git a/app/views/layouts/_lte_sidebar.html.erb b/app/views/layouts/_lte_sidebar.html.erb index fe4ab55639..0e3ff132bb 100644 --- a/app/views/layouts/_lte_sidebar.html.erb +++ b/app/views/layouts/_lte_sidebar.html.erb @@ -1,10 +1,31 @@