Merge pull request #589 from dod-ccpo/ko-review-task-order
KO Review Task Order
This commit is contained in:
commit
c1ff997fe5
@ -0,0 +1,30 @@
|
||||
"""Add Custom Clauses to Task Order
|
||||
|
||||
Revision ID: 0ff4c31c4d28
|
||||
Revises: da9d1c911a52
|
||||
Create Date: 2019-01-30 11:28:37.193854
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0ff4c31c4d28'
|
||||
down_revision = 'da9d1c911a52'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('task_orders', 'loa', type_=sa.String(), nullable=True)
|
||||
op.add_column('task_orders', sa.Column('custom_clauses', sa.String(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('task_orders', 'custom_clauses')
|
||||
op.alter_column('task_orders', 'loa', type_=sa.ARRAY(sa.String()), nullable=True)
|
||||
# ### end Alembic commands ###
|
@ -36,9 +36,15 @@ class Authorization(object):
|
||||
def is_ccpo(cls, user):
|
||||
return user.atat_role.name == "ccpo"
|
||||
|
||||
@classmethod
|
||||
def check_is_ko(cls, user, task_order):
|
||||
if task_order.contracting_officer != user:
|
||||
message = "review Task Order {}".format(task_order.id)
|
||||
raise UnauthorizedError(user, message)
|
||||
|
||||
@classmethod
|
||||
def check_task_order_permission(cls, user, task_order, permission, message):
|
||||
if Authorization._check_is_task_order_officer(task_order, user):
|
||||
if Authorization._check_is_task_order_officer(user, task_order):
|
||||
return True
|
||||
|
||||
Authorization.check_portfolio_permission(
|
||||
@ -46,7 +52,7 @@ class Authorization(object):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _check_is_task_order_officer(cls, task_order, user):
|
||||
def _check_is_task_order_officer(cls, user, task_order):
|
||||
for officer in [
|
||||
"contracting_officer",
|
||||
"contracting_officer_representative",
|
||||
|
@ -196,10 +196,7 @@ APPLICATION_COMPLEXITY = [
|
||||
]
|
||||
|
||||
DEV_TEAM = [
|
||||
(
|
||||
"government_civilians",
|
||||
translate("forms.task_order.dev_team.government_civilians"),
|
||||
),
|
||||
("civilians", translate("forms.task_order.dev_team.civilians")),
|
||||
("military", translate("forms.task_order.dev_team.military")),
|
||||
("contractor", translate("forms.task_order.dev_team.contractor")),
|
||||
("other", translate("forms.task_order.dev_team.other")),
|
||||
|
36
atst/forms/ko_review.py
Normal file
36
atst/forms/ko_review.py
Normal file
@ -0,0 +1,36 @@
|
||||
from flask_wtf.file import FileAllowed
|
||||
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.fields import StringField, TextAreaField, FileField
|
||||
from wtforms.validators import Optional, Length
|
||||
|
||||
from .forms import CacheableForm
|
||||
from .validators import IsNumber
|
||||
|
||||
from atst.utils.localization import translate
|
||||
|
||||
|
||||
class KOReviewForm(CacheableForm):
|
||||
start_date = DateField(
|
||||
translate("forms.ko_review.start_date_label"), format="%m/%d/%Y"
|
||||
)
|
||||
end_date = DateField(translate("forms.ko_review.end_date_label"), format="%m/%d/%Y")
|
||||
pdf = FileField(
|
||||
translate("forms.ko_review.pdf_label"),
|
||||
description=translate("forms.ko_review.pdf_description"),
|
||||
validators=[
|
||||
FileAllowed(["pdf"], translate("forms.task_order.file_format_not_allowed"))
|
||||
],
|
||||
render_kw={"required": False, "accept": ".pdf,application/pdf"},
|
||||
)
|
||||
number = StringField(
|
||||
translate("forms.ko_review.to_number"), validators=[Length(min=10)]
|
||||
)
|
||||
loa = StringField(
|
||||
translate("forms.ko_review.loa"), validators=[Length(min=10), IsNumber()]
|
||||
)
|
||||
custom_clauses = TextAreaField(
|
||||
translate("forms.ko_review.custom_clauses_label"),
|
||||
description=translate("forms.ko_review.custom_clauses_description"),
|
||||
validators=[Optional()],
|
||||
)
|
@ -72,7 +72,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
so_phone_number = Column(String) # Phone Number
|
||||
so_dod_id = Column(String) # DOD ID
|
||||
number = Column(String, unique=True) # Task Order Number
|
||||
loa = Column(ARRAY(String)) # Line of Accounting (LOA)
|
||||
loa = Column(String) # Line of Accounting (LOA)
|
||||
custom_clauses = Column(String) # Custom Clauses
|
||||
|
||||
@hybrid_property
|
||||
def csp_estimate(self):
|
||||
@ -93,7 +94,12 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
|
||||
@property
|
||||
def is_submitted(self):
|
||||
return self.number is not None
|
||||
|
||||
return (
|
||||
self.number is not None
|
||||
and self.start_date is not None
|
||||
and self.end_date is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
|
@ -7,8 +7,10 @@ from . import portfolios_bp
|
||||
from atst.database import db
|
||||
from atst.domain.task_orders import TaskOrders
|
||||
from atst.domain.portfolios import Portfolios
|
||||
from atst.domain.authz import Authorization
|
||||
from atst.forms.officers import EditTaskOrderOfficersForm
|
||||
from atst.models.task_order import Status as TaskOrderStatus
|
||||
from atst.forms.ko_review import KOReviewForm
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/task_orders")
|
||||
@ -66,9 +68,51 @@ def view_task_order(portfolio_id, task_order_id):
|
||||
portfolio=portfolio,
|
||||
task_order=task_order,
|
||||
all_sections_complete=completed,
|
||||
user=g.current_user,
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route("/portfolios/<portfolio_id>/task_order/<task_order_id>/review")
|
||||
def ko_review(portfolio_id, task_order_id):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
portfolio = Portfolios.get(g.current_user, portfolio_id)
|
||||
|
||||
Authorization.check_is_ko(g.current_user, task_order)
|
||||
return render_template(
|
||||
"/portfolios/task_orders/review.html",
|
||||
portfolio=portfolio,
|
||||
task_order=task_order,
|
||||
form=KOReviewForm(obj=task_order),
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/task_order/<task_order_id>/review", methods=["POST"]
|
||||
)
|
||||
def submit_ko_review(portfolio_id, task_order_id, form=None):
|
||||
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||
form_data = {**http_request.form, **http_request.files}
|
||||
form = KOReviewForm(form_data)
|
||||
|
||||
Authorization.check_is_ko(g.current_user, task_order)
|
||||
if form.validate():
|
||||
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||
return redirect(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=portfolio_id,
|
||||
task_order_id=task_order_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return render_template(
|
||||
"/portfolios/task_orders/review.html",
|
||||
portfolio=Portfolios.get(g.current_user, portfolio_id),
|
||||
task_order=task_order,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@portfolios_bp.route(
|
||||
"/portfolios/<portfolio_id>/task_order/<task_order_id>/invitations"
|
||||
)
|
||||
|
@ -126,6 +126,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
legend {
|
||||
@include h4;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
{% from "components/icon.html" import Icon %}
|
||||
|
||||
{% macro DatePicker(field, mindate=None, maxdate=None) -%}
|
||||
{% macro DatePicker(
|
||||
field,
|
||||
label=field.label | striptags,
|
||||
description=field.description,
|
||||
mindate=None,
|
||||
maxdate=None) -%}
|
||||
|
||||
<date-selector
|
||||
{% if maxdate %}maxdate="{{ maxdate.strftime("%Y-%m-%d") }}"{% endif %}
|
||||
@ -10,7 +15,16 @@
|
||||
initialyear="{{ field.data.year }}"
|
||||
inline-template>
|
||||
|
||||
<div class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid }">
|
||||
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid }">
|
||||
<legend>
|
||||
<div class="usa-input__title">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
{% if description %}
|
||||
<span class='usa-input__help'>{{ description | safe }}</span>
|
||||
{% endif %}
|
||||
</legend>
|
||||
|
||||
<div class="date-picker-component">
|
||||
<input name="{{ field.name }}" v-bind:value="formattedDate" type="hidden" />
|
||||
@ -51,6 +65,7 @@
|
||||
{% if maxdate %}max="{{ maxdate.year }}"{% endif %}
|
||||
{% if mindate %}min="{{ mindate.year }}"{% endif %}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="usa-form-group-date-ok" v-if="isDateValid">
|
||||
@ -63,7 +78,7 @@
|
||||
{% if maxdate and not mindate %}Date must be before or on {{maxdate.strftime("%m/%d/%Y")}}{% endif %}
|
||||
{% if mindate and not maxdate %}Date must be after or on {{mindate.strftime("%m/%d/%Y")}}{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</date-selector>
|
||||
|
||||
{%- endmacro %}
|
||||
|
11
templates/components/review_field.html
Normal file
11
templates/components/review_field.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% macro ReviewField(heading, field, filter=None) %}
|
||||
<div class="col col--grow">
|
||||
<h4 class='task-order-form__heading'>{{ heading }}</h4>
|
||||
{% if field %}
|
||||
<p>{{ field | findFilter(filter) }}</p>
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
7
templates/fragments/ko_review_alert.html
Normal file
7
templates/fragments/ko_review_alert.html
Normal file
@ -0,0 +1,7 @@
|
||||
<br>
|
||||
<p>{{ "fragments.ko_review_alert.make_sure" | translate }}:</p>
|
||||
<ul>
|
||||
<li>{{ "fragments.ko_review_alert.bullet_1" | translate }}</li>
|
||||
<li>{{ "fragments.ko_review_alert.bullet_2" | translate }}</li>
|
||||
<li>{{ "fragments.ko_review_alert.bullet_3" | translate }}</li>
|
||||
</ul>
|
9
templates/fragments/task_order_review/app_info.html
Normal file
9
templates/fragments/task_order_review/app_info.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
|
||||
<div class="row">
|
||||
{{ ReviewField(("task_orders.new.review.portfolio" | translate), task_order.portfolio_name) }}
|
||||
{{ ReviewField(("task_orders.new.review.dod" | translate), task_order.defense_component, filter="normalizeOrder") }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ ReviewField(("task_orders.new.review.scope" | translate), task_order.scope) }}
|
||||
</div>
|
72
templates/fragments/task_order_review/funding.html
Normal file
72
templates/fragments/task_order_review/funding.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
|
||||
<div class="row">
|
||||
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
|
||||
{% if task_order.csp_estimate %}
|
||||
<p>
|
||||
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<br/>
|
||||
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
<div class="col col--grow">
|
||||
<table class="funding-summary__table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><h4>{{ "task_orders.new.review.to_value"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.budget %}
|
||||
{{ task_order.budget | dollars }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_1"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_01 %}
|
||||
{{ task_order.clin_01 | dollars }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
|
||||
{{ "task_orders.new.review.clin_2"| translate }}
|
||||
{% if not config.CLASSIFIED %}
|
||||
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
|
||||
{% endif %}
|
||||
</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_02 and config.CLASSIFIED %}
|
||||
{{ task_order.clin_02 | dollars or RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_3"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_03 %}
|
||||
{{ task_order.clin_03 | dollars or RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
|
||||
{{ "task_orders.new.review.clin_4"| translate }}
|
||||
{% if not config.CLASSIFIED %}
|
||||
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
|
||||
{% endif %}
|
||||
</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_04 and config.CLASSIFIED %}
|
||||
{{ task_order.clin_04 | dollars or RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
25
templates/fragments/task_order_review/oversight.html
Normal file
25
templates/fragments/task_order_review/oversight.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
|
||||
<div class="col col--grow">
|
||||
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
||||
{{ first_name }} {{ last_name }}<br>
|
||||
{{ email }}<br>
|
||||
{% if phone_number %}
|
||||
{{ phone_number | usPhone }}
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
|
||||
{% if officer %}
|
||||
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
||||
{% else %}
|
||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="row">
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
|
||||
</div>
|
64
templates/fragments/task_order_review/reporting.html
Normal file
64
templates/fragments/task_order_review/reporting.html
Normal file
@ -0,0 +1,64 @@
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
|
||||
<div class="row">
|
||||
{{
|
||||
ReviewField(
|
||||
("forms.task_order.app_migration.label" | translate),
|
||||
(
|
||||
("forms.task_order.app_migration.{}".format(task_order.app_migration) | translate) if task_order.app_migration
|
||||
),
|
||||
filter='safe'
|
||||
)
|
||||
}}
|
||||
|
||||
{{
|
||||
ReviewField(
|
||||
("forms.task_order.native_apps.label" | translate),
|
||||
(
|
||||
("forms.task_order.native_apps.{}".format(task_order.native_apps) | translate) if task_order.native_apps
|
||||
)
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.complexity"| translate }}</h4>
|
||||
{% if task_order.complexity %}
|
||||
<ul class="checklist">
|
||||
{% for item in task_order.complexity %}
|
||||
<li>
|
||||
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.complexity.{}".format(item) | translate }}{% if item == 'other' %}: {{ task_order.complexity_other }}{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{{ RequiredLabel() }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col col--grow">
|
||||
<h4 class='task-order-form__heading'>{{ "task_orders.new.review.team"| translate }}</h4>
|
||||
{% if task_order.dev_team %}
|
||||
<ul class="checklist">
|
||||
{% for item in task_order.dev_team %}
|
||||
<li>
|
||||
{% if item == 'other' %}
|
||||
{{ Icon('ok', classes='icon--gray icon--medium') }}Other: {{ task_order.dev_team_other }}
|
||||
{% else %}
|
||||
{{ Icon('ok', classes='icon--gray icon--medium') }}{{ "forms.task_order.dev_team.{}".format(item) | translate }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{{ RequiredLabel() }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{
|
||||
ReviewField(
|
||||
("forms.task_order.team_experience.label" |translate),
|
||||
(
|
||||
("forms.task_order.team_experience.{}".format(task_order.team_experience) | translate) if task_order.team_experience
|
||||
)
|
||||
)
|
||||
}}
|
||||
</div>
|
82
templates/portfolios/task_orders/review.html
Normal file
82
templates/portfolios/task_orders/review.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% from "components/edit_link.html" import EditLink %}
|
||||
{% from "components/required_label.html" import RequiredLabel %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/date_picker.html" import DatePicker %}
|
||||
{% from "components/text_input.html" import TextInput %}
|
||||
{% from "components/alert.html" import Alert %}
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="col task-order-form">
|
||||
|
||||
{% include "fragments/flash.html" %}
|
||||
|
||||
<form method='POST' action="{{ url_for('portfolios.submit_ko_review', portfolio_id=portfolio.id, task_order_id=task_order.id, form=form) }}" autocomplete="off" enctype="multipart/form-data">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% set message = "task_orders.ko_review.submitted_by" | translate({"name": task_order.creator.full_name}) %}
|
||||
|
||||
{{ Alert(("task_orders.ko_review.alert_title" | translate), message, level='warning',
|
||||
fragment="fragments/ko_review_alert.html") }}
|
||||
|
||||
<div class="panel">
|
||||
|
||||
<div class="panel__heading">
|
||||
<h1 class="task-order-form__heading subheading">
|
||||
<div class="h2">{{ "task_orders.ko_review.title" | translate }}</div>
|
||||
{{ "task_orders.new.review.section_title"| translate }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
<div class="h2">{{ "task_orders.new.review.app_info"| translate }}</div>
|
||||
{% include "fragments/task_order_review/app_info.html" %}
|
||||
<hr>
|
||||
|
||||
<div class="h2">{{ "task_orders.new.review.reporting"| translate }}</div>
|
||||
{% include "fragments/task_order_review/reporting.html" %}
|
||||
<hr>
|
||||
|
||||
<div class="h2">{{ "task_orders.new.review.funding"| translate }}</div>
|
||||
{% include "fragments/task_order_review/funding.html" %}
|
||||
|
||||
<div class="form__sub-fields">
|
||||
{{ DatePicker(form.start_date) }}
|
||||
{{ DatePicker(form.end_date) }}
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="h2">{{ "task_orders.new.review.oversight"| translate }}</div>
|
||||
{% include "fragments/task_order_review/oversight.html" %}
|
||||
<hr>
|
||||
|
||||
<div class="h2">{{ "task_orders.ko_review.task_order_information"| translate }}</div>
|
||||
|
||||
<div class="form__sub-fields">
|
||||
<div class="usa-input">
|
||||
<div class="usa-input__title">{{ form.pdf.label }}</div>
|
||||
{{ form.pdf.description }}
|
||||
{{ form.pdf }}
|
||||
</div>
|
||||
{{ TextInput(form.number) }}
|
||||
{{ TextInput(form.loa) }}
|
||||
{{ TextInput(form.custom_clauses, paragraph=True) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<div class='action-group'>
|
||||
<input type='submit' class='usa-button usa-button-primary' value='Continue' />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
@ -92,8 +92,13 @@
|
||||
link_text="edit",
|
||||
complete=all_sections_complete) %}
|
||||
<div class="task-order-next-steps__action col">
|
||||
{% if user == task_order.contracting_officer %}
|
||||
{% set url=url_for("portfolios.ko_review", portfolio_id=portfolio.id, task_order_id=task_order.id) %}
|
||||
{% else %}
|
||||
{% set url = url_for("task_orders.new", screen=1, task_order_id=task_order.id) %}
|
||||
{% endif %}
|
||||
<a
|
||||
href="{{ url_for("task_orders.new", screen=1, task_order_id=task_order.id) }}"
|
||||
href="{{ url }}"
|
||||
class="usa-button usa-button-primary">
|
||||
Edit
|
||||
</a>
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% from "components/edit_link.html" import EditLink %}
|
||||
{% from "components/required_label.html" import RequiredLabel %}
|
||||
{% from "components/icon.html" import Icon %}
|
||||
{% from "components/review_field.html" import ReviewField %}
|
||||
|
||||
{% block heading %}
|
||||
{{ "task_orders.new.review.section_title"| translate }}
|
||||
@ -18,50 +19,9 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ReviewField(heading, field, filter=None) %}
|
||||
<div class="col col--grow">
|
||||
<h4 class='task-order-form__heading'>{{ heading }}</h4>
|
||||
{% if field %}
|
||||
<p>{{ field | findFilter(filter) }}</p>
|
||||
{% else %}
|
||||
{{ RequiredLabel() }}
|
||||
{% endif %}
|
||||
{% if caller %}
|
||||
{{ caller() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro ReviewOfficerInfo(heading, first_name, last_name, email, phone_number, dod_id, officer) %}
|
||||
<div class="col col--grow">
|
||||
<h4 class='task-order-form__heading'>{{ heading | translate }}</h4>
|
||||
{{ first_name }} {{ last_name }}<br>
|
||||
{{ email }}<br>
|
||||
{% if phone_number %}
|
||||
{{ phone_number | usPhone }}
|
||||
{% else %}
|
||||
{{ RequiredLabel() }}
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ "task_orders.new.review.dod_id" | translate }} {{ dod_id}}<br>
|
||||
{% if officer %}
|
||||
{{ Icon('ok', classes='icon--green') }} <span class="task-order-invite-message sent">{{ "task_orders.new.review.invited"| translate }}</<span>
|
||||
{% else %}
|
||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_invited"| translate }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
<h3 class="subheading">{{ "task_orders.new.review.app_info"| translate }} {{ TOEditLink(screen=1) }}</h3>
|
||||
|
||||
<div class="row">
|
||||
{{ ReviewField(("task_orders.new.review.portfolio" | translate), task_order.portfolio_name) }}
|
||||
{{ ReviewField(("task_orders.new.review.dod" | translate), task_order.defense_component, filter="normalizeOrder") }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ ReviewField(("task_orders.new.review.scope" | translate), task_order.scope) }}
|
||||
</div>
|
||||
{% include "fragments/task_order_review/app_info.html" %}
|
||||
<hr>
|
||||
|
||||
<h3 class="subheading">{{ "task_orders.new.review.reporting"| translate }} {{ TOEditLink(screen=1, anchor="reporting") }}</h3>
|
||||
@ -133,95 +93,11 @@
|
||||
<hr>
|
||||
|
||||
<h3 class="subheading">{{ "task_orders.new.review.funding"| translate }} {{ TOEditLink(screen=2) }}</h3>
|
||||
|
||||
<div class="row">
|
||||
{% call ReviewField(("task_orders.new.review.performance_period" | translate), task_order.performance_length, filter="translateDuration") %}
|
||||
{% if task_order.csp_estimate %}
|
||||
<p>
|
||||
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left' download>{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{{ url_for("task_orders.download_csp_estimate", task_order_id=task_order.id) }}" class='icon-link icon-link--left icon-link--disabled' aria-disabled="true">{{ Icon('download') }} {{ "task_orders.new.review.usage_est_link"| translate }}</a><br>
|
||||
{{ Icon('alert', classes='icon--red') }} <span class="task-order-invite-message not-sent">{{ "task_orders.new.review.not_uploaded"| translate }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
<div class="col col--grow">
|
||||
<table class="funding-summary__table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><h4>{{ "task_orders.new.review.to_value"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.budget %}
|
||||
{{ task_order.budget | dollars }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_1"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_01 %}
|
||||
{{ task_order.clin_01 | dollars }}
|
||||
{% else %}
|
||||
{{ RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
|
||||
{{ "task_orders.new.review.clin_2"| translate }}
|
||||
{% if not config.CLASSIFIED %}
|
||||
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
|
||||
{% endif %}
|
||||
</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_02 and config.CLASSIFIED %}
|
||||
{{ task_order.clin_02 | dollars or RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class='task-order-form__heading funding-summary__td'>{{ "task_orders.new.review.clin_3"| translate }}</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_03 %}
|
||||
{{ task_order.clin_03 | dollars or RequiredLabel() }}
|
||||
{% else %}
|
||||
{{ RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h4 class="task-order-form__heading funding-summary__td{% if not config.CLASSIFIED %} inactive{% endif %}">
|
||||
{{ "task_orders.new.review.clin_4"| translate }}
|
||||
{% if not config.CLASSIFIED %}
|
||||
<div>{{ "task_orders.new.review.classified_inactive"| translate }}</div>
|
||||
{% endif %}
|
||||
</h4></td>
|
||||
<td class="table-cell--align-right">
|
||||
{% if task_order.clin_04 and config.CLASSIFIED %}
|
||||
{{ task_order.clin_04 | dollars or RequiredLabel() }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "fragments/task_order_review/funding.html" %}
|
||||
<hr>
|
||||
|
||||
<h3 class="subheading">{{ "task_orders.new.review.oversight"| translate }} {{ TOEditLink(screen=3) }}</h3>
|
||||
|
||||
<div class="row">
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.ko", task_order.ko_first_name, task_order.ko_last_name, task_order.ko_email, task_order.ko_phone_number, task_order.ko_dod_id, task_order.contracting_officer) }}
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.cor", task_order.cor_first_name, task_order.cor_last_name, task_order.cor_email, task_order.cor_phone_number, task_order.cor_dod_id, task_order.contracting_officer_representative) }}
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ ReviewOfficerInfo("task_orders.new.review.so", task_order.so_first_name, task_order.so_last_name, task_order.so_email, task_order.so_phone_number, task_order.so_dod_id, task_order.security_officer) }}
|
||||
</div>
|
||||
|
||||
{% include "fragments/task_order_review/oversight.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block next %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
from werkzeug.datastructures import FileStorage
|
||||
import pytest
|
||||
import pytest, datetime
|
||||
|
||||
from atst.models.attachment import Attachment
|
||||
from atst.models.task_order import TaskOrder, Status
|
||||
@ -33,7 +33,11 @@ def test_is_submitted():
|
||||
to = TaskOrder()
|
||||
assert not to.is_submitted
|
||||
|
||||
to = TaskOrder(number="42")
|
||||
to = TaskOrder(
|
||||
number="42",
|
||||
start_date=datetime.date.today(),
|
||||
end_date=datetime.date.today() + datetime.timedelta(days=1),
|
||||
)
|
||||
assert to.is_submitted
|
||||
|
||||
|
||||
|
@ -68,41 +68,6 @@ class TestPortfolioFunding:
|
||||
assert context["total_balance"] == active_to1.budget + active_to2.budget
|
||||
|
||||
|
||||
def test_ko_can_view_task_order(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
ko = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
role=Roles.get("officer"),
|
||||
portfolio=portfolio,
|
||||
user=ko,
|
||||
status=PortfolioStatus.ACTIVE,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||
user_session(ko)
|
||||
response = client.get(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_can_view_task_order_invitations(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
||||
response = client.get(
|
||||
url_for(
|
||||
"portfolios.task_order_invitations",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class TestTaskOrderInvitations:
|
||||
def setup(self):
|
||||
self.portfolio = PortfolioFactory.create()
|
||||
@ -151,3 +116,89 @@ class TestTaskOrderInvitations:
|
||||
|
||||
updated_task_order = TaskOrders.get(self.portfolio.owner, self.task_order.id)
|
||||
assert updated_task_order.so_first_name != "Boba"
|
||||
|
||||
|
||||
def test_ko_can_view_task_order(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
ko = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
role=Roles.get("officer"),
|
||||
portfolio=portfolio,
|
||||
user=ko,
|
||||
status=PortfolioStatus.ACTIVE,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||
user_session(ko)
|
||||
response = client.get(
|
||||
url_for(
|
||||
"portfolios.view_task_order",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_can_view_task_order_invitations(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
||||
response = client.get(
|
||||
url_for(
|
||||
"portfolios.task_order_invitations",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_ko_can_view_ko_review_page(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
ko = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
role=Roles.get("officer"),
|
||||
portfolio=portfolio,
|
||||
user=ko,
|
||||
status=PortfolioStatus.ACTIVE,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio, contracting_officer=ko)
|
||||
user_session(ko)
|
||||
response = client.get(
|
||||
url_for(
|
||||
"portfolios.ko_review",
|
||||
portfolio_id=portfolio.id,
|
||||
task_order_id=task_order.id,
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_mo_redirected_to_build_page(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
user_session(portfolio.owner)
|
||||
task_order = TaskOrderFactory.create(portfolio=portfolio)
|
||||
|
||||
response = client.get(
|
||||
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_cor_redirected_to_build_page(client, user_session):
|
||||
portfolio = PortfolioFactory.create()
|
||||
cor = UserFactory.create()
|
||||
PortfolioRoleFactory.create(
|
||||
role=Roles.get("officer"),
|
||||
portfolio=portfolio,
|
||||
user=cor,
|
||||
status=PortfolioStatus.ACTIVE,
|
||||
)
|
||||
task_order = TaskOrderFactory.create(
|
||||
portfolio=portfolio, contracting_officer_representative=cor
|
||||
)
|
||||
user_session(cor)
|
||||
response = client.get(
|
||||
url_for("task_orders.new", screen=1, task_order_id=task_order.id)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
@ -54,6 +54,16 @@ forms:
|
||||
lname_mao_label: Last Name (optional)
|
||||
phone_ext_mao_label: Extension (optional)
|
||||
phone_mao_label: Mission Owner phone number (optional)
|
||||
ko_review:
|
||||
start_date_label: Period of Performance Start Date
|
||||
end_date_label: Period of Performance End Date
|
||||
invalid_date: Must be a date in the future.
|
||||
pdf_label: Upload a copy of your Task Order document
|
||||
pdf_description: Upload a PDF of the Task Order that you entered in your system of record for your organization.
|
||||
to_number: Task Order Number
|
||||
loa: Line of Accounting (LOA) #1
|
||||
custom_clauses_label: Task Order Custom Clauses (optional)
|
||||
custom_clauses_description: This will put a pause on the CSP access once the KO signs until the CCPO reviews that language to make sure it is legal.
|
||||
edit_member:
|
||||
portfolio_role_label: Portfolio Role
|
||||
edit_user:
|
||||
@ -198,7 +208,7 @@ forms:
|
||||
dev_team:
|
||||
label: Development Team
|
||||
description: Which people or teams will be completing the development work for your cloud applications? Select all that apply.
|
||||
government_civilians: Government Civilians
|
||||
civilians: Government Civilians
|
||||
military: Military
|
||||
contractor: Contractor
|
||||
other: "Other <em class='description'>(E.g. University or other partner)</em>"
|
||||
@ -264,6 +274,11 @@ fragments:
|
||||
learn_more_link_text: Learn more about the JEDI Cloud Task Order and the Financial Verification process.
|
||||
paragraph_1: 'The next step is to create a Task Order associated with JEDI Cloud. Please contact a Contracting Officer (KO), Contracting Officer Representative (COR), or a Financial Manager to help with this step.'
|
||||
paragraph_2: 'Once the Task Order has been created, you will be asked to provide details about the task order in the Financial Verification step.'
|
||||
ko_review_alert:
|
||||
make_sure: Make sure to take a moment to
|
||||
bullet_1: Verify that all information is accurate and up-to-date
|
||||
bullet_2: Upload your Task Order (TO) document
|
||||
bullet_3: Add both the Task Order (TO) and Lines of Accounting (LOA)
|
||||
login:
|
||||
ccpo_logo_alt_text: Cloud Computing Program Office Logo
|
||||
certificate_selection:
|
||||
@ -456,6 +471,11 @@ task_orders:
|
||||
description: Your Security Officer will need to answer some security configuration questions in order to generate a DD-254 document, then electronically sign.
|
||||
add_button_text: Add / Invite Security Officer
|
||||
invite_button_text: Invite Security Officer
|
||||
ko_review:
|
||||
alert_title: Verify Your Info
|
||||
title: Task Order Builder
|
||||
submitted_by: Below is an overview of the projected portfolio submitted by {name}
|
||||
task_order_information: Task Order Information
|
||||
testing:
|
||||
example_string: Hello World
|
||||
example_with_variables: 'Hello, {name}!'
|
||||
|
Loading…
x
Reference in New Issue
Block a user