def shouldStartAfter(self): if self.endDate: return tender = get_tender(self) lot = self.__parent__ statuses = [ "active.tendering", "active.pre-qualification.stand-still", "active.auction" ] if tender.status not in statuses or lot.status != "active": return start_after = None if tender.status == "active.tendering" and tender.tenderPeriod.endDate: start_after = calculate_tender_date(tender.tenderPeriod.endDate, TENDERING_AUCTION, tender) elif self.startDate and get_now() > calc_auction_end_time( lot.numberOfBids, self.startDate): start_after = calc_auction_end_time(lot.numberOfBids, self.startDate) elif tender.qualificationPeriod and tender.qualificationPeriod.endDate: decision_dates = [ datetime.combine( complaint.dateDecision.date() + timedelta(days=3), time(0, tzinfo=complaint.dateDecision.tzinfo)) for qualification in tender.qualifications for complaint in qualification.complaints if complaint.dateDecision ] decision_dates.append(tender.qualificationPeriod.endDate) start_after = max(decision_dates) if start_after: return normalize_should_start_after(start_after, tender).isoformat()
def calculate_period_date(self, date, period, startend, status): tender = self.tender_class(self.tender_document) period_date_item = self.periods[status][startend][period][date] return calculate_tender_date(self.now, period_date_item, tender=tender, working_days=False)
def _validate_tender_period_start_date(data, period, working_days=False, calendar=WORKING_DAYS): TENDER_CREATION_BUFFER_DURATION=timedelta(minutes=10) min_allowed_date = calculate_tender_date( get_now(), -TENDER_CREATION_BUFFER_DURATION, working_days=working_days, calendar=calendar ) if min_allowed_date >= period.startDate: raise ValidationError("tenderPeriod.startDate should be in greater than current date")
def check_complaint_status(request, complaint, now=None): if not now: now = get_now() if complaint.status == "answered": date = calculate_tender_date(complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, request.tender) if date < now: complaint.status = complaint.resolutionType elif complaint.status == "pending" and complaint.resolutionType and complaint.dateEscalated: complaint.status = complaint.resolutionType elif complaint.status == "pending": complaint.status = "ignored"
def check_period_and_items(request, tender): agreement_items = tender.agreements[0].items if tender.agreements[0].items else [] agreement_items_ids = {calculate_item_identification_tuple(agreement_item) for agreement_item in agreement_items} tender_items_ids = {calculate_item_identification_tuple(tender_item) for tender_item in tender.items} if not tender_items_ids.issubset(agreement_items_ids): drop_draft_to_unsuccessful(request, tender, AGREEMENT_ITEMS) return delta = -request.content_configurator.agreement_expired_until date = calculate_tender_date(tender.agreements[0].period.endDate, delta, tender) if get_now() > date: drop_draft_to_unsuccessful(request, tender, AGREEMENT_EXPIRED) elif tender.agreements[0].period.startDate > tender.date: drop_draft_to_unsuccessful(request, tender, AGREEMENT_START_DATE)
def next_check(self): now = get_now() checks = [] if self.status == "active.enquiries" and self.tenderPeriod.startDate: checks.append(self.tenderPeriod.startDate.astimezone(TZ)) elif self.status == "active.enquiries" and self.enquiryPeriod.endDate: checks.append(self.enquiryPeriod.endDate.astimezone(TZ)) elif self.status == "active.tendering" and self.tenderPeriod.endDate: checks.append(self.tenderPeriod.endDate.astimezone(TZ)) elif (not self.lots and self.status == "active.auction" and self.auctionPeriod and self.auctionPeriod.startDate and not self.auctionPeriod.endDate): if now < self.auctionPeriod.startDate: checks.append(self.auctionPeriod.startDate.astimezone(TZ)) else: auction_end_time = calc_auction_end_time( self.numberOfBids, self.auctionPeriod.startDate).astimezone(TZ) if now < auction_end_time: checks.append(auction_end_time) elif self.lots and self.status == "active.auction": for lot in self.lots: if (lot.status != "active" or not lot.auctionPeriod or not lot.auctionPeriod.startDate or lot.auctionPeriod.endDate): continue if now < lot.auctionPeriod.startDate: checks.append(lot.auctionPeriod.startDate.astimezone(TZ)) else: auction_end_time = calc_auction_end_time( lot.numberOfBids, lot.auctionPeriod.startDate).astimezone(TZ) if now < auction_end_time: checks.append(auction_end_time) elif (not self.lots and self.status == "active.awarded" and not any( [i.status in self.block_complaint_status for i in self.complaints]) and not any([ i.status in self.block_complaint_status for a in self.awards for i in a.complaints ])): standStillEnds = [ a.complaintPeriod.endDate.astimezone(TZ) for a in self.awards if a.complaintPeriod and a.complaintPeriod.endDate ] last_award_status = self.awards[-1].status if self.awards else "" if standStillEnds and last_award_status == "unsuccessful": checks.append(max(standStillEnds)) elif (self.lots and self.status in ["active.qualification", "active.awarded"] and not any([ i.status in self.block_complaint_status and i.relatedLot is None for i in self.complaints ])): for lot in self.lots: if lot["status"] != "active": continue lot_awards = [i for i in self.awards if i.lotID == lot.id] pending_complaints = any([ i["status"] in self.block_complaint_status and i.relatedLot == lot.id for i in self.complaints ]) pending_awards_complaints = any([ i.status in self.block_complaint_status for a in lot_awards for i in a.complaints ]) standStillEnds = [ a.complaintPeriod.endDate.astimezone(TZ) for a in lot_awards if a.complaintPeriod and a.complaintPeriod.endDate ] last_award_status = lot_awards[-1].status if lot_awards else "" if (not pending_complaints and not pending_awards_complaints and standStillEnds and last_award_status == "unsuccessful"): checks.append(max(standStillEnds)) if self.status.startswith("active"): for complaint in self.complaints: if complaint.status == "answered" and complaint.dateAnswered: check = calculate_tender_date(complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self) checks.append(check) elif complaint.status == "pending": checks.append(self.dateModified) for award in self.awards: if award.status == "active" and not any( [i.awardID == award.id for i in self.contracts]): checks.append(award.date) for complaint in award.complaints: if complaint.status == "answered" and complaint.dateAnswered: check = calculate_tender_date( complaint.dateAnswered, COMPLAINT_STAND_STILL_TIME, self) checks.append(check) elif complaint.status == "pending": checks.append(self.dateModified) return min(checks).isoformat() if checks else None
def test_24hours_milestone(self): self.app.authorization = ("Basic", ("broker", "")) # try upload documents response = self.app.get("/tenders/{}".format(self.tender_id)) context = response.json["data"]["{}s".format(self.context_name)][0] bid_id = context.get("bid_id") or context.get("bidID") # awards and qualifications developed on different days winner_token = self.initial_bids_tokens[bid_id] upload_allowed_by_default = response.json["data"]["procurementMethodType"] in \ ("aboveThresholdUA.defense", "simple.defense") self.assert_upload_docs_status(bid_id, winner_token, success=upload_allowed_by_default) # invalid creation response = self.app.post_json( "/tenders/{}/{}s/{}/milestones".format(self.tender_id, self.context_name, self.context_id), { "data": {} }, status=403 ) self.assertEqual( response.json, {"status": "error", "errors": [{"location": "url", "name": "permission", "description": "Forbidden"}]} ) response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), { "data": { "code": "alp" } }, status=403 ) if get_now() > RELEASE_2020_04_19: self.assertEqual( response.json, {"status": "error", "errors": [{"description": "The only allowed milestone code is '24h'", "location": "body", "name": "data"}]} ) else: self.assertEqual( response.json, {"status": "error", "errors": [{"location": "body", "name": "data", "description": "Forbidden"}]} ) return # valid creation request_data = { "code": "24h", "description": "One ring to bring them all and in the darkness bind them", "dueDate": (get_now() + timedelta(days=10)).isoformat() } response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), {"data": request_data}, ) self.assertEqual(response.status, "201 Created") created_milestone = response.json["data"] # get milestone from tender response = self.app.get("/tenders/{}".format(self.tender_id)) tender_data = response.json["data"] context = tender_data["{}s".format(self.context_name)][0] public_milestone = context["milestones"][0] self.assertEqual(created_milestone, public_milestone) self.assertEqual( set(created_milestone.keys()), { "id", "date", "code", "description", "dueDate", } ) self.assertEqual(created_milestone["code"], request_data["code"]) self.assertEqual(created_milestone["description"], request_data["description"]) self.assertNotEqual(created_milestone["dueDate"], request_data["dueDate"]) expected_date = calculate_tender_date( parse_date(created_milestone["date"]), timedelta(hours=24), tender_data ) self.assertEqual(created_milestone["dueDate"], expected_date.isoformat()) # get milestone by its direct link response = self.app.get("/tenders/{}/{}s/{}/milestones/{}".format( self.tender_id, self.context_name, self.context_id, created_milestone["id"] )) direct_milestone = response.json["data"] self.assertEqual(created_milestone, direct_milestone) # can't post another response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), {"data": request_data}, status=422 ) self.assertEqual( response.json, {"status": "error", "errors": [{"description": [ {"milestones": ["There can be only one '24h' milestone"]}], "location": "body", "name": "{}s".format(self.context_name)}]} ) # can't update status of context until dueDate activation_data = {"status": "active", "qualified": True, "eligible": True} response = self.app.patch_json( "/tenders/{}/{}s/{}?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), {"data": activation_data}, status=403 ) self.assertEqual( response.json, { "status": "error", "errors": [ { "description": "Can't change status to 'active' " "until milestone.dueDate: {}".format(created_milestone["dueDate"]), "location": "body", "name": "data" }] } ) # try upload documents self.assert_upload_docs_status(bid_id, winner_token) # wait until milestone dueDate ends with patch("openprocurement.tender.core.validation.get_now", lambda: get_now() + timedelta(hours=24)): self.assert_upload_docs_status(bid_id, winner_token, success=upload_allowed_by_default) response = self.app.patch_json( "/tenders/{}/{}s/{}?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), {"data": activation_data}, status=200 ) self.assertEqual(response.json["data"]["status"], "active") # check appending milestone at active qualification status # remove milestone to skip "only one" validator tender = self.db.get(self.tender_id) context = tender["{}s".format(self.context_name)][0] context["milestones"] = [] self.db.save(tender) response = self.app.post_json( "/tenders/{}/{}s/{}/milestones?acc_token={}".format( self.tender_id, self.context_name, self.context_id, self.tender_token ), {"data": request_data}, status=403 ) self.assertEqual( response.json, {"status": "error", "errors": [ {"description": "Not allowed in current 'active' {} status".format(self.context_name), "location": "body", "name": "data"}]} )