Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/production'
Browse files Browse the repository at this point in the history
  • Loading branch information
kroman0 committed Dec 30, 2016
2 parents 53ef544 + df3d282 commit 8ba05fa
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 11 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
}

setup(name='openprocurement.api',
version='2.3.38',
version='2.3.41',
description='openprocurement.api',
long_description=README,
classifiers=[
Expand Down
23 changes: 21 additions & 2 deletions src/openprocurement/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
TZ = timezone(os.environ['TZ'] if 'TZ' in os.environ else 'Europe/Kiev')
CANT_DELETE_PERIOD_START_DATE_FROM = datetime(2016, 8, 30, tzinfo=TZ)
BID_LOTVALUES_VALIDATION_FROM = datetime(2016, 10, 24, tzinfo=TZ)
CPV_ITEMS_CLASS_FROM = datetime(2017, 1, 1, tzinfo=TZ)
ITEMS_LOCATION_VALIDATION_FROM = datetime(2016, 11, 22, tzinfo=TZ)

coordinates_reg_exp = re.compile(r'-?\d{1,3}\.\d+|-?\d{1,3}')
Expand Down Expand Up @@ -369,6 +370,7 @@ def validate_longitude(self, data, longitude):


ADDITIONAL_CLASSIFICATIONS_SCHEMES = [u'ДКПП', u'NONE', u'ДК003', u'ДК015', u'ДК018']
ADDITIONAL_CLASSIFICATIONS_SCHEMES_2017 = [u'ДК003', u'ДК015', u'ДК018']


def validate_dkpp(items, *args):
Expand All @@ -384,14 +386,25 @@ class Item(Model):
description_en = StringType()
description_ru = StringType()
classification = ModelType(CPVClassification, required=True)
additionalClassifications = ListType(ModelType(Classification), default=list(), required=True, min_size=1, validators=[validate_dkpp])
additionalClassifications = ListType(ModelType(Classification), default=list())
unit = ModelType(Unit) # Description of the unit which the good comes in e.g. hours, kilograms
quantity = IntType() # The number of units required
deliveryDate = ModelType(Period)
deliveryAddress = ModelType(Address)
deliveryLocation = ModelType(Location)
relatedLot = MD5Type()

def validate_additionalClassifications(self, data, items):
tender = get_tender(data['__parent__'])
tender_from_2017 = (tender.get('revisions')[0].date if tender.get('revisions') else get_now()) > CPV_ITEMS_CLASS_FROM
not_cpv = data['classification']['id'] == '99999999-9'
if not items and (not tender_from_2017 or tender_from_2017 and not_cpv):
raise ValidationError(u'This field is required.')
elif tender_from_2017 and not_cpv and items and not any([i.scheme in ADDITIONAL_CLASSIFICATIONS_SCHEMES_2017 for i in items]):
raise ValidationError(u"One of additional classifications should be one of [{0}].".format(', '.join(ADDITIONAL_CLASSIFICATIONS_SCHEMES_2017)))
elif not tender_from_2017 and items and not any([i.scheme in ADDITIONAL_CLASSIFICATIONS_SCHEMES for i in items]):
raise ValidationError(u"One of additional classifications should be one of [{0}].".format(', '.join(ADDITIONAL_CLASSIFICATIONS_SCHEMES)))

def validate_relatedLot(self, data, relatedLot):
if relatedLot and isinstance(data['__parent__'], Model) and relatedLot not in [i.id for i in get_tender(data['__parent__']).lots]:
raise ValidationError(u"relatedLot should be one of lots")
Expand Down Expand Up @@ -1204,7 +1217,7 @@ def __local_roles__(self):
description_ru = StringType()
date = IsoDateTimeType()
tenderID = StringType() # TenderID should always be the same as the OCID. It is included to make the flattened data structure more convenient.
items = ListType(ModelType(Item), required=True, min_size=1, validators=[validate_cpv_group, validate_items_uniq]) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead.
items = ListType(ModelType(Item), required=True, min_size=1, validators=[validate_items_uniq]) # The goods and services to be purchased, broken into line items wherever possible. Items should not be duplicated, but a quantity of 2 specified instead.
value = ModelType(Value, required=True) # The total estimated value of the procurement.
procurementMethod = StringType(choices=['open', 'selective', 'limited'], default='open') # Specify tendering method as per GPA definitions of Open, Selective, Limited (http://www.wto.org/english/docs_e/legal_e/rev-gpr-94_01_e.htm)
procurementMethodRationale = StringType() # Justification of procurement method, especially in the case of Limited tendering.
Expand Down Expand Up @@ -1437,6 +1450,12 @@ def import_data(self, raw_data, **kw):
self._data.update(data)
return self

def validate_items(self, data, items):
if (data.get('revisions')[0].date if data.get('revisions') else get_now()) > CPV_ITEMS_CLASS_FROM and items and len(set([i.classification.id[:4] for i in items])) != 1:
raise ValidationError(u"CPV class of items should be identical")
else:
validate_cpv_group(items)

def validate_features(self, data, features):
if features and data['lots'] and any([
round(vnmax([
Expand Down
4 changes: 3 additions & 1 deletion src/openprocurement/api/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,15 @@ class BaseWebTest(unittest.TestCase):

@classmethod
def setUpClass(cls):
while True:
for _ in range(10):
try:
cls.app = webtest.TestApp("config:tests.ini", relative_to=cls.relative_to)
except:
pass
else:
break
else:
cls.app = webtest.TestApp("config:tests.ini", relative_to=cls.relative_to)
cls.app.RequestClass = PrefixedRequestClass
cls.couchdb_server = cls.app.app.registry.couchdb_server
cls.db = cls.app.app.registry.db
Expand Down
44 changes: 37 additions & 7 deletions src/openprocurement/api/tests/tender.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import timedelta

from openprocurement.api import ROUTE_PREFIX
from openprocurement.api.models import Tender, get_now, CANT_DELETE_PERIOD_START_DATE_FROM, ITEMS_LOCATION_VALIDATION_FROM, coordinates_reg_exp
from openprocurement.api.models import Tender, get_now, CANT_DELETE_PERIOD_START_DATE_FROM, CPV_ITEMS_CLASS_FROM, ITEMS_LOCATION_VALIDATION_FROM, coordinates_reg_exp
from openprocurement.api.tests.base import test_tender_data, test_organization, BaseWebTest, BaseTenderWebTest
from uuid import uuid4

Expand Down Expand Up @@ -534,16 +534,41 @@ def test_create_tender_invalid(self):
{u'description': [u'currency should be identical to currency of value of tender'], u'location': u'body', u'name': u'minimalStep'}
])

data = test_tender_data["items"][0].pop("additionalClassifications")
if get_now() > CPV_ITEMS_CLASS_FROM:
cpv_code = test_tender_data["items"][0]['classification']['id']
test_tender_data["items"][0]['classification']['id'] = '99999999-9'
response = self.app.post_json(request_path, {'data': test_tender_data}, status=422)
test_tender_data["items"][0]["additionalClassifications"] = data
if get_now() > CPV_ITEMS_CLASS_FROM:
test_tender_data["items"][0]['classification']['id'] = cpv_code
self.assertEqual(response.status, '422 Unprocessable Entity')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['status'], 'error')
self.assertEqual(response.json['errors'], [
{u'description': [{u'additionalClassifications': [u'This field is required.']}], u'location': u'body', u'name': u'items'}
])

data = test_tender_data["items"][0]["additionalClassifications"][0]["scheme"]
test_tender_data["items"][0]["additionalClassifications"][0]["scheme"] = 'Не ДКПП'
if get_now() > CPV_ITEMS_CLASS_FROM:
cpv_code = test_tender_data["items"][0]['classification']['id']
test_tender_data["items"][0]['classification']['id'] = '99999999-9'
response = self.app.post_json(request_path, {'data': test_tender_data}, status=422)
test_tender_data["items"][0]["additionalClassifications"][0]["scheme"] = data
if get_now() > CPV_ITEMS_CLASS_FROM:
test_tender_data["items"][0]['classification']['id'] = cpv_code
self.assertEqual(response.status, '422 Unprocessable Entity')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['status'], 'error')
self.assertEqual(response.json['errors'], [
{u'description': [{u'additionalClassifications': [u"One of additional classifications should be one of [ДКПП, NONE, ДК003, ДК015, ДК018]."]}], u'location': u'body', u'name': u'items'}
])
if get_now() > CPV_ITEMS_CLASS_FROM:
self.assertEqual(response.json['errors'], [
{u'description': [{u'additionalClassifications': [u"One of additional classifications should be one of [ДК003, ДК015, ДК018]."]}], u'location': u'body', u'name': u'items'}
])
else:
self.assertEqual(response.json['errors'], [
{u'description': [{u'additionalClassifications': [u"One of additional classifications should be one of [ДКПП, NONE, ДК003, ДК015, ДК018]."]}], u'location': u'body', u'name': u'items'}
])

data = test_organization["contactPoint"]["telephone"]
del test_organization["contactPoint"]["telephone"]
Expand All @@ -566,9 +591,14 @@ def test_create_tender_invalid(self):
self.assertEqual(response.status, '422 Unprocessable Entity')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['status'], 'error')
self.assertEqual(response.json['errors'], [
{u'description': [u'CPV group of items be identical'], u'location': u'body', u'name': u'items'}
])
if get_now() > CPV_ITEMS_CLASS_FROM:
self.assertEqual(response.json['errors'], [
{u'description': [u'CPV class of items should be identical'], u'location': u'body', u'name': u'items'}
])
else:
self.assertEqual(response.json['errors'], [
{u'description': [u'CPV group of items be identical'], u'location': u'body', u'name': u'items'}
])

procuringEntity = test_tender_data["procuringEntity"]
data = test_tender_data["procuringEntity"].copy()
Expand Down

0 comments on commit 8ba05fa

Please sign in to comment.