Skip to content

Commit

Permalink
Merge branch 'production' into a245493633932394_remove_features
Browse files Browse the repository at this point in the history
  • Loading branch information
kroman0 committed Feb 17, 2017
2 parents fc3d90e + fc95de0 commit 819557c
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 54 deletions.
18 changes: 8 additions & 10 deletions openprocurement/tender/openua/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from openprocurement.api.models import ListType
from openprocurement.api.models import Lot as BaseLot
from openprocurement.api.models import Period, IsoDateTimeType
from openprocurement.api.models import Address as BaseAddress
from openprocurement.api.models import Address
from openprocurement.api.models import Tender as BaseTender
from openprocurement.api.models import LotValue as BaseLotValue
from openprocurement.api.models import Item as BaseItem
Expand Down Expand Up @@ -194,13 +194,6 @@ class EnquiryPeriod(Period):
invalidationDate = IsoDateTimeType()


class Address(BaseAddress):

streetAddress = StringType(required=True)
locality = StringType(required=True)
region = StringType(required=True)
postalCode = StringType(required=True)

class Item(BaseItem):
"""A good, service, or work to be contracted."""

Expand Down Expand Up @@ -317,8 +310,8 @@ class Options:
'action': whitelist('tendererAction'),
'pending': whitelist('decision', 'status', 'rejectReason', 'rejectReasonDescription'),
'review': whitelist('decision', 'status', 'reviewDate', 'reviewPlace'),
'embedded': (blacklist('owner_token', 'owner') + schematics_embedded_role),
'view': (blacklist('owner_token', 'owner') + schematics_default_role),
'embedded': (blacklist('owner_token', 'owner', 'bid_id') + schematics_embedded_role),
'view': (blacklist('owner_token', 'owner', 'bid_id') + schematics_default_role),
}
status = StringType(choices=['draft', 'claim', 'answered', 'pending', 'accepted', 'invalid', 'resolved', 'declined', 'cancelled', 'satisfied', 'stopping', 'stopped', 'mistaken'], default='draft')
acceptance = BooleanType()
Expand All @@ -327,6 +320,7 @@ class Options:
rejectReasonDescription = StringType()
reviewDate = IsoDateTimeType()
reviewPlace = StringType()
bid_id = StringType()

def __acl__(self):
return [
Expand Down Expand Up @@ -597,6 +591,10 @@ def next_check(self):
last_award_status = lot_awards[-1].status if lot_awards else ''
if not pending_complaints and not pending_awards_complaints and standStillEnds and last_award_status == 'unsuccessful':
checks.append(max(standStillEnds))
if self.status.startswith('active'):
for award in self.awards:
if award.status == 'active' and not any([i.awardID == award.id for i in self.contracts]):
checks.append(award.date)
return min(checks).isoformat() if checks else None

def invalidate_bids_data(self):
Expand Down
44 changes: 44 additions & 0 deletions openprocurement/tender/openua/tests/award.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,50 @@ def test_create_tender_award_complaint_invalid(self):
{u'description': {u'contactPoint': [u'This field is required.'], u'identifier': {u'scheme': [u'This field is required.'], u'id': [u'This field is required.'], u'uri': [u'Not a well formed URL.']}, u'address': [u'This field is required.']}, u'location': u'body', u'name': u'author'}
])

def test_create_tender_award_claim(self):
auth = self.app.authorization
self.app.authorization = ('Basic', ('token', ''))
self.app.patch_json('/tenders/{}/awards/{}'.format(self.tender_id, self.award_id), {'data': {'status': 'cancelled'}})

response = self.app.get('/tenders/{}/awards'.format(self.tender_id))
award_id = [i['id'] for i in response.json['data'] if i['status'] == 'pending'][-1]
self.app.patch_json('/tenders/{}/awards/{}'.format(self.tender_id, award_id), {'data': {'status': 'unsuccessful'}})
self.app.authorization = auth
bid_token = self.initial_bids_tokens[self.initial_bids[1]['id']]

response = self.app.post_json('/tenders/{}/awards/{}/complaints?acc_token={}'.format(self.tender_id, award_id, bid_token), {'data': {
'title': 'complaint title',
'description': 'complaint description',
'author': test_organization,
'status': 'claim'
}}, status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['status'], 'error')
self.assertEqual(response.json['errors'], [
{u'description': u'Can add claim only on unsuccessful award of your bid', u'location': u'body', u'name': u'data'}
])

response = self.app.post_json('/tenders/{}/awards/{}/complaints?acc_token={}'.format(self.tender_id, award_id, bid_token), {'data': {
'title': 'complaint title',
'description': 'complaint description',
'author': test_organization,
'status': 'draft'
}})
self.assertEqual(response.status, '201 Created')
complaint = response.json['data']
owner_token = response.json['access']['token']

response = self.app.patch_json('/tenders/{}/awards/{}/complaints/{}?acc_token={}'.format(self.tender_id, award_id, complaint['id'], owner_token), {"data": {
"status": "claim",
}}, status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['status'], 'error')
self.assertEqual(response.json['errors'], [
{u'description': u'Can add claim only on unsuccessful award of your bid', u'location': u'body', u'name': u'data'}
])

def test_create_tender_award_complaint_not_active(self):
auth = self.app.authorization
self.app.authorization = ('Basic', ('token', ''))
Expand Down
9 changes: 5 additions & 4 deletions openprocurement/tender/openua/tests/bid.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,15 @@ def test_deleted_bid_is_not_restorable(self):
# try to restore deleted bid
response = self.app.patch_json('/tenders/{}/bids/{}?acc_token={}'.format(self.tender_id, bid['id'], bid_token), {"data": {
'status': 'active',
}})
self.assertEqual(response.status, '200 OK')
}}, status=403)
self.assertEqual(response.status, '403 Forbidden')
self.assertEqual(response.content_type, 'application/json')
self.assertEqual(response.json['errors'][0]["description"], "Can't update bid in (deleted) status")

response = self.app.get('/tenders/{}/bids/{}?acc_token={}'.format(self.tender_id, bid['id'], bid_token))
self.assertEqual(response.status, '200 OK')
self.assertEqual(response.content_type, 'application/json')
self.assertNotEqual(response.json['data']['status'], 'deleted')
self.assertEqual(response.json['data']['status'], 'active')
self.assertEqual(response.json['data']['status'], 'deleted')

def test_deleted_bid_do_not_locks_tender_in_state(self):
bids = []
Expand Down
72 changes: 72 additions & 0 deletions openprocurement/tender/openua/tests/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,78 @@ def test_patch_tender_question(self):
self.assertEqual(response.json['data']["answer"], "answer")
self.assertIn('dateAnswered', response.json['data'])

def create_question_for(self, questionOf, relatedItem):
response = self.app.post_json('/tenders/{}/questions'.format(self.tender_id), {'data': {
'title': 'question title',
'description': 'question description',
"questionOf": questionOf,
"relatedItem": relatedItem,
'author': test_organization
}})
self.assertEqual(response.status, '201 Created')
return response.json['data']['id']

def test_tender_has_unanswered_questions(self):
question_id = self.create_question_for("tender", self.tender_id)

self.set_status('active.auction', {'status': 'active.tendering'})
self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(self.tender_id), {"data": {"id": self.tender_id}})
self.assertEqual(response.json['data']['status'], 'active.tendering')

self.app.authorization = ('Basic', ('broker', ''))
response = self.app.post_json('/tenders/{}/cancellations?acc_token={}'.format(self.tender_id, self.tender_token), {'data': {
'reason': 'cancellation reason',
'status': 'active',
}})
self.assertEqual(response.status, '201 Created')

response = self.app.get('/tenders/{}'.format(self.tender_id))
self.assertEqual(response.json['data']['status'], 'cancelled')

def test_lot_has_unanswered_questions(self):
question_id = self.create_question_for("lot", self.initial_lots[0]['id'])

self.set_status('active.auction', {'status': 'active.tendering'})
self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(self.tender_id), {"data": {"id": self.tender_id}})
self.assertEqual(response.json['data']['status'], 'active.tendering')

self.app.authorization = ('Basic', ('broker', ''))
response = self.app.post_json('/tenders/{}/cancellations?acc_token={}'.format(self.tender_id, self.tender_token), {'data': {
'reason': 'cancellation reason',
'status': 'active',
"cancellationOf": "lot",
"relatedLot": self.initial_lots[0]['id']
}})
self.assertEqual(response.status, '201 Created')

self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(self.tender_id), {"data": {"id": self.tender_id}})
self.assertEqual(response.json['data']['status'], 'unsuccessful')

def test_item_has_unanswered_questions(self):
items = self.app.get('/tenders/{}'.format(self.tender_id)).json['data']['items']
question_id = self.create_question_for("item", items[0]['id'])

self.set_status('active.auction', {'status': 'active.tendering'})
self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(self.tender_id), {"data": {"id": self.tender_id}})
self.assertEqual(response.json['data']['status'], 'active.tendering')

self.app.authorization = ('Basic', ('broker', ''))
response = self.app.post_json('/tenders/{}/cancellations?acc_token={}'.format(self.tender_id, self.tender_token), {'data': {
'reason': 'cancellation reason',
'status': 'active',
"cancellationOf": "lot",
"relatedLot": self.initial_lots[0]['id']
}})
self.assertEqual(response.status, '201 Created')

self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(self.tender_id), {"data": {"id": self.tender_id}})
self.assertEqual(response.json['data']['status'], 'unsuccessful')


def suite():
suite = unittest.TestSuite()
Expand Down
84 changes: 80 additions & 4 deletions openprocurement/tender/openua/tests/tender.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ def test_create_tender_invalid(self):
self.assertEqual(response.json['status'], 'error')
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'}
{u'description': [{u'additionalClassifications': [u"One of additional classifications should be one of [ДК003, ДК015, ДК018, specialNorms]."]}], u'location': u'body', u'name': u'items'}
])
else:
self.assertEqual(response.json['errors'], [
Expand Down Expand Up @@ -566,15 +566,13 @@ def test_create_tender_invalid(self):
])

data = deepcopy(test_tender_data)
del data["items"][0]['deliveryAddress']['postalCode']
del data["items"][0]['deliveryAddress']['locality']
del data["items"][0]['deliveryDate']['endDate']
response = self.app.post_json(request_path, {'data': data}, status=422)
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'deliveryDate': {u'endDate': [u'This field is required.']}, u'deliveryAddress': {u'postalCode': [u'This field is required.'], u'locality': [u'This field is required.']}}], u'location': u'body', u'name': u'items'}
{u'description': [{u'deliveryDate': {u'endDate': [u'This field is required.']}}], u'location': u'body', u'name': u'items'}
])

def test_create_tender_generated(self):
Expand Down Expand Up @@ -699,6 +697,20 @@ def test_create_tender(self):
self.assertEqual(data['guarantee']['amount'], 100500)
self.assertEqual(data['guarantee']['currency'], "USD")

data = deepcopy(test_tender_data)
del data["items"][0]['deliveryAddress']['postalCode']
del data["items"][0]['deliveryAddress']['locality']
del data["items"][0]['deliveryAddress']['streetAddress']
del data["items"][0]['deliveryAddress']['region']
response = self.app.post_json('/tenders', {'data': data})
self.assertEqual(response.status, '201 Created')
self.assertEqual(response.content_type, 'application/json')
self.assertNotIn('postalCode', response.json['data']['items'][0]['deliveryAddress'])
self.assertNotIn('locality', response.json['data']['items'][0]['deliveryAddress'])
self.assertNotIn('streetAddress', response.json['data']['items'][0]['deliveryAddress'])
self.assertNotIn('region', response.json['data']['items'][0]['deliveryAddress'])


def test_get_tender(self):
response = self.app.get('/tenders')
self.assertEqual(response.status, '200 OK')
Expand Down Expand Up @@ -1559,6 +1571,70 @@ def test_first_bid_tender(self):
response = self.app.get('/tenders/{}'.format(tender_id))
self.assertEqual(response.json['data']['status'], 'complete')

def test_lost_contract_for_active_award(self):
self.app.authorization = ('Basic', ('broker', ''))
# create tender
response = self.app.post_json('/tenders',
{"data": test_tender_data})
tender_id = self.tender_id = response.json['data']['id']
owner_token = response.json['access']['token']
# create bid
self.app.authorization = ('Basic', ('broker', ''))
response = self.app.post_json('/tenders/{}/bids'.format(tender_id),
{'data': {'selfEligible': True, 'selfQualified': True,
'tenderers': [test_organization], "value": {"amount": 450}}})
# create bid #2
self.app.authorization = ('Basic', ('broker', ''))
response = self.app.post_json('/tenders/{}/bids'.format(tender_id),
{'data': {'selfEligible': True, 'selfQualified': True,
'tenderers': [test_organization], "value": {"amount": 450}}})
# switch to active.auction
self.set_status('active.auction')

# get auction info
self.app.authorization = ('Basic', ('auction', ''))
response = self.app.get('/tenders/{}/auction'.format(tender_id))
auction_bids_data = response.json['data']['bids']
# posting auction results
self.app.authorization = ('Basic', ('auction', ''))
response = self.app.post_json('/tenders/{}/auction'.format(tender_id),
{'data': {'bids': auction_bids_data}})
# get awards
self.app.authorization = ('Basic', ('broker', ''))
response = self.app.get('/tenders/{}/awards?acc_token={}'.format(tender_id, owner_token))
# get pending award
award_id = [i['id'] for i in response.json['data'] if i['status'] == 'pending'][0]
# set award as active
self.app.patch_json('/tenders/{}/awards/{}?acc_token={}'.format(tender_id, award_id, owner_token), {"data": {"status": "active", "qualified": True, "eligible": True}})
# lost contract
tender = self.db.get(tender_id)
tender['contracts'] = None
self.db.save(tender)
# check tender
response = self.app.get('/tenders/{}'.format(tender_id))
self.assertEqual(response.json['data']['status'], 'active.awarded')
self.assertNotIn('contracts', response.json['data'])
self.assertIn('next_check', response.json['data'])
# create lost contract
self.app.authorization = ('Basic', ('chronograph', ''))
response = self.app.patch_json('/tenders/{}'.format(tender_id), {"data": {"id": tender_id}})
self.assertEqual(response.json['data']['status'], 'active.awarded')
self.assertIn('contracts', response.json['data'])
self.assertNotIn('next_check', response.json['data'])
contract_id = response.json['data']['contracts'][-1]['id']
# time travel
tender = self.db.get(tender_id)
for i in tender.get('awards', []):
i['complaintPeriod']['endDate'] = i['complaintPeriod']['startDate']
self.db.save(tender)
# sign contract
self.app.authorization = ('Basic', ('broker', ''))
self.app.patch_json('/tenders/{}/contracts/{}?acc_token={}'.format(tender_id, contract_id, owner_token), {"data": {"status": "active"}})
# check status
self.app.authorization = ('Basic', ('broker', ''))
response = self.app.get('/tenders/{}'.format(tender_id))
self.assertEqual(response.json['data']['status'], 'complete')


def suite():
suite = unittest.TestSuite()
Expand Down
19 changes: 17 additions & 2 deletions openprocurement/tender/openua/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ def check_complaint_status(request, complaint):
def has_unanswered_questions(tender, filter_cancelled_lots=True):
if filter_cancelled_lots and tender.lots:
active_lots = [l.id for l in tender.lots if l.status == 'active']
return any([i.id for i in tender.questions if i.relatedItem in active_lots and not i.answer])
return any([i.id for i in tender.questions if not i.answer])
active_items = [i.id for i in tender.items if not i.relatedLot or i.relatedLot in active_lots]
return any([
not i.answer
for i in tender.questions
if i.questionOf == 'tender' or i.questionOf == 'lot' and i.relatedItem in active_lots or i.questionOf == 'item' and i.relatedItem in active_items
])
return any([not i.answer for i in tender.questions])


def has_unanswered_complaints(tender, filter_cancelled_lots=True):
Expand All @@ -52,6 +57,16 @@ def has_unanswered_complaints(tender, filter_cancelled_lots=True):
def check_status(request):
tender = request.validated['tender']
now = get_now()
for award in tender.awards:
if award.status == 'active' and not any([i.awardID == award.id for i in tender.contracts]):
tender.contracts.append(type(tender).contracts.model_class({
'awardID': award.id,
'suppliers': award.suppliers,
'value': award.value,
'date': now,
'items': [i for i in tender.items if i.relatedLot == award.lotID ],
'contractID': '{}-{}{}'.format(tender.tenderID, request.registry.server_id, len(tender.contracts) + 1) }))
add_next_award(request)
if not tender.lots and tender.status == 'active.tendering' and tender.tenderPeriod.endDate <= now and \
not has_unanswered_complaints(tender) and not has_unanswered_questions(tender):
for complaint in tender.complaints:
Expand Down

0 comments on commit 819557c

Please sign in to comment.