Add in basic implementation of the KO TO signature page
This commit is contained in:
parent
3c7fe2cf24
commit
8689748d10
50
alembic/versions/4f2d7a5076f2_record_signer_dod_id.py
Normal file
50
alembic/versions/4f2d7a5076f2_record_signer_dod_id.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""Record signer DOD ID
|
||||||
|
|
||||||
|
Revision ID: 4f2d7a5076f2
|
||||||
|
Revises: 1f690989e38e
|
||||||
|
Create Date: 2019-02-07 15:48:11.855697
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4f2d7a5076f2'
|
||||||
|
down_revision = '1f690989e38e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_index(op.f('ix_audit_events_portfolio_id'), 'audit_events', ['portfolio_id'], unique=False)
|
||||||
|
op.drop_index('ix_audit_events_workspace_id', table_name='audit_events')
|
||||||
|
op.create_index(op.f('ix_invitations_portfolio_role_id'), 'invitations', ['portfolio_role_id'], unique=False)
|
||||||
|
op.drop_index('ix_invitations_workspace_role_id', table_name='invitations')
|
||||||
|
op.create_index(op.f('ix_portfolio_roles_portfolio_id'), 'portfolio_roles', ['portfolio_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_portfolio_roles_user_id'), 'portfolio_roles', ['user_id'], unique=False)
|
||||||
|
op.create_index('portfolio_role_user_portfolio', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True)
|
||||||
|
op.drop_index('ix_workspace_roles_user_id', table_name='portfolio_roles')
|
||||||
|
op.drop_index('ix_workspace_roles_workspace_id', table_name='portfolio_roles')
|
||||||
|
op.drop_index('workspace_role_user_workspace', table_name='portfolio_roles')
|
||||||
|
op.add_column('task_orders', sa.Column('signed_at', sa.DateTime(), nullable=True))
|
||||||
|
op.add_column('task_orders', sa.Column('signer_dod_id', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('task_orders', 'signer_dod_id')
|
||||||
|
op.drop_column('task_orders', 'signed_at')
|
||||||
|
op.create_index('workspace_role_user_workspace', 'portfolio_roles', ['user_id', 'portfolio_id'], unique=True)
|
||||||
|
op.create_index('ix_workspace_roles_workspace_id', 'portfolio_roles', ['portfolio_id'], unique=False)
|
||||||
|
op.create_index('ix_workspace_roles_user_id', 'portfolio_roles', ['user_id'], unique=False)
|
||||||
|
op.drop_index('portfolio_role_user_portfolio', table_name='portfolio_roles')
|
||||||
|
op.drop_index(op.f('ix_portfolio_roles_user_id'), table_name='portfolio_roles')
|
||||||
|
op.drop_index(op.f('ix_portfolio_roles_portfolio_id'), table_name='portfolio_roles')
|
||||||
|
op.create_index('ix_invitations_workspace_role_id', 'invitations', ['portfolio_role_id'], unique=False)
|
||||||
|
op.drop_index(op.f('ix_invitations_portfolio_role_id'), table_name='invitations')
|
||||||
|
op.create_index('ix_audit_events_workspace_id', 'audit_events', ['portfolio_id'], unique=False)
|
||||||
|
op.drop_index(op.f('ix_audit_events_portfolio_id'), table_name='audit_events')
|
||||||
|
# ### end Alembic commands ###
|
@ -222,3 +222,32 @@ class OversightForm(CacheableForm):
|
|||||||
|
|
||||||
class ReviewForm(CacheableForm):
|
class ReviewForm(CacheableForm):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureForm(CacheableForm):
|
||||||
|
signer_dod_id = StringField("signer_dod_id")
|
||||||
|
|
||||||
|
signed_at = StringField("signed_at")
|
||||||
|
|
||||||
|
level_of_warrant = DecimalField(
|
||||||
|
translate("task_orders.sign.level_of_warrant_label"),
|
||||||
|
description=translate("task_orders.sign.level_of_warrant_description"),
|
||||||
|
validators=[
|
||||||
|
RequiredIf(
|
||||||
|
lambda form: (
|
||||||
|
form._fields.get("unlimited_level_of_warrant").data is not True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
unlimited_level_of_warrant = BooleanField(
|
||||||
|
translate("task_orders.sign.unlimited_level_of_warrant_description"),
|
||||||
|
validators=[Optional()],
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = BooleanField(
|
||||||
|
translate("task_orders.sign.digital_signature_label"),
|
||||||
|
description=translate("task_orders.sign.digital_signature_description"),
|
||||||
|
validators=[Required()],
|
||||||
|
)
|
||||||
|
@ -2,7 +2,7 @@ from enum import Enum
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
import pendulum
|
import pendulum
|
||||||
from sqlalchemy import Boolean, Column, Numeric, String, ForeignKey, Date, Integer
|
from sqlalchemy import Boolean, Column, Numeric, String, ForeignKey, Date, Integer, DateTime
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from sqlalchemy.types import ARRAY
|
from sqlalchemy.types import ARRAY
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
@ -81,6 +81,9 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
loa = Column(String) # Line of Accounting (LOA)
|
loa = Column(String) # Line of Accounting (LOA)
|
||||||
custom_clauses = Column(String) # Custom Clauses
|
custom_clauses = Column(String) # Custom Clauses
|
||||||
|
|
||||||
|
signer_dod_id = Column(String)
|
||||||
|
signed_at = Column(DateTime)
|
||||||
|
|
||||||
@hybrid_property
|
@hybrid_property
|
||||||
def csp_estimate(self):
|
def csp_estimate(self):
|
||||||
return self._csp_estimate
|
return self._csp_estimate
|
||||||
|
@ -101,11 +101,7 @@ def submit_ko_review(portfolio_id, task_order_id, form=None):
|
|||||||
if form.validate():
|
if form.validate():
|
||||||
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||||
return redirect(
|
return redirect(
|
||||||
url_for(
|
url_for("task_orders.signature_requested", task_order_id=task_order_id)
|
||||||
"portfolios.view_task_order",
|
|
||||||
portfolio_id=portfolio_id,
|
|
||||||
task_order_id=task_order_id,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -5,3 +5,4 @@ task_orders_bp = Blueprint("task_orders", __name__)
|
|||||||
from . import new
|
from . import new
|
||||||
from . import index
|
from . import index
|
||||||
from . import invite
|
from . import invite
|
||||||
|
from . import signing
|
||||||
|
61
atst/routes/task_orders/signing.py
Normal file
61
atst/routes/task_orders/signing.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from flask import render_template, g, request as http_request
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from . import task_orders_bp
|
||||||
|
from atst.domain.authz import Authorization
|
||||||
|
from atst.domain.exceptions import NotFoundError
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from atst.forms.task_order import SignatureForm
|
||||||
|
|
||||||
|
|
||||||
|
def find_unsigned_ko_to(task_order_id):
|
||||||
|
task_order = TaskOrders.get(g.current_user, task_order_id)
|
||||||
|
Authorization.check_is_ko(g.current_user, task_order)
|
||||||
|
|
||||||
|
if task_order.signer_dod_id is not None:
|
||||||
|
raise NotFoundError("task_order")
|
||||||
|
|
||||||
|
return task_order
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route("/task_orders/<task_order_id>/digital_signature", methods=["GET"])
|
||||||
|
def signature_requested(task_order_id):
|
||||||
|
task_order = find_unsigned_ko_to(task_order_id)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"task_orders/signing/signature_requested.html",
|
||||||
|
task_order_id=task_order.id,
|
||||||
|
form=SignatureForm(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@task_orders_bp.route(
|
||||||
|
"/task_orders/<task_order_id>/digital_signature", methods=["POST"]
|
||||||
|
)
|
||||||
|
def record_signature(task_order_id):
|
||||||
|
task_order = find_unsigned_ko_to(task_order_id)
|
||||||
|
|
||||||
|
form_data = {**http_request.form, **http_request.files}
|
||||||
|
form_data["signer_dod_id"] = g.current_user.dod_id
|
||||||
|
form_data["signed_at"] = datetime.datetime.now()
|
||||||
|
|
||||||
|
if "unlimited_level_of_warrant" in form_data and form_data[
|
||||||
|
"unlimited_level_of_warrant"
|
||||||
|
] == ["y"]:
|
||||||
|
del form_data["level_of_warrant"]
|
||||||
|
|
||||||
|
form = SignatureForm(form_data)
|
||||||
|
|
||||||
|
if form.validate():
|
||||||
|
TaskOrders.update(user=g.current_user, task_order=task_order, **form.data)
|
||||||
|
return render_template("task_orders/signing/success.html"), 201
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
render_template(
|
||||||
|
"task_orders/signing/signature_requested.html",
|
||||||
|
task_order_id=task_order_id,
|
||||||
|
form=form,
|
||||||
|
),
|
||||||
|
400,
|
||||||
|
)
|
29
js/components/levelofwarrant.js
Normal file
29
js/components/levelofwarrant.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import textinput from './text_input'
|
||||||
|
import checkboxinput from './checkbox_input'
|
||||||
|
import FormMixin from '../mixins/form'
|
||||||
|
|
||||||
|
export default Vue.component('levelofwarrant', {
|
||||||
|
mixins: [FormMixin],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
textinput,
|
||||||
|
checkboxinput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { unlimited_level_of_warrant = false } = this.initialData
|
||||||
|
|
||||||
|
return {
|
||||||
|
unlimited_level_of_warrant,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
@ -6,6 +6,7 @@ import classes from '../styles/atat.scss'
|
|||||||
import Vue from 'vue/dist/vue'
|
import Vue from 'vue/dist/vue'
|
||||||
import VTooltip from 'v-tooltip'
|
import VTooltip from 'v-tooltip'
|
||||||
|
|
||||||
|
import levelofwarrant from './components/levelofwarrant'
|
||||||
import optionsinput from './components/options_input'
|
import optionsinput from './components/options_input'
|
||||||
import multicheckboxinput from './components/multi_checkbox_input'
|
import multicheckboxinput from './components/multi_checkbox_input'
|
||||||
import textinput from './components/text_input'
|
import textinput from './components/text_input'
|
||||||
@ -45,6 +46,7 @@ const app = new Vue({
|
|||||||
el: '#app-root',
|
el: '#app-root',
|
||||||
components: {
|
components: {
|
||||||
toggler,
|
toggler,
|
||||||
|
levelofwarrant,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
multicheckboxinput,
|
multicheckboxinput,
|
||||||
textinput,
|
textinput,
|
||||||
|
54
templates/task_orders/signing/signature_requested.html
Normal file
54
templates/task_orders/signing/signature_requested.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% from "components/text_input.html" import TextInput %}
|
||||||
|
{% from "components/checkbox_input.html" import CheckboxInput %}
|
||||||
|
{% from "components/icon.html" import Icon %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action='{{ url_for("task_orders.record_signature", task_order_id=task_order_id) }}'>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class="row row--pad">
|
||||||
|
<div class="col col--pad">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel__heading">
|
||||||
|
<h1 class="task-order-form__heading subheading">
|
||||||
|
<div class="h2">{{ "task_orders.sign.task_order_builder_title" | translate }}</div>
|
||||||
|
{{ "task_orders.sign.title" | translate }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel__content">
|
||||||
|
<div is="levelofwarrant" inline-template v-bind:initial-data='{{ form.data|tojson }}'>
|
||||||
|
<div>
|
||||||
|
<span v-bind:class="{ hide: !unlimited_level_of_warrant }">
|
||||||
|
{{ TextInput(form.level_of_warrant, validation='dollars', placeholder='$0.00', disabled=True) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-bind:class="{ hide: unlimited_level_of_warrant }">
|
||||||
|
{{ TextInput(form.level_of_warrant, validation='dollars', placeholder='$0.00') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
{{ CheckboxInput(form.unlimited_level_of_warrant) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ CheckboxInput(form.signature) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="action-group">
|
||||||
|
<button class="usa-button usa-button-big usa-button-primary">
|
||||||
|
{{ "common.sign" | translate }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href="" class="action-group__action icon-link">
|
||||||
|
{{ Icon('caret_left') }}
|
||||||
|
<span class="icon icon--x"></span>
|
||||||
|
{{ "common.back" | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
1
templates/task_orders/signing/success.html
Normal file
1
templates/task_orders/signing/success.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<h1>TO was signed successfully</h1>
|
@ -284,8 +284,5 @@ def test_submit_completed_ko_review_page(client, user_session, pdf_upload):
|
|||||||
|
|
||||||
assert task_order.pdf
|
assert task_order.pdf
|
||||||
assert response.headers["Location"] == url_for(
|
assert response.headers["Location"] == url_for(
|
||||||
"portfolios.view_task_order",
|
"task_orders.signature_requested", task_order_id=task_order.id, _external=True
|
||||||
portfolio_id=portfolio.id,
|
|
||||||
task_order_id=task_order.id,
|
|
||||||
_external=True,
|
|
||||||
)
|
)
|
||||||
|
129
tests/routes/task_orders/test_sign.py
Normal file
129
tests/routes/task_orders/test_sign.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
from atst.domain.task_orders import TaskOrders
|
||||||
|
from tests.factories import UserFactory, TaskOrderFactory, PortfolioFactory
|
||||||
|
|
||||||
|
|
||||||
|
def create_ko_task_order(user_session, contracting_officer):
|
||||||
|
portfolio = PortfolioFactory.create(owner=contracting_officer)
|
||||||
|
user_session(contracting_officer)
|
||||||
|
|
||||||
|
return TaskOrderFactory.create(
|
||||||
|
portfolio=portfolio, contracting_officer=contracting_officer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_signature_requested_not_ko(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
TaskOrders.update(contracting_officer, task_order, contracting_officer=None)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url_for("task_orders.signature_requested", task_order_id=task_order.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_signature_requested(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url_for("task_orders.signature_requested", task_order_id=task_order.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_signature_requested_already_signed(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
TaskOrders.update(
|
||||||
|
contracting_officer, task_order, signer_dod_id=contracting_officer.dod_id
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url_for("task_orders.signature_requested", task_order_id=task_order.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_signing_task_order_not_ko(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
TaskOrders.update(contracting_officer, task_order, contracting_officer=None)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("task_orders.record_signature", task_order_id=task_order.id), data={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_singing_an_already_signed_task_order(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
TaskOrders.update(
|
||||||
|
contracting_officer, task_order, signer_dod_id=contracting_officer.dod_id
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("task_orders.record_signature", task_order_id=task_order.id),
|
||||||
|
data={"signature": "y", "level_of_warrant": "33.33"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_signing_a_task_order(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
|
||||||
|
assert task_order.signed_at is None
|
||||||
|
assert task_order.signer_dod_id is None
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("task_orders.record_signature", task_order_id=task_order.id),
|
||||||
|
data={"signature": "y", "level_of_warrant": "33.33"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert task_order.signer_dod_id == contracting_officer.dod_id
|
||||||
|
assert task_order.signed_at is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_signing_a_task_order_failure(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("task_orders.record_signature", task_order_id=task_order.id),
|
||||||
|
data={"level_of_warrant": "33.33"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_signing_a_task_order_unlimited_level_of_warrant(client, user_session):
|
||||||
|
contracting_officer = UserFactory.create()
|
||||||
|
task_order = create_ko_task_order(user_session, contracting_officer)
|
||||||
|
|
||||||
|
assert task_order.signed_at is None
|
||||||
|
assert task_order.signer_dod_id is None
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url_for("task_orders.record_signature", task_order_id=task_order.id),
|
||||||
|
data={
|
||||||
|
"signature": "y",
|
||||||
|
"level_of_warrant": "33.33",
|
||||||
|
"unlimited_level_of_warrant": "y",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 201
|
||||||
|
assert task_order.signed_at is not None
|
||||||
|
assert task_order.signer_dod_id == contracting_officer.dod_id
|
||||||
|
assert task_order.unlimited_level_of_warrant == True
|
||||||
|
assert task_order.level_of_warrant == None
|
@ -20,7 +20,9 @@ base_public:
|
|||||||
login: Log in
|
login: Log in
|
||||||
title_tag: JEDI Cloud
|
title_tag: JEDI Cloud
|
||||||
common:
|
common:
|
||||||
|
back: Back
|
||||||
save_and_continue: Save & Continue
|
save_and_continue: Save & Continue
|
||||||
|
sign: Sign
|
||||||
components:
|
components:
|
||||||
modal:
|
modal:
|
||||||
close: Close
|
close: Close
|
||||||
@ -397,6 +399,15 @@ requests:
|
|||||||
questions_title_text: Questions related to JEDI Cloud migration
|
questions_title_text: Questions related to JEDI Cloud migration
|
||||||
rationalization_software_systems_tooltip: Rationalization is the DoD process to determine whether the application should move to the cloud.
|
rationalization_software_systems_tooltip: Rationalization is the DoD process to determine whether the application should move to the cloud.
|
||||||
task_orders:
|
task_orders:
|
||||||
|
sign:
|
||||||
|
digital_signature_description: I acknowledge that I have read and fully understand the DoD CCPO JEDI agreements and completed all the necessary steps, as detailed in the FAR (Federal Acquisition Regulation).
|
||||||
|
digital_signature_label: Digital Signature
|
||||||
|
level_of_warrant_description: This is a dollar value indicating that you have permission to sign any Task Order under this max value.
|
||||||
|
level_of_warrant_label: Level of Warrant
|
||||||
|
task_order_builder_title: Task Order Builder
|
||||||
|
title: Sign Task Order
|
||||||
|
unlimited_level_of_warrant_description: Unlimited Level of Warrant funds
|
||||||
|
verify_warrant_level_paragraph: Verify your level of warrant and provide your digital signature to authorize this Task Order.
|
||||||
new:
|
new:
|
||||||
app_info:
|
app_info:
|
||||||
section_title: "What You're Making"
|
section_title: "What You're Making"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user