Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 221 additions & 68 deletions docs/source/locale/uk/LC_MESSAGES/tendering/pricequotation/tutorial.po

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions docs/source/tendering/pricequotation/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Procuring Entity can cancel `award` after acceptance by changing `award` status
:code:

After canceling `award` system creates `second` `award` for the same bid in status: `pending` with access for Procuring Entity only.
By the decision of Procuring Entity `second` `award` can be either changed for `active` or to `unsuccessful` with ability to upload supplementary documents.
Procuring Entity can change `second` `award` status to `unsuccessful` with ability to upload supplementary documents.

The Supplier-winner can decline `award` by transferring it to status: `unsuccessful`.

Expand All @@ -167,9 +167,8 @@ The Supplier-winner can decline `award` by transferring it to status: `unsuccess

1. Supplier-winner didn't accept `award` within two working days.
2. Supplier-winner declined `award`.
3. Supplier-winner refused to sign contract and `award` was canceled by Procuring Entity.

**Note !** In the case of `award` being transferred to `unsuccessful` status for the last bid, procedure will inherit termination status: **`unsuccessful`**.
**Note !** In the case of `award` being transferred to `unsuccessful` status for the last bid or if cancelled by Procuring Entity procedure will inherit termination status: **`unsuccessful`**.

.. index:: Setting Contract

Expand Down
15 changes: 0 additions & 15 deletions docs/tests/test_pricequotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,21 +283,6 @@ def test_docs_tutorial(self):
{"data": {"status": "active"}})
self.assertEqual(response.status, '200 OK')

with open(TARGET_DIR + 'award-cancelled.http', 'w') as self.app.file_obj:
response = self.app.patch_json(
'/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, owner_token),
{"data": {"status": "cancelled"}})
self.assertEqual(response.status, '200 OK')

response = self.app.get('/tenders/{}/awards'.format(self.tender_id))
award = [i for i in response.json['data'] if i['status'] == 'pending'][0]
award_id = award['id']

response = self.app.patch_json(
'/tenders/{}/awards/{}?acc_token={}'.format(self.tender_id, award_id, owner_token),
{"data": {"status": "active"}})
self.assertEqual(response.status, '200 OK')

with open(TARGET_DIR + 'contract-listing.http', 'w') as self.app.file_obj:
response = self.app.get('/tenders/{}/contracts'.format(self.tender_id))
self.assertEqual(response.status, '200 OK')
Expand Down
2 changes: 1 addition & 1 deletion src/openprocurement/tender/pricequotation/models/tender.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ class Options:
shortlistedFirms = ListType(ModelType(ShortlistedFirm), default=list())
criteria = ListType(ModelType(Criterion), default=list())
noticePublicationDate = IsoDateTimeType()
unsuccessfulReason = StringType()
unsuccessfulReason = ListType(StringType)

procuring_entity_kinds = PQ_KINDS

Expand Down
4 changes: 3 additions & 1 deletion src/openprocurement/tender/pricequotation/tests/award.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
check_tender_award_disqualification,
create_tender_award,
patch_tender_award,
tender_award_transitions
tender_award_transitions,
check_tender_award_cancellation
)
from openprocurement.tender.belowthreshold.tests.award import (
TenderAwardDocumentResourceTestMixin,
Expand Down Expand Up @@ -49,6 +50,7 @@ class TenderAwardResourceTest(TenderContentWebTest, TenderAwardResourceTestMixin
test_tender_award_transitions = snitch(tender_award_transitions)
test_check_tender_award = snitch(check_tender_award)
test_check_tender_award_disqualification = snitch(check_tender_award_disqualification)
test_check_tender_award_cancellation = snitch(check_tender_award_cancellation)


class TenderAwardResourceScaleTest(TenderContentWebTest):
Expand Down
88 changes: 78 additions & 10 deletions src/openprocurement/tender/pricequotation/tests/award_blanks.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,29 @@ def create_tender_award(self):
self.assertEqual(response.json["data"][-1], award)

award_request_path = "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award["id"], self.tender_token)
response = self.app.patch_json(award_request_path, {"data": {"status": "active"}})
self.assertEqual(response.status, "200 OK")

response = self.app.patch_json(award_request_path, {"data": {"status": "active"}}, status=403)
self.assertEqual(response.status, "403 Forbidden")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json["data"]["status"], u"active")
self.assertEqual(response.json['status'], "error")
self.assertEqual(
response.json['errors'],
[{
u'description': u"Can't change award status to active from pending",
u'location': u'body',
u'name': u'data'
}]
)

response = self.app.get("/tenders/{}".format(self.tender_id))
response = self.app.patch_json(award_request_path, {"data": {"status": "unsuccessful"}})
self.assertEqual(response.status, "200 OK")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json["data"]["status"], u"active.awarded")
self.assertEqual(response.json["data"]["status"], "unsuccessful")

award_request_path = "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award["id"], self.tender_token)
response = self.app.patch_json(award_request_path, {"data": {"status": "cancelled"}})
response = self.app.get("/tenders/{}".format(self.tender_id))
self.assertEqual(response.status, "200 OK")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json["data"]["status"], u"cancelled")
self.assertIn("Location", response.headers)

self.assertEqual(response.json["data"]["status"], u"unsuccessful")


def patch_tender_award(self):
Expand Down Expand Up @@ -528,3 +534,65 @@ def check_tender_award_disqualification(self):
response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[1]["tenderers"][0]["identifier"]["id"]
)
self.assertEqual(response.json["data"]["bid_id"], sorted_bids[1]["id"])


def check_tender_award_cancellation(self):
# get bids
response = self.app.get("/tenders/{}/bids".format(self.tender_id))
bids = response.json["data"]
bid_token = self.initial_bids_tokens[0]
tender_token = self.db.get(self.tender_id)['owner_token']
sorted_bids = sorted(bids, key=lambda bid: bid["value"]['amount'])

# get awards
response = self.app.get("/tenders/{}/awards".format(self.tender_id))
# get pending award
award = [i for i in response.json["data"] if i["status"] == "pending"][0]
award_id = award['id']
response = self.app.patch_json(
"/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token),
{"data": {"status": "active"}},
)
self.assertEqual(response.status, "200 OK")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json['data']['status'], "active")

response = self.app.patch_json(
"/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token),
{"data": {"status": "cancelled"}},
)

self.assertEqual(response.status, "200 OK")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json['data']['status'], "cancelled")
old_award = response.json['data']

response = self.app.get("/tenders/{}/awards".format(self.tender_id))

award = [i for i in response.json["data"] if i["status"] == "pending"][-1]
award_id = award['id']
self.assertEqual(old_award['bid_id'], award['bid_id'])

response = self.app.patch_json(
"/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, bid_token),
{"data": {"status": "active"}},
status=403
)
self.assertEqual(response.status, "403 Forbidden")
self.assertEqual(response.content_type, "application/json")
self.assertEqual(response.json['status'], "error")

for status in ('active', 'cancelled'):
response = self.app.patch_json(
"/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, tender_token),
{"data": {"status": status}},
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't change award status to {} from pending".format(status),
u'location': u'body',
u'name': u'data'
}])
4 changes: 2 additions & 2 deletions src/openprocurement/tender/pricequotation/tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@
{
"dataType": "integer",
"id": "655360-0004-001-01",
"minValue": 250,
"title": u"Яскравість дисплея",
"maxValue": 250,
"title": "Яскравість дисплея",
"unit": {
"code": "A24",
"name": u"кд/м²"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ def create_tender_draft(self):
)
response = self.app.patch_json(
"/tenders/{}?acc_token={}".format(tender["id"], token),
{"data": {"status": self.primary_tender_status, "unsuccessfulReason": "some value from buyer"}}
{"data": {"status": self.primary_tender_status, "unsuccessfulReason": ["some value from buyer"]}}
)
self.assertEqual(response.status, "200 OK")
self.assertEqual(response.content_type, "application/json")
Expand Down Expand Up @@ -1354,14 +1354,14 @@ def patch_tender_by_pq_bot(self):
with change_auth(self.app, ("Basic", ("pricequotation", ""))) as app:
self.app.patch_json(
"/tenders/{}".format(tender_id),
{"data": {"status": "draft.unsuccessful", "unsuccessfulReason": "Profile not found in catalogue"}}
{"data": {"status": "draft.unsuccessful", "unsuccessfulReason": ["Profile not found in catalogue"]}}
)

response = self.app.get("/tenders/{}".format(tender_id))
self.assertEqual(response.status, "200 OK")
tender = response.json["data"]
self.assertEqual(tender["status"], "draft.unsuccessful")
self.assertEqual(tender["unsuccessfulReason"], "Profile not found in catalogue")
self.assertEqual(tender["unsuccessfulReason"], ["Profile not found in catalogue"])
self.assertNotIn("classification", tender["items"][0])
self.assertNotIn("unit", tender["items"][0])
self.assertNotIn("shortlistedFirms", tender)
Expand Down
2 changes: 1 addition & 1 deletion src/openprocurement/tender/pricequotation/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def matches(criteria, response):
)
)
if not min_value and max_value:
if value < min_value:
if value > datatype.to_native(max_value):
raise ValidationError(
u'Value {} is higher then required {} in reqirement {}'.format(
value,
Expand Down
29 changes: 13 additions & 16 deletions src/openprocurement/tender/pricequotation/views/award.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,19 @@ def collection_post(self):
def patch(self):
tender = self.request.validated["tender"]
award = self.request.context
is_awarded = [
a for a in tender.awards
if a.bid_id == award.bid_id and a.id != award.id
]
award_status = award.status
apply_patch(self.request, save=False, src=self.request.context.serialize())

now = get_now()
if is_awarded and award.status != 'unsuccessful':
raise_operation_error(
self.request,
"Can't change award status to {} from {}".format(award.status, award_status)
)

if award_status == "pending" and award.status == "active":
add_contract(self.request, award, now)
Expand All @@ -83,22 +92,10 @@ def patch(self):
i.status = "cancelled"
add_next_award(self.request)
elif award_status == "pending" and award.status == "unsuccessful":
add_next_award(self.request)
elif (
award_status == "unsuccessful"
and award.status == "cancelled"
):
if tender.status == "active.awarded":
tender.status = "active.qualification"
tender.awardPeriod.endDate = None
cancelled_awards = []
for i in tender.awards[tender.awards.index(award):]:
i.status = "cancelled"
cancelled_awards.append(i.id)
for i in tender.contracts:
if i.awardID in cancelled_awards:
i.status = "cancelled"
add_next_award(self.request)
if is_awarded:
tender.status = 'unsuccessful'
else:
add_next_award(self.request)
elif self.request.authenticated_role != "Administrator" and not (
award_status == "pending" and award.status == "pending"
):
Expand Down