Merge pull request #1069 from dod-ccpo/clin-obligated-and-total-funding
CLIN obligated and total funding
This commit is contained in:
commit
1fe755bb69
29
alembic/versions/f03333c42bdb_add_total_amount_to_clins.py
Normal file
29
alembic/versions/f03333c42bdb_add_total_amount_to_clins.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""Add total amount to CLINs
|
||||||
|
|
||||||
|
Revision ID: f03333c42bdb
|
||||||
|
Revises: 4a3122ffe898
|
||||||
|
Create Date: 2019-09-04 15:35:39.446486
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f03333c42bdb' # pragma: allowlist secret
|
||||||
|
down_revision = '30ea1cb20807' # pragma: allowlist secret
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('clins', sa.Column('total_amount', sa.Numeric(scale=2), nullable=True))
|
||||||
|
op.execute("UPDATE clins SET total_amount = 0")
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('clins', 'total_amount')
|
||||||
|
# ### end Alembic commands ###
|
@ -58,6 +58,7 @@ class TaskOrders(BaseDomainClass):
|
|||||||
number=clin_data["number"],
|
number=clin_data["number"],
|
||||||
start_date=clin_data["start_date"],
|
start_date=clin_data["start_date"],
|
||||||
end_date=clin_data["end_date"],
|
end_date=clin_data["end_date"],
|
||||||
|
total_amount=clin_data["total_amount"],
|
||||||
obligated_amount=clin_data["obligated_amount"],
|
obligated_amount=clin_data["obligated_amount"],
|
||||||
jedi_clin_type=clin_data["jedi_clin_type"],
|
jedi_clin_type=clin_data["jedi_clin_type"],
|
||||||
)
|
)
|
||||||
|
@ -7,9 +7,10 @@ from wtforms.fields import (
|
|||||||
HiddenField,
|
HiddenField,
|
||||||
)
|
)
|
||||||
from wtforms.fields.html5 import DateField
|
from wtforms.fields.html5 import DateField
|
||||||
from wtforms.validators import Required, Optional, Length
|
from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from numbers import Number
|
||||||
|
|
||||||
from .data import JEDI_CLIN_TYPES
|
from .data import JEDI_CLIN_TYPES
|
||||||
from .fields import SelectField
|
from .fields import SelectField
|
||||||
@ -17,6 +18,8 @@ from .forms import BaseForm
|
|||||||
from atst.utils.localization import translate
|
from atst.utils.localization import translate
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
|
MAX_CLIN_AMOUNT = 1000000000
|
||||||
|
|
||||||
|
|
||||||
def coerce_enum(enum_inst):
|
def coerce_enum(enum_inst):
|
||||||
if getattr(enum_inst, "value", None):
|
if getattr(enum_inst, "value", None):
|
||||||
@ -25,6 +28,17 @@ def coerce_enum(enum_inst):
|
|||||||
return enum_inst
|
return enum_inst
|
||||||
|
|
||||||
|
|
||||||
|
def validate_funding(form, field):
|
||||||
|
if (
|
||||||
|
isinstance(form.total_amount.data, Number)
|
||||||
|
and isinstance(field.data, Number)
|
||||||
|
and form.total_amount.data < field.data
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
translate("forms.task_order.clin_funding_errors.obligated_amount_error")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CLINForm(FlaskForm):
|
class CLINForm(FlaskForm):
|
||||||
jedi_clin_type = SelectField(
|
jedi_clin_type = SelectField(
|
||||||
translate("task_orders.form.clin_type_label"),
|
translate("task_orders.form.clin_type_label"),
|
||||||
@ -47,9 +61,26 @@ class CLINForm(FlaskForm):
|
|||||||
format="%m/%d/%Y",
|
format="%m/%d/%Y",
|
||||||
validators=[Optional()],
|
validators=[Optional()],
|
||||||
)
|
)
|
||||||
|
total_amount = DecimalField(
|
||||||
|
label=translate("task_orders.form.total_funds_label"),
|
||||||
|
validators=[
|
||||||
|
NumberRange(
|
||||||
|
0,
|
||||||
|
MAX_CLIN_AMOUNT,
|
||||||
|
translate("forms.task_order.clin_funding_errors.funding_range_error"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
obligated_amount = DecimalField(
|
obligated_amount = DecimalField(
|
||||||
label=translate("task_orders.form.obligated_funds_label"),
|
label=translate("task_orders.form.obligated_funds_label"),
|
||||||
validators=[Optional()],
|
validators=[
|
||||||
|
validate_funding,
|
||||||
|
NumberRange(
|
||||||
|
0,
|
||||||
|
MAX_CLIN_AMOUNT,
|
||||||
|
translate("forms.task_order.clin_funding_errors.funding_range_error"),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, *args, **kwargs):
|
def validate(self, *args, **kwargs):
|
||||||
|
@ -23,6 +23,7 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
number = Column(String, nullable=True)
|
number = Column(String, nullable=True)
|
||||||
start_date = Column(Date, nullable=True)
|
start_date = Column(Date, nullable=True)
|
||||||
end_date = Column(Date, nullable=True)
|
end_date = Column(Date, nullable=True)
|
||||||
|
total_amount = Column(Numeric(scale=2), nullable=True)
|
||||||
obligated_amount = Column(Numeric(scale=2), nullable=True)
|
obligated_amount = Column(Numeric(scale=2), nullable=True)
|
||||||
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True)
|
jedi_clin_type = Column(SQLAEnum(JEDICLINType, native_enum=False), nullable=True)
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ class CLIN(Base, mixins.TimestampsMixin):
|
|||||||
self.number,
|
self.number,
|
||||||
self.start_date,
|
self.start_date,
|
||||||
self.end_date,
|
self.end_date,
|
||||||
|
self.total_amount,
|
||||||
self.obligated_amount,
|
self.obligated_amount,
|
||||||
self.jedi_clin_type,
|
self.jedi_clin_type,
|
||||||
]
|
]
|
||||||
|
@ -6,7 +6,6 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from atst.models import Attachment, Base, mixins, types
|
from atst.models import Attachment, Base, mixins, types
|
||||||
from atst.models.clin import JEDICLINType
|
|
||||||
from atst.utils.clock import Clock
|
from atst.utils.clock import Clock
|
||||||
|
|
||||||
|
|
||||||
@ -148,10 +147,7 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def total_obligated_funds(self):
|
def total_obligated_funds(self):
|
||||||
total = 0
|
total = 0
|
||||||
for clin in self.clins:
|
for clin in self.clins:
|
||||||
if clin.obligated_amount is not None and clin.jedi_clin_type in [
|
if clin.obligated_amount is not None:
|
||||||
JEDICLINType.JEDI_CLIN_1,
|
|
||||||
JEDICLINType.JEDI_CLIN_3,
|
|
||||||
]:
|
|
||||||
total += clin.obligated_amount
|
total += clin.obligated_amount
|
||||||
return total
|
return total
|
||||||
|
|
||||||
@ -159,8 +155,8 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
|||||||
def total_contract_amount(self):
|
def total_contract_amount(self):
|
||||||
total = 0
|
total = 0
|
||||||
for clin in self.clins:
|
for clin in self.clins:
|
||||||
if clin.obligated_amount is not None:
|
if clin.total_amount is not None:
|
||||||
total += clin.obligated_amount
|
total += clin.total_amount
|
||||||
return total
|
return total
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
39
js/components/clin_dollar_amount.js
Normal file
39
js/components/clin_dollar_amount.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import inputValidations from '../lib/input_validations'
|
||||||
|
import TextInputMixin from '../mixins/text_input_mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'clindollaramount',
|
||||||
|
|
||||||
|
mixins: [TextInputMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
fundingValid: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
rawValue: function() {
|
||||||
|
return this._rawValue(this.value)
|
||||||
|
},
|
||||||
|
showFundingError: function() {
|
||||||
|
return this.showError || !this.fundingValid
|
||||||
|
},
|
||||||
|
showFundingValid: function() {
|
||||||
|
return this.showValid && this.fundingValid
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fundingValid: function(oldVal, newVal) {
|
||||||
|
this._checkIfValid({ value: this.value, invalidate: true })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
_validate: function(value) {
|
||||||
|
const rawValue = this._rawValue(value)
|
||||||
|
if (rawValue < 0 || rawValue > 1000000000 || !this.fundingValid) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return inputValidations[this.validation].match.test(rawValue)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -6,7 +6,10 @@ import { emitEvent } from '../lib/emitters'
|
|||||||
import Modal from '../mixins/modal'
|
import Modal from '../mixins/modal'
|
||||||
import optionsinput from './options_input'
|
import optionsinput from './options_input'
|
||||||
import textinput from './text_input'
|
import textinput from './text_input'
|
||||||
|
import clindollaramount from './clin_dollar_amount'
|
||||||
|
|
||||||
|
const TOTAL_AMOUNT = 'total_amount'
|
||||||
|
const OBLIGATED_AMOUNT = 'obligated_amount'
|
||||||
const START_DATE = 'start_date'
|
const START_DATE = 'start_date'
|
||||||
const END_DATE = 'end_date'
|
const END_DATE = 'end_date'
|
||||||
const POP = 'period_of_performance'
|
const POP = 'period_of_performance'
|
||||||
@ -19,12 +22,21 @@ export default {
|
|||||||
DateSelector,
|
DateSelector,
|
||||||
optionsinput,
|
optionsinput,
|
||||||
textinput,
|
textinput,
|
||||||
|
clindollaramount,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [Modal],
|
mixins: [Modal],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
initialClinIndex: Number,
|
initialClinIndex: Number,
|
||||||
|
initialTotal: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
initialObligated: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
initialStartDate: {
|
initialStartDate: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
@ -54,6 +66,10 @@ export default {
|
|||||||
const end = !!this.initialEndDate
|
const end = !!this.initialEndDate
|
||||||
? new Date(this.initialEndDate)
|
? new Date(this.initialEndDate)
|
||||||
: undefined
|
: undefined
|
||||||
|
const fundingValidation =
|
||||||
|
this.initialObligated && this.initialTotal
|
||||||
|
? this.initialObligated <= this.initialTotal
|
||||||
|
: true
|
||||||
const popValidation = !this.initialStartDate ? false : start < end
|
const popValidation = !this.initialStartDate ? false : start < end
|
||||||
const clinNumber = !!this.initialClinNumber
|
const clinNumber = !!this.initialClinNumber
|
||||||
? this.initialClinNumber
|
? this.initialClinNumber
|
||||||
@ -63,6 +79,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
clinIndex: this.initialClinIndex,
|
clinIndex: this.initialClinIndex,
|
||||||
|
clinNumber: clinNumber,
|
||||||
startDate: start,
|
startDate: start,
|
||||||
endDate: end,
|
endDate: end,
|
||||||
popValid: popValidation,
|
popValid: popValidation,
|
||||||
@ -93,14 +110,23 @@ export default {
|
|||||||
)}.`,
|
)}.`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
totalAmount: this.initialTotal || 0,
|
||||||
|
obligatedAmount: this.initialObligated || 0,
|
||||||
|
fundingValid: fundingValidation,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
mounted: function() {
|
||||||
this.$root.$on('field-change', this.handleFieldChange)
|
this.$root.$on('field-change', this.handleFieldChange)
|
||||||
|
this.validateFunding()
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
|
emitEvent('clin-change', this, {
|
||||||
|
id: this._uid,
|
||||||
|
obligatedAmount: this.initialObligated,
|
||||||
|
totalAmount: this.initialTotal,
|
||||||
|
})
|
||||||
emitEvent('field-mount', this, {
|
emitEvent('field-mount', this, {
|
||||||
optional: false,
|
optional: false,
|
||||||
name: 'clins-' + this.clinIndex + '-' + POP,
|
name: 'clins-' + this.clinIndex + '-' + POP,
|
||||||
@ -109,6 +135,14 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
clinChangeEvent: function() {
|
||||||
|
emitEvent('clin-change', this, {
|
||||||
|
id: this._uid,
|
||||||
|
obligatedAmount: this.initialObligated,
|
||||||
|
totalAmount: this.initialTotal,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
checkPopValid: function() {
|
checkPopValid: function() {
|
||||||
return (
|
return (
|
||||||
this.popDateOrder() &&
|
this.popDateOrder() &&
|
||||||
@ -153,9 +187,25 @@ export default {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkFundingValid: function() {
|
||||||
|
return this.obligatedAmount <= this.totalAmount
|
||||||
|
},
|
||||||
|
|
||||||
|
validateFunding: function() {
|
||||||
|
if (this.totalAmount && this.obligatedAmount) {
|
||||||
|
this.fundingValid = this.checkFundingValid()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleFieldChange: function(event) {
|
handleFieldChange: function(event) {
|
||||||
if (this._uid === event.parent_uid) {
|
if (this._uid === event.parent_uid) {
|
||||||
if (event.name.includes(START_DATE)) {
|
if (event.name.includes(TOTAL_AMOUNT)) {
|
||||||
|
this.totalAmount = parseFloat(event.value)
|
||||||
|
this.validateFunding()
|
||||||
|
} else if (event.name.includes(OBLIGATED_AMOUNT)) {
|
||||||
|
this.obligatedAmount = parseFloat(event.value)
|
||||||
|
this.validateFunding()
|
||||||
|
} else if (event.name.includes(START_DATE)) {
|
||||||
if (!!event.value) this.startDate = new Date(event.value)
|
if (!!event.value) this.startDate = new Date(event.value)
|
||||||
if (!!event.valid) this.startDateValid = event.valid
|
if (!!event.valid) this.startDateValid = event.valid
|
||||||
this.validatePop()
|
this.validatePop()
|
||||||
@ -186,6 +236,20 @@ export default {
|
|||||||
return `CLIN`
|
return `CLIN`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
percentObligated: function() {
|
||||||
|
const percentage = (this.obligatedAmount / this.totalAmount) * 100
|
||||||
|
if (!!percentage) {
|
||||||
|
if (percentage > 0 && percentage < 1) {
|
||||||
|
return '<1%'
|
||||||
|
} else if (percentage > 99 && percentage < 100) {
|
||||||
|
return '>99%'
|
||||||
|
} else {
|
||||||
|
return `${percentage.toFixed(0)}%`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return '0%'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
removeModalId: function() {
|
removeModalId: function() {
|
||||||
return `remove-clin-${this.clinIndex}`
|
return `remove-clin-${this.clinIndex}`
|
||||||
|
@ -1,174 +1,6 @@
|
|||||||
import MaskedInput, { conformToMask } from 'vue-text-mask'
|
import TextInputMixin from '../mixins/text_input_mixin'
|
||||||
import inputValidations from '../lib/input_validations'
|
|
||||||
import { formatDollars } from '../lib/dollars'
|
|
||||||
import { emitEvent } from '../lib/emitters'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'textinput',
|
name: 'textinput',
|
||||||
|
mixins: [TextInputMixin],
|
||||||
components: {
|
|
||||||
MaskedInput,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
name: String,
|
|
||||||
validation: {
|
|
||||||
type: String,
|
|
||||||
default: () => 'anything',
|
|
||||||
},
|
|
||||||
initialValue: {
|
|
||||||
type: String,
|
|
||||||
default: () => '',
|
|
||||||
},
|
|
||||||
initialErrors: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
paragraph: String,
|
|
||||||
noMaxWidth: String,
|
|
||||||
optional: Boolean,
|
|
||||||
watch: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
showError: (this.initialErrors && this.initialErrors.length) || false,
|
|
||||||
showValid: false,
|
|
||||||
mask: inputValidations[this.validation].mask,
|
|
||||||
pipe: inputValidations[this.validation].pipe || undefined,
|
|
||||||
keepCharPositions:
|
|
||||||
inputValidations[this.validation].keepCharPositions || false,
|
|
||||||
validationError:
|
|
||||||
this.initialErrors.join(' ') ||
|
|
||||||
inputValidations[this.validation].validationError,
|
|
||||||
value: this.initialValue,
|
|
||||||
modified: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
rawValue: function() {
|
|
||||||
return this._rawValue(this.value)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted: function() {
|
|
||||||
if (this.value) {
|
|
||||||
this._checkIfValid({
|
|
||||||
value: this.value,
|
|
||||||
invalidate: true,
|
|
||||||
showValidationIcon: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.mask && this.validation !== 'email') {
|
|
||||||
const mask =
|
|
||||||
typeof this.mask.mask !== 'function'
|
|
||||||
? this.mask
|
|
||||||
: mask.mask(this.value).filter(val => val !== '[]')
|
|
||||||
|
|
||||||
this.value = conformToMask(this.value, mask).conformedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created: function() {
|
|
||||||
emitEvent('field-mount', this, {
|
|
||||||
optional: this.optional,
|
|
||||||
name: this.name,
|
|
||||||
valid: this._isValid(this.value),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
// When user types a character
|
|
||||||
onInput: function(e) {
|
|
||||||
// When we use the native textarea element, we receive an event object
|
|
||||||
// When we use the masked-input component, we receive the value directly
|
|
||||||
const value = typeof e === 'object' ? e.target.value : e
|
|
||||||
this.value = value
|
|
||||||
this.modified = true
|
|
||||||
this._checkIfValid({ value })
|
|
||||||
},
|
|
||||||
|
|
||||||
// When field is blurred (un-focused)
|
|
||||||
onChange: function(e) {
|
|
||||||
// Only invalidate the field when it blurs
|
|
||||||
this._checkIfValid({ value: e.target.value, invalidate: true })
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlur: function(e) {
|
|
||||||
if (!(this.optional && e.target.value === '')) {
|
|
||||||
this._checkIfValid({ value: e.target.value.trim(), invalidate: true })
|
|
||||||
} else if (this.modified && !this.optional) {
|
|
||||||
this._checkIfValid({ value: e.target.value.trim(), invalidate: true })
|
|
||||||
}
|
|
||||||
this.value = e.target.value.trim()
|
|
||||||
|
|
||||||
if (this.validation === 'dollars') {
|
|
||||||
let value = Number.isNaN(e.target.value) ? '0' : e.target.value
|
|
||||||
this.value = formatDollars(this._rawValue(value))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
_checkIfValid: function({
|
|
||||||
value,
|
|
||||||
invalidate = false,
|
|
||||||
showValidationIcon = true,
|
|
||||||
}) {
|
|
||||||
const valid = this._isValid(value)
|
|
||||||
if (this.modified) {
|
|
||||||
this.validationError = inputValidations[this.validation].validationError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error messages or not
|
|
||||||
if (valid) {
|
|
||||||
this.showError = false
|
|
||||||
} else if (invalidate) {
|
|
||||||
this.showError = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showValidationIcon) {
|
|
||||||
this.showValid = this.value != '' && valid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit a change event
|
|
||||||
emitEvent('field-change', this, {
|
|
||||||
value: this._rawValue(value),
|
|
||||||
valid: this._isValid(value),
|
|
||||||
name: this.name,
|
|
||||||
watch: this.watch,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
_rawValue: function(value) {
|
|
||||||
return inputValidations[this.validation].unmask.reduce(
|
|
||||||
(currentValue, character) => {
|
|
||||||
return currentValue.split(character).join('')
|
|
||||||
},
|
|
||||||
value
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
_validate: function(value) {
|
|
||||||
return inputValidations[this.validation].match.test(this._rawValue(value))
|
|
||||||
},
|
|
||||||
|
|
||||||
_isValid: function(value) {
|
|
||||||
let valid = this._validate(value)
|
|
||||||
|
|
||||||
if (!this.modified && this.initialErrors && this.initialErrors.length) {
|
|
||||||
valid = false
|
|
||||||
} else if (this.optional && value === '') {
|
|
||||||
valid = true
|
|
||||||
} else if (!this.optional && value === '') {
|
|
||||||
valid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return valid
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,13 @@ export default {
|
|||||||
unmask: ['$', ','],
|
unmask: ['$', ','],
|
||||||
validationError: 'Please enter a dollar amount',
|
validationError: 'Please enter a dollar amount',
|
||||||
},
|
},
|
||||||
|
clinDollars: {
|
||||||
|
mask: createNumberMask({ prefix: '$', allowDecimal: true }),
|
||||||
|
match: /^-?\d+\.?\d*$/,
|
||||||
|
unmask: ['$', ','],
|
||||||
|
validationError:
|
||||||
|
'Please enter a dollar amount between $0.00 and $1,000,000,000.00',
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
mask: emailMask,
|
mask: emailMask,
|
||||||
match: /^.+@[^.].*\.[a-zA-Z]{2,10}$/,
|
match: /^.+@[^.].*\.[a-zA-Z]{2,10}$/,
|
||||||
|
173
js/mixins/text_input_mixin.js
Normal file
173
js/mixins/text_input_mixin.js
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import MaskedInput, { conformToMask } from 'vue-text-mask'
|
||||||
|
import inputValidations from '../lib/input_validations'
|
||||||
|
import { formatDollars } from '../lib/dollars'
|
||||||
|
import { emitEvent } from '../lib/emitters'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'textinput',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
MaskedInput,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
validation: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'anything',
|
||||||
|
},
|
||||||
|
initialValue: {
|
||||||
|
type: String,
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
|
initialErrors: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
paragraph: String,
|
||||||
|
noMaxWidth: String,
|
||||||
|
optional: Boolean,
|
||||||
|
watch: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
showError: (this.initialErrors && this.initialErrors.length) || false,
|
||||||
|
showValid: false,
|
||||||
|
mask: inputValidations[this.validation].mask,
|
||||||
|
pipe: inputValidations[this.validation].pipe || undefined,
|
||||||
|
keepCharPositions:
|
||||||
|
inputValidations[this.validation].keepCharPositions || false,
|
||||||
|
validationError:
|
||||||
|
this.initialErrors.join(' ') ||
|
||||||
|
inputValidations[this.validation].validationError,
|
||||||
|
value: this.initialValue,
|
||||||
|
modified: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
rawValue: function() {
|
||||||
|
return this._rawValue(this.value)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
if (this.value) {
|
||||||
|
this._checkIfValid({
|
||||||
|
value: this.value,
|
||||||
|
invalidate: true,
|
||||||
|
showValidationIcon: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.mask && this.validation !== 'email') {
|
||||||
|
const mask =
|
||||||
|
typeof this.mask.mask !== 'function'
|
||||||
|
? this.mask
|
||||||
|
: mask.mask(this.value).filter(val => val !== '[]')
|
||||||
|
|
||||||
|
this.value = conformToMask(this.value, mask).conformedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function() {
|
||||||
|
emitEvent('field-mount', this, {
|
||||||
|
optional: this.optional,
|
||||||
|
name: this.name,
|
||||||
|
valid: this._isValid(this.value),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// When user types a character
|
||||||
|
onInput: function(e) {
|
||||||
|
// When we use the native textarea element, we receive an event object
|
||||||
|
// When we use the masked-input component, we receive the value directly
|
||||||
|
const value = typeof e === 'object' ? e.target.value : e
|
||||||
|
this.value = value
|
||||||
|
this.modified = true
|
||||||
|
this._checkIfValid({ value })
|
||||||
|
},
|
||||||
|
|
||||||
|
// When field is blurred (un-focused)
|
||||||
|
onChange: function(e) {
|
||||||
|
// Only invalidate the field when it blurs
|
||||||
|
this._checkIfValid({ value: e.target.value, invalidate: true })
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlur: function(e) {
|
||||||
|
if (!(this.optional && e.target.value === '')) {
|
||||||
|
this._checkIfValid({ value: e.target.value.trim(), invalidate: true })
|
||||||
|
} else if (this.modified && !this.optional) {
|
||||||
|
this._checkIfValid({ value: e.target.value.trim(), invalidate: true })
|
||||||
|
}
|
||||||
|
this.value = e.target.value.trim()
|
||||||
|
|
||||||
|
if (this.validation === 'dollars') {
|
||||||
|
let value = Number.isNaN(e.target.value) ? '0' : e.target.value
|
||||||
|
this.value = formatDollars(this._rawValue(value))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
_checkIfValid: function({
|
||||||
|
value,
|
||||||
|
invalidate = false,
|
||||||
|
showValidationIcon = true,
|
||||||
|
}) {
|
||||||
|
const valid = this._isValid(value)
|
||||||
|
if (this.modified) {
|
||||||
|
this.validationError = inputValidations[this.validation].validationError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error messages or not
|
||||||
|
if (valid) {
|
||||||
|
this.showError = false
|
||||||
|
} else if (invalidate) {
|
||||||
|
this.showError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showValidationIcon) {
|
||||||
|
this.showValid = this.value != '' && valid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit a change event
|
||||||
|
emitEvent('field-change', this, {
|
||||||
|
value: this._rawValue(value),
|
||||||
|
valid: this._isValid(value),
|
||||||
|
name: this.name,
|
||||||
|
watch: this.watch,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
_rawValue: function(value) {
|
||||||
|
return inputValidations[this.validation].unmask.reduce(
|
||||||
|
(currentValue, character) => {
|
||||||
|
return currentValue.split(character).join('')
|
||||||
|
},
|
||||||
|
value
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
_validate: function(value) {
|
||||||
|
return inputValidations[this.validation].match.test(this._rawValue(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
_isValid: function(value) {
|
||||||
|
let valid = this._validate(value)
|
||||||
|
if (!this.modified && this.initialErrors && this.initialErrors.length) {
|
||||||
|
valid = false
|
||||||
|
} else if (this.optional && value === '') {
|
||||||
|
valid = true
|
||||||
|
} else if (!this.optional && value === '') {
|
||||||
|
valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
70
templates/components/clin_dollar_amount.html
Normal file
70
templates/components/clin_dollar_amount.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% from 'components/icon.html' import Icon %}
|
||||||
|
|
||||||
|
{% macro CLINDollarAmount(type, field=None, funding_validation=False) -%}
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-col">
|
||||||
|
<clindollaramount
|
||||||
|
v-cloak
|
||||||
|
inline-template
|
||||||
|
{% if funding_validation %}
|
||||||
|
:funding-valid='fundingValid'
|
||||||
|
{% else %}
|
||||||
|
:funding-valid='true'
|
||||||
|
{% endif %}
|
||||||
|
{% if field %}
|
||||||
|
name='{{ field.name }}'
|
||||||
|
{% if field.data is not none %}initial-value='{{ field.data }}'{% endif %}
|
||||||
|
{% if field.errors %}v-bind:initial-errors='{{ field.errors | list }}'{% endif %}
|
||||||
|
key='{{ field.name }}'
|
||||||
|
{% else %}
|
||||||
|
:name="'clins-' + clinIndex + '-' + '{{ type }}' + '_amount'"
|
||||||
|
:key="'clins-' + clinIndex + '-' + '{{ type }}' + '_amount'"
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
validation="clinDollars"
|
||||||
|
:watch='true'>
|
||||||
|
<div v-bind:class="['usa-input usa-input--validation--dollars', { 'usa-input--error': showFundingError, 'usa-input--success': showFundingValid}]">
|
||||||
|
{% if field %}
|
||||||
|
<label for='{{ field.name }}'>
|
||||||
|
{% else %}
|
||||||
|
<label :for='name'>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if type=="obligated" %}
|
||||||
|
<div class="usa-input__title">{{ 'task_orders.form.obligated_funds_label' | translate }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="usa-input__title">{{ 'task_orders.form.total_funds_label' | translate }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<span v-show='showFundingError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
||||||
|
<span v-show='showFundingValid'>{{ Icon('ok',classes="icon-validation") }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<masked-input
|
||||||
|
v-on:input='onInput'
|
||||||
|
v-on:blur='onBlur'
|
||||||
|
v-on:change='onChange'
|
||||||
|
v-bind:value='value'
|
||||||
|
v-bind:mask='mask'
|
||||||
|
v-bind:pipe='pipe'
|
||||||
|
v-bind:keep-char-positions='keepCharPositions'
|
||||||
|
v-bind:aria-invalid='showError'
|
||||||
|
type='text'
|
||||||
|
:id='name'
|
||||||
|
ref='input'>
|
||||||
|
</masked-input>
|
||||||
|
|
||||||
|
<input type='hidden' v-bind:value='rawValue' :name='name' />
|
||||||
|
<template v-if='!fundingValid'>
|
||||||
|
<span class='usa-input__message'>{{ "forms.task_order.clin_funding_errors.obligated_amount_error" | translate }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if='showError'>
|
||||||
|
<span class='usa-input__message' v-html='validationError'></span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class='usa-input__message'></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</clindollaramount>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
@ -43,7 +43,7 @@
|
|||||||
<th>{{ "task_orders.review.clins.type" | translate }}</th>
|
<th>{{ "task_orders.review.clins.type" | translate }}</th>
|
||||||
<th>{{ "task_orders.review.clins.idiq_clin_description" | translate }}</th>
|
<th>{{ "task_orders.review.clins.idiq_clin_description" | translate }}</th>
|
||||||
<th>{{ "task_orders.review.clins.pop" | translate }}</th>
|
<th>{{ "task_orders.review.clins.pop" | translate }}</th>
|
||||||
<th class="task-order__amount">{{ "task_orders.review.clins.amount" | translate }}</th>
|
<th class="task-order__amount">{{ "task_orders.review.clins.total_amount" | translate }}</th>
|
||||||
<th class="task-order__amount">{{ "task_orders.review.clins.obligated" | translate }}</th>
|
<th class="task-order__amount">{{ "task_orders.review.clins.obligated" | translate }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
{{ clin.start_date | formattedDate }} - {{ clin.end_date | formattedDate }}
|
{{ clin.start_date | formattedDate }} - {{ clin.end_date | formattedDate }}
|
||||||
</td>
|
</td>
|
||||||
{# TODO: Swap in total CLIN amount #}
|
{# TODO: Swap in total CLIN amount #}
|
||||||
<td class="task-order__amount">$123,456,789.00</td>
|
<td class="task-order__amount">{{ clin.total_amount | dollars }}</td>
|
||||||
<td class="task-order__amount">{{ clin.obligated_amount | dollars }}</td>
|
<td class="task-order__amount">{{ clin.obligated_amount | dollars }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -81,6 +81,9 @@
|
|||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<h3>Task Order #{{ task_order.number }}</h3>
|
<h3>Task Order #{{ task_order.number }}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card__body">
|
||||||
|
<b>Total amount: </b>{{ task_order.total_contract_amount | dollars }}
|
||||||
|
</div>
|
||||||
<div class="card__body">
|
<div class="card__body">
|
||||||
<b>Obligated amount: </b>{{ task_order.total_obligated_funds | dollars }}
|
<b>Obligated amount: </b>{{ task_order.total_obligated_funds | dollars }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
{% from 'components/icon.html' import Icon %}
|
{% from 'components/icon.html' import Icon %}
|
||||||
{% from 'components/options_input.html' import OptionsInput %}
|
{% from 'components/options_input.html' import OptionsInput %}
|
||||||
{% from 'components/text_input.html' import TextInput %}
|
{% from 'components/text_input.html' import TextInput %}
|
||||||
|
{% from "components/clin_dollar_amount.html" import CLINDollarAmount %}
|
||||||
{% from 'task_orders/form_header.html' import TOFormStepHeader %}
|
{% from 'task_orders/form_header.html' import TOFormStepHeader %}
|
||||||
|
|
||||||
{% set action = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id) %}
|
{% set action = url_for("task_orders.submit_form_step_three_add_clins", task_order_id=task_order_id) %}
|
||||||
@ -16,6 +17,8 @@
|
|||||||
<clin-fields
|
<clin-fields
|
||||||
{% if fields %}
|
{% if fields %}
|
||||||
v-bind:initial-clin-index='{{ index }}'
|
v-bind:initial-clin-index='{{ index }}'
|
||||||
|
v-bind:initial-total='{{ fields.total_amount.data or 0 }}'
|
||||||
|
v-bind:initial-obligated='{{ fields.obligated_amount.data or 0 }}'
|
||||||
v-bind:initial-start-date="'{{ fields.start_date.data | string }}'"
|
v-bind:initial-start-date="'{{ fields.start_date.data | string }}'"
|
||||||
v-bind:initial-end-date="'{{ fields.end_date.data | string }}'"
|
v-bind:initial-end-date="'{{ fields.end_date.data | string }}'"
|
||||||
v-bind:initial-clin-number="'{{ fields.number.data | string }}'"
|
v-bind:initial-clin-number="'{{ fields.number.data | string }}'"
|
||||||
@ -115,56 +118,18 @@
|
|||||||
{{ 'task_orders.form.clin_funding' | translate }}
|
{{ 'task_orders.form.clin_funding' | translate }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if fields %}
|
{% if fields %}
|
||||||
<div class="form-row">
|
{{ CLINDollarAmount("total", field=fields.total_amount) }}
|
||||||
<div class="form-col">
|
{{ CLINDollarAmount("obligated", field=fields.obligated_amount, funding_validation=True) }}
|
||||||
{{ TextInput(fields.obligated_amount, validation='dollars', watch=True, optional=False) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="form-row">
|
{{ CLINDollarAmount("total") }}
|
||||||
<div class="form-col">
|
{{ CLINDollarAmount("obligated", funding_validation=True) }}
|
||||||
<textinput
|
|
||||||
v-cloak
|
|
||||||
inline-template
|
|
||||||
:name="'clins-' + clinIndex + '-obligated_amount'"
|
|
||||||
validation="dollars"
|
|
||||||
:watch='true'>
|
|
||||||
<div v-bind:class="['usa-input usa-input--validation--' + validation, { 'usa-input--error': showError, 'usa-input--success': showValid, 'usa-input--validation--paragraph': paragraph, 'no-max-width': noMaxWidth }]">
|
|
||||||
<label :for="name">
|
|
||||||
<div class="usa-input__title">{{ 'task_orders.form.obligated_funds_label' | translate }}</div>
|
|
||||||
<span v-show='showError'>{{ Icon('alert',classes="icon-validation") }}</span>
|
|
||||||
<span v-show='showValid'>{{ Icon('ok',classes="icon-validation") }}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<masked-input
|
|
||||||
v-on:input='onInput'
|
|
||||||
v-on:blur='onBlur'
|
|
||||||
v-on:change='onChange'
|
|
||||||
v-bind:value='value'
|
|
||||||
v-bind:mask='mask'
|
|
||||||
v-bind:pipe='pipe'
|
|
||||||
v-bind:keep-char-positions='keepCharPositions'
|
|
||||||
v-bind:aria-invalid='showError'
|
|
||||||
v-bind:show-mask='false'
|
|
||||||
type='text'
|
|
||||||
:id='name'
|
|
||||||
ref='input'>
|
|
||||||
</masked-input>
|
|
||||||
|
|
||||||
<input type='hidden' v-bind:value='rawValue' :name='name' />
|
|
||||||
|
|
||||||
<template v-if='showError'>
|
|
||||||
<span class='usa-input__message' v-html='validationError'></span>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class='usa-input__message'></span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</textinput>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="h5 clin-card__title">Percent Obligated</div>
|
||||||
|
<p v-html='percentObligated'></p>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="h4 clin-card__title">
|
<div class="h4 clin-card__title">
|
||||||
|
@ -84,6 +84,7 @@ def test_create_adds_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jedi_clin_type": "JEDI_CLIN_1",
|
"jedi_clin_type": "JEDI_CLIN_1",
|
||||||
@ -91,6 +92,7 @@ def test_create_adds_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
task_order = TaskOrders.create(
|
task_order = TaskOrders.create(
|
||||||
@ -113,6 +115,7 @@ def test_update_adds_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jedi_clin_type": "JEDI_CLIN_1",
|
"jedi_clin_type": "JEDI_CLIN_1",
|
||||||
@ -120,6 +123,7 @@ def test_update_adds_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
task_order = TaskOrders.create(
|
task_order = TaskOrders.create(
|
||||||
@ -144,6 +148,7 @@ def test_update_does_not_duplicate_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jedi_clin_type": "JEDI_CLIN_1",
|
"jedi_clin_type": "JEDI_CLIN_1",
|
||||||
@ -151,6 +156,7 @@ def test_update_does_not_duplicate_clins():
|
|||||||
"start_date": date(2020, 1, 1),
|
"start_date": date(2020, 1, 1),
|
||||||
"end_date": date(2021, 1, 1),
|
"end_date": date(2021, 1, 1),
|
||||||
"obligated_amount": Decimal("5000"),
|
"obligated_amount": Decimal("5000"),
|
||||||
|
"total_amount": Decimal("10000"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
task_order = TaskOrders.update(
|
task_order = TaskOrders.update(
|
||||||
|
@ -291,7 +291,8 @@ class CLINFactory(Base):
|
|||||||
number = factory.LazyFunction(random_task_order_number)
|
number = factory.LazyFunction(random_task_order_number)
|
||||||
start_date = datetime.date.today()
|
start_date = datetime.date.today()
|
||||||
end_date = factory.LazyFunction(random_future_date)
|
end_date = factory.LazyFunction(random_future_date)
|
||||||
obligated_amount = factory.LazyFunction(lambda *args: random.randint(100, 999999))
|
total_amount = factory.LazyFunction(lambda *args: random.randint(50000, 999999))
|
||||||
|
obligated_amount = factory.LazyFunction(lambda *args: random.randint(100, 50000))
|
||||||
jedi_clin_type = factory.LazyFunction(
|
jedi_clin_type = factory.LazyFunction(
|
||||||
lambda *args: random.choice(list(clin.JEDICLINType))
|
lambda *args: random.choice(list(clin.JEDICLINType))
|
||||||
)
|
)
|
||||||
|
@ -72,3 +72,34 @@ def test_clin_form_pop_dates_within_contract_dates():
|
|||||||
)
|
)
|
||||||
valid_clin_form = CLINForm(obj=valid_clin)
|
valid_clin_form = CLINForm(obj=valid_clin)
|
||||||
assert valid_clin_form.validate()
|
assert valid_clin_form.validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clin_form_obligated_greater_than_total():
|
||||||
|
invalid_clin = factories.CLINFactory.create(
|
||||||
|
total_amount=0,
|
||||||
|
obligated_amount=1,
|
||||||
|
start_date=datetime.date(2019, 9, 15),
|
||||||
|
end_date=datetime.date(2020, 9, 14),
|
||||||
|
)
|
||||||
|
invalid_clin_form = CLINForm(obj=invalid_clin)
|
||||||
|
assert not invalid_clin_form.validate()
|
||||||
|
assert (
|
||||||
|
translate("forms.task_order.clin_funding_errors.obligated_amount_error")
|
||||||
|
) in invalid_clin_form.obligated_amount.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_clin_form_dollar_amounts_out_of_range():
|
||||||
|
invalid_clin = factories.CLINFactory.create(
|
||||||
|
total_amount=-1,
|
||||||
|
obligated_amount=1000000001,
|
||||||
|
start_date=datetime.date(2019, 9, 15),
|
||||||
|
end_date=datetime.date(2020, 9, 14),
|
||||||
|
)
|
||||||
|
invalid_clin_form = CLINForm(obj=invalid_clin)
|
||||||
|
assert not invalid_clin_form.validate()
|
||||||
|
assert (
|
||||||
|
translate("forms.task_order.clin_funding_errors.funding_range_error")
|
||||||
|
) in invalid_clin_form.total_amount.errors
|
||||||
|
assert (
|
||||||
|
translate("forms.task_order.clin_funding_errors.funding_range_error")
|
||||||
|
) in invalid_clin_form.obligated_amount.errors
|
||||||
|
@ -152,19 +152,23 @@ class TestBudget:
|
|||||||
|
|
||||||
assert (
|
assert (
|
||||||
to.total_contract_amount
|
to.total_contract_amount
|
||||||
== clin1.obligated_amount + clin2.obligated_amount + clin3.obligated_amount
|
== clin1.total_amount + clin2.total_amount + clin3.total_amount
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_total_obligated_funds(self):
|
def test_total_obligated_funds(self):
|
||||||
to = TaskOrder()
|
to = TaskOrder()
|
||||||
clin4 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_4)
|
|
||||||
assert to.total_obligated_funds == 0
|
assert to.total_obligated_funds == 0
|
||||||
|
|
||||||
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)
|
clin1 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_1)
|
||||||
clin2 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_2)
|
clin2 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_2)
|
||||||
clin3 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_3)
|
clin3 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_3)
|
||||||
|
clin4 = CLINFactory(task_order=to, jedi_clin_type=JEDICLINType.JEDI_CLIN_4)
|
||||||
assert (
|
assert (
|
||||||
to.total_obligated_funds == clin1.obligated_amount + clin3.obligated_amount
|
to.total_obligated_funds
|
||||||
|
== clin1.obligated_amount
|
||||||
|
+ clin2.obligated_amount
|
||||||
|
+ clin3.obligated_amount
|
||||||
|
+ clin4.obligated_amount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,11 +194,13 @@ def test_task_orders_submit_form_step_three_add_clins(client, user_session, task
|
|||||||
"clins-0-start_date": "01/01/2020",
|
"clins-0-start_date": "01/01/2020",
|
||||||
"clins-0-end_date": "01/01/2021",
|
"clins-0-end_date": "01/01/2021",
|
||||||
"clins-0-obligated_amount": "5000",
|
"clins-0-obligated_amount": "5000",
|
||||||
|
"clins-0-total_amount": "10000",
|
||||||
"clins-1-jedi_clin_type": "JEDI_CLIN_1",
|
"clins-1-jedi_clin_type": "JEDI_CLIN_1",
|
||||||
"clins-1-number": "12312",
|
"clins-1-number": "12312",
|
||||||
"clins-1-start_date": "01/01/2020",
|
"clins-1-start_date": "01/01/2020",
|
||||||
"clins-1-end_date": "01/01/2021",
|
"clins-1-end_date": "01/01/2021",
|
||||||
"clins-1-obligated_amount": "5000",
|
"clins-1-obligated_amount": "5000",
|
||||||
|
"clins-1-total_amount": "5000",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_for(
|
url_for(
|
||||||
@ -221,6 +223,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
|
|||||||
"start_date": "01/01/2020",
|
"start_date": "01/01/2020",
|
||||||
"end_date": "01/01/2021",
|
"end_date": "01/01/2021",
|
||||||
"obligated_amount": "5000",
|
"obligated_amount": "5000",
|
||||||
|
"total_amount": "10000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"jedi_clin_type": "JEDI_CLIN_1",
|
"jedi_clin_type": "JEDI_CLIN_1",
|
||||||
@ -228,6 +231,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
|
|||||||
"start_date": "01/01/2020",
|
"start_date": "01/01/2020",
|
||||||
"end_date": "01/01/2021",
|
"end_date": "01/01/2021",
|
||||||
"obligated_amount": "5000",
|
"obligated_amount": "5000",
|
||||||
|
"total_amount": "10000",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
TaskOrders.create_clins(task_order.id, clin_list)
|
TaskOrders.create_clins(task_order.id, clin_list)
|
||||||
@ -240,6 +244,7 @@ def test_task_orders_submit_form_step_three_add_clins_existing_to(
|
|||||||
"clins-0-start_date": "01/01/2020",
|
"clins-0-start_date": "01/01/2020",
|
||||||
"clins-0-end_date": "01/01/2021",
|
"clins-0-end_date": "01/01/2021",
|
||||||
"clins-0-obligated_amount": "5000",
|
"clins-0-obligated_amount": "5000",
|
||||||
|
"clins-0-total_amount": "10000",
|
||||||
}
|
}
|
||||||
response = client.post(
|
response = client.post(
|
||||||
url_for(
|
url_for(
|
||||||
|
@ -530,6 +530,7 @@ def test_task_orders_new_post_routes(post_url_assert_status):
|
|||||||
"clins-0-start_date": "01/01/2020",
|
"clins-0-start_date": "01/01/2020",
|
||||||
"clins-0-end_date": "01/01/2021",
|
"clins-0-end_date": "01/01/2021",
|
||||||
"clins-0-obligated_amount": "5000",
|
"clins-0-obligated_amount": "5000",
|
||||||
|
"clins-0-total_amount": "10000",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -205,6 +205,9 @@ forms:
|
|||||||
start: PoP start date must be on or after {date}.
|
start: PoP start date must be on or after {date}.
|
||||||
scope_description: 'What do you plan to do on the cloud? Some examples might include migrating an existing application or creating a prototype. You don’t need to include a detailed plan of execution, but should list key requirements. This section will be reviewed by your contracting officer, but won’t be sent to the CCPO. <p>Not sure how to describe your scope? <a href="#">Read some examples</a> to get some inspiration.</p>'
|
scope_description: 'What do you plan to do on the cloud? Some examples might include migrating an existing application or creating a prototype. You don’t need to include a detailed plan of execution, but should list key requirements. This section will be reviewed by your contracting officer, but won’t be sent to the CCPO. <p>Not sure how to describe your scope? <a href="#">Read some examples</a> to get some inspiration.</p>'
|
||||||
scope_label: Cloud project scope
|
scope_label: Cloud project scope
|
||||||
|
clin_funding_errors:
|
||||||
|
obligated_amount_error: Obligated amount must be less than or equal to total amount
|
||||||
|
funding_range_error: Dollar amount must be from $0.00 to $1,000,000,000.00
|
||||||
team_experience:
|
team_experience:
|
||||||
built_1: Built, migrated, or consulted on 1-2 applications
|
built_1: Built, migrated, or consulted on 1-2 applications
|
||||||
built_3: Built, migrated, or consulted on 3-5 applications
|
built_3: Built, migrated, or consulted on 3-5 applications
|
||||||
@ -395,7 +398,7 @@ task_orders:
|
|||||||
type: CLIN Type
|
type: CLIN Type
|
||||||
idiq_clin_description: Description (IDIQ CLIN)
|
idiq_clin_description: Description (IDIQ CLIN)
|
||||||
pop: PoP
|
pop: PoP
|
||||||
amount: CLIN Value
|
total_amount: CLIN Value
|
||||||
obligated: Amount Obligated
|
obligated: Amount Obligated
|
||||||
form:
|
form:
|
||||||
add_clin: Add another CLIN
|
add_clin: Add another CLIN
|
||||||
@ -406,7 +409,7 @@ task_orders:
|
|||||||
clin_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)."
|
clin_description: "Refer to your task order to locate your Contract Line Item Numbers (CLINs)."
|
||||||
clin_details: CLIN Details
|
clin_details: CLIN Details
|
||||||
clin_funding: CLIN Funding
|
clin_funding: CLIN Funding
|
||||||
clin_number_label: CLIN Number
|
clin_number_label: CLIN
|
||||||
clin_type_label: Corresponding IDIQ CLIN
|
clin_type_label: Corresponding IDIQ CLIN
|
||||||
clin_remove_text: 'Do you want to remove '
|
clin_remove_text: 'Do you want to remove '
|
||||||
clin_remove_confirm: Yes, remove CLIN
|
clin_remove_confirm: Yes, remove CLIN
|
||||||
@ -415,6 +418,7 @@ task_orders:
|
|||||||
cloud_funding_text: Data must match with what is in your uploaded document.
|
cloud_funding_text: Data must match with what is in your uploaded document.
|
||||||
draft_alert_title: Your information has been saved
|
draft_alert_title: Your information has been saved
|
||||||
draft_alert_message: You can return to the Task Order Builder to enter missing information. Once you are finished, you’ll be ready to submit this request.
|
draft_alert_message: You can return to the Task Order Builder to enter missing information. Once you are finished, you’ll be ready to submit this request.
|
||||||
|
total_funds_label: Total CLIN Value
|
||||||
obligated_funds_label: Obligated Funds
|
obligated_funds_label: Obligated Funds
|
||||||
pop: Period of Performance
|
pop: Period of Performance
|
||||||
pop_end: End Date
|
pop_end: End Date
|
||||||
|
Loading…
x
Reference in New Issue
Block a user