diff --git a/backend/alembic/versions/2024_07_29_2149-fef3614083d9_remove_workflows_and_add_procurement_.py b/backend/alembic/versions/2024_07_29_2149-fef3614083d9_remove_workflows_and_add_procurement_.py new file mode 100644 index 0000000000..dcfd0301aa --- /dev/null +++ b/backend/alembic/versions/2024_07_29_2149-fef3614083d9_remove_workflows_and_add_procurement_.py @@ -0,0 +1,1194 @@ +"""remove workflows and add procurement tracker + +Revision ID: fef3614083d9 +Revises: c98733f2a098 +Create Date: 2024-07-29 21:49:32.306627+00:00 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from alembic_postgresql_enum import TableReference +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = "fef3614083d9" +down_revision: Union[str, None] = "4d898e1383a4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # drop constraints first + op.drop_constraint( + "procurement_step_workflow_step_id_fkey", "procurement_step", type_="foreignkey" + ) + op.drop_constraint( + "workflow_step_dependency_predecessor_step_id_fkey", + "workflow_step_dependency", + type_="foreignkey", + ) + op.drop_constraint( + "workflow_step_dependency_successor_step_id_fkey", + "workflow_step_dependency", + type_="foreignkey", + ) + op.drop_constraint( + "package_snapshot_package_id_fkey", + "package_snapshot", + type_="foreignkey", + ) + op.drop_constraint( + "workflow_step_instance_workflow_step_template_id_fkey", + "workflow_step_instance", + type_="foreignkey", + ) + op.drop_constraint( + "workflow_instance_workflow_template_id_fkey", + "workflow_instance", + type_="foreignkey", + ) + + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "procurement_tracker", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("agreement_id", sa.Integer(), nullable=True), + sa.Column("current_step_id", sa.Integer(), nullable=True), + sa.Column("created_by", sa.Integer(), nullable=True), + sa.Column("updated_by", sa.Integer(), nullable=True), + sa.Column("created_on", sa.DateTime(), nullable=True), + sa.Column("updated_on", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["agreement_id"], + ["agreement.id"], + ), + sa.ForeignKeyConstraint( + ["created_by"], + ["ops_user.id"], + ), + sa.ForeignKeyConstraint( + ["current_step_id"], + ["procurement_step.id"], + ), + sa.ForeignKeyConstraint( + ["updated_by"], + ["ops_user.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "procurement_tracker_version", + sa.Column("id", sa.Integer(), autoincrement=False, nullable=False), + sa.Column("agreement_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column("current_step_id", sa.Integer(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.Integer(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.Integer(), autoincrement=False, nullable=True), + sa.Column("created_on", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column("updated_on", sa.DateTime(), autoincrement=False, nullable=True), + sa.Column( + "transaction_id", sa.BigInteger(), autoincrement=False, nullable=False + ), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + ) + op.create_index( + op.f("ix_procurement_tracker_version_end_transaction_id"), + "procurement_tracker_version", + ["end_transaction_id"], + unique=False, + ) + op.create_index( + op.f("ix_procurement_tracker_version_operation_type"), + "procurement_tracker_version", + ["operation_type"], + unique=False, + ) + op.create_index( + op.f("ix_procurement_tracker_version_transaction_id"), + "procurement_tracker_version", + ["transaction_id"], + unique=False, + ) + op.drop_index("ix_package_version_end_transaction_id", table_name="package_version") + op.drop_index("ix_package_version_operation_type", table_name="package_version") + op.drop_index("ix_package_version_transaction_id", table_name="package_version") + op.drop_table("package_version") + op.drop_table("package") + op.drop_index( + "ix_workflow_step_template_version_end_transaction_id", + table_name="workflow_step_template_version", + ) + op.drop_index( + "ix_workflow_step_template_version_operation_type", + table_name="workflow_step_template_version", + ) + op.drop_index( + "ix_workflow_step_template_version_transaction_id", + table_name="workflow_step_template_version", + ) + op.drop_table("workflow_step_template_version") + op.drop_table("package_snapshot") + op.drop_table("step_approvers") + op.drop_index( + "ix_workflow_step_instance_version_end_transaction_id", + table_name="workflow_step_instance_version", + ) + op.drop_index( + "ix_workflow_step_instance_version_operation_type", + table_name="workflow_step_instance_version", + ) + op.drop_index( + "ix_workflow_step_instance_version_transaction_id", + table_name="workflow_step_instance_version", + ) + op.drop_table("workflow_step_instance_version") + op.drop_index( + "ix_workflow_template_version_end_transaction_id", + table_name="workflow_template_version", + ) + op.drop_index( + "ix_workflow_template_version_operation_type", + table_name="workflow_template_version", + ) + op.drop_index( + "ix_workflow_template_version_transaction_id", + table_name="workflow_template_version", + ) + op.drop_table("workflow_template_version") + op.drop_table("workflow_step_dependency") + op.drop_index( + "ix_workflow_instance_version_end_transaction_id", + table_name="workflow_instance_version", + ) + op.drop_index( + "ix_workflow_instance_version_operation_type", + table_name="workflow_instance_version", + ) + op.drop_index( + "ix_workflow_instance_version_transaction_id", + table_name="workflow_instance_version", + ) + op.drop_table("workflow_instance_version") + op.drop_table("workflow_step_template") + op.drop_table("workflow_template") + op.drop_table("workflow_step_instance") + op.drop_index( + "ix_workflow_step_dependency_version_end_transaction_id", + table_name="workflow_step_dependency_version", + ) + op.drop_index( + "ix_workflow_step_dependency_version_operation_type", + table_name="workflow_step_dependency_version", + ) + op.drop_index( + "ix_workflow_step_dependency_version_transaction_id", + table_name="workflow_step_dependency_version", + ) + op.drop_table("workflow_step_dependency_version") + op.drop_index( + "ix_package_snapshot_version_end_transaction_id", + table_name="package_snapshot_version", + ) + op.drop_index( + "ix_package_snapshot_version_operation_type", + table_name="package_snapshot_version", + ) + op.drop_index( + "ix_package_snapshot_version_transaction_id", + table_name="package_snapshot_version", + ) + op.drop_table("package_snapshot_version") + op.drop_table("workflow_instance") + op.drop_index( + "ix_step_approvers_version_end_transaction_id", + table_name="step_approvers_version", + ) + op.drop_index( + "ix_step_approvers_version_operation_type", table_name="step_approvers_version" + ) + op.drop_index( + "ix_step_approvers_version_transaction_id", table_name="step_approvers_version" + ) + op.drop_table("step_approvers_version") + op.add_column( + "procurement_step", + sa.Column("procurement_tracker_id", sa.Integer(), nullable=True), + ) + # op.drop_constraint('procurement_step_workflow_step_id_fkey', 'procurement_step', type_='foreignkey') + op.create_foreign_key( + None, + "procurement_step", + "procurement_tracker", + ["procurement_tracker_id"], + ["id"], + ) + op.drop_column("procurement_step", "workflow_step_id") + op.add_column( + "procurement_step_version", + sa.Column( + "procurement_tracker_id", sa.Integer(), autoincrement=False, nullable=True + ), + ) + op.drop_column("procurement_step_version", "workflow_step_id") + sa.Enum( + "REVIEW", + "APPROVED", + "REJECTED", + "CHANGES", + "COMPLETE", + name="workflowstepstatus", + ).drop(op.get_bind()) + sa.Enum( + "APPROVAL", + "DOCUMENT_MGMT", + "VALIDATION", + "PROCUREMENT", + "ATTESTATION", + name="workflowsteptype", + ).drop(op.get_bind()) + sa.Enum( + "DRAFT_TO_PLANNED", + "PLANNED_TO_EXECUTING", + "GENERIC", + "PROCUREMENT_TRACKING", + name="workflowaction", + ).drop(op.get_bind()) + sa.Enum("CAN", "PROCUREMENT_SHOP", "AGREEMENT", name="workflowtriggertype").drop( + op.get_bind() + ) + op.sync_enum_values( + "ops", + "opseventtype", + [ + "CREATE_BLI", + "UPDATE_BLI", + "DELETE_BLI", + "SEND_BLI_FOR_APPROVAL", + "CREATE_PROJECT", + "CREATE_NEW_AGREEMENT", + "UPDATE_AGREEMENT", + "DELETE_AGREEMENT", + "ACKNOWLEDGE_NOTIFICATION", + "CREATE_BLI_PACKAGE", + "UPDATE_BLI_PACKAGE", + "CREATE_SERVICES_COMPONENT", + "UPDATE_SERVICES_COMPONENT", + "DELETE_SERVICES_COMPONENT", + "CREATE_PROCUREMENT_ACQUISITION_PLANNING", + "UPDATE_PROCUREMENT_ACQUISITION_PLANNING", + "DELETE_PROCUREMENT_ACQUISITION_PLANNING", + "CREATE_DOCUMENT", + "LOGIN_ATTEMPT", + "LOGOUT", + "GET_USER_DETAILS", + "CREATE_USER", + "UPDATE_USER", + "DEACTIVATE_USER", + ], + [ + TableReference( + table_schema="ops", table_name="ops_event", column_name="event_type" + ), + TableReference( + table_schema="ops", + table_name="ops_event_version", + column_name="event_type", + ), + ], + enum_values_to_rename=[], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values( + "ops", + "opseventtype", + [ + "LOGIN_ATTEMPT", + "CREATE_BLI", + "UPDATE_BLI", + "DELETE_BLI", + "CREATE_PROJECT", + "CREATE_NEW_AGREEMENT", + "UPDATE_AGREEMENT", + "SEND_BLI_FOR_APPROVAL", + "DELETE_AGREEMENT", + "ACKNOWLEDGE_NOTIFICATION", + "LOGOUT", + "GET_USER_DETAILS", + "CREATE_USER", + "UPDATE_USER", + "DEACTIVATE_USER", + "CREATE_BLI_PACKAGE", + "UPDATE_BLI_PACKAGE", + "CREATE_SERVICES_COMPONENT", + "UPDATE_SERVICES_COMPONENT", + "DELETE_SERVICES_COMPONENT", + "CREATE_PROCUREMENT_ACQUISITION_PLANNING", + "UPDATE_PROCUREMENT_ACQUISITION_PLANNING", + "DELETE_PROCUREMENT_ACQUISITION_PLANNING", + "CREATE_DOCUMENT", + ], + [ + TableReference( + table_schema="ops", table_name="ops_event", column_name="event_type" + ), + TableReference( + table_schema="ops", + table_name="ops_event_version", + column_name="event_type", + ), + ], + enum_values_to_rename=[], + ) + sa.Enum("CAN", "PROCUREMENT_SHOP", "AGREEMENT", name="workflowtriggertype").create( + op.get_bind() + ) + sa.Enum( + "DRAFT_TO_PLANNED", + "PLANNED_TO_EXECUTING", + "GENERIC", + "PROCUREMENT_TRACKING", + name="workflowaction", + ).create(op.get_bind()) + sa.Enum( + "APPROVAL", + "DOCUMENT_MGMT", + "VALIDATION", + "PROCUREMENT", + "ATTESTATION", + name="workflowsteptype", + ).create(op.get_bind()) + sa.Enum( + "REVIEW", + "APPROVED", + "REJECTED", + "CHANGES", + "COMPLETE", + name="workflowstepstatus", + ).create(op.get_bind()) + op.add_column( + "procurement_step_version", + sa.Column("workflow_step_id", sa.INTEGER(), autoincrement=False, nullable=True), + ) + op.drop_column("procurement_step_version", "procurement_tracker_id") + op.add_column( + "procurement_step", + sa.Column("workflow_step_id", sa.INTEGER(), autoincrement=False, nullable=True), + ) + op.drop_constraint(None, "procurement_step", type_="foreignkey") + op.create_foreign_key( + "procurement_step_workflow_step_id_fkey", + "procurement_step", + "workflow_step_instance", + ["workflow_step_id"], + ["id"], + ) + op.drop_column("procurement_step", "procurement_tracker_id") + op.create_table( + "step_approvers_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column( + "workflow_step_template_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("role_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("group_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="step_approvers_version_pkey" + ), + ) + op.create_index( + "ix_step_approvers_version_transaction_id", + "step_approvers_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_step_approvers_version_operation_type", + "step_approvers_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_step_approvers_version_end_transaction_id", + "step_approvers_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "workflow_instance", + sa.Column( + "id", + sa.INTEGER(), + server_default=sa.text("nextval('workflow_instance_id_seq'::regclass)"), + autoincrement=True, + nullable=False, + ), + sa.Column("associated_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column( + "associated_type", + postgresql.ENUM( + "CAN", + "PROCUREMENT_SHOP", + "AGREEMENT", + name="workflowtriggertype", + create_type=False, + ), + autoincrement=False, + nullable=False, + ), + sa.Column( + "workflow_template_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_action", + postgresql.ENUM( + "DRAFT_TO_PLANNED", + "PLANNED_TO_EXECUTING", + "GENERIC", + "PROCUREMENT_TRACKING", + name="workflowaction", + create_type=False, + ), + autoincrement=False, + nullable=False, + ), + sa.Column( + "current_workflow_step_instance_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], ["ops_user.id"], name="workflow_instance_created_by_fkey" + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["ops_user.id"], name="workflow_instance_updated_by_fkey" + ), + sa.ForeignKeyConstraint( + ["workflow_template_id"], + ["workflow_template.id"], + name="workflow_instance_workflow_template_id_fkey", + ), + sa.PrimaryKeyConstraint("id", name="workflow_instance_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "package_snapshot_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("package_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("version", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("object_type", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("object_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("bli_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="package_snapshot_version_pkey" + ), + ) + op.create_index( + "ix_package_snapshot_version_transaction_id", + "package_snapshot_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_package_snapshot_version_operation_type", + "package_snapshot_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_package_snapshot_version_end_transaction_id", + "package_snapshot_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "workflow_step_dependency_version", + sa.Column( + "predecessor_step_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + sa.Column( + "successor_step_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "predecessor_step_id", + "successor_step_id", + "transaction_id", + name="workflow_step_dependency_version_pkey", + ), + ) + op.create_index( + "ix_workflow_step_dependency_version_transaction_id", + "workflow_step_dependency_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_workflow_step_dependency_version_operation_type", + "workflow_step_dependency_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_workflow_step_dependency_version_end_transaction_id", + "workflow_step_dependency_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "workflow_step_instance", + sa.Column( + "id", + sa.INTEGER(), + server_default=sa.text( + "nextval('workflow_step_instance_id_seq'::regclass)" + ), + autoincrement=True, + nullable=False, + ), + sa.Column( + "workflow_instance_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_step_template_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("index", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "status", + postgresql.ENUM( + "REVIEW", + "APPROVED", + "REJECTED", + "CHANGES", + "COMPLETE", + name="workflowstepstatus", + create_type=False, + ), + autoincrement=False, + nullable=False, + ), + sa.Column("notes", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column( + "time_started", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "time_completed", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], + ["ops_user.id"], + name="workflow_step_instance_created_by_fkey", + ), + sa.ForeignKeyConstraint( + ["updated_by"], + ["ops_user.id"], + name="workflow_step_instance_updated_by_fkey", + ), + sa.ForeignKeyConstraint( + ["workflow_instance_id"], + ["workflow_instance.id"], + name="workflow_step_instance_workflow_instance_id_fkey", + ), + sa.ForeignKeyConstraint( + ["workflow_step_template_id"], + ["workflow_step_template.id"], + name="workflow_step_instance_workflow_step_template_id_fkey", + ), + sa.PrimaryKeyConstraint("id", name="workflow_step_instance_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "workflow_template", + sa.Column( + "id", + sa.INTEGER(), + server_default=sa.text("nextval('workflow_template_id_seq'::regclass)"), + autoincrement=True, + nullable=False, + ), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], ["ops_user.id"], name="workflow_template_created_by_fkey" + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["ops_user.id"], name="workflow_template_updated_by_fkey" + ), + sa.PrimaryKeyConstraint("id", name="workflow_template_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "workflow_step_template", + sa.Column( + "id", + sa.INTEGER(), + server_default=sa.text( + "nextval('workflow_step_template_id_seq'::regclass)" + ), + autoincrement=True, + nullable=False, + ), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column( + "workflow_template_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_type", + postgresql.ENUM( + "APPROVAL", + "DOCUMENT_MGMT", + "VALIDATION", + "PROCUREMENT", + "ATTESTATION", + name="workflowsteptype", + create_type=False, + ), + autoincrement=False, + nullable=False, + ), + sa.Column("index", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], + ["ops_user.id"], + name="workflow_step_template_created_by_fkey", + ), + sa.ForeignKeyConstraint( + ["updated_by"], + ["ops_user.id"], + name="workflow_step_template_updated_by_fkey", + ), + sa.ForeignKeyConstraint( + ["workflow_template_id"], + ["workflow_template.id"], + name="workflow_step_template_workflow_template_id_fkey", + ), + sa.PrimaryKeyConstraint("id", name="workflow_step_template_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "workflow_instance_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("associated_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "associated_type", + postgresql.ENUM( + "CAN", + "PROCUREMENT_SHOP", + "AGREEMENT", + name="workflowtriggertype", + create_type=False, + ), + autoincrement=False, + nullable=True, + ), + sa.Column( + "workflow_template_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_action", + postgresql.ENUM( + "DRAFT_TO_PLANNED", + "PLANNED_TO_EXECUTING", + "GENERIC", + "PROCUREMENT_TRACKING", + name="workflowaction", + create_type=False, + ), + autoincrement=False, + nullable=True, + ), + sa.Column( + "current_workflow_step_instance_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="workflow_instance_version_pkey" + ), + ) + op.create_index( + "ix_workflow_instance_version_transaction_id", + "workflow_instance_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_workflow_instance_version_operation_type", + "workflow_instance_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_workflow_instance_version_end_transaction_id", + "workflow_instance_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "workflow_step_dependency", + sa.Column( + "predecessor_step_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + sa.Column( + "successor_step_id", sa.INTEGER(), autoincrement=False, nullable=False + ), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], + ["ops_user.id"], + name="workflow_step_dependency_created_by_fkey", + ), + sa.ForeignKeyConstraint( + ["predecessor_step_id"], + ["workflow_step_instance.id"], + name="workflow_step_dependency_predecessor_step_id_fkey", + ), + sa.ForeignKeyConstraint( + ["successor_step_id"], + ["workflow_step_instance.id"], + name="workflow_step_dependency_successor_step_id_fkey", + ), + sa.ForeignKeyConstraint( + ["updated_by"], + ["ops_user.id"], + name="workflow_step_dependency_updated_by_fkey", + ), + sa.PrimaryKeyConstraint( + "predecessor_step_id", + "successor_step_id", + name="workflow_step_dependency_pkey", + ), + ) + op.create_table( + "workflow_template_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="workflow_template_version_pkey" + ), + ) + op.create_index( + "ix_workflow_template_version_transaction_id", + "workflow_template_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_workflow_template_version_operation_type", + "workflow_template_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_workflow_template_version_end_transaction_id", + "workflow_template_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "workflow_step_instance_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column( + "workflow_instance_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_step_template_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("index", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "status", + postgresql.ENUM( + "REVIEW", + "APPROVED", + "REJECTED", + "CHANGES", + "COMPLETE", + name="workflowstepstatus", + create_type=False, + ), + autoincrement=False, + nullable=True, + ), + sa.Column("notes", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column( + "time_started", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "time_completed", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="workflow_step_instance_version_pkey" + ), + ) + op.create_index( + "ix_workflow_step_instance_version_transaction_id", + "workflow_step_instance_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_workflow_step_instance_version_operation_type", + "workflow_step_instance_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_workflow_step_instance_version_end_transaction_id", + "workflow_step_instance_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "step_approvers", + sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column( + "workflow_step_template_id", + sa.INTEGER(), + autoincrement=False, + nullable=True, + ), + sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("role_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("group_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], ["ops_user.id"], name="step_approvers_created_by_fkey" + ), + sa.ForeignKeyConstraint( + ["group_id"], ["group.id"], name="step_approvers_group_id_fkey" + ), + sa.ForeignKeyConstraint( + ["role_id"], ["role.id"], name="step_approvers_role_id_fkey" + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["ops_user.id"], name="step_approvers_updated_by_fkey" + ), + sa.ForeignKeyConstraint( + ["user_id"], ["ops_user.id"], name="step_approvers_user_id_fkey" + ), + sa.ForeignKeyConstraint( + ["workflow_step_template_id"], + ["workflow_step_template.id"], + name="step_approvers_workflow_step_template_id_fkey", + ), + sa.PrimaryKeyConstraint("id", name="step_approvers_pkey"), + ) + op.create_table( + "package_snapshot", + sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column("package_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("version", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("object_type", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("object_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("bli_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["bli_id"], + ["budget_line_item.id"], + name="package_snapshot_bli_id_fkey", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["created_by"], ["ops_user.id"], name="package_snapshot_created_by_fkey" + ), + sa.ForeignKeyConstraint( + ["package_id"], ["package.id"], name="package_snapshot_package_id_fkey" + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["ops_user.id"], name="package_snapshot_updated_by_fkey" + ), + sa.PrimaryKeyConstraint("id", name="package_snapshot_pkey"), + ) + op.create_table( + "workflow_step_template_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column( + "workflow_template_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column( + "workflow_type", + postgresql.ENUM( + "APPROVAL", + "DOCUMENT_MGMT", + "VALIDATION", + "PROCUREMENT", + "ATTESTATION", + name="workflowsteptype", + create_type=False, + ), + autoincrement=False, + nullable=True, + ), + sa.Column("index", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint( + "id", "transaction_id", name="workflow_step_template_version_pkey" + ), + ) + op.create_index( + "ix_workflow_step_template_version_transaction_id", + "workflow_step_template_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_workflow_step_template_version_operation_type", + "workflow_step_template_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_workflow_step_template_version_end_transaction_id", + "workflow_step_template_version", + ["end_transaction_id"], + unique=False, + ) + op.create_table( + "package", + sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column("submitter_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "workflow_instance_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column("notes", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.ForeignKeyConstraint( + ["created_by"], ["ops_user.id"], name="package_created_by_fkey" + ), + sa.ForeignKeyConstraint( + ["submitter_id"], ["ops_user.id"], name="package_submitter_id_fkey" + ), + sa.ForeignKeyConstraint( + ["updated_by"], ["ops_user.id"], name="package_updated_by_fkey" + ), + sa.ForeignKeyConstraint( + ["workflow_instance_id"], + ["workflow_instance.id"], + name="package_workflow_instance_id_fkey", + ), + sa.PrimaryKeyConstraint("id", name="package_pkey"), + ) + op.create_table( + "package_version", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("submitter_id", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "workflow_instance_id", sa.INTEGER(), autoincrement=False, nullable=True + ), + sa.Column("notes", sa.VARCHAR(), autoincrement=False, nullable=True), + sa.Column("created_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column("updated_by", sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column( + "created_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column( + "updated_on", postgresql.TIMESTAMP(), autoincrement=False, nullable=True + ), + sa.Column("transaction_id", sa.BIGINT(), autoincrement=False, nullable=False), + sa.Column( + "end_transaction_id", sa.BIGINT(), autoincrement=False, nullable=True + ), + sa.Column("operation_type", sa.SMALLINT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id", name="package_version_pkey"), + ) + op.create_index( + "ix_package_version_transaction_id", + "package_version", + ["transaction_id"], + unique=False, + ) + op.create_index( + "ix_package_version_operation_type", + "package_version", + ["operation_type"], + unique=False, + ) + op.create_index( + "ix_package_version_end_transaction_id", + "package_version", + ["end_transaction_id"], + unique=False, + ) + op.drop_index( + op.f("ix_procurement_tracker_version_transaction_id"), + table_name="procurement_tracker_version", + ) + op.drop_index( + op.f("ix_procurement_tracker_version_operation_type"), + table_name="procurement_tracker_version", + ) + op.drop_index( + op.f("ix_procurement_tracker_version_end_transaction_id"), + table_name="procurement_tracker_version", + ) + op.drop_table("procurement_tracker_version") + op.drop_table("procurement_tracker") + # ### end Alembic commands ### diff --git a/backend/data_tools/data/workflow_data.json5 b/backend/data_tools/data/workflow_data.json5 deleted file mode 100644 index 72af30a14e..0000000000 --- a/backend/data_tools/data/workflow_data.json5 +++ /dev/null @@ -1,112 +0,0 @@ -{ - workflow_template: [ - { - id: 1, - name: "Basic Approval", - created_on: "2023-11-07T10:00:00Z", - updated_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 2, - name: "Procurement Tracker", - created_on: "2023-11-07T10:00:00Z", - updated_on: "2023-11-07T10:00:00Z", - created_by: 520, - } - ], - - workflow_step_template: [ - { - id: 1, - name: "Initial Review", - workflow_template_id: 1, - workflow_type: "APPROVAL", - index: 0, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 2, - name: "Final Approval", - workflow_template_id: 1, - workflow_type: "APPROVAL", - index: 1, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 3, - name: "Acquisition Planning", - workflow_template_id: 2, - workflow_type: "ATTESTATION", - index: 0, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 4, - name: "Pre-Solicitation", - workflow_template_id: 2, - workflow_type: "ATTESTATION", - index: 1, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 5, - name: "Solicitation", - workflow_template_id: 2, - workflow_type: "ATTESTATION", - index: 2, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 6, - name: "Evaluation Approval", - workflow_template_id: 2, - workflow_type: "APPROVAL", - index: 3, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 7, - name: "Evaluation Attestation", - workflow_template_id: 2, - workflow_type: "ATTESTATION", - index: 4, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 8, - name: "Pre-Award", - workflow_template_id: 2, - workflow_type: "ATTESTATION", - index: 5, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - }, - { - id: 9, - name: "Award", - workflow_template_id: 2, - workflow_type: "APPROVAL", - index: 6, - updated_on: "2023-11-07T10:00:00Z", - created_on: "2023-11-07T10:00:00Z", - created_by: 520, - } - ], - -} diff --git a/backend/data_tools/scripts/import_test_data.sh b/backend/data_tools/scripts/import_test_data.sh index 02df313c0c..cb48fb0fb8 100755 --- a/backend/data_tools/scripts/import_test_data.sh +++ b/backend/data_tools/scripts/import_test_data.sh @@ -45,7 +45,4 @@ DATA=./data_tools/data/first_contract_data.json5 python ./data_tools/src/import_ echo "Loading 'agreements_and_blin_data.json5'..." DATA=./data_tools/data/agreements_and_blin_data.json5 python ./data_tools/src/import_static_data/import_data.py -echo "Loading 'workflow_data.json5'..." -DATA=./data_tools/data/workflow_data.json5 python ./data_tools/src/import_static_data/import_data.py - echo "Data Loading Complete!" diff --git a/backend/models/__init__.py b/backend/models/__init__.py index ed08f55893..2b99b5fb00 100644 --- a/backend/models/__init__.py +++ b/backend/models/__init__.py @@ -1,6 +1,7 @@ from .auth import * from .base import * from .cans import * +from .change_requests import * from .document import * from .events import * from .history import * @@ -10,4 +11,3 @@ from .projects import * from .users import * from .vendors import * -from .workflows import * diff --git a/backend/models/cans.py b/backend/models/cans.py index d28161b5f1..21972982ac 100644 --- a/backend/models/cans.py +++ b/backend/models/cans.py @@ -27,19 +27,10 @@ from typing_extensions import Any, override from models.base import BaseModel +from models.change_requests import BudgetLineItemChangeRequest, ChangeRequestStatus from models.portfolios import Portfolio +from models.procurement_tracker import ProcurementTracker from models.users import User -from models.workflows import ( - BudgetLineItemChangeRequest, - ChangeRequestStatus, - Package, - PackageSnapshot, - WorkflowAction, - WorkflowInstance, - WorkflowStepInstance, - WorkflowStepStatus, - WorkflowTriggerType, -) class BudgetLineItemStatus(Enum): @@ -48,16 +39,19 @@ class BudgetLineItemStatus(Enum): IN_EXECUTION = auto() OBLIGATED = auto() + class ModType(Enum): ADMIN = auto() AMOUNT_TBD = auto() AS_IS = auto() REPLACEMENT_AMOUNT_FINAL = auto() + class ContractCategory(Enum): RESEARCH = auto() SERVICE = auto() + class CANArrangementType(Enum): OPRE_APPROPRIATION = auto() COST_SHARE = auto() @@ -238,20 +232,15 @@ def display_name(self): } @property - def procurement_tracker_workflow_id(self): + def procurement_tracker_id(self): if object_session(self) is None: return False - workflow_id = object_session(self).scalar( - select(WorkflowInstance.id).where( - and_( - WorkflowInstance.workflow_action - == WorkflowAction.PROCUREMENT_TRACKING, - WorkflowInstance.associated_type == WorkflowTriggerType.AGREEMENT, - WorkflowInstance.associated_id == self.id, - ) + tracker_id = object_session(self).scalar( + select(ProcurementTracker.id).where( + ProcurementTracker.agreement_id == self.id ) ) - return workflow_id + return tracker_id contract_support_contacts = Table( @@ -319,7 +308,9 @@ class ContractAgreement(Agreement): service_requirement_type: Mapped[Optional[ServiceRequirementType]] = mapped_column( ENUM(ServiceRequirementType) ) - contract_category: Mapped[Optional[ContractCategory]] = mapped_column(ENUM(ContractCategory)) + contract_category: Mapped[Optional[ContractCategory]] = mapped_column( + ENUM(ContractCategory) + ) __mapper_args__ = { "polymorphic_identity": AgreementType.CONTRACT, @@ -587,9 +578,7 @@ class BudgetLineItem(BaseModel): clin: Mapped[Optional[CLIN]] = relationship(CLIN, backref="budget_line_items") amount: Mapped[Optional[decimal]] = mapped_column(Numeric(12, 2)) - mod_type: Mapped[Optional[ModType]] = mapped_column( - sa.Enum(ModType) - ) + mod_type: Mapped[Optional[ModType]] = mapped_column(sa.Enum(ModType)) status: Mapped[Optional[BudgetLineItemStatus]] = mapped_column( sa.Enum(BudgetLineItemStatus) @@ -602,7 +591,9 @@ class BudgetLineItem(BaseModel): requisition_number: Mapped[Optional[int]] = mapped_column(Integer) requisition_date: Mapped[Optional[date]] = mapped_column(Date) - is_under_current_resolution: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) + is_under_current_resolution: Mapped[Optional[bool]] = mapped_column( + Boolean, default=False + ) date_needed: Mapped[Optional[date]] = mapped_column(Date) @@ -799,7 +790,6 @@ def appropriation_term(self): return None return self.expiration_date.year - self.appropriation_date.year - @BaseModel.display_name.getter def display_name(self): return self.number diff --git a/backend/models/change_requests.py b/backend/models/change_requests.py new file mode 100644 index 0000000000..cb49456a77 --- /dev/null +++ b/backend/models/change_requests.py @@ -0,0 +1,139 @@ +"""Workflow models.""" + +from enum import Enum, auto +from typing import Optional + +import sqlalchemy as sa +from sqlalchemy import DateTime, ForeignKey, Integer, event +from sqlalchemy.dialects.postgresql import ENUM, JSONB +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models import BaseModel + +# ---=== CHANGE REQUESTS ===--- + + +class ChangeRequestStatus(Enum): + IN_REVIEW = auto() + APPROVED = auto() + REJECTED = auto() + + +class ChangeRequestType(Enum): + CHANGE_REQUEST = auto() + AGREEMENT_CHANGE_REQUEST = auto() + BUDGET_LINE_ITEM_CHANGE_REQUEST = auto() + + +class ChangeRequest(BaseModel): + __tablename__ = "change_request" + id: Mapped[int] = mapped_column(Integer, primary_key=True) + change_request_type: Mapped[ChangeRequestType] = mapped_column( + ENUM(ChangeRequestType) + ) + + # agreement_type: Mapped[AgreementType] = mapped_column(ENUM(AgreementType)) + status: Mapped[ChangeRequestStatus] = mapped_column( + ENUM(ChangeRequestStatus), nullable=False, default=ChangeRequestStatus.IN_REVIEW + ) + requested_change_data: Mapped[JSONB] = mapped_column(JSONB) + requested_change_diff: Mapped[Optional[JSONB]] = mapped_column(JSONB) + requested_change_info: Mapped[Optional[JSONB]] = mapped_column(JSONB) + # BaseModel.created_by is the requestor, so there's no need for another column for that + requestor_notes: Mapped[Optional[str]] = mapped_column(sa.String) + + managing_division_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("division.id") + ) + managing_division = relationship( + "Division", + passive_deletes=True, + ) + reviewed_by_id: Mapped[Optional[int]] = mapped_column(ForeignKey("ops_user.id")) + reviewed_on: Mapped[Optional[DateTime]] = mapped_column(DateTime) + reviewer_notes: Mapped[Optional[str]] = mapped_column(sa.String) + + __mapper_args__ = { + "polymorphic_on": "change_request_type", + "polymorphic_identity": ChangeRequestType.CHANGE_REQUEST, + } + + +class AgreementChangeRequest(ChangeRequest): + # if this isn't optional here, SQL will make the column non-nullable + agreement_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("agreement.id", ondelete="CASCADE") + ) + agreement = relationship( + "Agreement", + passive_deletes=True, + ) + + __mapper_args__ = { + "polymorphic_identity": ChangeRequestType.AGREEMENT_CHANGE_REQUEST, + } + + budget_field_names = ["procurement_shop_id"] + + @hybrid_property + def has_budget_change(self): + return any(key in self.requested_change_data for key in self.budget_field_names) + + @has_budget_change.expression + def has_budget_change(cls): + return cls.requested_change_data.has_any(cls.budget_field_names) + + +class BudgetLineItemChangeRequest(AgreementChangeRequest): + budget_line_item_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("budget_line_item.id", ondelete="CASCADE") + ) + budget_line_item = relationship( + "BudgetLineItem", + passive_deletes=True, + ) + + __mapper_args__ = { + "polymorphic_identity": ChangeRequestType.BUDGET_LINE_ITEM_CHANGE_REQUEST, + } + + budget_field_names = ["amount", "can_id", "date_needed"] + + @hybrid_property + def has_budget_change(self): + return any(key in self.requested_change_data for key in self.budget_field_names) + + @has_budget_change.expression + def has_budget_change(cls): + return cls.requested_change_data.has_any(cls.budget_field_names) + + @hybrid_property + def has_status_change(self): + return "status" in self.requested_change_data + + @has_status_change.expression + def has_status_change(cls): + return cls.requested_change_data.has_key("status") + + +# require agreement_id for Agreement changes. +# (It has to be Optional in the model to keep the column nullable for other types) +@event.listens_for(AgreementChangeRequest, "before_insert") +@event.listens_for(AgreementChangeRequest, "before_update") +@event.listens_for(BudgetLineItemChangeRequest, "before_insert") +@event.listens_for(BudgetLineItemChangeRequest, "before_update") +def check_agreement_id(mapper, connection, target): + if target.agreement_id is None: + raise ValueError("agreement_id is required for AgreementChangeRequest") + + +# require budget_line_item_id for BLI changes. +# (It has to be Optional in the model to keep the column nullable for other types) +@event.listens_for(BudgetLineItemChangeRequest, "before_insert") +@event.listens_for(BudgetLineItemChangeRequest, "before_update") +def check_budget_line_id(mapper, connection, target): + if target.budget_line_item_id is None: + raise ValueError( + "budget_line_item_id is required for BudgetLineItemChangeRequest" + ) diff --git a/backend/models/procurement_tracker.py b/backend/models/procurement_tracker.py new file mode 100644 index 0000000000..80bc79da26 --- /dev/null +++ b/backend/models/procurement_tracker.py @@ -0,0 +1,102 @@ +import sqlalchemy as sa +from sqlalchemy.orm import relationship + +from models import BaseModel + + +class ProcurementTracker(BaseModel): + __tablename__ = "procurement_tracker" + id = BaseModel.get_pk_column() + agreement_id = sa.Column(sa.Integer, sa.ForeignKey("agreement.id")) + procurement_steps = relationship( + "ProcurementStep", + back_populates="procurement_tracker", + foreign_keys="ProcurementStep.procurement_tracker_id", + ) + current_step_id = sa.Column( + sa.Integer, sa.ForeignKey("procurement_step.id"), nullable=True + ) + + +class ProcurementStep(BaseModel): + __tablename__ = "procurement_step" + + id = BaseModel.get_pk_column() + agreement_id = sa.Column(sa.Integer, sa.ForeignKey("agreement.id")) + procurement_tracker_id = sa.Column( + sa.Integer, sa.ForeignKey("procurement_tracker.id") + ) + procurement_tracker = relationship( + "ProcurementTracker", + back_populates="procurement_steps", + foreign_keys=[procurement_tracker_id], + ) + + type = sa.Column(sa.String, nullable=False) + __mapper_args__ = { + "polymorphic_identity": "procurement_step", + "polymorphic_on": type, + } + + +class Attestation(object): + is_complete = sa.Column(sa.Boolean, nullable=False, default=False) + actual_date = sa.Column(sa.Date, nullable=True) + completed_by = sa.Column(sa.Integer, sa.ForeignKey("ops_user.id"), nullable=True) + notes = sa.Column(sa.String, nullable=True) + + +class TargetDate(object): + target_date = sa.Column(sa.Date, nullable=True) + + +class AcquisitionPlanning(ProcurementStep, Attestation): + __tablename__ = "procurement_acquisition_planning" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_acquisition_planning", + } + + +class PreSolicitation(ProcurementStep, Attestation, TargetDate): + __tablename__ = "procurement_pre_solicitation" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_pre_solicitation", + } + # documents = relationship("PreSolicitationDocument", backref="pre_solicitation") + + +class Solicitation(ProcurementStep, Attestation, TargetDate): + __tablename__ = "procurement_solicitation" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_solicitation", + } + + +class Evaluation(ProcurementStep, Attestation, TargetDate): + __tablename__ = "procurement_evaluation" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_evaluation", + } + + +class PreAward(ProcurementStep, Attestation, TargetDate): + __tablename__ = "procurement_preaward" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_preaward", + } + + +class Award(ProcurementStep, Attestation): + __tablename__ = "procurement_award" + id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) + __mapper_args__ = { + "polymorphic_identity": "procurement_award", + } + vendor = sa.Column(sa.String, nullable=True) + vendor_type = sa.Column(sa.String, nullable=True) + financial_number = sa.Column(sa.String, nullable=True) diff --git a/backend/models/workflows.py b/backend/models/workflows.py deleted file mode 100644 index b3edce601e..0000000000 --- a/backend/models/workflows.py +++ /dev/null @@ -1,524 +0,0 @@ -"""Workflow models.""" - -from enum import Enum, auto -from typing import Optional - -import sqlalchemy as sa -from sqlalchemy import DateTime, ForeignKey, Integer, event -from sqlalchemy.dialects.postgresql import ENUM, JSONB -from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.ext.orderinglist import ordering_list -from sqlalchemy.orm import Mapped, mapped_column, object_session, relationship -from typing_extensions import Any, override - -from models import BaseModel - - -class WorkflowAction(Enum): - DRAFT_TO_PLANNED = 1 - PLANNED_TO_EXECUTING = 2 - GENERIC = 3 - PROCUREMENT_TRACKING = 4 - - -class WorkflowStepType(Enum): - APPROVAL = 1 - DOCUMENT_MGMT = 2 - VALIDATION = 3 - PROCUREMENT = 4 - ATTESTATION = 5 - - -class WorkflowStepStatus(Enum): - REVIEW = "In-Review" - APPROVED = "Approved" - REJECTED = "Rejected" - CHANGES = "Changes Required" - COMPLETE = "Complete" - - -class WorkflowTriggerType(Enum): - CAN = auto() - PROCUREMENT_SHOP = auto() - AGREEMENT = auto() - - -class WorkflowTemplate(BaseModel): - """Workflow structure without being tied to any specific real-world entity""" - - __tablename__ = "workflow_template" - id = BaseModel.get_pk_column() - name = sa.Column(sa.String, nullable=False) - steps = relationship( - "WorkflowStepTemplate", - backref="workflow_template", - order_by="WorkflowStepTemplate.index", - collection_class=ordering_list("index"), - ) - - @BaseModel.display_name.getter - def display_name(self): - return self.name - - -class WorkflowInstance(BaseModel): - """Main Workflow model. - It should be considered the top-level container for a workflows. - TODO: determine if this should be locked to a CAN, or Procurement Shop, both or - any other object that may require a workflow. For now, going to attempt a generic - approach with `associated_id` and `associated_type` fields. - """ - - __tablename__ = "workflow_instance" - - id = BaseModel.get_pk_column() - associated_id = sa.Column(sa.Integer, nullable=False) - associated_type = sa.Column( - sa.Enum(WorkflowTriggerType), nullable=False - ) # could use Enum based on the entities - workflow_template_id = sa.Column(sa.Integer, sa.ForeignKey("workflow_template.id")) - steps = relationship( - "WorkflowStepInstance", - backref="workflow_instance", - order_by="WorkflowStepInstance.index", - collection_class=ordering_list("index"), - ) - workflow_action = sa.Column(sa.Enum(WorkflowAction), nullable=False) - current_workflow_step_instance_id = sa.Column(sa.Integer, nullable=True) - - # REJECTED = "Rejected" (any --> Rejected) - # CHANGES = "Changes Required" (any --> Changes Required) - # REVIEW = "In-Review" (any --> In-Review) - # APPROVED = "Approved" (all --> Approved) - - @property - def workflow_status(self): - status_order = [ - WorkflowStepStatus.REJECTED, - WorkflowStepStatus.CHANGES, - WorkflowStepStatus.REVIEW, - ] - return next( - ( - status - for status in status_order - if any(item.status == status for item in self.steps) - ), - ( - WorkflowStepStatus.APPROVED - if all( - item.status == WorkflowStepStatus.APPROVED for item in self.steps - ) - else None - ), - ) - - @override - def to_dict(self) -> dict[str, Any]: # type: ignore[override] - d: dict[str, Any] = super().to_dict() # type: ignore[no-untyped-call] - - if isinstance(self.associated_type, str): - self.associated_type = WorkflowTriggerType[self.associated_type] - - d.update( - associated_type=self.associated_type.name if self.associated_type else None, - workflow_status=self.workflow_status.name if self.workflow_status else None, - workflow_action=self.workflow_action.name if self.workflow_action else None, - # package_entities = self.package_entities - ) - return d - - -class WorkflowStepTemplate(BaseModel): - """Step structure belonging to a WorkflowTemplate""" - - __tablename__ = "workflow_step_template" - - id = BaseModel.get_pk_column() - name = sa.Column(sa.String, nullable=False) - workflow_template_id = sa.Column(sa.Integer, sa.ForeignKey("workflow_template.id")) - workflow_type = sa.Column(sa.Enum(WorkflowStepType), nullable=False) - index = sa.Column(sa.Integer, nullable=False) - step_approvers = relationship("StepApprovers", backref="workflow_step_template") - - @override - def to_dict(self) -> dict[str, Any]: # type: ignore[override] - d: dict[str, Any] = super().to_dict() # type: ignore[no-untyped-call] - - d.update( - workflow_type=self.workflow_type.name if self.workflow_type else None, - ) - return d - - -class WorkflowStepInstance(BaseModel): - """Specific instance of a WorkflowStepTemplate - This is intended to be a one-to-many relationship between WorkflowsInstance and workflow steps. - This effectively outlines the steps in a workflow, and the order in which they are completed. - """ - - __tablename__ = "workflow_step_instance" - - id = BaseModel.get_pk_column() - workflow_instance_id = sa.Column(sa.Integer, sa.ForeignKey("workflow_instance.id")) - workflow_step_template_id = sa.Column( - sa.Integer, sa.ForeignKey("workflow_step_template.id") - ) - workflow_step_template = relationship( - "WorkflowStepTemplate", backref="workflow_step_instance" - ) - index = sa.Column(sa.Integer) - status = sa.Column(sa.Enum(WorkflowStepStatus), nullable=False) - notes = sa.Column(sa.String, nullable=True) - time_started = sa.Column(sa.DateTime, nullable=True) - time_completed = sa.Column(sa.DateTime, nullable=True) - updated_by = sa.Column(sa.Integer, sa.ForeignKey("ops_user.id"), nullable=True) - successor_dependencies = relationship( - "WorkflowStepDependency", - foreign_keys="WorkflowStepDependency.predecessor_step_id", - back_populates="predecessor_step", - cascade="all, delete-orphan", - ) - predecessor_dependencies = relationship( - "WorkflowStepDependency", - foreign_keys="WorkflowStepDependency.successor_step_id", - back_populates="successor_step", - cascade="all, delete-orphan", - ) - - @property - def approvers(self): - if self.workflow_step_template is None: - return None - return { - "users": [ - approver.user_id - for approver in self.workflow_step_template.step_approvers - if approver.user_id is not None - ], - "groups": [ - approver.group_id - for approver in self.workflow_step_template.step_approvers - if approver.group_id is not None - ], - "roles": [ - approver.role_id - for approver in self.workflow_step_template.step_approvers - if approver.role_id is not None - ], - } - - @property - def package_entities(self): - if object_session(self) is None: - return None - results = ( - object_session(self) - .execute( - sa.select(PackageSnapshot.bli_id, Package.notes) - .join(Package, Package.id == PackageSnapshot.package_id) - .join( - WorkflowInstance, - Package.workflow_instance_id == WorkflowInstance.id, - ) - .join( - WorkflowStepInstance, - WorkflowInstance.id == WorkflowStepInstance.workflow_instance_id, - ) - .where(WorkflowStepInstance.id == self.id) - ) - .all() - ) - bli_ids = [row[0] for row in results] - notes = results[0][1] if len(results) > 0 else None - return {"budget_line_item_ids": bli_ids, "notes": notes} - - @override - def to_dict(self) -> dict[str, Any]: # type: ignore[override] - d: dict[str, Any] = super().to_dict() # type: ignore[no-untyped-call] - - d.update( - status=self.status.name if self.status else None, - # TODO: format for these times? - time_started=str(self.time_started) if self.time_started else None, - time_completed=str(self.time_completed) if self.time_completed else None, - package_entities=self.package_entities, - approvers=self.approvers, - ) - return d - - -class WorkflowStepDependency(BaseModel): - """Association model to handle multiple dependencies between WorkflowStepInstances""" - - __tablename__ = "workflow_step_dependency" - predecessor_step_id = sa.Column( - sa.Integer, sa.ForeignKey("workflow_step_instance.id"), primary_key=True - ) - successor_step_id = sa.Column( - sa.Integer, sa.ForeignKey("workflow_step_instance.id"), primary_key=True - ) - predecessor_step = relationship( - "WorkflowStepInstance", - foreign_keys=[predecessor_step_id], - overlaps="predecessor_step_instance,successor_dependencies", - ) - successor_step = relationship( - "WorkflowStepInstance", - foreign_keys=[successor_step_id], - back_populates="predecessor_dependencies", - ) - - -class StepApprovers(BaseModel): - """Step Approvers model for WorkflowStepTemplates""" - - __tablename__ = "step_approvers" - id = BaseModel.get_pk_column() - workflow_step_template_id = sa.Column( - sa.Integer, sa.ForeignKey("workflow_step_template.id") - ) - user_id = sa.Column(sa.Integer, sa.ForeignKey("ops_user.id"), nullable=True) - role_id = sa.Column(sa.Integer, sa.ForeignKey("role.id"), nullable=True) - group_id = sa.Column(sa.Integer, sa.ForeignKey("group.id"), nullable=True) - - -class Package(BaseModel): - """Base package, used for sending groups of things around in a workflow""" - - __tablename__ = "package" - - id = BaseModel.get_pk_column() - submitter_id = sa.Column(sa.Integer, sa.ForeignKey("ops_user.id")) - workflow_instance_id = sa.Column( - sa.Integer, sa.ForeignKey("workflow_instance.id"), nullable=True - ) - notes = sa.Column(sa.String, nullable=True) - package_snapshots = relationship("PackageSnapshot", backref="package") - - @BaseModel.display_name.getter - def display_name(self): - return f"Package-{self.id}" - - -class PackageSnapshot(BaseModel): - __tablename__ = "package_snapshot" - id = BaseModel.get_pk_column() - # make package_id a read-only field - package_id = sa.Column(sa.Integer, sa.ForeignKey("package.id"), nullable=True) - version = sa.Column(sa.Integer, nullable=True) - # TODO: What should we do when we delete an Agreement (or a BLI)? - # This CASCADE fixes the existing Agreement delete, but leaves behind empty workflows - object_type = sa.Column(sa.String, nullable=True) - object_id = sa.Column(sa.Integer, nullable=True) - bli_id = sa.Column( - sa.Integer, - sa.ForeignKey("budget_line_item.id", ondelete="CASCADE"), - nullable=True, - ) - - -class ProcurementStep(BaseModel): - __tablename__ = "procurement_step" - - id = BaseModel.get_pk_column() - agreement_id = sa.Column(sa.Integer, sa.ForeignKey("agreement.id")) - # TODO: Q: should this be named workflow_step_instance_id (or alternatively leave off _instance in all FKs) - workflow_step_id = sa.Column(sa.Integer, sa.ForeignKey("workflow_step_instance.id")) - - type = sa.Column(sa.String, nullable=False) - __mapper_args__ = { - "polymorphic_identity": "procurement_step", - "polymorphic_on": type, - } - - -class Attestation(object): - is_complete = sa.Column(sa.Boolean, nullable=False, default=False) - actual_date = sa.Column(sa.Date, nullable=True) - completed_by = sa.Column(sa.Integer, sa.ForeignKey("ops_user.id"), nullable=True) - notes = sa.Column(sa.String, nullable=True) - - -class TargetDate(object): - target_date = sa.Column(sa.Date, nullable=True) - - -class AcquisitionPlanning(ProcurementStep, Attestation): - __tablename__ = "procurement_acquisition_planning" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_acquisition_planning", - } - - -class PreSolicitation(ProcurementStep, Attestation, TargetDate): - __tablename__ = "procurement_pre_solicitation" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_pre_solicitation", - } - # documents = relationship("PreSolicitationDocument", backref="pre_solicitation") - - -class Solicitation(ProcurementStep, Attestation, TargetDate): - __tablename__ = "procurement_solicitation" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_solicitation", - } - - -class Evaluation(ProcurementStep, Attestation, TargetDate): - __tablename__ = "procurement_evaluation" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_evaluation", - } - - -class PreAward(ProcurementStep, Attestation, TargetDate): - __tablename__ = "procurement_preaward" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_preaward", - } - - -class Award(ProcurementStep, Attestation): - __tablename__ = "procurement_award" - id = sa.Column(sa.Integer, sa.ForeignKey("procurement_step.id"), primary_key=True) - __mapper_args__ = { - "polymorphic_identity": "procurement_award", - } - vendor = sa.Column(sa.String, nullable=True) - vendor_type = sa.Column(sa.String, nullable=True) - financial_number = sa.Column(sa.String, nullable=True) - - -# ---=== CHANGE REQUESTS ===--- - - -class ChangeRequestStatus(Enum): - IN_REVIEW = auto() - APPROVED = auto() - REJECTED = auto() - - -class ChangeRequestType(Enum): - CHANGE_REQUEST = auto() - AGREEMENT_CHANGE_REQUEST = auto() - BUDGET_LINE_ITEM_CHANGE_REQUEST = auto() - - -class ChangeRequest(BaseModel): - __tablename__ = "change_request" - id: Mapped[int] = mapped_column(Integer, primary_key=True) - change_request_type: Mapped[ChangeRequestType] = mapped_column( - ENUM(ChangeRequestType) - ) - - # agreement_type: Mapped[AgreementType] = mapped_column(ENUM(AgreementType)) - status: Mapped[ChangeRequestStatus] = mapped_column( - ENUM(ChangeRequestStatus), nullable=False, default=ChangeRequestStatus.IN_REVIEW - ) - requested_change_data: Mapped[JSONB] = mapped_column(JSONB) - requested_change_diff: Mapped[Optional[JSONB]] = mapped_column(JSONB) - requested_change_info: Mapped[Optional[JSONB]] = mapped_column(JSONB) - # BaseModel.created_by is the requestor, so there's no need for another column for that - requestor_notes: Mapped[Optional[str]] = mapped_column(sa.String) - - managing_division_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("division.id") - ) - managing_division = relationship( - "Division", - passive_deletes=True, - ) - reviewed_by_id: Mapped[Optional[int]] = mapped_column(ForeignKey("ops_user.id")) - reviewed_on: Mapped[Optional[DateTime]] = mapped_column(DateTime) - reviewer_notes: Mapped[Optional[str]] = mapped_column(sa.String) - - __mapper_args__ = { - "polymorphic_on": "change_request_type", - "polymorphic_identity": ChangeRequestType.CHANGE_REQUEST, - } - - -class AgreementChangeRequest(ChangeRequest): - # if this isn't optional here, SQL will make the column non-nullable - agreement_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("agreement.id", ondelete="CASCADE") - ) - agreement = relationship( - "Agreement", - passive_deletes=True, - ) - - __mapper_args__ = { - "polymorphic_identity": ChangeRequestType.AGREEMENT_CHANGE_REQUEST, - } - - budget_field_names = ["awarding_entity_id"] - - @hybrid_property - def has_budget_change(self): - return any(key in self.requested_change_data for key in self.budget_field_names) - - @has_budget_change.expression - def has_budget_change(cls): - return cls.requested_change_data.has_any(cls.budget_field_names) - - -class BudgetLineItemChangeRequest(AgreementChangeRequest): - budget_line_item_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("budget_line_item.id", ondelete="CASCADE") - ) - budget_line_item = relationship( - "BudgetLineItem", - passive_deletes=True, - ) - - __mapper_args__ = { - "polymorphic_identity": ChangeRequestType.BUDGET_LINE_ITEM_CHANGE_REQUEST, - } - - budget_field_names = ["amount", "can_id", "date_needed"] - - @hybrid_property - def has_budget_change(self): - return any(key in self.requested_change_data for key in self.budget_field_names) - - @has_budget_change.expression - def has_budget_change(cls): - return cls.requested_change_data.has_any(cls.budget_field_names) - - @hybrid_property - def has_status_change(self): - return "status" in self.requested_change_data - - @has_status_change.expression - def has_status_change(cls): - return cls.requested_change_data.has_key("status") - - -# require agreement_id for Agreement changes. -# (It has to be Optional in the model to keep the column nullable for other types) -@event.listens_for(AgreementChangeRequest, "before_insert") -@event.listens_for(AgreementChangeRequest, "before_update") -@event.listens_for(BudgetLineItemChangeRequest, "before_insert") -@event.listens_for(BudgetLineItemChangeRequest, "before_update") -def check_agreement_id(mapper, connection, target): - if target.agreement_id is None: - raise ValueError("agreement_id is required for AgreementChangeRequest") - - -# require budget_line_item_id for BLI changes. -# (It has to be Optional in the model to keep the column nullable for other types) -@event.listens_for(BudgetLineItemChangeRequest, "before_insert") -@event.listens_for(BudgetLineItemChangeRequest, "before_update") -def check_budget_line_id(mapper, connection, target): - if target.budget_line_item_id is None: - raise ValueError( - "budget_line_item_id is required for BudgetLineItemChangeRequest" - ) diff --git a/backend/openapi.yml b/backend/openapi.yml index 40748ecd2a..3b1901b396 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -2041,7 +2041,7 @@ components: project_officer_id: type: integer example: 1 - procurement_tracker_workflow_id: + procurement_tracker_id: type: integer example: 1 created_on: diff --git a/backend/ops_api/ops/resources/change_requests.py b/backend/ops_api/ops/resources/change_requests.py index 9fde17ed01..407678b002 100644 --- a/backend/ops_api/ops/resources/change_requests.py +++ b/backend/ops_api/ops/resources/change_requests.py @@ -14,7 +14,7 @@ from ops_api.ops.resources.budget_line_items import validate_and_prepare_change_data from ops_api.ops.schemas.budget_line_items import PATCHRequestBodySchema from ops_api.ops.schemas.change_requests import GenericChangeRequestResponseSchema -from ops_api.ops.utils import procurement_workflow_helper +from ops_api.ops.utils import procurement_tracker_helper from ops_api.ops.utils.change_requests import create_notification_of_reviews_request_to_submitter from ops_api.ops.utils.response import make_response_with_headers @@ -47,13 +47,13 @@ def review_change_request( change_request.status = status_after_review change_request.reviewer_notes = reviewer_notes session.add(change_request) - should_create_procurement_workflow = False + should_create_procurement_tracker = False # If approved, then apply the changes if status_after_review == ChangeRequestStatus.APPROVED: if isinstance(change_request, BudgetLineItemChangeRequest): budget_line_item = session.get(BudgetLineItem, change_request.budget_line_item_id) - should_create_procurement_workflow = ( + should_create_procurement_tracker = ( change_request.has_status_change and change_request.requested_change_data["status"] == "IN_EXECUTION" ) # need to copy to avoid changing the original data in the ChangeRequest and triggering an update @@ -79,8 +79,8 @@ def review_change_request( create_notification_of_reviews_request_to_submitter(change_request) - if should_create_procurement_workflow: - procurement_workflow_helper.create_procurement_workflow(change_request.agreement_id) + if should_create_procurement_tracker: + procurement_tracker_helper.create_procurement_tracker(change_request.agreement_id) return change_request diff --git a/backend/ops_api/ops/resources/package.py b/backend/ops_api/ops/resources/package.py deleted file mode 100644 index 279a55bace..0000000000 --- a/backend/ops_api/ops/resources/package.py +++ /dev/null @@ -1,44 +0,0 @@ -from flask import Response - -from models.base import BaseModel -from ops_api.ops.auth.auth_types import Permission, PermissionType -from ops_api.ops.auth.decorators import is_authorized -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI - -ENDPOINT_STRING = "/package" - - -class PackageItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class PackageListAPI(BaseListAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() - - -class PackageSnapshotItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class PackageSnapshotListAPI(BaseListAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/resources/package_snapshot.py b/backend/ops_api/ops/resources/package_snapshot.py deleted file mode 100644 index 2861f41769..0000000000 --- a/backend/ops_api/ops/resources/package_snapshot.py +++ /dev/null @@ -1,25 +0,0 @@ -from flask import Response - -from models.base import BaseModel -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI -from ops_api.ops.utils.auth import Permission, PermissionType, is_authorized - -ENDPOINT_STRING = "/package-snapshot" - - -class PackageSnapshotItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class PackageSnapshotListAPI(BaseListAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/resources/procurement_steps.py b/backend/ops_api/ops/resources/procurement_steps.py index a7424e694d..1908a37ea7 100644 --- a/backend/ops_api/ops/resources/procurement_steps.py +++ b/backend/ops_api/ops/resources/procurement_steps.py @@ -8,7 +8,7 @@ from models import Agreement, OpsEventType from models.base import BaseModel -from models.workflows import ( +from models.procurement_tracker import ( AcquisitionPlanning, Award, Evaluation, diff --git a/backend/ops_api/ops/resources/workflow_instance.py b/backend/ops_api/ops/resources/workflow_instance.py deleted file mode 100644 index 789eac20e8..0000000000 --- a/backend/ops_api/ops/resources/workflow_instance.py +++ /dev/null @@ -1,95 +0,0 @@ -from dataclasses import dataclass, field -from datetime import datetime -from typing import Optional - -import marshmallow_dataclass as mmdc -from flask import Response -from flask.views import MethodView -from marshmallow import fields -from marshmallow_enum import EnumField - -from models.base import BaseModel -from models.workflows import ( - WorkflowAction, - WorkflowInstance, - WorkflowStepDependency, - WorkflowStepStatus, - WorkflowTriggerType, -) -from ops_api.ops.auth.auth_types import Permission, PermissionType -from ops_api.ops.auth.decorators import is_authorized -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI -from ops_api.ops.resources.workflow_step_instance import WorkflowStepInstanceResponse - -ENDPOINT_STRING = "/workflow-instance" - - -@dataclass -class WorkflowInstanceResponse: - id: int - associated_id: Optional[int] = None - associated_type: Optional[WorkflowTriggerType] = EnumField(WorkflowTriggerType) - workflow_template_id: Optional[int] = None - steps: Optional[list[WorkflowStepInstanceResponse]] = fields.List( - fields.Nested(WorkflowStepInstanceResponse), default=[] - ) - workflow_action: Optional[WorkflowAction] = EnumField(WorkflowAction) - current_workflow_step_instance_id: Optional[int] = None - workflow_status: Optional[WorkflowStepStatus] = EnumField(WorkflowStepStatus) - created_on: datetime = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - updated_on: datetime = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - created_by: Optional[int] = None - package_entities: Optional[list[int]] = None - - -# Workflows Metadata Endpoings -class WorkflowTriggerTypeListAPI(MethodView): - @is_authorized(PermissionType.GET, Permission.AGREEMENT) - def get(self) -> Response: - reasons = [item.name for item in WorkflowTriggerType] - return reasons - - -class WorkflowActionListAPI(MethodView): - @is_authorized(PermissionType.GET, Permission.AGREEMENT) - def get(self) -> Response: - reasons = [item.name for item in WorkflowAction] - return reasons - - -class WorkflowStatusListAPI(MethodView): - @is_authorized(PermissionType.GET, Permission.AGREEMENT) - def get(self) -> Response: - reasons = [item.name for item in WorkflowStepStatus] - return reasons - - -class WorkflowStepDependencyListAPI(MethodView): - @is_authorized(PermissionType.GET, Permission.AGREEMENT) - def get(self) -> Response: - reasons = [item.name for item in WorkflowStepDependency] - return reasons - - -# Workflows Endpoints -class WorkflowInstanceItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel = WorkflowInstance): - super().__init__(model) - # self._response_schema = desert.schema(WorkflowInstanceResponse) - self._response_schema = mmdc.class_schema(WorkflowInstanceResponse)() - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class WorkflowInstanceListAPI(BaseListAPI): - def __init__(self, model: BaseModel = WorkflowInstance): - super().__init__(model) - # self._post_schema = desert.schema(RequestBody) # TODO implement - # self._response_schema = desert.schema(WorkflowInstanceResponse) - self._response_schema = mmdc.class_schema(WorkflowInstanceResponse)() - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/resources/workflow_step_instance.py b/backend/ops_api/ops/resources/workflow_step_instance.py deleted file mode 100644 index 5ee2a06398..0000000000 --- a/backend/ops_api/ops/resources/workflow_step_instance.py +++ /dev/null @@ -1,49 +0,0 @@ -from dataclasses import dataclass, field -from datetime import datetime -from typing import Optional - -from flask import Response -from marshmallow_enum import EnumField - -from models.base import BaseModel -from models.workflows import WorkflowStepInstance, WorkflowStepStatus -from ops_api.ops.auth.auth_types import Permission, PermissionType -from ops_api.ops.auth.decorators import is_authorized -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI - -ENDPOINT_STRING = "/workflow-step-instance" - - -@dataclass -class WorkflowStepInstanceResponse: - id: int - workflow_instance_id: Optional[int] = None - workflow_step_template_id: Optional[int] = None - status: Optional[WorkflowStepStatus] = EnumField(WorkflowStepStatus) - notes: Optional[str] = None - time_started: Optional[datetime] = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - time_completed: Optional[datetime] = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - created_on: datetime = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - updated_on: datetime = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) - created_by: Optional[int] = None - # approvers: Optional[list[union[User, Group, Role]]] = fields.List(fields.Nested(User, Group, Role)) - - -class WorkflowStepInstanceItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel = WorkflowStepInstance): - super().__init__(model) - # self._response_schema = desert.schema(WorkflowStepInstanceResponse) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class WorkflowStepInstanceListAPI(BaseListAPI): - def __init__(self, model: BaseModel = WorkflowStepInstance): - super().__init__(model) - # self._response_schema = desert.schema(WorkflowStepInstanceResponse) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/resources/workflow_step_template.py b/backend/ops_api/ops/resources/workflow_step_template.py deleted file mode 100644 index b3e0632ad5..0000000000 --- a/backend/ops_api/ops/resources/workflow_step_template.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Response - -from models.base import BaseModel -from ops_api.ops.auth.auth_types import Permission, PermissionType -from ops_api.ops.auth.decorators import is_authorized -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI - -ENDPOINT_STRING = "/workflow-step-template" - - -class WorkflowStepTemplateItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class WorkflowStepTemplateListAPI(BaseListAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/resources/workflow_template.py b/backend/ops_api/ops/resources/workflow_template.py deleted file mode 100644 index ab98449685..0000000000 --- a/backend/ops_api/ops/resources/workflow_template.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Response - -from models.base import BaseModel -from ops_api.ops.auth.auth_types import Permission, PermissionType -from ops_api.ops.auth.decorators import is_authorized -from ops_api.ops.base_views import BaseItemAPI, BaseListAPI - -ENDPOINT_STRING = "/workflow-template" - - -class WorkflowTemplateItemAPI(BaseItemAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self, id: int) -> Response: - return self._get_item_with_try(id) - - -class WorkflowTemplateListAPI(BaseListAPI): - def __init__(self, model: BaseModel): - super().__init__(model) - - @is_authorized(PermissionType.GET, Permission.WORKFLOW) - def get(self) -> Response: - return super().get() diff --git a/backend/ops_api/ops/schemas/agreements.py b/backend/ops_api/ops/schemas/agreements.py index 60d9db0d7f..e662df87e9 100644 --- a/backend/ops_api/ops/schemas/agreements.py +++ b/backend/ops_api/ops/schemas/agreements.py @@ -29,7 +29,7 @@ class AgreementData(Schema): project_id = fields.Integer(allow_none=True) awarding_entity_id = fields.Integer(allow_none=True) notes = fields.String(allow_none=True) - procurement_tracker_workflow_id = fields.Integer(allow_none=True) + procurement_tracker_id = fields.Integer(allow_none=True) class ContractAgreementData(AgreementData): diff --git a/backend/ops_api/ops/schemas/procurement_steps.py b/backend/ops_api/ops/schemas/procurement_steps.py index be7319f683..6108c0cde7 100644 --- a/backend/ops_api/ops/schemas/procurement_steps.py +++ b/backend/ops_api/ops/schemas/procurement_steps.py @@ -7,7 +7,7 @@ class ProcurementStepRequest: type: Optional[str] = None agreement_id: Optional[int] = None - workflow_step_id: Optional[int] = None + procurement_tracker_id: Optional[int] = None # From BaseModel display_name: Optional[str] = None created_on: datetime = field(default=None, metadata={"format": "%Y-%m-%dT%H:%M:%S.%fZ"}) diff --git a/backend/ops_api/ops/urls.py b/backend/ops_api/ops/urls.py index 346d7e607e..e8ca3a02ea 100644 --- a/backend/ops_api/ops/urls.py +++ b/backend/ops_api/ops/urls.py @@ -27,10 +27,6 @@ NOTIFICATIONS_ITEM_API_VIEW_FUNC, NOTIFICATIONS_LIST_API_VIEW_FUNC, OPS_DB_HISTORY_LIST_API_VIEW_FUNC, - PACKAGE_ITEM_API_VIEW_FUNC, - PACKAGE_LIST_API_VIEW_FUNC, - PACKAGE_SNAPSHOT_ITEM_API_VIEW_FUNC, - PACKAGE_SNAPSHOT_LIST_API_VIEW_FUNC, PORTFOLIO_CALCULATE_FUNDING_API_VIEW_FUNC, PORTFOLIO_CANS_API_VIEW_FUNC, PORTFOLIO_FUNDING_SUMMARY_ITEM_API_VIEW_FUNC, @@ -60,14 +56,6 @@ USERS_ITEM_API_VIEW_FUNC, USERS_LIST_API_VIEW_FUNC, VERSION_API_VIEW_FUNC, - WORKFLOW_INSTANCE_ITEM_API_VIEW_FUNC, - WORKFLOW_INSTANCE_LIST_API_VIEW_FUNC, - WORKFLOW_STEP_INSTANCE_ITEM_API_VIEW_FUNC, - WORKFLOW_STEP_INSTANCE_LIST_API_VIEW_FUNC, - WORKFLOW_STEP_TEMPLATE_ITEM_API_VIEW_FUNC, - WORKFLOW_STEP_TEMPLATE_LIST_API_VIEW_FUNC, - WORKFLOW_TEMPLATE_ITEM_API_VIEW_FUNC, - WORKFLOW_TEMPLATE_LIST_API_VIEW_FUNC, ) # Ideas from Flask docs: https://flask.palletsprojects.com/en/2.2.x/views/#method-dispatching-and-apis @@ -134,23 +122,6 @@ def register_api(api_bp: Blueprint) -> None: view_func=BUDGET_LINE_ITEMS_LIST_API_VIEW_FUNC, ) - api_bp.add_url_rule( - "/packages/", - view_func=PACKAGE_ITEM_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/packages/", - view_func=PACKAGE_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/package-snapshots/", - view_func=PACKAGE_SNAPSHOT_ITEM_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/package-snapshots/", - view_func=PACKAGE_SNAPSHOT_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( "/procurement-shops/", view_func=PROCUREMENT_SHOPS_ITEM_API_VIEW_FUNC, @@ -276,38 +247,6 @@ def register_api(api_bp: Blueprint) -> None: view_func=NOTIFICATIONS_ITEM_API_VIEW_FUNC, ) - api_bp.add_url_rule( - "/workflow-instance/", - view_func=WORKFLOW_INSTANCE_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-instance/", - view_func=WORKFLOW_INSTANCE_ITEM_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-step-instance/", - view_func=WORKFLOW_STEP_INSTANCE_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-step-instance/", - view_func=WORKFLOW_STEP_INSTANCE_ITEM_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-template/", - view_func=WORKFLOW_TEMPLATE_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-template/", - view_func=WORKFLOW_TEMPLATE_ITEM_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-step-template/", - view_func=WORKFLOW_STEP_TEMPLATE_LIST_API_VIEW_FUNC, - ) - api_bp.add_url_rule( - "/workflow-step-template/", - view_func=WORKFLOW_STEP_TEMPLATE_ITEM_API_VIEW_FUNC, - ) api_bp.add_url_rule( "/services-components/", view_func=SERVICES_COMPONENT_ITEM_API_VIEW_FUNC, diff --git a/backend/ops_api/ops/utils/procurement_tracker_helper.py b/backend/ops_api/ops/utils/procurement_tracker_helper.py new file mode 100644 index 0000000000..9f48218425 --- /dev/null +++ b/backend/ops_api/ops/utils/procurement_tracker_helper.py @@ -0,0 +1,61 @@ +from flask import current_app + +from models import Agreement +from models.procurement_tracker import ( + AcquisitionPlanning, + Award, + Evaluation, + PreAward, + PreSolicitation, + ProcurementTracker, + Solicitation, +) + +procurement_step_classes = [ + AcquisitionPlanning, + PreSolicitation, + Solicitation, + Evaluation, + PreAward, + Award, +] + + +def create_procurement_tracker(agreement_id) -> ProcurementTracker: + session = current_app.db_session + agreement = session.get(Agreement, agreement_id) + if not agreement: + raise ValueError("Invalid Agreement ID") + + # if it already exists, just return it + if agreement.procurement_tracker_id: + return session.get(ProcurementTracker, agreement.procurement_tracker_id) + + procurement_tracker = ProcurementTracker(agreement_id=agreement_id) + session.add(procurement_tracker) + session.commit() + + for procurement_step_class in procurement_step_classes: + proc_step = procurement_step_class() + proc_step.agreement_id = agreement_id + proc_step.procurement_tracker = procurement_tracker + session.add(proc_step) + session.commit() + assert proc_step.id + + return procurement_tracker + + +def delete_procurement_tracker(agreement_id): + session = current_app.db_session + agreement = session.get(Agreement, agreement_id) + if not agreement.procurement_tracker_id: + return + + proc_tracker = session.get(ProcurementTracker, agreement.procurement_tracker_id) + # remove procurement steps + for proc_step in proc_tracker.procurement_steps: + session.delete(proc_step) + # remove procurement tracker + session.delete(proc_tracker) + session.commit() diff --git a/backend/ops_api/ops/utils/procurement_workflow_helper.py b/backend/ops_api/ops/utils/procurement_workflow_helper.py deleted file mode 100644 index b05da18f16..0000000000 --- a/backend/ops_api/ops/utils/procurement_workflow_helper.py +++ /dev/null @@ -1,157 +0,0 @@ -from datetime import datetime - -from flask import current_app -from sqlalchemy import select - -# from flask_jwt_extended import verify_jwt_in_request -from models import ( - AcquisitionPlanning, - Agreement, - Award, - Evaluation, - Package, - PackageSnapshot, - PreAward, - PreSolicitation, - ProcurementStep, - Solicitation, - WorkflowAction, - WorkflowInstance, - WorkflowStepInstance, - WorkflowStepStatus, - WorkflowStepTemplate, - WorkflowTemplate, - WorkflowTriggerType, -) - -# from sqlalchemy import select - -# from ops_api.ops.auth.utils import get_user_from_sub - -PROCUREMENT_WORKFLOW_TEMPLATE_NAME = "Procurement Tracker" - -workflow_step_to_procurement_class_map = { - "Acquisition Planning": AcquisitionPlanning, - "Pre-Solicitation": PreSolicitation, - "Solicitation": Solicitation, - # ISSUE: can't have two workflow steps go the same procurement step (ProcurementStep.workflow_step_id) - # "Evaluation Approval": Evaluation, - "Evaluation Attestation": Evaluation, - "Pre-Award": PreAward, - "Award": Award, -} - - -def get_procurement_workflow_template() -> WorkflowTemplate: - procurement_workflow_template = ( - current_app.db_session.query(WorkflowTemplate).filter_by(name=PROCUREMENT_WORKFLOW_TEMPLATE_NAME).first() - ) - return procurement_workflow_template - - -def create_procurement_workflow(agreement_id) -> WorkflowInstance: - session = current_app.db_session - agreement = session.get(Agreement, agreement_id) - if not agreement: - raise ValueError("Invalid Agreement ID") - - # if it already exists, just return it - if agreement.procurement_tracker_workflow_id: - return session.get(WorkflowInstance, agreement.procurement_tracker_workflow_id) - - user_id = None - # TODO: How to get user when there might not be a request (in testing, etc) - # token = verify_jwt_in_request() - # user = get_user_from_sub(token[1]) - - workflow_template = get_procurement_workflow_template() - - workflow_instance = WorkflowInstance() - workflow_instance.workflow_template_id = workflow_template.id - workflow_instance.associated_id = agreement_id - workflow_instance.associated_type = WorkflowTriggerType.AGREEMENT - workflow_instance.workflow_action = WorkflowAction.PROCUREMENT_TRACKING - workflow_instance.created_by = user_id - - session.add(workflow_instance) - # This fails since no ID before save - # assert workflow_instance.id is not None - session.commit() - assert workflow_instance.id - - # package and snapshot - package = Package() - package.submitter_id = user_id - package.workflow_instance_id = workflow_instance.id - # package.notes = "" - session.add(package) - session.commit() - assert package.id - package_snapshot = PackageSnapshot() - package_snapshot.package_id = package.id - package_snapshot.object_type = "AGREEMENT" - package_snapshot.object_id = agreement_id - session.add(package_snapshot) - session.commit() - assert package_snapshot.id - - workflow_step_template: WorkflowStepTemplate - for workflow_step_template in workflow_template.steps: - # workflow step - workflow_step_instance = WorkflowStepInstance() - workflow_step_instance.workflow_instance_id = workflow_instance.id - workflow_step_instance.workflow_step_template_id = workflow_step_template.id - workflow_step_instance.index = workflow_step_template.index - workflow_step_instance.status = WorkflowStepStatus.REVIEW # ??? - # workflow_step_instance.notes = "" - workflow_step_instance.time_started = datetime.now() - workflow_step_instance.created_by = user_id - session.add(workflow_step_instance) - session.commit() - assert workflow_step_instance.id - - # procurement step - procurement_step_class = workflow_step_to_procurement_class_map.get(workflow_step_template.name, None) - if procurement_step_class is not None: - proc_step: ProcurementStep = procurement_step_class() - proc_step.agreement_id = agreement_id - proc_step.workflow_step_id = workflow_step_instance.id - proc_step.created_by = user_id - session.add(proc_step) - session.commit() - assert proc_step.id - - return workflow_instance - - -def delete_procurement_workflow(agreement_id): - session = current_app.db_session - agreement = session.get(Agreement, agreement_id) - - # remove procurement steps - stmt = select(ProcurementStep).where(ProcurementStep.agreement_id == agreement_id) - procurement_step_results = session.execute(stmt).all() - procurement_steps = [p[0] for p in procurement_step_results] - procurement_step: ProcurementStep - for procurement_step in procurement_steps: - session.delete(procurement_step) - - # remove workflow, it's steps, packages, and package snapshots (Should there be more cascading for this?) - workflow_id = agreement.procurement_tracker_workflow_id - if workflow_id is not None: - workflow_instance = session.get(WorkflowInstance, workflow_id) - for workflow_step in workflow_instance.steps: - session.delete(workflow_step) - stmt = select(Package).where(Package.workflow_instance_id == workflow_instance.id) - package_results = session.execute(stmt).all() - packages = [p[0] for p in package_results] - for package in packages: - stmt = select(PackageSnapshot).where(PackageSnapshot.package_id == package.id) - snapshot_results = session.execute(stmt).all() - package_snapshots = [p[0] for p in snapshot_results] - for package_snapshot in package_snapshots: - session.delete(package_snapshot) - session.delete(package) - session.delete(workflow_instance) - - session.commit() diff --git a/backend/ops_api/ops/views.py b/backend/ops_api/ops/views.py index 48e965012b..5580d48955 100644 --- a/backend/ops_api/ops/views.py +++ b/backend/ops_api/ops/views.py @@ -9,27 +9,21 @@ ProductServiceCode, ServicesComponent, ) +from models.change_requests import ChangeRequest from models.history import OpsDBHistory from models.portfolios import Division, Portfolio, PortfolioStatus from models.procurement_shops import ProcurementShop -from models.projects import ResearchProject, ResearchType -from models.users import User -from models.workflows import ( +from models.procurement_tracker import ( AcquisitionPlanning, Award, - ChangeRequest, Evaluation, - Package, - PackageSnapshot, PreAward, PreSolicitation, ProcurementStep, Solicitation, - WorkflowInstance, - WorkflowStepInstance, - WorkflowStepTemplate, - WorkflowTemplate, ) +from models.projects import ResearchProject, ResearchType +from models.users import User from ops_api.ops.resources.administrative_and_support_projects import ( AdministrativeAndSupportProjectItemAPI, AdministrativeAndSupportProjectListAPI, @@ -52,7 +46,6 @@ from ops_api.ops.resources.health_check import HealthCheckAPI from ops_api.ops.resources.history import OpsDBHistoryListAPI from ops_api.ops.resources.notifications import NotificationItemAPI, NotificationListAPI -from ops_api.ops.resources.package import PackageItemAPI, PackageListAPI, PackageSnapshotItemAPI, PackageSnapshotListAPI from ops_api.ops.resources.portfolio_calculate_funding import PortfolioCalculateFundingAPI from ops_api.ops.resources.portfolio_cans import PortfolioCansAPI from ops_api.ops.resources.portfolio_funding_summary import PortfolioFundingSummaryItemAPI @@ -74,9 +67,6 @@ from ops_api.ops.resources.research_type import ResearchTypeListAPI from ops_api.ops.resources.services_component import ServicesComponentItemAPI, ServicesComponentListAPI from ops_api.ops.resources.users import UsersItemAPI, UsersListAPI -from ops_api.ops.resources.workflow_instance import WorkflowInstanceItemAPI, WorkflowInstanceListAPI -from ops_api.ops.resources.workflow_step_template import WorkflowStepTemplateItemAPI, WorkflowStepTemplateListAPI -from ops_api.ops.resources.workflow_template import WorkflowTemplateItemAPI, WorkflowTemplateListAPI from ops_api.ops.utils.version import VersionAPI # AGREEMENT ENDPOINTS @@ -114,12 +104,6 @@ BUDGET_LINE_ITEMS_ITEM_API_VIEW_FUNC = BudgetLineItemsItemAPI.as_view("budget-line-items-item", BudgetLineItem) BUDGET_LINE_ITEMS_LIST_API_VIEW_FUNC = BudgetLineItemsListAPI.as_view("budget-line-items-group", BudgetLineItem) -# PACKAGE ENDPOINTS -PACKAGE_ITEM_API_VIEW_FUNC = PackageItemAPI.as_view("package-item", Package) -PACKAGE_LIST_API_VIEW_FUNC = PackageListAPI.as_view("package-group", Package) - -PACKAGE_SNAPSHOT_ITEM_API_VIEW_FUNC = PackageSnapshotItemAPI.as_view("package-snapshot-item", PackageSnapshot) -PACKAGE_SNAPSHOT_LIST_API_VIEW_FUNC = PackageSnapshotListAPI.as_view("package-snapshot-group", PackageSnapshot) # PRODUCT SERVICE CODES ENDPOINTS PRODUCT_SERVICE_CODE_ITEM_API_VIEW_FUNC = ProductServiceCodeItemAPI.as_view( @@ -189,28 +173,6 @@ NOTIFICATIONS_ITEM_API_VIEW_FUNC = NotificationItemAPI.as_view("notifications-item", Notification) NOTIFICATIONS_LIST_API_VIEW_FUNC = NotificationListAPI.as_view("notifications-group", Notification) -# WORKFLOW INSTANCE ENDPOINTS -WORKFLOW_INSTANCE_ITEM_API_VIEW_FUNC = WorkflowInstanceItemAPI.as_view("workflow-instance-item", WorkflowInstance) -WORKFLOW_INSTANCE_LIST_API_VIEW_FUNC = WorkflowInstanceListAPI.as_view("workflow-instance-group", WorkflowInstance) - -# WORKFLOW STEP INSTANCE ENDPOINTS -WORKFLOW_STEP_INSTANCE_ITEM_API_VIEW_FUNC = WorkflowInstanceItemAPI.as_view( - "workflow-step-instance-item", WorkflowStepInstance -) -WORKFLOW_STEP_INSTANCE_LIST_API_VIEW_FUNC = WorkflowInstanceListAPI.as_view( - "workflow-step-instance-group", WorkflowStepInstance -) - -WORKFLOW_TEMPLATE_LIST_API_VIEW_FUNC = WorkflowTemplateListAPI.as_view("workflow-template-group", WorkflowTemplate) -WORKFLOW_TEMPLATE_ITEM_API_VIEW_FUNC = WorkflowTemplateItemAPI.as_view("workflow-template-item", WorkflowTemplate) - -WORKFLOW_STEP_TEMPLATE_ITEM_API_VIEW_FUNC = WorkflowStepTemplateItemAPI.as_view( - "workflow-step-template-item", WorkflowStepTemplate -) -WORKFLOW_STEP_TEMPLATE_LIST_API_VIEW_FUNC = WorkflowStepTemplateListAPI.as_view( - "workflow-step-template-group", WorkflowStepTemplate -) - # ServicesComponent ENDPOINTS SERVICES_COMPONENT_ITEM_API_VIEW_FUNC = ServicesComponentItemAPI.as_view("services-component-item", ServicesComponent) SERVICES_COMPONENT_LIST_API_VIEW_FUNC = ServicesComponentListAPI.as_view("services-component-group", ServicesComponent) diff --git a/backend/ops_api/tests/docker-compose.yml b/backend/ops_api/tests/docker-compose.yml index 9894eea387..95cc9c6cac 100644 --- a/backend/ops_api/tests/docker-compose.yml +++ b/backend/ops_api/tests/docker-compose.yml @@ -41,8 +41,7 @@ services: DATA=./data_tools/data/research_project_data.json5 python ./data_tools/src/import_static_data/import_data.py && DATA=./data_tools/data/can_data.json5 python ./data_tools/src/import_static_data/import_data.py && DATA=./data_tools/data/first_contract_data.json5 python ./data_tools/src/import_static_data/import_data.py && - DATA=./data_tools/data/agreements_and_blin_data.json5 python ./data_tools/src/import_static_data/import_data.py && - DATA=./data_tools/data/workflow_data.json5 python ./data_tools/src/import_static_data/import_data.py + DATA=./data_tools/data/agreements_and_blin_data.json5 python ./data_tools/src/import_static_data/import_data.py " depends_on: diff --git a/backend/ops_api/tests/ops/agreement/test_agreement.py b/backend/ops_api/tests/ops/agreement/test_agreement.py index bbb83f4eb9..ba6100bf31 100644 --- a/backend/ops_api/tests/ops/agreement/test_agreement.py +++ b/backend/ops_api/tests/ops/agreement/test_agreement.py @@ -17,7 +17,7 @@ def test_agreement_retrieve(loaded_db): assert agreement.display_name == agreement.name assert agreement.id == 1 assert agreement.agreement_type.name == "CONTRACT" - assert agreement.procurement_tracker_workflow_id is None + assert agreement.procurement_tracker_id is None @pytest.mark.usefixtures("app_ctx") @@ -49,8 +49,8 @@ def test_agreements_get_by_id(auth_client, loaded_db): response = auth_client.get(url_for("api.agreements-item", id=1)) assert response.status_code == 200 assert response.json["name"] == "Contract #1: African American Child and Family Research Center" - assert "procurement_tracker_workflow_id" in response.json - assert response.json["procurement_tracker_workflow_id"] is None + assert "procurement_tracker_id" in response.json + assert response.json["procurement_tracker_id"] is None assert "budget_line_items" in response.json assert "can_id" in response.json["budget_line_items"][0] assert "can" in response.json["budget_line_items"][0] diff --git a/backend/ops_api/tests/ops/utils/test_procurement_tracker_helper.py b/backend/ops_api/tests/ops/utils/test_procurement_tracker_helper.py new file mode 100644 index 0000000000..aa10256986 --- /dev/null +++ b/backend/ops_api/tests/ops/utils/test_procurement_tracker_helper.py @@ -0,0 +1,39 @@ +import pytest + +from models import Agreement +from models.procurement_tracker import ( + AcquisitionPlanning, + Award, + Evaluation, + PreAward, + PreSolicitation, + ProcurementTracker, + Solicitation, +) +from ops_api.ops.utils.procurement_tracker_helper import create_procurement_tracker, delete_procurement_tracker + + +@pytest.mark.usefixtures("app_ctx", "loaded_db") +def test_create_procurement_tracker(loaded_db, auth_client): + test_agreement_id = 1 + procurement_tracker: ProcurementTracker = create_procurement_tracker(test_agreement_id) + assert procurement_tracker.id is not None + procurement_tracker_id = procurement_tracker.id + + steps = procurement_tracker.procurement_steps + assert len(steps) == 6 + assert steps[0].__class__ == AcquisitionPlanning + assert steps[1].__class__ == PreSolicitation + assert steps[2].__class__ == Solicitation + assert steps[3].__class__ == Evaluation + assert steps[4].__class__ == PreAward + assert steps[5].__class__ == Award + + # delete workflow + delete_procurement_tracker(test_agreement_id) + + # verify removal + agreement = loaded_db.get(Agreement, test_agreement_id) + assert agreement.procurement_tracker_id is None + procurement_tracker = loaded_db.get(ProcurementTracker, procurement_tracker_id) + assert procurement_tracker is None diff --git a/backend/ops_api/tests/ops/utils/test_procurement_workflow_helper.py b/backend/ops_api/tests/ops/utils/test_procurement_workflow_helper.py deleted file mode 100644 index 8a83c81909..0000000000 --- a/backend/ops_api/tests/ops/utils/test_procurement_workflow_helper.py +++ /dev/null @@ -1,108 +0,0 @@ -import pytest -from sqlalchemy import select - -from models import ( - AcquisitionPlanning, - Agreement, - Award, - Evaluation, - Package, - PackageSnapshot, - PreAward, - PreSolicitation, - ProcurementStep, - Solicitation, - WorkflowAction, - WorkflowInstance, - WorkflowTemplate, -) -from ops_api.ops.utils.procurement_workflow_helper import ( - create_procurement_workflow, - delete_procurement_workflow, - get_procurement_workflow_template, -) - - -@pytest.mark.usefixtures("app_ctx", "loaded_db") -def test_get_procurement_workflow_template(loaded_db): - template: WorkflowTemplate = get_procurement_workflow_template() - assert template.name == "Procurement Tracker" - - -@pytest.mark.usefixtures("app_ctx", "loaded_db") -def test_create_procurement_workflow(loaded_db): - test_agreement_id = 1 - workflow_instance: WorkflowInstance = create_procurement_workflow(test_agreement_id) - assert workflow_instance.id is not None - workflow_instance_id = workflow_instance.id - - assert len(workflow_instance.steps) == 7 - assert workflow_instance.steps[0].workflow_step_template.name == "Acquisition Planning" - acquisition_planning_workflow_step = workflow_instance.steps[0] - assert workflow_instance.steps[1].workflow_step_template.name == "Pre-Solicitation" - pre_solicitation_workflow_step = workflow_instance.steps[1] - assert workflow_instance.steps[2].workflow_step_template.name == "Solicitation" - solicitation_workflow_step = workflow_instance.steps[2] - assert workflow_instance.steps[3].workflow_step_template.name == "Evaluation Approval" - # evaluation_approval_workflow_step = workflow_instance.steps[3] - assert workflow_instance.steps[4].workflow_step_template.name == "Evaluation Attestation" - evaluation_attestation_workflow_step = workflow_instance.steps[4] - assert workflow_instance.steps[5].workflow_step_template.name == "Pre-Award" - pre_award_workflow_step = workflow_instance.steps[5] - assert workflow_instance.steps[6].workflow_step_template.name == "Award" - award_workflow_step = workflow_instance.steps[6] - - # package - # Q: package model is 1-to-many with workflow? if it was 1-1 this could be just workflow_instance.package - stmt = select(Package).where(Package.workflow_instance_id == workflow_instance.id) - package_results = loaded_db.execute(stmt).all() - assert len(package_results) == 1 - package: Package = package_results[0][0] - assert package.id - assert package.workflow_instance_id == workflow_instance.id - - # snapshot has agreement ID - stmt = select(PackageSnapshot).where(PackageSnapshot.package_id == package.id) - snapshot_results = loaded_db.execute(stmt).all() - assert len(snapshot_results) == 1 - package_snapshot: PackageSnapshot = snapshot_results[0][0] - assert package_snapshot.object_type == "AGREEMENT" - assert package_snapshot.object_id == test_agreement_id - - agreement = loaded_db.get(Agreement, test_agreement_id) - assert agreement.procurement_tracker_workflow_id == workflow_instance.id - workflow_instance = loaded_db.get(WorkflowInstance, workflow_instance.id) - assert workflow_instance.workflow_action == WorkflowAction.PROCUREMENT_TRACKING - - # procurement steps tied to this agreement and workflow steps - stmt = select(ProcurementStep).where(ProcurementStep.agreement_id == test_agreement_id) - procurement_step_results = loaded_db.execute(stmt).all() - assert len(procurement_step_results) == 6 - procurement_steps = [p[0] for p in procurement_step_results] - procurement_step: ProcurementStep - for procurement_step in procurement_steps: - if isinstance(procurement_step, AcquisitionPlanning): - assert procurement_step.workflow_step_id == acquisition_planning_workflow_step.id - elif isinstance(procurement_step, PreSolicitation): - assert procurement_step.workflow_step_id == pre_solicitation_workflow_step.id - elif isinstance(procurement_step, Solicitation): - assert procurement_step.workflow_step_id == solicitation_workflow_step.id - elif isinstance(procurement_step, Evaluation): - assert procurement_step.workflow_step_id == evaluation_attestation_workflow_step.id - elif isinstance(procurement_step, PreAward): - assert procurement_step.workflow_step_id == pre_award_workflow_step.id - elif isinstance(procurement_step, Award): - assert procurement_step.workflow_step_id == award_workflow_step.id - - # delete workflow - delete_procurement_workflow(test_agreement_id) - - # verify removal - agreement = loaded_db.get(Agreement, test_agreement_id) - assert agreement.procurement_tracker_workflow_id is None - stmt = select(Package).where(Package.workflow_instance_id == workflow_instance_id) - package_results = loaded_db.execute(stmt).all() - assert len(package_results) == 0 - stmt = select(PackageSnapshot).where(PackageSnapshot.package_id == package.id) - snapshot_results = loaded_db.execute(stmt).all() - assert len(snapshot_results) == 0 diff --git a/backend/ops_api/tests/ops/workflows/test_workflows.py b/backend/ops_api/tests/ops/workflows/test_change_requests.py similarity index 93% rename from backend/ops_api/tests/ops/workflows/test_workflows.py rename to backend/ops_api/tests/ops/workflows/test_change_requests.py index 78ced90c49..88dda9e43a 100644 --- a/backend/ops_api/tests/ops/workflows/test_workflows.py +++ b/backend/ops_api/tests/ops/workflows/test_change_requests.py @@ -19,52 +19,12 @@ ContractType, Division, ServiceRequirementType, - WorkflowStepTemplate, - WorkflowStepType, - WorkflowTemplate, ) from ops_api.ops.resources.agreement_history import find_agreement_histories -from ops_api.ops.utils.procurement_workflow_helper import delete_procurement_workflow +from ops_api.ops.utils.procurement_tracker_helper import delete_procurement_tracker test_no_perms_user_id = 506 - -@pytest.mark.usefixtures("app_ctx") -def test_workflow_template_retrieve(auth_client, loaded_db): - workflow_template = loaded_db.get(WorkflowTemplate, 1) - - assert workflow_template is not None - assert workflow_template.name == "Basic Approval" - assert workflow_template.steps is not None - - -@pytest.mark.usefixtures("app_ctx") -def test_workflow_step_template_retrieve(auth_client, loaded_db): - workflow_step_template = loaded_db.get(WorkflowStepTemplate, 1) - - assert workflow_step_template is not None - assert workflow_step_template.name == "Initial Review" - assert workflow_step_template.workflow_type == WorkflowStepType.APPROVAL - assert workflow_step_template.index == 0 - assert workflow_step_template.step_approvers is not None - - -@pytest.mark.usefixtures("app_ctx") -@pytest.mark.usefixtures("loaded_db") -def test_get_workflow_template_by_id(auth_client): - response = auth_client.get("/api/v1/workflow-template/1") - assert response.status_code == 200 - assert response.json["id"] == 1 - - -@pytest.mark.usefixtures("app_ctx") -@pytest.mark.usefixtures("loaded_db") -def test_get_workflow_step_template_by_id(auth_client): - response = auth_client.get("/api/v1/workflow-step-template/1") - assert response.status_code == 200 - assert response.json["id"] == 1 - - # ---=== CHANGE REQUESTS ===--- @@ -541,7 +501,7 @@ def test_status_change_request_creates_procurement_workflow( loaded_db.commit() assert agreement.id is not None agreement_id = agreement.id - assert agreement.procurement_tracker_workflow_id is None + assert agreement.procurement_tracker_id is None # create DRAFT BLI bli = BudgetLineItem( @@ -555,7 +515,7 @@ def test_status_change_request_creates_procurement_workflow( loaded_db.commit() assert bli.id is not None bli_id = bli.id - assert bli.agreement.procurement_tracker_workflow_id is None + assert bli.agreement.procurement_tracker_id is None # submit PATCH BLI which creates change request for status change data = {"status": "IN_EXECUTION"} @@ -575,10 +535,10 @@ def test_status_change_request_creates_procurement_workflow( bli = loaded_db.get(BudgetLineItem, bli_id) assert bli is not None assert bli.status == BudgetLineItemStatus.IN_EXECUTION - assert bli.agreement.procurement_tracker_workflow_id is not None + assert bli.agreement.procurement_tracker_id is not None # cleanup - delete_procurement_workflow(bli.agreement_id) + delete_procurement_tracker(bli.agreement_id) loaded_db.delete(bli) loaded_db.delete(agreement) loaded_db.commit() diff --git a/backend/ops_api/tests/ops/workflows/test_packages.py b/backend/ops_api/tests/ops/workflows/test_packages.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/ops_api/tests/ops/workflows/test_procurement_steps.py b/backend/ops_api/tests/ops/workflows/test_procurement_steps.py index c376886278..d50442b5e0 100644 --- a/backend/ops_api/tests/ops/workflows/test_procurement_steps.py +++ b/backend/ops_api/tests/ops/workflows/test_procurement_steps.py @@ -1,7 +1,7 @@ import pytest from flask import url_for -from models.workflows import AcquisitionPlanning, Award, Evaluation, PreAward, PreSolicitation, Solicitation +from models.procurement_tracker import AcquisitionPlanning, Award, Evaluation, PreAward, PreSolicitation, Solicitation TEST_AGREEMENT_ID = 1 TEST_AGREEMENT_ID2 = 2 @@ -158,7 +158,6 @@ def test_acquisition_planning_get_by_id(auth_client, loaded_db, test_acquisition "notes", "type", "updated_on", - "workflow_step_id", ] for key in keys: assert key in resp_json @@ -218,7 +217,6 @@ def test_pre_solicitation_get_by_id(auth_client, loaded_db, test_pre_solicitatio "target_date", "type", "updated_on", - "workflow_step_id", ] for key in keys: assert key in resp_json @@ -284,7 +282,6 @@ def test_solicitation_get_by_id(auth_client, loaded_db, test_solicitation): "target_date", "type", "updated_on", - "workflow_step_id", ] for key in keys: assert key in resp_json @@ -350,7 +347,6 @@ def test_evaluation_get_by_id(auth_client, loaded_db, test_evaluation): "target_date", "type", "updated_on", - "workflow_step_id", ] for key in keys: assert key in resp_json @@ -416,7 +412,6 @@ def test_pre_award_get_by_id(auth_client, loaded_db, test_pre_award): "target_date", "type", "updated_on", - "workflow_step_id", ] for key in keys: assert key in resp_json @@ -479,7 +474,6 @@ def test_award_get_by_id(auth_client, loaded_db, test_award): "updated_on", "vendor", "vendor_type", - "workflow_step_id", ] for key in keys: assert key in resp_json diff --git a/backend/ops_api/tests/ops/workflows/test_workflow_procurement.py b/backend/ops_api/tests/ops/workflows/test_workflow_procurement.py deleted file mode 100644 index cb30da5d39..0000000000 --- a/backend/ops_api/tests/ops/workflows/test_workflow_procurement.py +++ /dev/null @@ -1,75 +0,0 @@ -from datetime import datetime -from unittest.mock import Mock - -import pytest - -from models import Package, PackageSnapshot, WorkflowAction, WorkflowInstance, WorkflowStepInstance, WorkflowStepStatus -from models.workflows import WorkflowTriggerType - - -@pytest.mark.usefixtures("app_ctx", "loaded_db") -def test_creating_procurement_tracker_workflow(loaded_db): - # Create a mock WorkflowStepInstance with the required attributes - - # Where and how should we create a Procurement workflow? - # Do we want to rework the dependencies - - procurement_workflow = Mock(spec=WorkflowInstance) - procurement_workflow.workflow_template_id = 2 - procurement_workflow.associated_id = 1 - procurement_workflow.associated_type = WorkflowTriggerType.AGREEMENT - procurement_workflow.workflow_action = WorkflowAction.GENERIC - - # workflow_template = Mock(spec=WorkflowTemplate) - # workflow_template = - # for step in workflow_template.steps: - # step_instance = Mock(spec=WorkflowStepInstance) - # step_instance.workflow_instance_id = procurement_workflow.id - # step_instance.workflow_step_template_id = step.id - # step_instance.status = WorkflowStepStatus.REVIEW - # step_instance.notes = "" - # step_instance.time_started = datetime.now() - - workflow_step_instance_acquisition = Mock(spec=WorkflowStepInstance) - workflow_step_instance_acquisition.workflow_instance_id = procurement_workflow.id - workflow_step_instance_acquisition.workflow_step_template_id = 3 - workflow_step_instance_acquisition.status = WorkflowStepStatus.REVIEW - workflow_step_instance_acquisition.notes = "" - workflow_step_instance_acquisition.time_started = datetime.now() - - workflow_step_instance_pre_solicitation = Mock(spec=WorkflowStepInstance) - workflow_step_instance_pre_solicitation.workflow_instance_id = procurement_workflow.id - workflow_step_instance_pre_solicitation.workflow_step_template_id = 3 - workflow_step_instance_pre_solicitation.status = WorkflowStepStatus.REVIEW - workflow_step_instance_pre_solicitation.notes = "" - workflow_step_instance_pre_solicitation.time_started = datetime.now() - - # workflow_step_dependency = Mock(spec=WorkflowStepInstance) - # TODO: Refactor the step dependencies to be on the Step Template - - package = Mock(Package) - package.submitter_id = 21 - package.workflow_instance_id = workflow_step_instance_acquisition.id - package.notes = "" - - package_snapshot = Mock(PackageSnapshot) - package_snapshot.package_id = package.id - package_snapshot.object_type = "AGREEMENT" - package_snapshot.object_id = 1 - - # - # # workflow_step_instance.workflow_instance.workflow_action = WorkflowAction.GENERIC - # # workflow_step_instance.package_entities = {"budget_line_item_ids": [1, 2, 3]} - # - # # Use loaded_db to create or mock BudgetLineItems - # bli1 = loaded_db.get(BudgetLineItem, 1) - # bli2 = loaded_db.get(BudgetLineItem, 2) - # bli3 = loaded_db.get(BudgetLineItem, 3) - # blis = [bli1, bli2, bli3] - # - # # Call the function - # update_blis(workflow_step_instance) - # - # # Assert that the status of each BudgetLineItem is set to BudgetLineItemStatus.PLANNED - # for bli in blis: - # assert bli.status == BudgetLineItemStatus.PLANNED diff --git a/backend/ops_api/tests_manual/ops/test_change_requests.py b/backend/ops_api/tests_manual/ops/test_change_requests.py index 0fb66d5e23..e072874047 100644 --- a/backend/ops_api/tests_manual/ops/test_change_requests.py +++ b/backend/ops_api/tests_manual/ops/test_change_requests.py @@ -6,7 +6,7 @@ from flask import url_for from models import Agreement, BudgetLineItem, BudgetLineItemStatus, Division -from models.workflows import BudgetLineItemChangeRequest, ChangeRequestStatus, ChangeRequestType +from models.change_requests import BudgetLineItemChangeRequest, ChangeRequestStatus, ChangeRequestType from ops_api.ops.resources.agreement_history import find_agreement_histories test_user_id = 4 diff --git a/frontend/src/tests/data.js b/frontend/src/tests/data.js index dd67bee391..bd870d59a9 100644 --- a/frontend/src/tests/data.js +++ b/frontend/src/tests/data.js @@ -175,7 +175,7 @@ export const agreement = { name: "Product Service Center" }, awarding_entity_id: 1, - procurement_tracker_workflow_id: null, + procurement_tracker_id: null, product_service_code: { description: "", id: 1,