diff --git a/src/openprocurement/api/models.py b/src/openprocurement/api/models.py index 568b8e4a9a..e966fa48a0 100644 --- a/src/openprocurement/api/models.py +++ b/src/openprocurement/api/models.py @@ -673,7 +673,7 @@ class Contract(Model): description = StringType() # Contract description description_en = StringType() description_ru = StringType() - status = StringType(choices=["pending", "terminated", "active", "cancelled"], default="pending") + status = StringType(choices=["pending", "pending.winner-signing", "terminated", "active", "cancelled"], default="pending") period = ModelType(Period) value = ModelType(Value) dateSigned = IsoDateTimeType() diff --git a/src/openprocurement/tender/belowthreshold/tests/contract.py b/src/openprocurement/tender/belowthreshold/tests/contract.py index c7c495a874..f88314ff51 100644 --- a/src/openprocurement/tender/belowthreshold/tests/contract.py +++ b/src/openprocurement/tender/belowthreshold/tests/contract.py @@ -30,6 +30,18 @@ lot2_patch_tender_contract_document, patch_tender_contract_value_vat_not_included, patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_supplier, + patch_tender_contract_status_by_others, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, + lot2_create_tender_contract_document_by_supplier, + lot2_create_tender_contract_document_by_others, + lot2_put_tender_contract_document_by_supplier, + lot2_patch_tender_contract_document_by_supplier, ) @@ -82,6 +94,9 @@ def setUp(self): test_create_tender_contract_in_complete_status = snitch(create_tender_contract_in_complete_status) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) class TenderContractVATNotIncludedResourceTest(TenderContentWebTest, TenderContractResourceTestMixin): @@ -119,6 +134,9 @@ def setUp(self): self.create_award() test_patch_tender_contract_value_vat_not_included = snitch(patch_tender_contract_value_vat_not_included) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) class Tender2LotContractResourceTest(TenderContentWebTest): @@ -189,6 +207,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = auth + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + class Tender2LotContractDocumentResourceTest(TenderContentWebTest): initial_status = "active.qualification" @@ -231,15 +255,21 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = auth - lot2_create_tender_contract_document = snitch(lot2_create_tender_contract_document) - lot2_put_tender_contract_document = snitch(lot2_put_tender_contract_document) - lot2_patch_tender_contract_document = snitch(lot2_patch_tender_contract_document) + test_lot2_create_tender_contract_document = snitch(lot2_create_tender_contract_document) + test_lot2_put_tender_contract_document = snitch(lot2_put_tender_contract_document) + test_lot2_patch_tender_contract_document = snitch(lot2_patch_tender_contract_document) + test_lot2_create_tender_contract_document_by_supplier = snitch(lot2_create_tender_contract_document_by_supplier) + test_lot2_create_tender_contract_document_by_others = snitch(lot2_create_tender_contract_document_by_others) + test_lot2_put_tender_contract_document_by_supplier = snitch(lot2_put_tender_contract_document_by_supplier) + test_lot2_patch_tender_contract_document_by_supplier = snitch(lot2_patch_tender_contract_document_by_supplier) def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TenderContractResourceTest)) suite.addTest(unittest.makeSuite(TenderContractDocumentResourceTest)) + suite.addTest(unittest.makeSuite(TenderContractVATNotIncludedResourceTest)) + suite.addTest(unittest.makeSuite(Tender2LotContractDocumentResourceTest)) return suite diff --git a/src/openprocurement/tender/belowthreshold/tests/contract_blanks.py b/src/openprocurement/tender/belowthreshold/tests/contract_blanks.py index 4eb695b95a..28e2e419cc 100644 --- a/src/openprocurement/tender/belowthreshold/tests/contract_blanks.py +++ b/src/openprocurement/tender/belowthreshold/tests/contract_blanks.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import jmespath from datetime import timedelta from copy import deepcopy from openprocurement.api.utils import get_now @@ -541,6 +542,173 @@ def patch_tender_contract_value_vat_not_included(self): ) +def patch_tender_contract_status_by_owner(self): + response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) + contract = response.json["data"][0] + contract_id = contract["id"] + self.set_status("complete", {"status": "active.awarded"}) + + # prepare contract + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + + # Tender onwer + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "pending"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "active"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], u"active") + + +def patch_tender_contract_status_by_supplier(self): + response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) + contract = response.json["data"][0] + contract_id = contract["id"] + self.set_status("complete", {"status": "active.awarded"}) + + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + for bid in doc.get("bids", []): + if bid["id"] == bid_id and bid["status"] == "pending": + bid["status"] = "active" + for i in doc.get("awards", []): + if "complaintPeriod" in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if doc["contracts"][0]["value"]["valueAddedTaxIncluded"]: + doc["contracts"][0]["value"]["amountNet"] = str(float(doc["contracts"][0]["value"]["amount"]) - 1) + self.db.save(doc) + + # Supplier + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"status": "pending.winner-signing"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"], + [{u'description': u'Supplier can change status to `pending`', u'location': u'body', u'name': u'data'}] + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"value": {"amount": 10000}}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"], + [{u'description': u'Supplier can change status to `pending`', u'location': u'body', u'name': u'data'}] + ) + + # Tender onwer + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + + # Supplier + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"status": "active"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual( + response.json["errors"], + [{u'description': u'Supplier can change status to `pending`', u'location': u'body', u'name': u'data'}] + ) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"value": {"amount": 10000}, "status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertNotEqual(response.json["data"]["value"]["amount"], u"10000") + self.assertEqual(response.json["data"]["status"], u"pending") + + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, contract_id)) + self.assertNotEqual(response.json["data"]["value"]["amount"], u"10000") + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "active"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], u"active") + + +def patch_tender_contract_status_by_others(self): + response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) + contract = response.json["data"][0] + contract_id = contract["id"] + self.set_status("complete", {"status": "active.awarded"}) + + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id!='{}'].owner_token".format(bid_id), doc)[0] + + self.app.authorization = ("Basic", ("broker", "")) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"status": "pending.winner-signing"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, contract_id, bid_token), + {"data": {"status": "pending"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + def get_tender_contract(self): self.app.authorization = ("Basic", ("token", "")) response = self.app.post_json( @@ -771,6 +939,17 @@ def not_found(self): def create_tender_contract_document(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + self.assertEqual(response.json["data"]["status"], "pending") + + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if "complaintPeriod" in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc["contracts"][0] and doc["contracts"][0]["value"]["valueAddedTaxIncluded"]: + doc["contracts"][0]["value"]["amountNet"] = str(float(doc["contracts"][0]["value"]["amount"]) - 1) + self.db.save(doc) + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], @@ -794,6 +973,32 @@ def create_tender_contract_document(self): self.assertEqual(doc_id, response.json["data"][0]["id"]) self.assertEqual("name.doc", response.json["data"][0]["title"]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "contract.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u"Tender onwer can't add document in current contract status", + u'location': u'body', + u'name': u'data'}]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + + response = self.app.get( "/tenders/{}/contracts/{}/documents/{}?download=some_id".format(self.tender_id, self.contract_id, doc_id), status=404, @@ -849,6 +1054,187 @@ def create_tender_contract_document(self): ) +def create_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + for bid in doc.get("bids", []): + if bid["id"] == bid_id and bid["status"] == "pending": + bid["status"] = "active" + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u"Supplier can't add document in current contract status", + u'location': u'body', + u'name': u'data'}]) + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get("/tenders/{}/contracts/{}/documents".format(self.tender_id, self.contract_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"][0]["id"]) + self.assertEqual("name.doc", response.json["data"][0]["title"]) + + response = self.app.get("/tenders/{}/contracts/{}/documents?all=true".format(self.tender_id, self.contract_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"][0]["id"]) + self.assertEqual("name.doc", response.json["data"][0]["title"]) + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?download=some_id".format(self.tender_id, self.contract_id, doc_id), + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"download"}] + ) + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 7) + self.assertEqual(response.body, "content") + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + +def create_tender_contract_document_by_others(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id!='{}'].owner_token".format(bid_id), doc)[0] + + # Bid owner + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Bid onwer + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], [{"location": "url", "name": "permission", "description": "Forbidden"}]) + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Forbidden") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Forbidden") + + def put_tender_contract_document(self): response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), @@ -871,6 +1257,31 @@ def put_tender_contract_document(self): self.assertEqual(response.json["status"], "error") self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content2")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -950,9 +1361,45 @@ def put_tender_contract_document(self): ) -def patch_tender_contract_document(self): +def put_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + for bid in doc.get("bids", []): + if bid["id"] == bid_id and bid["status"] == "pending": + bid["status"] = "active" + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + + # Supplier response = self.app.post( - "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), upload_files=[("file", "name.doc", "content")], ) self.assertEqual(response.status, "201 Created") @@ -960,18 +1407,277 @@ def patch_tender_contract_document(self): doc_id = response.json["data"]["id"] self.assertIn(doc_id, response.headers["Location"]) - response = self.app.patch_json( + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( - self.tender_id, self.contract_id, doc_id, self.tender_token + self.tender_id, self.contract_id, doc_id, bid_token ), - {"data": {"description": "document description"}}, + status=404, + upload_files=[("invalid_name", "name.doc", "content")], ) - self.assertEqual(response.status, "200 OK") + self.assertEqual(response.status, "404 Not Found") self.assertEqual(response.content_type, "application/json") - self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) - response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) - self.assertEqual(response.status, "200 OK") + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + upload_files=[("file", "name.doc", "content2")], + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 8) + self.assertEqual(response.body, "content2") + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + "content3", + content_type="application/msword", + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 8) + self.assertEqual(response.body, "content3") + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + upload_files=[("file", "name.doc", "content3")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't update document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + upload_files=[("file", "name.doc", "content3")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't update document in current contract status") + + +def put_tender_contract_document_by_others(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id!='{}'].owner_token".format(bid_id), doc)[0] + + # Bid onwer + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Bid owner + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + +def patch_tender_contract_document(self): + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("document description", response.json["data"]["description"]) + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + 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 document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + 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 document in current ({}) tender status".format( + self.forbidden_contract_document_modification_actions_status + ), + ) + + +def patch_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + for bid in doc.get("bids", []): + if bid["id"] == bid_id and bid["status"] == "pending": + bid["status"] = "active" + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc['contracts'][0] and doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + {"data": {"description": "document description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") self.assertEqual(response.content_type, "application/json") self.assertEqual(doc_id, response.json["data"]["id"]) self.assertEqual("document description", response.json["data"]["description"]) @@ -1014,9 +1720,94 @@ def patch_tender_contract_document(self): def lot2_create_tender_contract_document(self): + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't add document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + key = response.json["data"]["url"].split("?")[-1] + + cancellation = dict(**test_cancellation) + cancellation.update({ + "status": "active", + "cancellationOf": "lot", + "relatedLot": self.initial_lots[0]["id"], + }) + + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), {"data": cancellation}, + ) + + if RELEASE_2020_04_19 < get_now(): + activate_cancellation_after_2020_04_19(self, response.json['data']['id']) + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can add document only in active lot status") + + +def lot2_create_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], ) self.assertEqual(response.status, "201 Created") self.assertEqual(response.content_type, "application/json") @@ -1036,8 +1827,12 @@ def lot2_create_tender_contract_document(self): {"data": cancellation}, ) + if RELEASE_2020_04_19 < get_now(): + activate_cancellation_after_2020_04_19(self, response.json['data']['id']) + + # Supplier response = self.app.post( - "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), upload_files=[("file", "name.doc", "content")], status=403, ) @@ -1046,6 +1841,43 @@ def lot2_create_tender_contract_document(self): self.assertEqual(response.json["errors"][0]["description"], "Can add document only in active lot status") +def lot2_create_tender_contract_document_by_others(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id!='{}'].owner_token".format(bid_id), doc)[0] + + # Bid owner + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Bid owner + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"], + [{u'description': u'Forbidden', u'location': u'url', u'name': u'permission'}]) + + def lot2_put_tender_contract_document(self): response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), @@ -1068,6 +1900,31 @@ def lot2_put_tender_contract_document(self): self.assertEqual(response.json["status"], "error") self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content2")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -1090,6 +1947,9 @@ def lot2_put_tender_contract_document(self): {"data": cancellation}, ) + if RELEASE_2020_04_19 < get_now(): + activate_cancellation_after_2020_04_19(self, response.json['data']['id']) + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -1102,6 +1962,93 @@ def lot2_put_tender_contract_document(self): self.assertEqual(response.json["errors"][0]["description"], "Can update document only in active lot status") +def lot2_put_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't add document in current contract status") + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + status=404, + upload_files=[("invalid_name", "name.doc", "content")], + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + upload_files=[("file", "name.doc", "content2")], + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + key = response.json["data"]["url"].split("?")[-1] + + # Tender owner + cancellation = dict(**test_cancellation) + cancellation.update({ + "status": "active", + "cancellationOf": "lot", + "relatedLot": self.initial_lots[0]["id"], + }) + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), + {"data": cancellation}, + ) + + if RELEASE_2020_04_19 < get_now(): + activate_cancellation_after_2020_04_19(self, response.json['data']['id']) + + # Supplier + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + upload_files=[("file", "name.doc", "content3")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can update document only in active lot status") + + def lot2_patch_tender_contract_document(self): response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), @@ -1112,12 +2059,38 @@ def lot2_patch_tender_contract_document(self): doc_id = response.json["data"]["id"] self.assertIn(doc_id, response.headers["Location"]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.patch_json( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token ), {"data": {"description": "document description"}}, ) + self.assertEqual(response.status, "200 OK") self.assertEqual(response.content_type, "application/json") self.assertEqual(doc_id, response.json["data"]["id"]) @@ -1133,6 +2106,9 @@ def lot2_patch_tender_contract_document(self): {"data": cancellation}, ) + if RELEASE_2020_04_19 < get_now(): + activate_cancellation_after_2020_04_19(self, response.json['data']['id']) + response = self.app.patch_json( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -1143,3 +2119,85 @@ def lot2_patch_tender_contract_document(self): self.assertEqual(response.status, "403 Forbidden") self.assertEqual(response.content_type, "application/json") self.assertEqual(response.json["errors"][0]["description"], "Can update document only in active lot status") + + +def lot2_patch_tender_contract_document_by_supplier(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + contract = response.json["data"] + self.assertEqual(response.json["data"]["status"], "pending") + doc = self.db.get(self.tender_id) + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract["awardID"]), doc)[0] + bid_token = jmespath.search("bids[?id=='{}'].owner_token".format(bid_id), doc)[0] + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + # Supplier + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, bid_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + {"data": {"description": "document description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + + # Tender owner + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + + # Supplier + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + {"data": {"description": "document description"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't update document in current contract status") + + # Tender owner + cancellation = dict(**test_cancellation) + cancellation.update({ + "status": "active", + "cancellationOf": "lot", + "relatedLot": self.initial_lots[0]["id"], + }) + + response = self.app.post_json( + "/tenders/{}/cancellations?acc_token={}".format(self.tender_id, self.tender_token), {"data": cancellation}, + ) + + # Supplier + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, bid_token + ), + {"data": {"description": "new document description"}}, + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], + "Supplier can't update document in current contract status") diff --git a/src/openprocurement/tender/belowthreshold/views/contract.py b/src/openprocurement/tender/belowthreshold/views/contract.py index 0d0b7c90d2..1dcccd3955 100644 --- a/src/openprocurement/tender/belowthreshold/views/contract.py +++ b/src/openprocurement/tender/belowthreshold/views/contract.py @@ -11,6 +11,7 @@ validate_update_contract_value_with_award, validate_update_contract_value_amount, validate_update_contract_value_net_required, + validate_update_contract_status_by_supplier, ) from openprocurement.tender.belowthreshold.utils import check_tender_status @@ -63,11 +64,12 @@ def get(self): @json_view( content_type="application/json", - permission="edit_tender", + permission="edit_contract", validators=( validate_patch_contract_data, validate_contract_operation_not_in_allowed_status, validate_update_contract_only_for_active_lots, + validate_update_contract_status_by_supplier, validate_update_contract_value, validate_contract_signing, validate_update_contract_value_net_required, @@ -80,9 +82,9 @@ def patch(self): """ contract_status = self.request.context.status apply_patch(self.request, save=False, src=self.request.context.serialize()) - if contract_status != self.request.context.status and ( - contract_status != "pending" or self.request.context.status != "active" - ): + if contract_status != self.request.context.status and \ + (contract_status not in ("pending", "pending.winner-signing",) or \ + self.request.context.status not in ("active", "pending", "pending.winner-signing",)): raise_operation_error(self.request, "Can't update contract status") if self.request.context.status == "active" and not self.request.context.dateSigned: self.request.context.dateSigned = get_now() diff --git a/src/openprocurement/tender/belowthreshold/views/contract_document.py b/src/openprocurement/tender/belowthreshold/views/contract_document.py index 8837c7803f..82d7aaccd9 100644 --- a/src/openprocurement/tender/belowthreshold/views/contract_document.py +++ b/src/openprocurement/tender/belowthreshold/views/contract_document.py @@ -11,6 +11,7 @@ from openprocurement.api.validation import validate_file_update, validate_file_upload, validate_patch_document_data from openprocurement.tender.core.utils import save_tender, optendersresource, apply_patch +from openprocurement.tender.core.validation import validate_role_for_contract_document_operation @optendersresource( @@ -46,7 +47,7 @@ def validate_contract_document(self, operation): ] ): raise_operation_error(self.request, "Can {} document only in active lot status".format(operation)) - if self.request.validated["contract"].status not in ["pending", "active"]: + if self.request.validated["contract"].status not in ["pending", "pending.winner-signing", "active"]: raise_operation_error(self.request, "Can't {} document in current contract status".format(operation)) return True @@ -62,7 +63,8 @@ def collection_get(self): ) return {"data": collection_data} - @json_view(permission="edit_tender", validators=(validate_file_upload,)) + @json_view(permission="upload_contract_documents", validators=(validate_file_upload, + validate_role_for_contract_document_operation,)) def collection_post(self): """Tender Contract Document Upload """ @@ -96,7 +98,8 @@ def get(self): ] return {"data": document_data} - @json_view(validators=(validate_file_update,), permission="edit_tender") + @json_view(validators=(validate_file_update, validate_role_for_contract_document_operation,), + permission="upload_contract_documents") def put(self): """Tender Contract Document Update""" if not self.validate_contract_document("update"): @@ -110,7 +113,9 @@ def put(self): ) return {"data": document.serialize("view")} - @json_view(content_type="application/json", validators=(validate_patch_document_data,), permission="edit_tender") + @json_view(content_type="application/json", + validators=(validate_patch_document_data, validate_role_for_contract_document_operation,), + permission="upload_contract_documents") def patch(self): """Tender Contract Document Update""" if not self.validate_contract_document("update"): diff --git a/src/openprocurement/tender/cfaselectionua/models/submodels/Contract.csv b/src/openprocurement/tender/cfaselectionua/models/submodels/Contract.csv index 0ae8e46410..3e46f14cb5 100644 --- a/src/openprocurement/tender/cfaselectionua/models/submodels/Contract.csv +++ b/src/openprocurement/tender/cfaselectionua/models/submodels/Contract.csv @@ -1,5 +1,7 @@ rolename,status,value,documents,description,title,items,suppliers,contractNumber,title_en,period,description_en,dateSigned,title_ru,date,awardID,description_ru,id,__parent__,contractID -edit,1,1,,1,1,,,1,1,1,1,1,1,,,1,,1, +admins,1,1,,1,1,,,1,1,1,1,1,1,,,1,,1, +edit_tender_owner,1,1,,1,1,,,1,1,1,1,1,1,,,1,,1, +edit_contract_supplier,1,,,,,,,,,,,,,,,,,, default,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,,1 create,,1,,1,1,1,1,1,1,1,1,,1,,1,1,,1,1 embedded,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,,1 diff --git a/src/openprocurement/tender/cfaselectionua/models/submodels/contract.py b/src/openprocurement/tender/cfaselectionua/models/submodels/contract.py index 5517ef9c7b..9e5f9f3663 100644 --- a/src/openprocurement/tender/cfaselectionua/models/submodels/contract.py +++ b/src/openprocurement/tender/cfaselectionua/models/submodels/contract.py @@ -4,6 +4,7 @@ from schematics.types.compound import ModelType from schematics.types import StringType from openprocurement.tender.core.models import ContractValue +from openprocurement.tender.core.utils import get_contract_supplier_roles, get_contract_supplier_permissions from openprocurement.api.utils import get_now from openprocurement.api.models import Model, ListType, Contract as BaseContract, Document @@ -16,6 +17,23 @@ class Options: awardID = StringType(required=True) documents = ListType(ModelType(Document, required=True), default=list()) + def __acl__(self): + return get_contract_supplier_permissions(self) + + def get_role(self): + root = self.get_root() + request = root.request + if request.authenticated_role in ("tender_owner", "contract_supplier"): + role = "edit_{}".format(request.authenticated_role) + else: + role = request.authenticated_role + return role + + def __local_roles__(self): + roles = {} + roles.update(get_contract_supplier_roles(self)) + return roles + def validate_awardID(self, data, awardID): parent = data["__parent__"] if awardID and isinstance(parent, Model) and awardID not in [i.id for i in parent.awards]: diff --git a/src/openprocurement/tender/cfaselectionua/models/tender.py b/src/openprocurement/tender/cfaselectionua/models/tender.py index fa99ed1d76..32946860c7 100644 --- a/src/openprocurement/tender/cfaselectionua/models/tender.py +++ b/src/openprocurement/tender/cfaselectionua/models/tender.py @@ -225,6 +225,8 @@ def __acl__(self): acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), (Allow, "g:agreement_selection", "edit_agreement_selection"), (Allow, "g:agreement_selection", "edit_tender"), (Allow, "g:brokers", "create_cancellation_complaint") diff --git a/src/openprocurement/tender/cfaselectionua/tests/contract.py b/src/openprocurement/tender/cfaselectionua/tests/contract.py index 4da17128dd..80e5f23d75 100644 --- a/src/openprocurement/tender/cfaselectionua/tests/contract.py +++ b/src/openprocurement/tender/cfaselectionua/tests/contract.py @@ -33,6 +33,18 @@ from openprocurement.tender.belowthreshold.tests.contract_blanks import ( patch_tender_contract_value_vat_not_included, patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, + lot2_create_tender_contract_document_by_supplier, + lot2_create_tender_contract_document_by_others, + lot2_put_tender_contract_document_by_supplier, + lot2_patch_tender_contract_document_by_supplier, ) @@ -58,6 +70,9 @@ class TenderContractResourceTest(TenderContentWebTest, TenderContractResourceTes test_create_tender_contract_in_complete_status = snitch(create_tender_contract_in_complete_status) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractVATNotIncludedResourceTest(TenderContentWebTest, TenderContractResourceTestMixin): @@ -67,7 +82,11 @@ class TenderContractVATNotIncludedResourceTest(TenderContentWebTest, TenderContr def update_vat_fields(self, items): for item in items: - item["value"]["valueAddedTaxIncluded"] = False + if "lotValues" in item: + for lot_value in item["lotValues"]: + lot_value["value"]["valueAddedTaxIncluded"] = False + else: + item["value"]["valueAddedTaxIncluded"] = False def generate_bids(self, status, start_end="start"): self.initial_bids = deepcopy(self.initial_bids) @@ -81,6 +100,9 @@ def calculate_agreement_contracts_value_amount(self, agreement, items): self.update_vat_fields(agreement["contracts"]) test_patch_tender_contract_value_vat_not_included = snitch(patch_tender_contract_value_vat_not_included) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) @unittest.skip("Skip multi-lots tests") @@ -122,6 +144,11 @@ class TenderContractDocumentResourceTest(TenderContentWebTest, TenderContractDoc initial_bids = test_bids initial_lots = test_lots + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) @unittest.skip("Skip multi-lots tests") class Tender2LotContractDocumentResourceTest(TenderContentWebTest): @@ -168,6 +195,10 @@ def setUp(self): lot2_create_tender_contract_document = snitch(lot2_create_tender_contract_document) lot2_put_tender_contract_document = snitch(lot2_put_tender_contract_document) lot2_patch_tender_contract_document = snitch(lot2_patch_tender_contract_document) + test_lot2_create_tender_contract_document_by_supplier = snitch(lot2_create_tender_contract_document_by_supplier) + test_lot2_create_tender_contract_document_by_others = snitch(lot2_create_tender_contract_document_by_others) + test_lot2_put_tender_contract_document_by_supplier = snitch(lot2_put_tender_contract_document_by_supplier) + test_lot2_patch_tender_contract_document_by_supplier = snitch(lot2_patch_tender_contract_document_by_supplier) def suite(): diff --git a/src/openprocurement/tender/cfaselectionua/tests/contract_blanks.py b/src/openprocurement/tender/cfaselectionua/tests/contract_blanks.py index 8c4226a085..f984ed3f5c 100644 --- a/src/openprocurement/tender/cfaselectionua/tests/contract_blanks.py +++ b/src/openprocurement/tender/cfaselectionua/tests/contract_blanks.py @@ -577,6 +577,37 @@ def not_found(self): def create_tender_contract_document(self): + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't add document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], @@ -656,6 +687,14 @@ def create_tender_contract_document(self): def put_tender_contract_document(self): + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], @@ -677,6 +716,31 @@ def put_tender_contract_document(self): self.assertEqual(response.json["status"], "error") self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content2")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -757,6 +821,14 @@ def put_tender_contract_document(self): def patch_tender_contract_document(self): + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if 'complaintPeriod' in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if doc['contracts'][0]['value']['valueAddedTaxIncluded']: + doc['contracts'][0]['value']['amountNet'] = str(float(doc['contracts'][0]['value']['amount']) - 1) + self.db.save(doc) + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], @@ -766,6 +838,31 @@ def patch_tender_contract_document(self): doc_id = response.json["data"]["id"] self.assertIn(doc_id, response.headers["Location"]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.patch_json( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -820,6 +917,29 @@ def patch_tender_contract_document(self): def lot2_create_tender_contract_document(self): + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't add document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.post( "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), upload_files=[("file", "name.doc", "content")], @@ -873,6 +993,31 @@ def lot2_put_tender_contract_document(self): self.assertEqual(response.json["status"], "error") self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content2")], + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.put( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token @@ -916,6 +1061,31 @@ def lot2_patch_tender_contract_document(self): doc_id = response.json["data"]["id"] self.assertIn(doc_id, response.headers["Location"]) + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending.winner-signing"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending.winner-signing") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + status=403 + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.json["errors"][0]["description"], + "Tender onwer can't update document in current contract status") + + response = self.app.patch_json( + "/tenders/{}/contracts/{}?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + {"data": {"status": "pending"}} + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.json["data"]["status"], "pending") + response = self.app.patch_json( "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( self.tender_id, self.contract_id, doc_id, self.tender_token diff --git a/src/openprocurement/tender/cfaselectionua/views/contract.py b/src/openprocurement/tender/cfaselectionua/views/contract.py index 9fbd7586d4..a795aeda86 100644 --- a/src/openprocurement/tender/cfaselectionua/views/contract.py +++ b/src/openprocurement/tender/cfaselectionua/views/contract.py @@ -10,6 +10,7 @@ validate_update_contract_value_with_award, validate_update_contract_value_amount, validate_update_contract_value_net_required, + validate_update_contract_status_by_supplier, ) from openprocurement.tender.cfaselectionua.utils import check_tender_status @@ -62,10 +63,11 @@ def get(self): @json_view( content_type="application/json", - permission="edit_tender", + permission="edit_contract", validators=( validate_patch_contract_data, validate_contract_operation_not_in_allowed_status, + validate_update_contract_status_by_supplier, validate_update_contract_only_for_active_lots, validate_update_contract_value, validate_update_contract_value_net_required, @@ -78,9 +80,9 @@ def patch(self): """ contract_status = self.request.context.status apply_patch(self.request, save=False, src=self.request.context.serialize()) - if contract_status != self.request.context.status and ( - contract_status != "pending" or self.request.context.status != "active" - ): + if contract_status != self.request.context.status and \ + (contract_status not in ("pending", "pending.winner-signing",) or \ + self.request.context.status not in ("active", "pending", "pending.winner-signing",)): raise_operation_error(self.request, "Can't update contract status") if self.request.context.status == "active" and not self.request.context.dateSigned: self.request.context.dateSigned = get_now() diff --git a/src/openprocurement/tender/cfaselectionua/views/contract_document.py b/src/openprocurement/tender/cfaselectionua/views/contract_document.py index bf5a53d403..ff14b3c0e6 100644 --- a/src/openprocurement/tender/cfaselectionua/views/contract_document.py +++ b/src/openprocurement/tender/cfaselectionua/views/contract_document.py @@ -11,6 +11,7 @@ from openprocurement.api.validation import validate_file_update, validate_file_upload, validate_patch_document_data from openprocurement.tender.core.utils import save_tender, optendersresource, apply_patch +from openprocurement.tender.core.validation import validate_role_for_contract_document_operation @optendersresource( @@ -46,7 +47,7 @@ def validate_contract_document(self, operation): ] ): raise_operation_error(self.request, "Can {} document only in active lot status".format(operation)) - if self.request.validated["contract"].status not in ["pending", "active"]: + if self.request.validated["contract"].status not in ["pending", "pending.winner-signing", "active"]: raise_operation_error(self.request, "Can't {} document in current contract status".format(operation)) return True @@ -62,7 +63,8 @@ def collection_get(self): ) return {"data": collection_data} - @json_view(permission="edit_tender", validators=(validate_file_upload,)) + @json_view(permission="upload_contract_documents", validators=(validate_file_upload, + validate_role_for_contract_document_operation,)) def collection_post(self): """Tender Contract Document Upload """ @@ -96,7 +98,8 @@ def get(self): ] return {"data": document_data} - @json_view(validators=(validate_file_update,), permission="edit_tender") + @json_view(validators=(validate_file_update, validate_role_for_contract_document_operation,), + permission="upload_contract_documents") def put(self): """Tender Contract Document Update""" if not self.validate_contract_document("update"): @@ -110,7 +113,9 @@ def put(self): ) return {"data": document.serialize("view")} - @json_view(content_type="application/json", validators=(validate_patch_document_data,), permission="edit_tender") + @json_view(content_type="application/json", + validators=(validate_patch_document_data, validate_role_for_contract_document_operation,), + permission="upload_contract_documents") def patch(self): """Tender Contract Document Update""" if not self.validate_contract_document("update"): diff --git a/src/openprocurement/tender/competitivedialogue/models.py b/src/openprocurement/tender/competitivedialogue/models.py index 0fae268357..db704b7d53 100644 --- a/src/openprocurement/tender/competitivedialogue/models.py +++ b/src/openprocurement/tender/competitivedialogue/models.py @@ -377,6 +377,8 @@ def stage2__acl__(obj): acl = [ (Allow, "{}_{}".format(obj.owner, obj.dialogue_token), "generate_credentials"), (Allow, "{}_{}".format(obj.owner, obj.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(obj.owner, obj.owner_token), "edit_contract"), + (Allow, "{}_{}".format(obj.owner, obj.owner_token), "upload_contract_documents"), (Allow, "g:competitive_dialogue", "edit_tender"), (Allow, "g:competitive_dialogue", "edit_cancellation") ] @@ -394,6 +396,7 @@ def stage2__acl__(obj): if i.status == "active" ] ) + return acl diff --git a/src/openprocurement/tender/competitivedialogue/tests/stage2/contract.py b/src/openprocurement/tender/competitivedialogue/tests/stage2/contract.py index 0e954ac182..91ced82f82 100644 --- a/src/openprocurement/tender/competitivedialogue/tests/stage2/contract.py +++ b/src/openprocurement/tender/competitivedialogue/tests/stage2/contract.py @@ -21,6 +21,14 @@ create_tender_contract, patch_tender_contract_value_vat_not_included, patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, ) from openprocurement.tender.openua.tests.contract_blanks import ( # TenderStage2EU(UA)ContractResourceTest @@ -78,6 +86,9 @@ def setUp(self): test_create_tender_contract = snitch(create_tender_contract) test_patch_tender_contract_datesigned = snitch(patch_tender_contract_datesigned) test_patch_tender_contract = snitch(patch_tender_contract_eu) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderStage2EUContractDocumentResourceTest( @@ -111,6 +122,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = ("Basic", ("broker", "")) + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + class TenderStage2UAContractResourceTest(BaseCompetitiveDialogUAStage2ContentWebTest): initial_status = "active.qualification" @@ -147,6 +164,9 @@ def setUp(self): test_patch_tender_contract_datesigned = snitch(patch_tender_contract_datesigned) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractVATNotIncludedResourceTest(BaseCompetitiveDialogUAStage2ContentWebTest): @@ -183,6 +203,9 @@ def setUp(self): self.create_award() test_patch_tender_contract_value_vat_not_included = snitch(patch_tender_contract_value_vat_not_included) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderStage2UAContractDocumentResourceTest( @@ -215,6 +238,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = auth + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + def suite(): suite = unittest.TestSuite() diff --git a/src/openprocurement/tender/core/models.py b/src/openprocurement/tender/core/models.py index 9412cc54a0..71dcd634ee 100644 --- a/src/openprocurement/tender/core/models.py +++ b/src/openprocurement/tender/core/models.py @@ -55,6 +55,7 @@ from openprocurement.tender.core.utils import ( calc_auction_end_time, rounding_shouldStartAfter, restrict_value_to_bounds, round_up_to_ten, + get_contract_supplier_roles, get_contract_supplier_permissions, ) from openprocurement.tender.core.validation import ( validate_lotvalue_value, @@ -374,7 +375,9 @@ class Contract(BaseContract): class Options: roles = { "create": blacklist("id", "status", "date", "documents", "dateSigned"), - "edit": blacklist("id", "documents", "date", "awardID", "suppliers", "items", "contractID"), + "admins": blacklist("id", "documents", "date", "awardID", "suppliers", "items", "contractID"), + "edit_tender_owner": blacklist("id", "documents", "date", "awardID", "suppliers", "items", "contractID"), + "edit_contract_supplier": whitelist("status"), "embedded": schematics_embedded_role, "view": schematics_default_role, } @@ -383,6 +386,23 @@ class Options: awardID = StringType(required=True) documents = ListType(ModelType(Document, required=True), default=list()) + def __acl__(self): + return get_contract_supplier_permissions(self) + + def get_role(self): + root = self.get_root() + request = root.request + if request.authenticated_role in ("tender_owner", "contract_supplier"): + role = "edit_{}".format(request.authenticated_role) + else: + role = request.authenticated_role + return role + + def __local_roles__(self): + roles = {} + roles.update(get_contract_supplier_roles(self)) + return roles + def validate_awardID(self, data, awardID): parent = data["__parent__"] if awardID and isinstance(parent, Model) and awardID not in [i.id for i in parent.awards]: @@ -793,7 +813,7 @@ def validate_relatedLot(self, data, relatedLot): parent = data["__parent__"] if relatedLot and isinstance(parent, Model): validate_relatedlot(get_tender(parent), relatedLot) - + def get_related_lot_obj(self, tender): lot_id = ( self.get("relatedLot") # tender lot @@ -1406,6 +1426,8 @@ def __acl__(self): acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), ] ) self._acl_cancellation_complaint(acl) diff --git a/src/openprocurement/tender/core/utils.py b/src/openprocurement/tender/core/utils.py index 0f1831b60c..aac5ffeda2 100644 --- a/src/openprocurement/tender/core/utils.py +++ b/src/openprocurement/tender/core/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import jmespath from decimal import Decimal from re import compile @@ -15,6 +16,7 @@ from time import sleep from pyramid.exceptions import URLDecodeError from pyramid.compat import decode_path_info +from pyramid.security import Allow from cornice.resource import resource from couchdb.http import ResourceConflict from openprocurement.api.constants import ( @@ -573,3 +575,27 @@ def check_complaints(complaints): complaint_period = getattr(award, "complaintPeriod", None) if complaint_period and complaint_period.endDate and complaint_period.endDate < now: check_complaints(award.complaints) + + +def get_contract_supplier_permissions(contract): + """ + Set `upload_contract_document` permissions for award in `active` status owners + """ + suppliers_permissions = [] + if not hasattr(contract, "__parent__") or 'bids' not in contract.__parent__: + return suppliers_permissions + win_bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract.awardID), contract.__parent__._data)[0] + win_bid = jmespath.search("bids[?id=='{}'].[owner,owner_token]".format(win_bid_id), contract.__parent__._data)[0] + bid_acl = "_".join(win_bid) + suppliers_permissions.extend([(Allow, bid_acl, "upload_contract_documents"), (Allow, bid_acl, "edit_contract")]) + return suppliers_permissions + + +def get_contract_supplier_roles(contract): + roles = {} + if 'bids' not in contract.__parent__: + return roles + bid_id = jmespath.search("awards[?id=='{}'].bid_id".format(contract.awardID), contract.__parent__)[0] + bid_data = jmespath.search("bids[?id=='{}'].[owner,owner_token]".format(bid_id), contract.__parent__)[0] + roles['_'.join(bid_data)] = 'contract_supplier' + return roles diff --git a/src/openprocurement/tender/core/validation.py b/src/openprocurement/tender/core/validation.py index 8fe2426954..c4ef47d916 100644 --- a/src/openprocurement/tender/core/validation.py +++ b/src/openprocurement/tender/core/validation.py @@ -1460,3 +1460,25 @@ def validate_complaint_type_change(request): complaint = request.validated["complaint"] if complaint.type == "claim": raise_operation_error(request, "Can't update claim to complaint") + + +def validate_update_contract_status_by_supplier(request): + if request.authenticated_role == "contract_supplier": + data = request.validated["data"] + if "status" in data and data["status"] != "pending" or request.context.status != "pending.winner-signing": + raise_operation_error(request, "Supplier can change status to `pending`") + + +def validate_role_for_contract_document_operation(request): + if request.authenticated_role not in ("tender_owner", "contract_supplier",): + raise_operation_error(request, "Can {} document only buyer or supplier".format(OPERATIONS.get(request.method))) + if request.authenticated_role == "contract_supplier" and \ + request.validated["contract"].status != "pending.winner-signing": + raise_operation_error( + request, "Supplier can't {} document in current contract status".format(OPERATIONS.get(request.method)) + ) + if request.authenticated_role == "tender_owner" and \ + request.validated["contract"].status == "pending.winner-signing": + raise_operation_error( + request, "Tender onwer can't {} document in current contract status".format(OPERATIONS.get(request.method)) + ) diff --git a/src/openprocurement/tender/esco/models.py b/src/openprocurement/tender/esco/models.py index 282462f9cc..6a98adcba2 100644 --- a/src/openprocurement/tender/esco/models.py +++ b/src/openprocurement/tender/esco/models.py @@ -655,10 +655,13 @@ def __acl__(self): acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), ] ) self._acl_cancellation_complaint(acl) + return acl @serializable(serialized_name="enquiryPeriod", type=ModelType(EnquiryPeriod)) diff --git a/src/openprocurement/tender/esco/tests/contract.py b/src/openprocurement/tender/esco/tests/contract.py index 71b5078b68..a680e5edb4 100644 --- a/src/openprocurement/tender/esco/tests/contract.py +++ b/src/openprocurement/tender/esco/tests/contract.py @@ -11,6 +11,16 @@ TenderContractResourceTestMixin, TenderContractDocumentResourceTestMixin, ) +from openprocurement.tender.belowthreshold.tests.contract_blanks import ( + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, +) from openprocurement.tender.openua.tests.contract_blanks import ( # TenderContractResourceTest @@ -102,6 +112,9 @@ def setUp(self): test_create_tender_contract = snitch(create_tender_contract) test_patch_tender_contract_datesigned = snitch(patch_tender_contract_datesigned) test_patch_tender_contract = snitch(patch_tender_contract) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractDocumentResourceTest(BaseESCOContentWebTest, TenderContractDocumentResourceTestMixin): @@ -133,6 +146,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = ("Basic", ("broker", "")) + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + def suite(): suite = unittest.TestSuite() diff --git a/src/openprocurement/tender/esco/views/contract.py b/src/openprocurement/tender/esco/views/contract.py index 0500e19719..f5268d48d6 100644 --- a/src/openprocurement/tender/esco/views/contract.py +++ b/src/openprocurement/tender/esco/views/contract.py @@ -9,6 +9,7 @@ validate_update_contract_value_with_award, validate_update_contract_value_amount, validate_update_contract_value_net_required, + validate_update_contract_status_by_supplier, ) from openprocurement.tender.esco.validation import validate_update_contract_value_esco from openprocurement.tender.openeu.views.contract import TenderAwardContractResource as TenderEUContractResource @@ -29,10 +30,11 @@ class TenderESCOContractResource(TenderEUContractResource): @json_view( content_type="application/json", - permission="edit_tender", + permission="edit_contract", validators=( validate_patch_contract_data, validate_contract_operation_not_in_allowed_status, + validate_update_contract_status_by_supplier, validate_update_contract_only_for_active_lots, validate_contract_update_with_accepted_complaint, validate_update_contract_value_esco, diff --git a/src/openprocurement/tender/limited/tests/contract.py b/src/openprocurement/tender/limited/tests/contract.py index 800dc175bd..1a54565143 100644 --- a/src/openprocurement/tender/limited/tests/contract.py +++ b/src/openprocurement/tender/limited/tests/contract.py @@ -36,6 +36,9 @@ patch_tender_contract, tender_contract_signature_date, award_id_change_is_not_allowed, + create_tender_contract_document, + patch_tender_contract_document, + put_tender_contract_document, ) from openprocurement.tender.belowthreshold.tests.contract_blanks import ( patch_tender_contract_value_vat_not_included, @@ -387,6 +390,10 @@ def setUp(self): response = self.app.get("/tenders/{}/contracts".format(self.tender_id)) self.contract_id = response.json["data"][0]["id"] + test_create_tender_contract_document = snitch(create_tender_contract_document) + test_patch_tender_contract_document = snitch(patch_tender_contract_document) + test_put_tender_contract_document = snitch(put_tender_contract_document) + class TenderContractNegotiationDocumentResourceTest(TenderContractDocumentResourceTest): initial_data = test_tender_negotiation_data diff --git a/src/openprocurement/tender/limited/tests/contract_blanks.py b/src/openprocurement/tender/limited/tests/contract_blanks.py index 249a9421b2..8fd8852846 100644 --- a/src/openprocurement/tender/limited/tests/contract_blanks.py +++ b/src/openprocurement/tender/limited/tests/contract_blanks.py @@ -907,3 +907,254 @@ def create_tender_contract_negotiation_quick(self): {"data": {"status": "active", "value": {"valueAddedTaxIncluded": False}}}, ) self.assertEqual(response.status, "200 OK") + + +def create_tender_contract_document(self): + response = self.app.get("/tenders/{}/contracts/{}".format(self.tender_id, self.contract_id)) + self.assertEqual(response.json["data"]["status"], "pending") + + doc = self.db.get(self.tender_id) + for i in doc.get("awards", []): + if "complaintPeriod" in i: + i["complaintPeriod"]["endDate"] = i["complaintPeriod"]["startDate"] + if 'value' in doc["contracts"][0] and doc["contracts"][0]["value"]["valueAddedTaxIncluded"]: + doc["contracts"][0]["value"]["amountNet"] = str(float(doc["contracts"][0]["value"]["amount"]) - 1) + self.db.save(doc) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get("/tenders/{}/contracts/{}/documents".format(self.tender_id, self.contract_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"][0]["id"]) + self.assertEqual("name.doc", response.json["data"][0]["title"]) + + response = self.app.get("/tenders/{}/contracts/{}/documents?all=true".format(self.tender_id, self.contract_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"][0]["id"]) + self.assertEqual("name.doc", response.json["data"][0]["title"]) + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?download=some_id".format(self.tender_id, self.contract_id, doc_id), + status=404, + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual( + response.json["errors"], [{u"description": u"Not Found", u"location": u"url", u"name": u"download"}] + ) + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 7) + self.assertEqual(response.body, "content") + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["errors"][0]["description"], "Can't add document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + status=403, + ) + self.assertEqual(response.status, "403 Forbidden") + self.assertEqual(response.content_type, "application/json") + self.assertEqual( + response.json["errors"][0]["description"], + "Can't add document in current ({}) tender status".format( + self.forbidden_contract_document_modification_actions_status + ), + ) + + +def put_tender_contract_document(self): + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + status=404, + upload_files=[("invalid_name", "name.doc", "content")], + ) + self.assertEqual(response.status, "404 Not Found") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(response.json["status"], "error") + self.assertEqual(response.json["errors"], [{u"description": u"Not Found", u"location": u"body", u"name": u"file"}]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content2")], + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 8) + self.assertEqual(response.body, "content2") + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("name.doc", response.json["data"]["title"]) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + "content3", + content_type="application/msword", + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + key = response.json["data"]["url"].split("?")[-1] + + response = self.app.get( + "/tenders/{}/contracts/{}/documents/{}?{}".format(self.tender_id, self.contract_id, doc_id, key) + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/msword") + self.assertEqual(response.content_length, 8) + self.assertEqual(response.body, "content3") + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content3")], + 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 document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.put( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + upload_files=[("file", "name.doc", "content3")], + 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 document in current ({}) tender status".format( + self.forbidden_contract_document_modification_actions_status + ), + ) + + +def patch_tender_contract_document(self): + response = self.app.post( + "/tenders/{}/contracts/{}/documents?acc_token={}".format(self.tender_id, self.contract_id, self.tender_token), + upload_files=[("file", "name.doc", "content")], + ) + self.assertEqual(response.status, "201 Created") + self.assertEqual(response.content_type, "application/json") + doc_id = response.json["data"]["id"] + self.assertIn(doc_id, response.headers["Location"]) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + ) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + + response = self.app.get("/tenders/{}/contracts/{}/documents/{}".format(self.tender_id, self.contract_id, doc_id)) + self.assertEqual(response.status, "200 OK") + self.assertEqual(response.content_type, "application/json") + self.assertEqual(doc_id, response.json["data"]["id"]) + self.assertEqual("document description", response.json["data"]["description"]) + + tender = self.db.get(self.tender_id) + tender["contracts"][-1]["status"] = "cancelled" + self.db.save(tender) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + 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 document in current contract status") + + self.set_status("{}".format(self.forbidden_contract_document_modification_actions_status)) + + response = self.app.patch_json( + "/tenders/{}/contracts/{}/documents/{}?acc_token={}".format( + self.tender_id, self.contract_id, doc_id, self.tender_token + ), + {"data": {"description": "document description"}}, + 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 document in current ({}) tender status".format( + self.forbidden_contract_document_modification_actions_status + ), + ) diff --git a/src/openprocurement/tender/openeu/models.py b/src/openprocurement/tender/openeu/models.py index a38e50af90..73aeb2e6dc 100644 --- a/src/openprocurement/tender/openeu/models.py +++ b/src/openprocurement/tender/openeu/models.py @@ -646,8 +646,11 @@ def __acl__(self): acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), ] ) + self._acl_cancellation_complaint(acl) return acl diff --git a/src/openprocurement/tender/openeu/tests/contract.py b/src/openprocurement/tender/openeu/tests/contract.py index 9685522bc0..40650d08d9 100644 --- a/src/openprocurement/tender/openeu/tests/contract.py +++ b/src/openprocurement/tender/openeu/tests/contract.py @@ -9,7 +9,17 @@ TenderContractResourceTestMixin, TenderContractDocumentResourceTestMixin, ) -from openprocurement.tender.belowthreshold.tests.contract_blanks import patch_tender_contract_value +from openprocurement.tender.belowthreshold.tests.contract_blanks import ( + patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, +) from openprocurement.tender.openua.tests.contract_blanks import ( # TenderContractResourceTest @@ -62,6 +72,9 @@ def setUp(self): test_patch_tender_contract_datesigned = snitch(patch_tender_contract_datesigned) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractDocumentResourceTest(BaseTenderContentWebTest, TenderContractDocumentResourceTestMixin): @@ -94,6 +107,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = ("Basic", ("broker", "")) + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + def suite(): suite = unittest.TestSuite() diff --git a/src/openprocurement/tender/openua/models.py b/src/openprocurement/tender/openua/models.py index 69012b3e16..684c7b15cd 100644 --- a/src/openprocurement/tender/openua/models.py +++ b/src/openprocurement/tender/openua/models.py @@ -611,6 +611,8 @@ def __acl__(self): acl.extend( [ (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_complaint"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "edit_contract"), + (Allow, "{}_{}".format(self.owner, self.owner_token), "upload_contract_documents"), ] ) diff --git a/src/openprocurement/tender/openua/tests/contract.py b/src/openprocurement/tender/openua/tests/contract.py index 7162cc6e40..2d54dcc3f9 100644 --- a/src/openprocurement/tender/openua/tests/contract.py +++ b/src/openprocurement/tender/openua/tests/contract.py @@ -19,6 +19,14 @@ from openprocurement.tender.belowthreshold.tests.contract_blanks import ( patch_tender_contract_value_vat_not_included, patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, ) @@ -55,6 +63,9 @@ def setUp(self): test_patch_tender_contract_datesigned = snitch(patch_tender_contract_datesigned) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractVATNotIncludedResourceTest(BaseTenderUAContentWebTest, TenderContractResourceTestMixin): @@ -92,6 +103,9 @@ def setUp(self): self.create_award() test_patch_tender_contract_value_vat_not_included = snitch(patch_tender_contract_value_vat_not_included) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractDocumentResourceTest(BaseTenderUAContentWebTest, TenderContractDocumentResourceTestMixin): @@ -122,6 +136,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = auth + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + def suite(): suite = unittest.TestSuite() diff --git a/src/openprocurement/tender/openua/views/contract.py b/src/openprocurement/tender/openua/views/contract.py index 279e32e770..e0eaa9b236 100644 --- a/src/openprocurement/tender/openua/views/contract.py +++ b/src/openprocurement/tender/openua/views/contract.py @@ -11,6 +11,7 @@ validate_update_contract_value_with_award, validate_update_contract_value_amount, validate_update_contract_value_net_required, + validate_update_contract_status_by_supplier, ) from openprocurement.tender.core.utils import save_tender, apply_patch, optendersresource from openprocurement.tender.openua.validation import validate_contract_update_with_accepted_complaint @@ -26,10 +27,11 @@ class TenderUaAwardContractResource(TenderAwardContractResource): @json_view( content_type="application/json", - permission="edit_tender", + permission="edit_contract", validators=( validate_patch_contract_data, validate_contract_operation_not_in_allowed_status, + validate_update_contract_status_by_supplier, validate_update_contract_only_for_active_lots, validate_contract_update_with_accepted_complaint, validate_update_contract_value, @@ -44,9 +46,9 @@ def patch(self): """ contract_status = self.request.context.status apply_patch(self.request, save=False, src=self.request.context.serialize()) - if contract_status != self.request.context.status and ( - contract_status != "pending" or self.request.context.status != "active" - ): + if contract_status != self.request.context.status and \ + (contract_status not in ("pending", "pending.winner-signing",) or \ + self.request.context.status not in ("active", "pending", "pending.winner-signing",)): raise_operation_error(self.request, "Can't update contract status") if self.request.context.status == "active" and not self.request.context.dateSigned: self.request.context.dateSigned = get_now() diff --git a/src/openprocurement/tender/openua/views/contract_document.py b/src/openprocurement/tender/openua/views/contract_document.py index c6b11029a7..b31890aff7 100644 --- a/src/openprocurement/tender/openua/views/contract_document.py +++ b/src/openprocurement/tender/openua/views/contract_document.py @@ -37,7 +37,7 @@ def validate_contract_document(self, operation): ] ): raise_operation_error(self.request, "Can {} document only in active lot status".format(operation)) - if self.request.validated["contract"].status not in ["pending", "active"]: + if self.request.validated["contract"].status not in ["pending", "pending.winner-signing", "active"]: raise_operation_error(self.request, "Can't {} document in current contract status".format(operation)) if any( [ diff --git a/src/openprocurement/tender/openuadefense/tests/contract.py b/src/openprocurement/tender/openuadefense/tests/contract.py index 31e13ee3c4..60826cf057 100644 --- a/src/openprocurement/tender/openuadefense/tests/contract.py +++ b/src/openprocurement/tender/openuadefense/tests/contract.py @@ -21,6 +21,14 @@ from openprocurement.tender.belowthreshold.tests.contract_blanks import ( patch_tender_contract_value_vat_not_included, patch_tender_contract_value, + patch_tender_contract_status_by_owner, + patch_tender_contract_status_by_others, + patch_tender_contract_status_by_supplier, + create_tender_contract_document_by_supplier, + create_tender_contract_document_by_others, + put_tender_contract_document_by_supplier, + put_tender_contract_document_by_others, + patch_tender_contract_document_by_supplier, ) @@ -56,6 +64,9 @@ def setUp(self): test_create_tender_contract = snitch(create_tender_contract) test_patch_tender_contract = snitch(patch_tender_contract) test_patch_tender_contract_value = snitch(patch_tender_contract_value) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractVATNotIncludedResourceTest(BaseTenderUAContentWebTest, TenderContractResourceTestMixin): @@ -94,6 +105,9 @@ def setUp(self): self.create_award() test_patch_tender_contract_value_vat_not_included = snitch(patch_tender_contract_value_vat_not_included) + test_patch_tender_contract_status_by_owner = snitch(patch_tender_contract_status_by_owner) + test_patch_tender_contract_status_by_others = snitch(patch_tender_contract_status_by_others) + test_patch_tender_contract_status_by_supplier = snitch(patch_tender_contract_status_by_supplier) class TenderContractDocumentResourceTest(BaseTenderUAContentWebTest, TenderContractDocumentResourceTestMixin): @@ -124,6 +138,12 @@ def setUp(self): self.contract_id = contract["id"] self.app.authorization = auth + test_create_tender_contract_document_by_supplier = snitch(create_tender_contract_document_by_supplier) + test_create_tender_contract_document_by_others = snitch(create_tender_contract_document_by_others) + test_put_tender_contract_document_by_supplier = snitch(put_tender_contract_document_by_supplier) + test_put_tender_contract_document_by_others = snitch(put_tender_contract_document_by_others) + test_patch_tender_contract_document_by_supplier = snitch(patch_tender_contract_document_by_supplier) + def suite(): suite = unittest.TestSuite()