def patch(self): cancellation = self.request.context prev_status = cancellation.status apply_patch(self.request, save=False, src=cancellation.serialize()) new_rules = get_first_revision_date( self.request.tender, default=get_now()) > RELEASE_2020_04_19 if new_rules: if prev_status == "draft" and cancellation.status == "pending": validate_absence_of_pending_accepted_satisfied_complaints( self.request) tender = self.request.validated["tender"] now = get_now() cancellation.complaintPeriod = { "startDate": now.isoformat(), "endDate": calculate_complaint_business_date(now, timedelta(days=10), tender).isoformat() } if cancellation.status == "active" and prev_status != "active": self.cancel_tender_lot_method(self.request, cancellation) if save_tender(self.request): self.LOGGER.info( "Updated tender cancellation {}".format(cancellation.id), extra=context_unpack( self.request, {"MESSAGE_ID": "tender_cancellation_patch"}), ) return {"data": cancellation.serialize("view")}
def __call__(self, obj, *args, **kwargs): complaintPeriod_class = obj._fields["tenderPeriod"] endDate = calculate_complaint_business_date(obj.tenderPeriod.endDate, -COMPLAINT_SUBMIT_TIME, obj) return complaintPeriod_class( dict(startDate=obj.tenderPeriod.startDate, endDate=endDate))
def patch(self): """Tender Edit (partial) For example here is how procuring entity can change number of items to be procured and total Value of a tender: .. sourcecode:: http PATCH /tenders/4879d3f8ee2443169b5fbbc9f89fa607 HTTP/1.1 Host: example.com Accept: application/json { "data": { "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } And here is the response to be expected: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "data": { "id": "4879d3f8ee2443169b5fbbc9f89fa607", "tenderID": "UA-64e93250be76435397e8c992ed4214d1", "dateModified": "2014-10-27T08:12:34.956Z", "value": { "amount": 600 }, "itemsToBeProcured": [ { "quantity": 6 } ] } } """ tender = self.context config = getAdapter(tender, IContentConfigurator) data = self.request.validated["data"] now = get_now() if (self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.tendering"): if "tenderPeriod" in data and "endDate" in data["tenderPeriod"]: self.request.validated["tender"].tenderPeriod.import_data( data["tenderPeriod"]) validate_tender_period_extension(self.request) self.request.registry.notify( TenderInitializeEvent(self.request.validated["tender"])) self.request.validated["data"][ "enquiryPeriod"] = self.request.validated[ "tender"].enquiryPeriod.serialize() apply_patch(self.request, save=False, src=self.request.validated["tender_src"]) if self.request.authenticated_role == "chronograph": check_status(self.request) elif self.request.authenticated_role == "tender_owner" and tender.status == "active.tendering": tender.invalidate_bids_data() elif (self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.pre-qualification" and tender.status == "active.pre-qualification.stand-still"): active_lots = [ lot.id for lot in tender.lots if lot.status == "active" ] if tender.lots else [None] if any([ i["status"] in self.request.validated["tender"].block_complaint_status for q in self.request.validated["tender"]["qualifications"] for i in q["complaints"] if q["lotID"] in active_lots ]): raise_operation_error( self.request, "Can't switch to 'active.pre-qualification.stand-still' before resolve all complaints" ) if all_bids_are_reviewed(self.request): tender.qualificationPeriod.endDate = calculate_complaint_business_date( now, config.prequalification_complaint_stand_still, self.request.validated["tender"]) tender.check_auction_time() else: raise_operation_error( self.request, "Can't switch to 'active.pre-qualification.stand-still' while not all bids are qualified", ) elif (self.request.authenticated_role == "tender_owner" and self.request.validated["tender_status"] == "active.qualification" and tender.status == "active.qualification.stand-still"): active_lots = [ lot.id for lot in tender.lots if lot.status == "active" ] if tender.lots else [None] if any([ i["status"] in self.request.validated["tender"].block_complaint_status for a in self.request.validated["tender"]["awards"] for i in a["complaints"] if a["lotID"] in active_lots ]): raise_operation_error( self.request, "Can't switch to 'active.qualification.stand-still' before resolve all complaints" ) if all_awards_are_reviewed(self.request): tender.awardPeriod.endDate = calculate_complaint_business_date( now, config.qualification_complaint_stand_still, self.request.validated["tender"]) for award in [ a for a in tender.awards if a.status != "cancelled" ]: award["complaintPeriod"] = { "startDate": now.isoformat(), "endDate": tender.awardPeriod.endDate.isoformat(), } else: raise_operation_error( self.request, "Can't switch to 'active.qualification.stand-still' while not all awards are qualified", ) save_tender(self.request) self.LOGGER.info("Updated tender {}".format(tender.id), extra=context_unpack(self.request, {"MESSAGE_ID": "tender_patch"})) return {"data": tender.serialize(tender.status)}
def complaintPeriod(self): endDate = calculate_complaint_business_date(self.tenderPeriod.endDate, -COMPLAINT_SUBMIT_TIME, self) return Period( dict(startDate=self.tenderPeriod.startDate, endDate=endDate))
def patch(self): """Update of award Example request to change the award: .. sourcecode:: http PATCH /tenders/4879d3f8ee2443169b5fbbc9f89fa607/awards/71b6c23ed8944d688e92a31ec8c3f61a HTTP/1.1 Host: example.com Accept: application/json { "data": { "value": { "amount": 600 } } } And here is the response to be expected: .. sourcecode:: http HTTP/1.0 200 OK Content-Type: application/json { "data": { "id": "4879d3f8ee2443169b5fbbc9f89fa607", "date": "2014-10-28T11:44:17.947Z", "status": "active", "suppliers": [ { "id": { "name": "Державне управління справами", "scheme": "https://ns.openprocurement.org/ua/edrpou", "uid": "00037256", "uri": "http://www.dus.gov.ua/" }, "address": { "countryName": "Україна", "postalCode": "01220", "region": "м. Київ", "locality": "м. Київ", "streetAddress": "вул. Банкова, 11, корпус 1" } } ], "value": { "amount": 600, "currency": "UAH", "valueAddedTaxIncluded": true } } } """ tender = self.request.validated["tender"] award = self.request.context award_status = award.status apply_patch(self.request, save=False, src=self.request.context.serialize()) configurator = self.request.content_configurator now = get_now() if award_status != award.status and award.status in [ "active", "unsuccessful" ]: if award.complaintPeriod: award.complaintPeriod.startDate = now else: award.complaintPeriod = {"startDate": now.isoformat()} if award_status == "pending" and award.status == "active": award.complaintPeriod.endDate = calculate_complaint_business_date( now, STAND_STILL_TIME, tender) add_contract(self.request, award, now) add_next_award( self.request, reverse=configurator.reverse_awarding_criteria, awarding_criteria_key=configurator.awarding_criteria_key, ) elif (award_status == "active" and award.status == "cancelled" and any([i.status == "satisfied" for i in award.complaints])): cancelled_awards = [] for i in tender.awards: if i.lotID != award.lotID: continue if not i.complaintPeriod.endDate or i.complaintPeriod.endDate > now: i.complaintPeriod.endDate = now i.status = "cancelled" cancelled_awards.append(i.id) for i in tender.contracts: if i.awardID in cancelled_awards: i.status = "cancelled" add_next_award( self.request, reverse=configurator.reverse_awarding_criteria, awarding_criteria_key=configurator.awarding_criteria_key, ) elif award_status == "active" and award.status == "cancelled": if award.complaintPeriod.endDate > now: award.complaintPeriod.endDate = now for i in tender.contracts: if i.awardID == award.id: i.status = "cancelled" add_next_award( self.request, reverse=configurator.reverse_awarding_criteria, awarding_criteria_key=configurator.awarding_criteria_key, ) elif award_status == "pending" and award.status == "unsuccessful": award.complaintPeriod.endDate = calculate_complaint_business_date( get_now(), STAND_STILL_TIME, tender) add_next_award( self.request, reverse=configurator.reverse_awarding_criteria, awarding_criteria_key=configurator.awarding_criteria_key, ) elif (award_status == "unsuccessful" and award.status == "cancelled" and any([i.status == "satisfied" for i in award.complaints])): if tender.status == "active.awarded": tender.status = "active.qualification" tender.awardPeriod.endDate = None if award.complaintPeriod.endDate > now: award.complaintPeriod.endDate = now cancelled_awards = [] for i in tender.awards: if i.lotID != award.lotID: continue if not i.complaintPeriod.endDate or i.complaintPeriod.endDate > now: i.complaintPeriod.endDate = now i.status = "cancelled" cancelled_awards.append(i.id) for i in tender.contracts: if i.awardID in cancelled_awards: i.status = "cancelled" add_next_award( self.request, reverse=configurator.reverse_awarding_criteria, awarding_criteria_key=configurator.awarding_criteria_key, ) elif self.request.authenticated_role != "Administrator" and not ( award_status == "pending" and award.status == "pending"): raise_operation_error( self.request, "Can't update award in current ({}) status".format( award_status)) if save_tender(self.request): self.LOGGER.info( "Updated tender award {}".format(self.request.context.id), extra=context_unpack(self.request, {"MESSAGE_ID": "tender_award_patch"}), ) return {"data": award.serialize("view")}
def test_milestone(self): """ test alp milestone is created in two cases 1. amount less by >=40% than mean of amount before auction 2. amount less by >=30% than the next amount :return: """ # sending auction results auction_results = deepcopy(self.initial_bids) if "lotValues" in self.initial_bids[0]: lot_id = auction_results[0]["lotValues"][0]["relatedLot"] auction_results[0]["lotValues"][0]["value"]["amount"] = 29 # only 1 case auction_results[1]["lotValues"][0]["value"]["amount"] = 30 # both 1 and 2 case auction_results[2]["lotValues"][0]["value"]["amount"] = 350 # only 2 case auction_results[3]["lotValues"][0]["value"]["amount"] = 500 # no milestones else: lot_id = None auction_results[0]["value"]["amount"] = 29 # only 1 case auction_results[1]["value"]["amount"] = 30 # both 1 and 2 case auction_results[2]["value"]["amount"] = 350 # only 2 case auction_results[3]["value"]["amount"] = 500 # no milestones with change_auth(self.app, ("Basic", ("auction", ""))): url = "/tenders/{}/auction".format(self.tender_id) if lot_id: url += "/" + lot_id response = self.app.post_json( url, {"data": {"bids": auction_results}}, status=200 ) tender = response.json["data"] self.assertEqual("active.qualification", tender["status"]) self.assertGreater(len(tender["awards"]), 0) award = tender["awards"][0] bid_id = award["bid_id"] self.assertEqual(bid_id, auction_results[0]["id"]) if get_now() < RELEASE_2020_04_19: return self.assertEqual(len(award.get("milestones", [])), 0) # check that a milestone's been created self.assertEqual(len(award.get("milestones", [])), 1) milestone = award["milestones"][0] self.assertEqual(milestone["code"], "alp") self.assertEqual(milestone["description"], ALP_MILESTONE_REASONS[0]) # try to change award status unsuccessful_data = {"status": "unsuccessful"} response = self.app.patch_json( "/tenders/{}/awards/{}?acc_token={}".format( self.tender_id, award["id"], self.tender_token ), {"data": unsuccessful_data}, status=403 ) expected_due_date = calculate_complaint_business_date( parse_date(milestone["date"]), timedelta(days=1), tender, working_days=True, ) self.assertEqual( response.json, { u'status': u'error', u'errors': [{ u'description': u"Can't change status to 'unsuccessful' until milestone.dueDate: {}".format( expected_due_date.isoformat() ), u'location': u'body', u'name': u'data' }] } ) # try to post/put/patch docs for doc_type in ["evidence", None]: self._test_doc_upload( tender["procurementMethodType"], doc_type, bid_id, self.initial_bids_tokens[bid_id], expected_due_date ) # setting "dueDate" to now self.wait_until_award_milestone_due_date(award_index=0) # after milestone dueDate tender owner can change award status response = self.app.patch_json( "/tenders/{}/awards/{}?acc_token={}".format( self.tender_id, award["id"], self.tender_token ), {"data": unsuccessful_data}, status=200 ) self.assertEqual(response.json["data"]["status"], "unsuccessful") # check second award response = self.app.get( "/tenders/{}/awards?acc_token={}".format(self.tender_id, self.tender_token), status=200 ) self.assertGreater(len(response.json["data"]), 1) second_award = response.json["data"][1] self.assertEqual(len(second_award.get("milestones", [])), 1) self.assertEqual(second_award["milestones"][0]["description"], u" / ".join(ALP_MILESTONE_REASONS)) # proceed to the third award self.wait_until_award_milestone_due_date(award_index=1) response = self.app.patch_json( "/tenders/{}/awards/{}?acc_token={}".format( self.tender_id, second_award["id"], self.tender_token ), {"data": unsuccessful_data}, status=200 ) self.assertEqual(response.json["data"]["status"], "unsuccessful") # checking 3rd award response = self.app.get( "/tenders/{}/awards?acc_token={}".format(self.tender_id, self.tender_token), status=200 ) self.assertGreater(len(response.json["data"]), 2) third_award = response.json["data"][2] self.assertEqual(len(third_award.get("milestones", [])), 1) self.assertEqual(third_award["milestones"][0]["description"], ALP_MILESTONE_REASONS[1]) # proceed to the last award self.wait_until_award_milestone_due_date(award_index=2) response = self.app.patch_json( "/tenders/{}/awards/{}?acc_token={}".format( self.tender_id, third_award["id"], self.tender_token ), {"data": unsuccessful_data}, status=200 ) self.assertEqual(response.json["data"]["status"], "unsuccessful") # checking last award response = self.app.get( "/tenders/{}/awards?acc_token={}".format(self.tender_id, self.tender_token), status=200 ) self.assertGreater(len(response.json["data"]), 3) last_award = response.json["data"][3] self.assertNotIn("milestones", last_award)