diff --git a/src/openprocurement/tender/pricequotation/models/award.py b/src/openprocurement/tender/pricequotation/models/award.py index 8da6b1167c..06e32789e8 100644 --- a/src/openprocurement/tender/pricequotation/models/award.py +++ b/src/openprocurement/tender/pricequotation/models/award.py @@ -3,6 +3,7 @@ from openprocurement.api.models import\ schematics_default_role, schematics_embedded_role from openprocurement.tender.core.models import BaseAward +from openprocurement.tender.pricequotation.utils import get_bid_owned_award_acl class Award(BaseAward): @@ -18,9 +19,29 @@ class Options: "status", "title", "title_en", "title_ru", "description", "description_en", "description_ru" ), + "edit_tender_owner": whitelist( + "status", "title", "title_en", "title_ru", + "description", "description_en", "description_ru" + ), + "edit_bid_owner": whitelist( + "status", "title", "title_en", "title_ru", + "description", "description_en", "description_ru" + ), "embedded": schematics_embedded_role, "view": schematics_default_role, "Administrator": whitelist(), } bid_id = MD5Type(required=True) + + def __acl__(self): + return get_bid_owned_award_acl(self) + + def get_role(self): + root = self.get_root() + request = root.request + if request.authenticated_role in ("tender_owner", "bid_owner"): + role = "edit_{}".format(request.authenticated_role) + else: + role = request.authenticated_role + return role diff --git a/src/openprocurement/tender/pricequotation/models/tender.py b/src/openprocurement/tender/pricequotation/models/tender.py index 3a5e1654c5..a1df22377e 100644 --- a/src/openprocurement/tender/pricequotation/models/tender.py +++ b/src/openprocurement/tender/pricequotation/models/tender.py @@ -5,6 +5,7 @@ from schematics.types import IntType, StringType from schematics.types.compound import ModelType from schematics.types.serializable import serializable +from pyramid.security import Allow from zope.interface import implementer from openprocurement.api.constants import TZ, CPV_ITEMS_CLASS_FROM from openprocurement.api.models import\ @@ -81,7 +82,6 @@ class Options: "tenderPeriod", "procuringEntity", "guarantee", - "value", "minimalStep", ) _edit_role = _core_roles["edit"] \ @@ -92,12 +92,16 @@ class Options: "profile" ) _create_role = _core_roles["create"] + _edit_role - _edit_pq_bot_role = whitelist("items", "shortlistedFirms", "status", "criteria", "value") + _edit_pq_bot_role = whitelist( + "items", "shortlistedFirms", + "status", "criteria", "value", + ) _view_tendering_role = ( _core_roles["view"] + _edit_fields + whitelist( "awards", + 'value', "awardPeriod", "cancellations", "contracts", @@ -113,7 +117,7 @@ class Options: "edit": _edit_role, "edit_draft": _edit_role, "edit_draft.unsuccessful": _edit_role, - "edit_draft.publishing": _all_forbidden, + "edit_draft.publishing": _edit_pq_bot_role, "edit_active.tendering": _all_forbidden, "edit_active.qualification": _all_forbidden, "edit_active.awarded": _all_forbidden, @@ -161,7 +165,7 @@ class Options: validators=[validate_items_uniq], ) # The total estimated value of the procurement. - value = ModelType(Value, required=True) + value = ModelType(Value) # The period when the tender is open for submissions. # The end date is the closing date for tender submissions. tenderPeriod = ModelType( @@ -211,8 +215,6 @@ def get_role(self): if request.authenticated_role in\ ("Administrator", "chronograph", "contracting", "bots"): role = request.authenticated_role - elif request.authenticated_role == "auction": - role = "auction_{}".format(request.method.lower()) else: role = "edit_{}".format(request.context.status) return role @@ -225,6 +227,12 @@ def next_check(self): if self.status.startswith("active"): for award in self.awards: + if award.status == 'pending': + checks.append( + calculate_tender_business_date(award.date, + timedelta(days=2), + self) + ) if award.status == "active" and not\ any([i.awardID == award.id for i in self.contracts]): checks.append(award.date) @@ -271,3 +279,16 @@ def validate_tenderPeriod(self, data, period): and period.endDate < calculate_tender_business_date(period.startDate, timedelta(days=2), data, True) ): raise ValidationError(u"the tenderPeriod cannot end earlier than 2 business days after the start") + + def __local_roles__(self): + roles = dict([("{}_{}".format(self.owner, self.owner_token), "tender_owner")]) + for i in self.bids: + roles["{}_{}".format(i.owner, i.owner_token)] = "bid_owner" + return roles + + def __acl__(self): + acl = [ + (Allow, "g:bots", "upload_award_documents"), + ] + self._acl_cancellation(acl) + return acl diff --git a/src/openprocurement/tender/pricequotation/tests/award.py b/src/openprocurement/tender/pricequotation/tests/award.py index 0cbc116bad..a7e711a05c 100644 --- a/src/openprocurement/tender/pricequotation/tests/award.py +++ b/src/openprocurement/tender/pricequotation/tests/award.py @@ -18,8 +18,8 @@ patch_tender_award, # patch_tender_award_unsuccessful, get_tender_award, - # TenderLotAwardCheckResourceTest check_tender_award, + check_tender_award_disqualification, # TenderAwardDocumentResourceTest not_found_award_document, create_tender_award_document, @@ -51,10 +51,12 @@ class TenderAwardDocumentResourceTestMixin(object): class TenderAwardResourceTest(TenderContentWebTest, TenderAwardResourceTestMixin): initial_status = "active.qualification" initial_bids = test_bids + maxAwards = 1 test_create_tender_award = snitch(create_tender_award) test_patch_tender_award = snitch(patch_tender_award) test_check_tender_award = snitch(check_tender_award) + test_check_tender_award_disqualification = snitch(check_tender_award_disqualification) class TenderAwardResourceScaleTest(TenderContentWebTest): diff --git a/src/openprocurement/tender/pricequotation/tests/award_blanks.py b/src/openprocurement/tender/pricequotation/tests/award_blanks.py index 43ca846037..67a4566519 100644 --- a/src/openprocurement/tender/pricequotation/tests/award_blanks.py +++ b/src/openprocurement/tender/pricequotation/tests/award_blanks.py @@ -4,6 +4,7 @@ import mock from openprocurement.api.utils import get_now +from openprocurement.tender.core.tests.base import change_auth from openprocurement.tender.pricequotation.tests.base import test_organization @@ -162,18 +163,18 @@ def create_tender_award_invalid(self): def create_tender_award(self): - self.app.authorization = ("Basic", ("token", "")) - request_path = "/tenders/{}/awards".format(self.tender_id) - response = self.app.post_json( - request_path, - {"data": {"suppliers": [test_organization], "status": "pending", "bid_id": self.initial_bids[0]["id"]}}, - ) - self.assertEqual(response.status, "201 Created") - self.assertEqual(response.content_type, "application/json") - award = response.json["data"] - self.assertEqual(award["suppliers"][0]["name"], test_organization["name"]) - self.assertIn("id", award) - self.assertIn(award["id"], response.headers["Location"]) + with change_auth(self.app, ("Basic", ("token", ""))): + request_path = "/tenders/{}/awards".format(self.tender_id) + response = self.app.post_json( + request_path, + {"data": {"suppliers": [test_organization], "status": "pending", "bid_id": self.initial_bids[0]["id"]}}, + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + award = response.json["data"] + self.assertEqual(award["suppliers"][0]["name"], test_organization["name"]) + self.assertIn("id", award) + self.assertIn(award["id"], response.headers["Location"]) response = self.app.get(request_path) self.assertEqual(response.status, "200 OK") @@ -224,8 +225,9 @@ def patch_tender_award(self): response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"tender_id"}] ) award_id = self.award_ids[0] + token = self.initial_bids_tokens[0] response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), {"data": {"awardStatus": "unsuccessful"}}, status=422, ) @@ -235,15 +237,16 @@ def patch_tender_award(self): response.json["errors"], [{"location": "body", "name": "awardStatus", "description": "Rogue field"}] ) + token = self.initial_bids_tokens[0] response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), {"data": {"status": "unsuccessful"}}, ) self.assertEqual(response.status, "200 OK") self.assertEqual(response.content_type, "application/json") response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), {"data": {"status": "pending"}}, status=403, ) @@ -257,8 +260,9 @@ def patch_tender_award(self): self.assertEqual(len(response.json["data"]), 2) new_award = response.json["data"][-1] + token = self.initial_bids_tokens[1] response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], token), {"data": {"title": "title", "description": "description"}}, ) self.assertEqual(response.status, "200 OK") @@ -267,7 +271,7 @@ def patch_tender_award(self): self.assertEqual(response.json["data"]["description"], "description") response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], token), {"data": {"status": "active"}}, ) self.assertEqual(response.status, "200 OK") @@ -279,7 +283,7 @@ def patch_tender_award(self): self.assertEqual(len(response.json["data"]), 2) response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, new_award["id"], token), {"data": {"status": "cancelled"}}, ) self.assertEqual(response.status, "200 OK") @@ -299,7 +303,7 @@ def patch_tender_award(self): self.assertEqual(response.json["data"]["value"]["amount"], 469.0) response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.initial_bids_tokens[0]), {"data": {"status": "unsuccessful"}}, status=403, ) @@ -496,9 +500,9 @@ def not_found_award_document(self): self.assertEqual( response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"award_id"}] ) - + token = self.initial_bids_tokens[0] response = self.app.post( - "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, token), status=404, upload_files=[("invalid_value", "name.doc", "content")], ) @@ -585,8 +589,9 @@ def not_found_award_document(self): def create_tender_award_document(self): + token = self.initial_bids_tokens[0] response = self.app.post( - "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, token), upload_files=[("file", "name.doc", "content")], ) self.assertEqual(response.status, "201 Created") @@ -660,8 +665,9 @@ def create_tender_award_document(self): self.set_status("complete") + token = self.initial_bids_tokens[0] response = self.app.post( - "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, token), upload_files=[("file", "name.doc", "content")], status=403, ) @@ -673,8 +679,9 @@ def create_tender_award_document(self): def put_tender_award_document(self): + token = self.initial_bids_tokens[0] response = self.app.post( - "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, token), upload_files=[("file", "name.doc", "content")], ) self.assertEqual(response.status, "201 Created") @@ -684,7 +691,7 @@ def put_tender_award_document(self): response = self.app.put( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), status=404, upload_files=[("invalid_name", "name.doc", "content")], @@ -696,7 +703,7 @@ def put_tender_award_document(self): response = self.app.put( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), upload_files=[("file", "name.doc", "content2")], ) @@ -744,7 +751,7 @@ def put_tender_award_document(self): response = self.app.put( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), "content3", content_type="application/msword", @@ -789,7 +796,7 @@ def put_tender_award_document(self): response = self.app.put( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), upload_files=[("file", "name.doc", "content3")], status=403, @@ -802,8 +809,9 @@ def put_tender_award_document(self): def patch_tender_award_document(self): + token = self.initial_bids_tokens[0] response = self.app.post( - "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}/documents?acc_token={}".format(self.tender_id, self.award_id, token), upload_files=[("file", "name.doc", "content")], ) self.assertEqual(response.status, "201 Created") @@ -813,7 +821,7 @@ def patch_tender_award_document(self): response = self.app.patch_json( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), {"data": {"description": "document description"}}, ) @@ -831,7 +839,7 @@ def patch_tender_award_document(self): response = self.app.patch_json( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, token ), {"data": {"description": "document description"}}, status=403, @@ -883,8 +891,9 @@ def create_award_document_bot(self): ) except AppError: pass + token = self.initial_bids_tokens[0] response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, self.award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, self.award_id, token), {"data": {"qualified": True, "status": "active"}}, ) self.assertEqual(response.status, "200 OK") @@ -914,7 +923,7 @@ def patch_not_author(self): self.app.authorization = authorization response = self.app.patch_json( "/tenders/{}/awards/{}/documents/{}?acc_token={}".format( - self.tender_id, self.award_id, doc_id, self.tender_token + self.tender_id, self.award_id, doc_id, self.initial_bids_tokens[0] ), {"data": {"description": "document description"}}, status=403, @@ -946,9 +955,10 @@ def check_tender_award(self): self.assertEqual(response.json["data"]["bid_id"], sorted_bids[0]["id"]) # cancel award + token = self.initial_bids_tokens[0] self.app.authorization = ("Basic", ("broker", "")) response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, self.tender_token), + "/tenders/{}/awards/{}?acc_token={}".format(self.tender_id, award_id, token), {"data": {"status": "unsuccessful"}}, ) self.assertEqual(response.status, "200 OK") @@ -965,3 +975,48 @@ def check_tender_award(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_disqualification(self): + # get bids + response = self.app.get("/tenders/{}/bids".format(self.tender_id)) + self.assertEqual(response.status, "200 OK") + bids = response.json["data"] + 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'] + # check award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[0]["tenderers"][0]["name"]) + self.assertEqual( + response.json["data"]["suppliers"][0]["identifier"]["id"], sorted_bids[0]["tenderers"][0]["identifier"]["id"] + ) + self.assertEqual(response.json["data"]["bid_id"], sorted_bids[0]["id"]) + + # wait 2 days + date = (get_now() - timedelta(days=2)).isoformat() + self.tender_document_patch = self.db.get(self.tender_id) + self.tender_document_patch['awards'][0]['date'] = date + self.save_changes() + self.check_chronograph() + + # get awards + response = self.app.get("/tenders/{}/awards".format(self.tender_id)) + # # get pending award + awards = response.json['data'] + self.assertEqual(len(awards), 2) + self.assertEqual(awards[0]['status'], "unsuccessful") + award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] + # check new award + response = self.app.get("/tenders/{}/awards/{}".format(self.tender_id, award_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["suppliers"][0]["name"], sorted_bids[1]["tenderers"][0]["name"]) + self.assertEqual( + 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"]) diff --git a/src/openprocurement/tender/pricequotation/tests/base.py b/src/openprocurement/tender/pricequotation/tests/base.py index 3d8a7fc5bc..3b9925e0cd 100644 --- a/src/openprocurement/tender/pricequotation/tests/base.py +++ b/src/openprocurement/tender/pricequotation/tests/base.py @@ -80,7 +80,6 @@ def activate_awards(self): def generate_bids(self, status, startend): tenderPeriod_startDate = self.now + self.periods[status][startend]["tenderPeriod"]["startDate"] bids = self.tender_document.get("bids", []) - # import pdb; pdb.set_trace() if self.initial_bids and not bids: self.tender_document_patch["bids"] = [] self.initial_bids_tokens = [] @@ -150,10 +149,14 @@ def patch_tender_bot(self): "classification": test_short_profile["classification"], "unit": test_short_profile["unit"] }) + value = deepcopy(test_short_profile['value']) + amount = sum([item["quantity"] for item in items]) * test_short_profile['value']['amount'] + value["amount"] = amount self.tender_document_patch.update({ "shortlistedFirms": test_shortlisted_firms, 'criteria': test_short_profile['criteria'], - "items": items + "items": items, + 'value': value }) self.save_changes() diff --git a/src/openprocurement/tender/pricequotation/tests/bid_blanks.py b/src/openprocurement/tender/pricequotation/tests/bid_blanks.py index d2bd5434b7..b943c9cb28 100644 --- a/src/openprocurement/tender/pricequotation/tests/bid_blanks.py +++ b/src/openprocurement/tender/pricequotation/tests/bid_blanks.py @@ -246,7 +246,7 @@ def patch_tender_bid(self): response = self.app.patch_json( "/tenders/{}/bids/{}?acc_token={}".format(self.tender_id, bid["id"], token), - {"data": {"value": {"amount": 600}}}, + {"data": {"value": {"amount": 60000}}}, status=422, ) self.assertEqual(response.status, "422 Unprocessable Entity") diff --git a/src/openprocurement/tender/pricequotation/tests/contract.py b/src/openprocurement/tender/pricequotation/tests/contract.py index 99a92cd13b..656e1267d4 100644 --- a/src/openprocurement/tender/pricequotation/tests/contract.py +++ b/src/openprocurement/tender/pricequotation/tests/contract.py @@ -55,7 +55,7 @@ def setUp(self): "suppliers": [test_organization], "status": "pending", "bid_id": self.initial_bids[0]["id"], - "value": self.initial_data["value"], + "value": self.tender_document["value"], "items": self.initial_data["items"], } }, @@ -93,8 +93,8 @@ def create_award(self): "bid_id": self.initial_bids[0]["id"], "items": self.initial_data["items"], "value": { - "amount": self.initial_data["value"]["amount"], - "currency": self.initial_data["value"]["currency"], + "amount": self.tender_document["value"]["amount"], + "currency": self.tender_document["value"]["currency"], "valueAddedTaxIncluded": False, }, } diff --git a/src/openprocurement/tender/pricequotation/tests/contract_blanks.py b/src/openprocurement/tender/pricequotation/tests/contract_blanks.py index 304551514c..33a2b17d23 100644 --- a/src/openprocurement/tender/pricequotation/tests/contract_blanks.py +++ b/src/openprocurement/tender/pricequotation/tests/contract_blanks.py @@ -355,15 +355,17 @@ def patch_tender_contract_value(self): response = self.app.patch_json( "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), - {"data": {"value": {"amount": 501}}}, + {"data": {"value": {"amount": 22501}}}, status=403, ) self.assertEqual(response.status, "403 Forbidden") - self.assertEqual(response.json["errors"][0]["description"], "Amount should be less or equal to awarded amount") + self.assertEqual( + response.json["errors"][0]["description"], "Amount should be less or equal to awarded amount" + ) response = self.app.patch_json( "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), - {"data": {"value": {"amount": 502, "amountNet": 501}}}, + {"data": {"value": {"amount": 22502, "amountNet": 22501}}}, status=403, ) self.assertEqual(response.status, "403 Forbidden") @@ -441,7 +443,7 @@ def patch_tender_contract_value_vat_not_included(self): response = self.app.patch_json( "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract["id"], self.tender_token), - {"data": {"value": {"amount": 600, "amountNet": 600}}}, + {"data": {"value": {"amount": 22600, "amountNet": 22600}}}, status=403, ) self.assertEqual(response.status, "403 Forbidden") diff --git a/src/openprocurement/tender/pricequotation/tests/data.py b/src/openprocurement/tender/pricequotation/tests/data.py index a69cede1d3..f3b695b732 100644 --- a/src/openprocurement/tender/pricequotation/tests/data.py +++ b/src/openprocurement/tender/pricequotation/tests/data.py @@ -203,7 +203,6 @@ "profile": "655360-30230000-889652-40000777", "mainProcurementCategory": "goods", "procuringEntity": test_procuringEntity, - "value": {"amount": 500, "currency": u"UAH"}, "items": [deepcopy(test_item)], "tenderPeriod": {"endDate": (now + timedelta(days=14)).isoformat()}, "procurementMethodType": PMT, diff --git a/src/openprocurement/tender/pricequotation/tests/tender_blanks.py b/src/openprocurement/tender/pricequotation/tests/tender_blanks.py index 431ed31444..dccd50d5cb 100644 --- a/src/openprocurement/tender/pricequotation/tests/tender_blanks.py +++ b/src/openprocurement/tender/pricequotation/tests/tender_blanks.py @@ -442,10 +442,6 @@ def create_tender_invalid(self): {u"description": [u"This field is required."], u"location": u"body", u"name": u"items"}, response.json["errors"] ) - self.assertIn( - {u"description": [u"This field is required."], u"location": u"body", u"name": u"value"}, response.json["errors"] - ) - data = self.initial_data["tenderPeriod"] self.initial_data["tenderPeriod"] = {"startDate": "2014-10-31T00:00:00", "endDate": "2014-10-01T00:00:00"} response = self.app.post_json(request_path, {"data": self.initial_data}, status=422) @@ -640,7 +636,6 @@ def create_tender_generated(self): u"status", u"tenderPeriod", u"items", - u"value", u"procuringEntity", u"procurementMethod", u"awardCriteria", @@ -667,13 +662,6 @@ def create_tender_draft(self): token = response.json["access"]["token"] self.assertEqual(tender["status"], "draft") - response = self.app.patch_json( - "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": {"value": {"amount": 100}}} - ) - self.assertEqual(response.status, "200 OK") - self.assertEqual(response.content_type, "application/json") - self.assertEqual(response.json['data']['value']['amount'] , 100) - response = self.app.patch_json( "/tenders/{}?acc_token={}".format(tender["id"], token), {"data": {"status": self.primary_tender_status}} ) @@ -705,7 +693,6 @@ def tender_owner_can_change_in_draft(self): "procuringEntity": {"name": u"Національне управління справами"}, "mainProcurementCategory": u"services", "guarantee": {"amount": 50}, - "value": {"amount": 110}, } descriptions = { "description": u"Some text 1", @@ -785,8 +772,6 @@ def tender_owner_can_change_in_draft(self): self.assertNotEqual(tender["procuringEntity"]["name"], data.get("procuringEntity", {}).get("name")) self.assertEqual(tender["guarantee"]["amount"], general["guarantee"]["amount"]) self.assertNotEqual(tender["guarantee"]["amount"], data.get("guarantee", {}).get("amount")) - self.assertEqual(tender["value"]["amount"], general["value"]["amount"]) - self.assertNotEqual(tender["value"]["amount"], data.get("value", {}).get("amount")) # descriptions response = self.app.patch_json( @@ -1652,7 +1637,10 @@ def patch_tender_by_pq_bot(self): self.assertIn("classification", tender["items"][0]) self.assertNotIn("unit", tender["items"][0]) - data = {"data": {"status": "draft.publishing", "profile": test_short_profile["id"]}} + data = {"data": { + "status": "draft.publishing", + "profile": test_short_profile["id"]} + } response = self.app.patch_json("/tenders/{}?acc_token={}".format(tender_id, owner_token), data) self.assertEqual(response.status, "200 OK") tender = response.json["data"] @@ -1676,8 +1664,7 @@ 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) - + resp = app.patch_json("/tenders/{}".format(tender_id), data) response = self.app.get("/tenders/{}".format(tender_id)) self.assertEqual(response.status, "200 OK") tender = response.json["data"] @@ -1752,10 +1739,14 @@ def one_valid_bid_tender(self): owner_token = self.tender_token # create bid self.app.authorization = ("Basic", ("broker", "")) - self.app.post_json( - "/tenders/{}/bids".format(tender_id), - {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}} + resp = self.app.post_json( + "/tenders/{}/bids".format(tender_id), {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + }} ) + token = resp.json['access']['token'] # switch to active.qualification self.set_status("active.qualification") self.app.authorization = ("Basic", ("chronograph", "")) @@ -1768,7 +1759,8 @@ def one_valid_bid_tender(self): award_date = [i["date"] for i in response.json["data"] if i["status"] == "pending"][0] # set award as active response = self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, owner_token), {"data": {"status": "active"}} + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), + {"data": {"status": "active"}} ) self.assertNotEqual(response.json["data"]["date"], award_date) @@ -1794,9 +1786,10 @@ def one_invalid_bid_tender(self): owner_token = self.tender_token # create bid self.app.authorization = ("Basic", ("broker", "")) - self.app.post_json( + resp = self.app.post_json( "/tenders/{}/bids".format(tender_id), {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}} ) + token = resp.json['access']['token'] # switch to active.qualification self.set_status('active.tendering', 'end') resp = self.check_chronograph() @@ -1808,7 +1801,7 @@ def one_invalid_bid_tender(self): award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] # set award as unsuccessful self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, owner_token), + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), {"data": {"status": "unsuccessful"}}, ) # check status @@ -1830,11 +1823,11 @@ def first_bid_tender(self): "requirementResponses": test_requirement_response_valid }} ) - # bid_id = response.json["data"]["id"] - # bid_token = response.json["access"]["token"] + bid_token1 = response.json["access"]["token"] + # create second bid self.app.authorization = ("Basic", ("broker", "")) - self.app.post_json( + response = self.app.post_json( "/tenders/{}/bids".format(tender_id), {"data": { "tenderers": [test_organization], @@ -1842,6 +1835,7 @@ def first_bid_tender(self): "requirementResponses": test_requirement_response_valid }} ) + bid_token2 = response.json["access"]["token"] self.set_status('active.tendering', 'end') resp = self.check_chronograph() self.assertEqual(resp.json['data']['status'], 'active.qualification') @@ -1852,7 +1846,7 @@ def first_bid_tender(self): award_id = [i["id"] for i in response.json["data"] if i["status"] == "pending"][0] # set award as unsuccessful self.app.patch_json( - "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, owner_token), + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, bid_token1), {"data": {"status": "unsuccessful"}}, ) # get awards @@ -1869,7 +1863,8 @@ def first_bid_tender(self): 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"}} + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, bid_token2), + {"data": {"status": "active"}} ) # get contract id response = self.app.get("/tenders/{}".format(tender_id)) @@ -1935,9 +1930,14 @@ def lost_contract_for_active_award(self): owner_token = self.tender_token # create bid self.app.authorization = ("Basic", ("broker", "")) - self.app.post_json( - "/tenders/{}/bids".format(tender_id), {"data": {"tenderers": [test_organization], "value": {"amount": 500}, "requirementResponses": test_requirement_response_valid}} + resp = self.app.post_json( + "/tenders/{}/bids".format(tender_id), {"data": { + "tenderers": [test_organization], + "value": {"amount": 500}, + "requirementResponses": test_requirement_response_valid + }} ) + token = resp.json['access']['token'] # switch to active.qualification self.set_status("active.tendering", 'end') resp = self.check_chronograph().json @@ -1950,7 +1950,7 @@ def lost_contract_for_active_award(self): 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"}} + "/tenders/{}/awards/{}?acc_token={}".format(tender_id, award_id, token), {"data": {"status": "active"}} ) # lost contract tender = self.db.get(tender_id) diff --git a/src/openprocurement/tender/pricequotation/utils.py b/src/openprocurement/tender/pricequotation/utils.py index c91b6957ae..74b313734e 100644 --- a/src/openprocurement/tender/pricequotation/utils.py +++ b/src/openprocurement/tender/pricequotation/utils.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- +from datetime import timedelta from logging import getLogger +from pyramid.security import Allow from openprocurement.api.constants import RELEASE_2020_04_19 from openprocurement.api.utils import get_now, context_unpack -from openprocurement.tender.core.utils import remove_draft_bids +from openprocurement.tender.core.utils import ( + remove_draft_bids, + calculate_tender_business_date, +) + from openprocurement.tender.core.utils import get_first_revision_date @@ -62,15 +68,27 @@ def check_cancellation_status(request): cancel_tender(request) -def check_status(request): +def check_award_status(request): tender = request.validated["tender"] now = get_now() - check_cancellation_status(request) - - for award in tender.awards: + awards = tender.awards + for award in awards: + if award.status == 'pending' and calculate_tender_business_date(award.date, timedelta(days=2), tender) <= now: + award.status = 'unsuccessful' + add_next_award(request) if award.status == "active" and not any([i.awardID == award.id for i in tender.contracts]): add_contract(request, award, now) add_next_award(request) + + +def check_status(request): + + check_cancellation_status(request) + check_award_status(request) + + tender = request.validated["tender"] + now = get_now() + if tender.status == "active.tendering" and tender.tenderPeriod.endDate <= now: tender.status = "active.qualification" remove_draft_bids(request) @@ -78,7 +96,8 @@ def check_status(request): status = tender.status LOGGER.info( "Switched tender {} to {}".format(tender["id"], status), - extra=context_unpack(request, {"MESSAGE_ID": "switched_tender_{}".format(status)}), + extra=context_unpack(request, + {"MESSAGE_ID": "switched_tender_{}".format(status)}), ) return elif tender.status == "active.awarded": @@ -159,3 +178,26 @@ def reformat_criteria(criterias): for req_group in criteria['requirementGroups'] for req in req_group['requirements'] ] + + +def get_bid_owned_award_acl(award): + acl = [] + if not hasattr(award, "__parent__") or 'bids' not in award.__parent__: + return acl + tender = award.__parent__ + awarded_bid = [bid for bid in tender.bids if bid.id == award.bid_id][0] + prev_awards = [a for a in tender.awards + if a.bid_id == awarded_bid.id and a.id != award.id] + bid_acl = "_".join((awarded_bid.owner, awarded_bid.owner_token)) + owner_acl = "_".join((tender.owner, tender.owner_token)) + if prev_awards: + acl.extend([ + (Allow, owner_acl, "upload_award_documents"), + (Allow, owner_acl, "edit_award") + ]) + else: + acl.extend([ + (Allow, bid_acl, "upload_award_documents"), + (Allow, bid_acl, "edit_award") + ]) + return acl diff --git a/src/openprocurement/tender/pricequotation/validation.py b/src/openprocurement/tender/pricequotation/validation.py index fb5b6cdbbe..924b0bf024 100644 --- a/src/openprocurement/tender/pricequotation/validation.py +++ b/src/openprocurement/tender/pricequotation/validation.py @@ -47,11 +47,9 @@ def validate_create_award_not_in_allowed_period(request): raise_operation_error(request, "Can't create award in current ({}) tender status".format(tender.status)) -def validate_create_award_only_for_active_lot(request): +def validate_update_award_role(request): tender = request.validated["tender"] award = request.validated["award"] - if any([i.status != "active" for i in tender.lots if i.id == award.lotID]): - raise_operation_error(request, "Can create award only in active lot status") # contract document diff --git a/src/openprocurement/tender/pricequotation/views/award.py b/src/openprocurement/tender/pricequotation/views/award.py index 8e8508243e..2d75a430c8 100644 --- a/src/openprocurement/tender/pricequotation/views/award.py +++ b/src/openprocurement/tender/pricequotation/views/award.py @@ -13,8 +13,9 @@ validate_patch_award_data, validate_update_award_in_not_allowed_status, ) -from openprocurement.tender.belowthreshold.validation import ( +from openprocurement.tender.pricequotation.validation import ( validate_create_award_not_in_allowed_period, + validate_update_award_role ) @@ -58,10 +59,11 @@ def collection_post(self): @json_view( content_type="application/json", - permission="edit_tender", + permission="edit_award", validators=( validate_patch_award_data, validate_update_award_in_not_allowed_status, + validate_update_award_role, ), ) def patch(self): diff --git a/src/openprocurement/tender/pricequotation/views/award_document.py b/src/openprocurement/tender/pricequotation/views/award_document.py index 9d90b2fbe7..1cb918976c 100644 --- a/src/openprocurement/tender/pricequotation/views/award_document.py +++ b/src/openprocurement/tender/pricequotation/views/award_document.py @@ -22,14 +22,14 @@ class PQTenderAwardDocumentResource(TenderAwardDocumentResource): @json_view( validators=(validate_file_upload, validate_award_document), - permission="upload_tender_documents" + permission="upload_award_documents" ) def collection_post(self): return super(TenderAwardDocumentResource, self).collection_post() @json_view( validators=(validate_file_update, validate_award_document), - permission="edit_tender" + permission="upload_award_documents" ) def put(self): return super(TenderAwardDocumentResource, self).put() @@ -37,7 +37,7 @@ def put(self): @json_view( content_type="application/json", validators=(validate_patch_document_data, validate_award_document), - permission="edit_tender", + permission="upload_award_documents", ) def patch(self): return super(TenderAwardDocumentResource, self).patch()